The Qt Graphics View Framework provides a rich set of resources to create applications with interactive graphics scenes that display arbitrary shapes, text and even Qt widgets. While the framework provides almost everything you need regarding interactivity, scene hierarchy management and graphics display, allowing you to concentrate on your application-specific code, some things require doing some extra work.
One of those things is setting the text alignment on QGraphicsTextItem items. Even though the class doesn’t provide a setAlignment() method, you have access to the underlying QTextDocument and QTextCursor objects that manage all the text processing and layout for the item. However, simply setting the text alignment on those objects also doesn’t get the job done.
The text in a QGraphicsTextItem is only displayed with any set alignment if the text width for the item is set. The text width determines the maximum width the text displayed by the item might have. If the text is actually larger than that width, automatic line breaks are inserted so that the text doesn’t extend past it. The text width property is also used by the framework to calculate the text position when the alignment is set to something other than left.
So how can the text width of a QGraphicsTextItem be set so that no automatic line breaks are inserted by the framework, and so that the text alignment can be set and be made to work? The answer is quite simple: just set it to the width of the item’s bounding rectangle:
item->setTextWidth(item->boundingRect().width()); |
Now, to align the text to the right, for example, you can use the item’s QTextCursor to merge it with a QTextBlockFormat with the alignment set to Qt::AlignRight:
QTextBlockFormat format; format.setAlignment(Qt::AlignRight); QTextCursor cursor = item->textCursor(); cursor.select(QTextCursor::Document); cursor.mergeBlockFormat(format); cursor.clearSelection(); item->setTextCursor(cursor); |
Notice that before setting the alignment, the entire text under the cursor i.e. the text in the QGraphicsTextItem object must be selected, and aftwerwards the selection should be cleared so that the text is not displayed as selected. Failing to select all the text might result in having only part of the text aligned in the desired way.
Here’s a screenshot of a QGraphicsView displaying a scene with a right-aligned QGraphicsTextItem:
So that was easy. Now let’s improve this.
The preceding approach is limited if you are not setting the QGraphicsTextItem’s text and alignment only once in your application. It requires you to set the text width and the text alignment every time the text content changes.
In the remainder of this post I will demonstrate how to create a subclass of QGraphicsTextItem with a setAlignment() method for setting the text alignment and that automatically updates the text width of the item whenever a change in its contents occurs. Also, this subclass will have the interesting characteristic of growing or shriking to the left when the text is right-aligned, thus anchoring it to its top-right corner, which might make sense in some applications.
Let’s start out by the class definition. We must use the Q_OBJECT macro since QGraphicsTextItem is a subclass of QObject (QGraphicsTextItem -> QGraphicsObject -> QObject) and we’re going to define a slot on the new class.
#include <QGraphicsTextItem> class TextItem : public QGraphicsTextItem { Q_OBJECT public: enum { Type = UserType + 1 }; TextItem(QGraphicsItem* parent = 0); TextItem(const QString& text, QGraphicsItem* parent = 0); void setAlignment(Qt::Alignment alignment); virtual int type() const; public slots: void updateGeometry(int, int, int); void updateGeometry(); private: void init(); }; |
The constructors are quite simple: they just initialize the base object and the alignment_ attribute and invoke the private method init() to do the rest of the initialization:
TextItem::TextItem(QGraphicsItem* parent) : QGraphicsTextItem(parent), alignment_(Qt::AlignLeft) { init(); } TextItem::TextItem(const QString& text, QGraphicsItem* parent) : QGraphicsTextItem(text, parent), alignment_(Qt::AlignLeft) { init(); } |
The init() method is responsible for properly initializing the object. It calls updateGeometry() (which will soon be explained) to set the initial item’s text width. It also connects the QTextDocument::contentsChange() signal to the updateGeometry() slot so that the text width is recomputed after any change to the text, thus preventing automatic line breaks:
void TextItem::init() { updateGeometry(); connect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(updateGeometry(int, int, int))); } |
Note that the updateGeometry() slot has two different signatures: one which receives no arguments and one which receives the three int arguments from the QTextDocument::contentsChange() signal. As will be shown later, the one with three int arguments just calls the one that doesn’t take any arguments. QTextDocument has another signal called contentsChanged() that carries no arguments, but unfortunatelt is cannot be used here. This will be explained after the code to updateGeometry() is presented.
The setAlignment() method contains the code for setting the text alignment that was shown at the beginning of this post. It also stores the last set alignment in the alignment_ attribute so that the object “remembers” its last alignment:
void TextItem::setAlignment(Qt::Alignment alignment) { alignment_ = alignment; QTextBlockFormat format; format.setAlignment(alignment); QTextCursor cursor = textCursor(); cursor.select(QTextCursor::Document); cursor.mergeBlockFormat(format); cursor.clearSelection(); setTextCursor(cursor); } |
The updateGeometry() method is responsible for setting the item’s text width to a size that removes any automatic line breaks that might have been inserted after a change to the item’s contents:
void TextItem::updateGeometry(int, int, int) { updateGeometry(); } void TextItem::updateGeometry() { setTextWidth(-1); setTextWidth(boundingRect().width()); setAlignment(alignment_); } |
Note that the text width is first set to -1, then to the item’s bounding rectangle’s width. Setting the item’s text width to -1 “resets” the text width, changing the item’s size so that the text is displayed without any automatic line breaks that might have been inserted to the limit imposed by the previous text width. This effectively recomputes the item’s bounding rectangle, allowing its new width to be used again to set the item’s text width.
Also note that setAlignment() is called after the new text width is set. When the text width is set to -1, any alignment that might have been set before is lost, because a text width value of -1 indicates the item has no text width value. As explained before, the text width value is required by the framework to align the text to any position other than left. Since the alignment was lost, setAlignment() is called to restore the item’s last set alignment.
Remember I said that the QTextDocument::contentsChanged() signal could not be used to directly connect it to updateGeometry()? The problem is that QTextDocument::contentsChanged() is not emitted exactly like contentsChange(). The contentsChanged() signal is also emitted when the text alignment is changed, so that would start an infinite recursion (and eventually a stack overflow would happen) in updateGeometry() because of the call to setAlignment(). It is not actually necessary to have two different signatures for updateGeometry(), but since the int arguments from the QTextDocument::contentsChange() signal are ignored, having an updateGeometry() signature which takes no arguments provides a cleaner interface for TextItem.
The Type constant and the type() method are required by the framework if item type information is necessary. That is not really relevant in the present discussion, but here’s the code for type():
int TextItem::type() const { return Type; } |
Obviously, Type might be set to any other value allowed by Qt.
What about making the item grow or shrink to the left when the text is right-aligned? This requires a slight modification to updateGeometry(). The trick is to save the item’s top right position before recomputing it’s text width and then displacing its position by the difference between the new top right position and the saved one:
void TextItem::updateGeometry() { QPointF topRightPrev = boundingRect().topRight(); setTextWidth(-1); setTextWidth(boundingRect().width()); setAlignment(alignment_); QPointF topRight = boundingRect().topRight(); if (alignment_ & Qt::AlignRight) { setPos(pos() + (topRightPrev - topRight)); } } |
Another change is necessary for TextItem to work properly. If it is editable i.e. its text interaction flags are set to something like Qt::TextEditorInteraction, the cursor position must be saved and restored whenever the alignment is set:
void TextItem::setAlignment(Qt::Alignment alignment) { alignment_ = alignment; QTextBlockFormat format; format.setAlignment(alignment); QTextCursor cursor = textCursor(); // save cursor position int position = textCursor().position(); cursor.select(QTextCursor::Document); cursor.mergeBlockFormat(format); cursor.clearSelection(); cursor.setPosition(position); // restore cursor position setTextCursor(cursor); } |
This is necessary because the cursor position is moved to the end of the text when the cursor selects all the text in the item. Failing to save and restore the cursor position completely breaks the interaction when the item is editable, since setAlignment() is called on every key stroke (because QTextDocument::contentsChange() is emitted due to the change in content caused by the keystroke).
Unfortunately, I don’t know if there is a way to detect a font change on a QGraphicsTextItem. So TextItem works neatly when the text itself is changed, but if the font is changed to a larger size, automatic line breaks will be inserted by the framework because updateGeometry() will not get called in that case. Of course, you can work around that by calling updateGeometry() manually on a TextItem whenever its font is changed by the application. Another strategy would be to override setFont(), but then polymorphism will not work because setFont() is not virtual. Also, a different method like changeFont() could be added to TextItem that calls setFont() and then updateGeometry(), but that defines an interface different from QGraphicsTextItem. If you know how to make the item update itself when a font change occurs, please leave a comment!