Shift `QGraphicsTextItem` position relative to the center of the text? - qt

I have a number of classes that inherit from QGraphicsItem, that get to be arranged in a certain way. For simplicity of calculations, I made the scenes, and items, centered in (0, 0) (with the boundingRect() having +/- coordinates).
QGraphicsTextItem subclass defies me, its pos() is relative to top left point.
I have tried a number of things to shift it so it centers in the text center (for example, the suggested solution here - the code referenced actually cuts my text and only shows the bottom left quarter).
I imagined that the solution should be something simple, like
void TextItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
painter->translate( -boundingRect().width()/2.0, -boundingRect().height()/2.0 );
QGraphicsTextItem::paint(painter, option, widget );
}
the above "sort of" works - but as I increase the item scale -> increase the font, the displayed item is cut off...
I tried to set the pos() - but the problem is, I still need to track the actual position on the scene, so I cannot just replace it.
A slightly unpleasant side effect - centering the QGraphicsView on the element does not work either.
How can I make my QGraphicsTextItem show its position relative to the center of the text ?
Edit: one of the experiments of changing the boundingRect():
QRectF TextItem::boundingRect() const
{
QRectF rect = QGraphicsTextItem::boundingRect();
rect.translate(QPointF(-rect.width()/2.0, -rect.height()/2.0));
return rect;
}

I had to shift the initial position, as well as the resize, to trigger a new position - I was unable to do it in paint() because, as I thought from the start, any repaint would continuously recalculate the position.
Only the initial position needs to be adjusted - but as the font size (or style...) changes, its bounding rectangle also changes, so the position must be recalculated - based on previous position.
In the constructor,
setPos(- boundingRect().width()/2, - boundingRect().height()/2);
in the function that modifies item (font) size,
void TextItem::setSize(int s)
{
QRectF oldRect = boundingRect();
QFont f;
f.setPointSize(s);
setFont(f);
if(m_scale != s)
{
m_scale = s;
qreal x = pos().x() - boundingRect().width()/2.0 + oldRect.width()/2.0;
qreal y = pos().y() - boundingRect().height()/2.0 + oldRect.height()/2.0;
setPos(QPointF(x, y));
}
}

Related

Set QGraphicsTextItem text contents of exact height and width

I am required to create text items with exact width and height of text contents.
The height of the text is the most important requirement.
The position of the text should be relative to the text itself.
I also have to be able to place it on canvas in an exact spot.
Assuming a (printable) canvas (on a larger QGraphicsScene), say 5 inch width and 1 inch height, my text should be able to stretch top-bottom-left-right - and be placed on canvas, not part in part out.
I am sub-classing QGraphicsTextItem for my item type. I am resizing it, using QTransform(), to required size - in inches or mm or pixels (72*in).
Also setting the document() margin to 0, and anything inside (like QTextBlockFormat margins) also to 0.
I have implemented a setItemSize(QSizeF sz) (with sz in pixels), that resizes the QGraphicsTextItem as required.
The sz is initialized using the item bounding rect.
Assuming no wrap, single line text (multi-line could be solved separately once this issue is resolved).
When adding the item to canvas, I still see a top and bottom margin - and this varies based on font choice.
I drew a rectangle around the item to see it.
The top/bottom distances depend on font choices.
I have tried to use font metrics to determine these distances (in paint() I have been drawing lines to try to determine the position and rectangle in which the text fits).
I would be happy to at least be able to determine correct size to use for upper case, no accents or special characters fonts (it would be a start, though naturally I would need to be able to use any characters).
But at least some way to determine the size and position (relative to the (0,0) of item) of the text content even in the simplest case.....
The font metrics tightBoundingRect() seems the most accurate for size, but it seems impossible to determine its position so that I can somehow create my items correctly, and maybe resize/shift them correctly to fit on canvas.
Here are some examples of my struggle to determine at least exact size and position of text, relative to the (0,0) of the item (assuming that once I do that, I am able to expose that info to outside or include the shift in the item transform on resize).
Notice that the size of the text advertised by font metrics does not always cover the text, and for different fonts I am not able to position the tight bounding rect (magenta) around the text itself. (I did multiple guesses, the code below is just one - the lines are trying to show different font metrics sizes).
The above were experiments in paint function of the text item inheriting QGraphicsTextItem:
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
// draw text
QGraphicsTextItem::paint(painter, option, widget);
QPen p;
p.setWidthF(0);
QFontMetricsF fm(this->font());
qreal ascent = fm.ascent(),
descent = fm.descent(),
hheight = fm.height();
QRectF r = QGraphicsTextItem::boundingRect();
QRectF rFont= fm.tightBoundingRect(toPlainText());
qreal xmax = r.right();
painter->save();
painter->setBrush(Qt::NoBrush);
// where is "ascent + descent"
p.setColor(Qt::green);
painter->setPen(p);
painter->drawLine(QPointF(2, ascent), QPointF(2, ascent + descent));
painter->drawLine(QPointF(2, ascent + descent), QPointF(xmax/2, ascent + descent));
// where is "height"
p.setColor(Qt::red);
painter->setPen(p);
painter->drawLine(QPointF(xmax/2, 0), QPointF(xmax/2, hheight));
painter->drawLine(QPointF(xmax/2, ascent + descent), QPointF(xmax, ascent + descent));
// where is "ascent"
p.setColor(Qt::yellow);
painter->setPen(p);
painter->drawLine(QPointF(6, 0), QPointF(6, ascent));
painter->drawLine(QPointF(6, ascent), QPointF(xmax, ascent));
// something that may look like top of the text
p.setColor(Qt::blue);
painter->setPen(p);
qreal yyy = ascent + rFont.y() + 1;
painter->drawLine(QPointF(5, yyy), QPointF(xmax, yyy));
// this should be useful... should be the natural offset
qreal yoffset = (r.height() - rFont.height()) / 2;
// qDebug() << yoffset << r << rFont;
//qreal y0 = (r.height() - fm.height())/2;
p.setColor(Qt::darkGreen);
painter->drawEllipse(10, yoffset, 1, 1);
// where is the font rect
p.setColor(Qt::magenta);
painter->setPen(p);
yoffset = (r.height() + rFont.height()) / 2;
painter->translate(0, yoffset);
painter->drawRect(rFont);
painter->restore();
}
I have also tried not using QGraphicsTextItem, but paint text inside a rectangle. The same thing happens.
(Qt 4.7 - 5.x)
This is not a good solution. This is an attempt to solve my own problem - in a first iteration - of setting text with given width and height, using font metrics.
Reasons for not being good -
Even after resize, text is smaller than desired, I don't understand why
The position is incorrect, based on font style the text can be above or below the canvas, meaning it gets clipped.
I resize it using a factor calculated from the item bounding rect size and the font metrics bounding rect (I used the tight bounding rect for more accurate size).
myText->setItemFontSize(12); // If I use font metrics I need to reset text size on every change, because resizing loses font info
QFontMetricsF fm(myText->font());
QRectF fmRect = fm.tightBoundingRect(myText.toPlainText().toUpper());
// without toUpper() the size is too small - even so it is a bit small
// I read tightBoundingRect is slow - but boundingRect and height and ascent all give values that result in even smaller size
//qreal absH = fm.ascent();
qreal absH = fmRect.height();
qreal absW = fmRect.width();
qreal absHeightRatio = myText->getItemSize().height() / absH;
qreal absWidthRatio = myText->getItemSize().width() / absW;
Then setting size:
myText->setItemSize(QSizeF(absWidthRatio * textLength, absHeightRatio * fontHeight));
// This function scales the `QTransform` on item
// but since I request a final position dependent on item size
// (including blank space around it) - it has no chance of being accurate.....
// This is where my next effort will go, figuring out how to get rid of the fluff
// around the item inside the scaling
The function for setting position: trying to center text:
// leftShift = 10, rightShift = 10 in example
myText->setPos(0,0);
QRectF r = myText->mapToScene(myText->boundingRect()).boundingRect();
QSizeF sz = r.size();
qreal w = sz.width();
qreal h = sz.height();
qreal cx = (m_docLength - w + leftShift - rightShift)/2 - r.left();
qreal cy = (m_docHeight - h)/2 - r.top();
myText->setPos(cx, cy);
The images below are for fontHeight = m_docHeight -
Desirable:
- either the entire size of text (ascent + descent) equals doc height, and text is centered vertically based on content
- or the upper case size text equals doc height and the descent is below document (text centered based on only upper case) - this would seem easier based on how QGraphicsTextItem seems to position it
Actual:
- the text is smaller no matter which parameters I use to scale, and centered based on upper case text
As shown above - I have no idea how I could center vertically based on content (so for edge-to-edge text the descent would fit in) - and in lieu of that, all I really want is edge-to-edge uppercase letters, but I can't seem able to achieve that.
Oh and these are for Arial type font - one of the best-behaved. Other fonts jump all over the place, either above or below the canvas. And for some fonts, the resulting text is actually smaller - which is inexplicable to me, because how can the tight bounding rectangle be smaller than the item bounding rectangle...
Still this is as far as I got to getting my text as close to "true" size and placed on a canvas that matches its size.

QGraphicsItem leaves artifacts when changing boundingRect

My LineItem inheriting from QGraphicsLineItem can change its pen width.
I have created a boundingRect that uses the QGraphicsLineItem::boundingRect adjusted by pads that get calculated based on pen width and arrows. It works.
void LineItem::calculateStuff() // called on any change including pen width
{
qreal padLeft, padRight, padT;
padLeft = 0.5 * m_pen.width(); // if no arrows
padT = padLeft;
padRight = padLeft;
m_boundingRect = QGraphicsLineItem::boundingRect().adjusted(-padLeft, -padT, padRight, padT);
update();
}
QRectF LineItem::boundingRect() const
{
return m_boundingRect;
}
QPainterPath LineItem::shape() const
{
QPainterPath p;
p.addRect(m_boundingRect);
return p;
}
There is only one artifact that I get:
if I increase the pen width, then decrease it, I get traces:
these of course disappear as soon as i move mouse or any action (I had a hard time getting the screen shots)
As pretty as they are (seriously I consider them a "feature :-) ) - I am trying to eliminate them. I tried to remember previous bounding rectangle, and update the item with the previous bounding rectangle - i thought that was what the option was for - but it didn't work.
QRectF oldRect = selectedItem->boundingRect();
item->setItemPenWidth(p);
selectedItem->update(oldRect);
selectedItem->update();
My viewport has
setViewportUpdateMode(BoundingRectViewportUpdate);
If I change to
setViewportUpdateMode(FullViewportUpdate);
I don't get artifacts - but I think this will impact performance which is a major constraint.
How can I fix these artifacts - that only occur in that specific situation, decreasing pen width / decreasing bounding rect of line, without impacting performance ?
Simple fix... I had to add
prepareGeometryChange();
in my calculateStuff() function.
I have not seen any changes from this before, it is the first time I change my boundingRect that it does not update seamlessly.

How can I wrap text in QGraphicsItem?

1) How can I wrap text in a QGraphicsTextItem to fit a fixed rectangle, with width and height ?
Right now I am experimenting with creating a text, getting its bounding rectangle, and resizing it to fit the box - but I can't get wrapping.
class TTT: public QGraphicsTextItem {
TTT() {
{
setPlainText("abcd");
qreal x = m_itemSize.width()/boundingRect().width();
qreal y = m_itemSize.height()/boundingRect().height();
scale(x, y);
}
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) {
// experiment with clip regions
// text gets covered by hole in clip
QRegion r0(boundingRect().toRect());
QRegion r1(QRect(5, 5, 10, 10), QRegion::Ellipse);
QRegion r2 = r0.subtracted(r1);
painter->setClipRegion(r2);
painter->setBrush(Qt::yellow);
painter->drawRect(boundingRect());
QGraphicsTextItem::paint(painter, option, widget);
}
}
What makes wrapping happen, how can I trigger it ?
Right now, as I keep typing, the box is automatically expanding.
2) Is it possible to wrap the text in a QGraphicsItem / QGraphicTextItem subclass in a shape that is not a rectangle ?
(Something like in the image above)
I tried to use clipRegion, see code above, but I guess it is not the right way to go, clipping cuts the text but did not wrap.
Maybe it would... If I could figure out how to wrap text in the first place ?
Qt 4.8
You did not specify Qt version but try:
void QGraphicsTextItem::setTextWidth(qreal width)
Sets the preferred width for the item's text. If the actual text is wider than >the specified width then it will be broken into multiple lines.
If width is set to -1 then the text will not be broken into multiple lines >unless it is enforced through an explicit line break or a new paragraph.
The default value is -1.
In answer to 1) I'd opt not to use the QGraphicsTextItem, but draw the text directly in your QGraphicsItem's paint function using the drawText overloaded function, which takes a QTextOption parameter.
Using this, you can set the WrapMode, for example, with a call to
QTextOption::setWrapMode(QTextOption:: WordWrap)
As for 2) with a non-rectangular shape, I don't think Qt will do this for you.
Doing it yourself you can use QFontMetrics, to work out just how much text would fit in each line, depending upon where it lies within its bounding item.
Alternatively, you could adapt the concept of a text-to-path method.

Preventing font scale in QGraphicsItem

I am using QGraphicsTextItem to paint the text on the scene. Text is painted along the path (QGraphicsPathItem), wich is parent of my QGraphicsTextItem - so the text rotation is changed to be along the path element and is sticked to it while zooming the view. But the font size of QGraphicsTextItem is also changing while zooming the view - this is what I am trying to avoid. Of I set QGraphicsItem::ItemIgnoresTransformations flag to the QGraphicsTextItem it stops rotating while it's parent (QGraphicsPathItem) does.
I do understand that I have to re-implement QGraphicsTextItem::paint function, but I am stuck with the coordination system. Here is the code (Label class inherits public QGraphicsTextItem):
void Label::paint( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget )
{
// Store current position and rotation
QPointF position = pos();
qreal angle = rotation();
// Store current transformation matrix
QTransform transform = painter->worldTransform();
// Reset painter transformation
painter->setTransform( QTransform() );
// Rotate painter to the stored angle
painter->rotate( angle );
// Draw the text
painter->drawText( mapToScene( position ), toPlainText() );
// Restore transformation matrix
painter->setTransform( transform );
}
The position (and rotation) of my text on the screen is unpredictable :(
What am I doing wrong? Thank you very much in advance.
I solved a problem this way - for drawing a line/circle/rectangle/path, which I want to be transformed, I use an appropriate QGraphicsLine/Ellipse/Rect/PathItem. For drawing the text (which I do NOT want to be transformed) I use QGraphicsSimpleTextItem. I set text's flag to ignore transormations and set it's parent to Line/Ellipse/Rect/Path item. The Line/Ellipse/Rect/Path item transforms, but text does not - this is what I wanted. I can also rotate text and set it's position.
Thank you very much for answers.
The following solution worked perfectly for me:
void MyDerivedQGraphicsItem::paint(QPainter *painter, const StyleOptionGraphicsItem *option, QWidget *widget)
{
double scaleValue = scale()/painter->transform().m11();
painter->save();
painter->scale(scaleValue, scaleValue);
painter->drawText(...);
painter->restore();
...
}
We can also multiply the scaleValue by other mesures we want to keep its size constant outside the save/restore environment.
QPointF ref(500, 500);
QPointF vector = scaleValue * QPointF(100, 100);
painter->drawLine(ref+vector, ref-vector);
I had this issue once. Instead of ignoring transformations, you need to scale down the items you don't want to be zoomed in in your zoom-in function.
When you zoom in, if you change the scale by ds for example, scale the items by 1.0 / ds
You might need to change their positions though.
I hope this helps.
Edit: I hope I understood the question right.

Zooming in/out on a mouser point ?

As seen in the pictures.
I have QWidget inside a QScrollArea.
QWidget act as a render widget for cell image and some vector based contour data.
User can performe zoom in/out and what simply happens is, it changes the QPainters scale and change the size of QWidget size accordinly.
Now I want to perform the zooming in/out on the point under the mouse. (like zooming action in GIMP).
How to calculate the new positions of the scrollbars according to the zoom level ?
Is it better to implement this using transformations without using a scrollarea?
One solution could be to derive a new class from QScrollArea and reimplementing wheelEvent for example so that zooming is performed with the mouse wheel and at the current mouse cursor position.
This method works by adjusting scroll bar positions accordingly to reflect the new zoom level. This means as long as there is no visible scroll bar, zooming does not take place under mouse cursor position. This is the behavior of most image viewer applications.
void wheelEvent(QWheelEvent* e) {
double OldScale = ... // Get old scale factor
double NewScale = ... // Set new scale, use QWheelEvent...
QPointF ScrollbarPos = QPointF(horizontalScrollBar()->value(), verticalScrollBar()->value());
QPointF DeltaToPos = e->posF() / OldScale - widget()->pos() / OldScale;
QPointF Delta = DeltaToPos * NewScale - DeltaToPos * OldScale;
widget()->resize(/* Resize according to new scale factor */);
horizontalScrollBar()->setValue(ScrollbarPos.x() + Delta.x());
verticalScrollBar()->setValue(ScrollbarPos.y() + Delta.y());
}
Will void QScrollArea::ensureVisible(int x, int y, int xmargin = 50, int ymargin = 50) do what you need?
You need to pick up the wheelEvent() on the QWidget, get the event.pos() and pass it into the QscrollArea.ensureVisible(), right after scaling your QWidget.
def wheelEvent(self, event):
self.setFixedSize(newWidth, newHeight)
self.parent().ensureVisible(event.pos())
That should more or less produce what you want.

Resources