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.
像
QLabel
,
ElidedLabel
继承自
QFrame
. Here's the definition of the
ElidedLabel
类:
class ElidedLabel : public QFrame { Q_OBJECT Q_PROPERTY(QString text READ text WRITE setText) Q_PROPERTY(bool isElided READ isElided) public: explicit ElidedLabel(const QString &text, QWidget *parent = 0); void setText(const QString &text); const QString & text() const { return content; } bool isElided() const { return elided; } protected: void paintEvent(QPaintEvent *event) override; signals: void elisionChanged(bool elided); private: bool elided; QString content; };
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.
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.
ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) : QFrame(parent) , elided(false) , content(text) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); }
Changing the
content
require a repaint of the widget.
void ElidedLabel::setText(const QString &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()
方法为
QTextLine
will draw the line using the coordinate point as the top left corner.
void ElidedLabel::paintEvent(QPaintEvent *event) { QFrame::paintEvent(event); QPainter painter(this); QFontMetrics fontMetrics = painter.fontMetrics(); bool didElide = false; int lineSpacing = fontMetrics.lineSpacing(); int y = 0; QTextLayout textLayout(content, painter.font()); textLayout.beginLayout(); forever { QTextLine line = textLayout.createLine(); if (!line.isValid()) break; line.setLineWidth(width()); int 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()
方法为
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 { QString lastLine = content.mid(line.textStart()); QString 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; emit elisionChanged(didElide); } }
TestWidget
是
QWidget
and is the main window of the example. It contains an
ElidedLabel
which can be resized with two
QSlider
widgets.
class TestWidget : public QWidget { Q_OBJECT public: TestWidget(QWidget *parent = 0); protected: void resizeEvent(QResizeEvent *event) override; private slots: void switchText(); void onWidthChanged(int width); void onHeightChanged(int height); private: int sampleIndex; QStringList textSamples; ElidedLabel *elidedText; QSlider *heightSlider; QSlider *widthSlider; };
The constructor initializes the whole widget. Strings of different length are stored in
textSamples
. The user is able to switch between these.
TestWidget::TestWidget(QWidget *parent): QWidget(parent) { const QString 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." ); const QString macbeth = tr( "To-morrow, and to-morrow, and to-morrow, / " "Creeps in this 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 candle! / " "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." ); const QString 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 = new ElidedLabel(textSamples[sampleIndex], this); elidedText->setFrameStyle(QFrame::Box);
The buttons and the elision label are created. By connecting the
elisionChanged()
信号到
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.
QPushButton *switchButton = new QPushButton(tr("Switch text")); connect(switchButton, SIGNAL(clicked(bool)), this, SLOT(switchText())); QPushButton *exitButton = new QPushButton(tr("Exit")); connect(exitButton, SIGNAL(clicked(bool)), this, SLOT(close())); QLabel *label = new QLabel(tr("Elided")); label->setVisible(elidedText->isElided()); connect(elidedText, SIGNAL(elisionChanged(bool)), label, SLOT(setVisible(bool)));
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 = new QSlider(Qt::Horizontal); widthSlider->setMinimum(0); connect(widthSlider, SIGNAL(valueChanged(int)), this, SLOT(onWidthChanged(int))); heightSlider = new QSlider(Qt::Vertical); heightSlider->setInvertedAppearance(true); heightSlider->setMinimum(0); connect(heightSlider, SIGNAL(valueChanged(int)), this, SLOT(onHeightChanged(int)));
The components are all stored in a
QGridLayout
, which is made the layout of the
TestWidget
.
QGridLayout *layout = new 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.
void TestWidget::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) int maxWidth = widthSlider->width(); widthSlider->setMaximum(maxWidth); widthSlider->setValue(maxWidth / 2); int 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.
void TestWidget::switchText() { 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.
main()
函数
The
main()
function creates an instance of
TestWidget
fullscreen and enters the message loop.
int main( int argc, char *argv[] ) { QApplication application( argc, argv ); TestWidget w; w.showFullScreen(); return application.exec(); }
文件: