Elided Label Example#

This example creates a widget similar to QLabel , that elides the last visible line, if the text is too long to fit the widget’s geometry.

../_images/elidedlabel-example.png

When text of varying length has to be displayed in a uniformly sized area, for instance within a list or grid view where all list items have the same size, it can be useful to give the user a visual clue when not all text is visible. QLabel can elide text that doesn’t fit within it, but only in one line. The ElidedLabel widget shown in this example word wraps its text by its width, and elides the last visible line if some text is left out. TestWidget gives control to the features of ElidedWidget and forms the example application.

ElidedLabel Class Definition#

Like QLabel , ElidedLabel inherits from QFrame . Here’s the definition of the ElidedLabel class:

class ElidedLabel(QFrame):

    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE setText)
    Q_PROPERTY(bool isElided READ isElided)
# public
    ElidedLabel = explicit(QString text, QWidget parent = None)
    def setText(text):
    QString  text() { return content; }
    bool isElided() { return elided; }
protected:
    def paintEvent(event):
signals:
    def elisionChanged(elided):
# private
    elided = bool()
    content = QString()

The isElided property depends the font, text content and geometry of the widget. Whenever any of these change, the elisionChanged() signal might trigger. We cache the current elision value in elided, so that it doesn’t have to be recomputed every time it’s asked for.

ElidedLabel Class Implementation#

Except for initializing the member variables, the constructor sets the size policy to be horizontally expanding, since it’s meant to fill the width of its container and grow vertically.

def __init__(self, text, parent):
    QFrame.__init__(self, parent)
    , elided(False)
    , content(text)

    setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)

Changing the content require a repaint of the widget.

def setText(self, newText):

    content = newText
    update()

QTextLayout is used in the paintEvent() to divide the content into lines, that wrap on word boundaries. Each line, except the last visible one, is drawn lineSpacing pixels below the previous one. The draw() method of QTextLine will draw the line using the coordinate point as the top left corner.

def paintEvent(self, event):

    QFrame.paintEvent(event)
    painter = QPainter(self)
    fontMetrics = painter.fontMetrics()
    didElide = False()
    lineSpacing = fontMetrics.lineSpacing()
    y = 0
    textLayout = QTextLayout(content, painter.font())
    textLayout.beginLayout()
    forever {
        line = textLayout.createLine()
        if (not line.isValid())
            break
        line.setLineWidth(width())
        nextLineY = y + lineSpacing
        if (height() >= nextLineY + lineSpacing) {
            line.draw(painter, QPoint(0, y))
            y = nextLineY

Unfortunately, QTextLayout does not elide text, so the last visible line has to be treated differently. This last line is elided if it is too wide. The drawText() method of QPainter draws the text starting from the base line, which is ascecnt() pixels below the last drawn line.

Finally, one more line is created to see if everything fit on this line.

    else:
        lastLine = content.mid(line.textStart())
        elidedLastLine = fontMetrics.elidedText(lastLine, Qt.ElideRight, width())
        painter.drawText(QPoint(0, y + fontMetrics.ascent()), elidedLastLine)
        line = textLayout.createLine()
        didElide = line.isValid()
        break


textLayout.endLayout()

If the text was elided and wasn’t before or vice versa, cache it in elided and emit the change.

if (didElide != elided) {
    elided = didElide
    elisionChanged.emit(didElide)

TestWidget Class Definition#

TestWidget is a QWidget and is the main window of the example. It contains an ElidedLabel which can be resized with two QSlider widgets.

class TestWidget(QWidget):

    Q_OBJECT
# public
    TestWidget(QWidget parent = None)
protected:
    def resizeEvent(event):
slots: = private()
    def switchText():
    def onWidthChanged(width):
    def onHeightChanged(height):
# private
    sampleIndex = int()
    textSamples = QStringList()
    elidedText = ElidedLabel()
    heightSlider = QSlider()
    widthSlider = QSlider()

TestWidget Class Implementation#

The constructor initializes the whole widget. Strings of different length are stored in textSamples. The user is able to switch between these.

def __init__(self, parent):
    QWidget.__init__(self, parent)

    romeo = tr(
        "But soft, what light through yonder window breaks? / "
        "It is the east, and Juliet is the sun. / "
        "Arise, fair sun, and kill the envious moon, / "
        "Who is already sick and pale with grief / "
        "That thou, her maid, art far more fair than she."
    )
    macbeth = tr(
        "To-morrow, and to-morrow, and to-morrow, / "
        "Creeps in self petty pace from day to day, / "
        "To the last syllable of recorded time; / "
        "And all our yesterdays have lighted fools / "
        "The way to dusty death. Out, out, brief candlenot / "
        "Life's but a walking shadow, a poor player, / "
        "That struts and frets his hour upon the stage, / "
        "And then is heard no more. It is a tale / "
        "Told by an idiot, full of sound and fury, / "
        "Signifying nothing."
    )
    harry = tr("Feeling lucky, punk?")
    textSamples << romeo << macbeth << harry

An ElidedLabel is created to contain the first of the sample strings. The frame is made visible to make it easier to see the actual size of the widget.

sampleIndex = 0
elidedText = ElidedLabel(textSamples[sampleIndex], self)
elidedText.setFrameStyle(QFrame.Box)

The buttons and the elision label are created. By connecting the elisionChanged() signal to the setVisible() slot of the label, it will act as an indicator to when the text is elided or not. This signal could, for instance, be used to make a “More” button visible, or similar.

switchButton = QPushButton(tr("Switch text"))
connect(switchButton, QPushButton.clicked, self, TestWidget.switchText)
exitButton = QPushButton(tr("Exit"))
connect(exitButton, QPushButton.clicked, self, TestWidget.close)
label = QLabel(tr("Elided"))
label.setVisible(elidedText.isElided())
connect(elidedText, ElidedLabel.elisionChanged, label, QLabel.setVisible)

The widthSlider and heightSlider specify the size of the elidedText. Since the y-axis is inverted, the heightSlider has to be inverted to act appropriately.

widthSlider = QSlider(Qt.Horizontal)
widthSlider.setMinimum(0)
connect(widthSlider, QSlider.valueChanged, self, TestWidget.onWidthChanged)
heightSlider = QSlider(Qt.Vertical)
heightSlider.setInvertedAppearance(True)
heightSlider.setMinimum(0)
connect(heightSlider, QSlider.valueChanged, self, TestWidget.onHeightChanged)

The components are all stored in a QGridLayout , which is made the layout of the TestWidget.

layout = QGridLayout()
layout.addWidget(label, 0, 1, Qt.AlignCenter)
layout.addWidget(switchButton, 0, 2)
layout.addWidget(exitButton, 0, 3)
layout.addWidget(widthSlider, 1, 1, 1, 3)
layout.addWidget(heightSlider, 2, 0)
layout.addWidget(elidedText, 2, 1, 1, 3, Qt.AlignTop | Qt.AlignLeft)
setLayout(layout)

The widthSlider and heightSlider have the exact same length as the dimensions of the elidedText. The maximum value for both of them is thus their lengths, and each tick indicates one pixel.

def resizeEvent(self, event):

    Q_UNUSED(event)
    maxWidth = widthSlider.width()
    widthSlider.setMaximum(maxWidth)
    widthSlider.setValue(maxWidth / 2)
    maxHeight = heightSlider.height()
    heightSlider.setMaximum(maxHeight)
    heightSlider.setValue(maxHeight / 2)
    elidedText.setFixedSize(widthSlider.value(), heightSlider.value())

The switchText() slot simply cycles through all the available sample texts.

def switchText(self):

    sampleIndex = (sampleIndex + 1) % textSamples.size()
    elidedText.setText(textSamples.at(sampleIndex))

These slots set the width and height of the elided text, in response to changes in the sliders.

The `` main()``

Function#

The main() function creates an instance of TestWidget fullscreen and enters the message loop.

if __name__ == "__main__":

    application = QApplication( argc, argv )
    w = TestWidget()
    w.showFullScreen()
    return application.exec()

Example project @ code.qt.io