Maintaining relative child position after applying QGraphicsItem::ItemIgnoresTransformations - qt

I have a QGraphicsTextItem parented to a QGraphicsItem. I want the QGraphicsTextItem to always reside directly above the QGraphicsItem, but I also want the text to remain the same size when the scale factor goes below 1, i.e. the text remains the size it is at a scale factor of 1 even when the parent graphics item is scaled smaller. I have found that setting the QGraphicsItem::ItemIgnoresTransformations flag to true when the scale factor is below 1 does the trick for retaining the size.
But I can’t seem to find a way to get the position of the text to always remain above the QGraphicsItem. Is there a way to do this? I tried using deviceTransform () function, but the text still moved off of the QGraphicsItem as I scrolled out. What was worse is that some of the text items started “jiggling”, i.e. they started continuously changing their position ever so slightly, so that it looked like they were shaking. If this is the function I need to use, I guess I don’t know how to use it properly.
In the constructor of my QGraphicsItem I’ve added a QGraphicsTextItem:
fTextItem = new QGraphicsTextItem(getName(), this);
fTextItem->setFlag(QGraphicsItem::ItemIgnoresTransformations);
Here is code snippet from paint function of QGraphicsItem
qreal lod = painter->worldTransform().m22();
if(lod <= 1.0) {
fTextItem-setFlag(QGraphicsItem::ItemIgnoresTransformations);
fTextItem->setPos(fTextItem->deviceTransform(view-viewportTransform()).inverted().map(view->mapFromScene(mapToScene(0,0))));
} else {
fTextItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, false);
fTextItem->setPos(0, 0);
}

My suggestion is to subclass QGraphicsSimpleTextItem in this manner:
class TextItem
: public QGraphicsSimpleTextItem
{
public:
TextItem(const QString &text)
: QGraphicsSimpleTextItem(text)
{
}
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->translate(boundingRect().topLeft());
QGraphicsSimpleTextItem::paint(painter, option, widget);
painter->translate(-boundingRect().topLeft());
}
QRectF boundingRect() const
{
QRectF b = QGraphicsSimpleTextItem::boundingRect();
return QRectF(b.x()-b.width()/2.0, b.y()-b.height()/2.0,
b.width(), b.height());
}
};
QGraphicsSimpleTextItem *mText = new TextItem("Item");
scene()->addItem(mText);
mText->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
mText->setPos(itemToFollow->pos());

Disclaimer: this may be overkill for what you are trying to do. We had some additional restrictions in our project that made this solution the easiest for us.
We had to do something similar in a project, and it ended up being easiest for us to not use ItemIgnoresTransformations and instead roll our own transform. Here is the main function we use to create a translation-only (no scaling) transform for drawing an item at a specific location. You might be able to modify it for your usage.
static QTransform GenerateTranslationOnlyTransform(
const QTransform &original_transform,
const QPointF &target_point) {
// To draw the unscaled icons, we desire a transform with scaling factors
// of 1 and shearing factors of 0 and the appropriate translation such that
// our icon center ends up at the same point. According to the
// documentation, QTransform transforms a point in the plane to another
// point using the following formulas:
// x' = m11*x + m21*y + dx
// y' = m22*y + m12*x + dy
//
// For our new transform, m11 and m22 (scaling) are 1, and m21 and m12
// (shearing) are 0. Since we want x' and y' to be the same, we have the
// following equations:
// m11*x + m21*y + dx = x + dx[new]
// m22*y + m12*x + dy = y + dy[new]
//
// Thus,
// dx[new] = m11*x - x + m21*y + dx
// dy[new] = m22*y - y + m12*x + dy
qreal dx = original_transform.m11() * target_point.x()
- target_point.x()
+ original_transform.m21() * target_point.y()
+ original_transform.m31();
qreal dy = original_transform.m22() * target_point.y()
- target_point.y()
+ original_transform.m12() * target_point.x()
+ original_transform.m32();
return QTransform::fromTranslate(dx, dy);
}
To use, take the QPainter transform that is passed to the paint method and do something like:
painter->save();
painter->setTransform(GenerateTranslationOnlyTransform(painter->transform(),
some_point));
// Draw your item.
painter->restore();

I've found another solution, which does not involve messing with any transformations or by hand scaling/positioning. There is a hint in QGraphicsItem::ItemIgnoresTransformations flag description:
QGraphicsItem::ItemIgnoresTransformations
The item ignores inherited transformations (i.e., its position is
still anchored to its parent, but the parent or view rotation, zoom or
shear transformations are ignored). [...]
And that's the key! We need two items: a parent that will keep the relative position (without any flags set) and a child item that will do the drawing at parent's (0,0) point (with QGraphicsItem::ItemIgnoresTransformations flag set). Simple as that!
I've encapsulated this functionality into a single class - here is some code:
#include <QGraphicsItem>
#include <QPainter>
class SampleShape : public QGraphicsItem
{
private:
/* This class implements shape drawing */
class SampleShapeImpl : public QGraphicsItem
{
public:
SampleShapeImpl (qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_len(len)
{
/* ignore transformations (!) */
setFlag(QGraphicsItem::ItemIgnoresTransformations);
}
QRectF boundingRect (void) const override
{
/* sample bounding rectangle */
return QRectF(-m_len, -m_len, m_len*2, m_len*2);
}
void paint (QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
/* draw a shape, (0,0) is an anchor */
painter->drawLine(0, -m_len, 0, m_len);
painter->drawLine(-m_len, 0, m_len, 0);
// ...
}
private:
qreal m_len; // sample shape parameter
};
public:
/* This is actually almost an empty class, you only need to set
* a position and pass any parameters to a SampleShapeImpl class.
*/
SampleShape (qreal x, qreal y, qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_impl(len, this) // <-- IMPORTANT!!!
{
/* set position at (x, y), view transformations will apply */
setPos(x, y);
}
QRectF boundingRect (void) const override
{
return QRectF(); // it's just a point, no size
}
void paint (QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override
{
// empty, drawing is done in SampleShapeImpl
}
private:
SampleShapeImpl m_impl;
};

Great answer by Dave Mateer! I had the problem that I wanted to define a different scale factor at different zoom levels. This is how I did it:
void MyGraphicsItem::paint(QPainter * painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
//save painter for later operations
painter->save();
QTransform originalTransform = painter->transform();
QPointF originalCenter = rect().center();
qreal dx = originalTransform.m11() * originalCenter.x() + originalTransform.m21() * originalCenter.y() + originalTransform.m31();
qreal dy = originalTransform.m22() * originalCenter.y() + originalTransform.m12() * originalCenter.x() + originalTransform.m32();
//normally our target scale factor is 1, meaning the item has keeps its size, regardless of zoom
//we adjust the scale factor though when the item is smaller than one pixel in comparison to the background image
qreal factor = 1.0;
//check if scale factor if bigger that the item size, and thus it occupies less that a pixel in comparision to the background image
if (rect().width() < originalTransform.m11()) {
//calculate adjusted scale factor
factor = originalTransform.m11() / rect().width();
}
//adjust position according to scale factor
dx -= factor * originalCenter.x();
dy -= factor * originalCenter.y();
//set the new transform for painting
painter->setTransform(QTransform::fromScale(factor, factor) * QTransform::fromTranslate(dx, dy));
//now paint...
QGraphicsXYZItem::paint(painter, option, widget);
//restore original painter
painter->restore();
}
You do need to adjust the bounding rectangle too in that case:
QRectF MyGraphicsItem::boundingRect() const
{
QRectF rect = QGraphicsEllipseItem::boundingRect();
//this is a bit hackish, let me know if you know another way...
if (scene() != NULL && scene()->views().at(0) != NULL)
{
//get viewport transform
QTransform itemTransform = scene()->views().at(0)->transform();
QPointF originalCenter = rect.center();
//calculate back-projected original size of item
qreal realSizeX = rect.width() / itemTransform.m11();
qreal realSizeY = rect.height() / itemTransform.m11();
//check if scale factor is bigger that the item size, and thus it occupies less that a pixel in comparison
//to the background image and adjust size back to equivalent of 1 pixel
realSizeX = realSizeX < 1.0 ? 1.0 : realSizeX;
realSizeY = realSizeY < 1.0 ? 1.0 : realSizeY;
//set adjusted position and size according to scale factor
rect = QRectF(rect.center().x() - realSizeX / 2.0, rect.center().y() - realSizeY / 2.0, realSizeX, realSizeY);
}
return rect;
}
With this solution the item work very well in my case.

Adding to Dave Mateer's answer, I think it'd be helpful to add that in some scenario, you should also maintain proper bounding rectangle (as well as shape) of the object. For me, I need to modify boundingRect() a little too for proper object selection behavior. Remember that the bounding rect of the object will be scaled and transformed as usual if we do NOT use ItemIgnoresTransformations flag. So we also need to rescale the boundingRect to maintain the view independence effect.
To maintain the view-independent bounding rectangle turns out to be quite easy: just grab the scaling factor from deviceTransform(m_view->viewportTransform()).inverted().m11() and multiply this constant to your local coordinate bounding rectangle. For example:
qreal m = this->deviceTransform(m_view->viewportTransform()).inverted().m11();
return QRectF(m*(m_shapeX), m*(m_shapeY),
m*(m_shapeR), m*(m_shapeR));

here is a solution I devised of very moderate complexity :
1) Get the boundingRect() of the parent and map it to scene
2) take the minimum X and Y of this list of points, this is the real origin of your item, in scene coordinates
3) set the position of the child
In Pyside :
br = parent.mapToScene(parent.boundingRect())
realX = min([item.x() for item in br])
realY = min([item.y() for item in br])
child.setPos(parent.mapFromScene(realX, realY)) #modify according to need

Related

How to size the texture to occupy only a portion of a QQuickItem UI

I have overriden updatePaintNode in the following way to draw an OpenGL texture on a QQuickItem derived class called MyQQuickItem here.
QSGNode *MyQQuickItem::updatePaintNode(QSGNode * oldNode, QQuickItem::UpdatePaintNodeData * /*updatePaintNodeData*/)
{
QSGSimpleTextureNode * textureNode = static_cast<QSGSimpleTextureNode *>(oldNode);
if (!textureNode) {
textureNode = new QSGSimpleTextureNode();
}
QSize size(800, 800);
// myTextureId is a GLuint here
textureNode.reset(window()->createTextureFromId(myTextureId, size));
textureNode->setTexture(my_texture);
textureNode->markDirty(QSGBasicGeometryNode::DirtyMaterial);
QSizeF myiewport = boundingRect().size();
qreal xOffset = 0;
qreal yOffset = 10;
textureNode->setRect(xOffset, yOffset, myViewport.width(), myViewport.height());
return textureNode;
}
This renders the texture content well but covers the whole of my MyQQuickItem UI.
How can reduce the bottom margin of the texture to say fit 80% of the height of MyQQuickItem.
I want to render the texture to a portion of MyQQuickItem & leave the rest blank or black? Is that possible within updatePaintNode.
Note that the texture size is not the UI window size here. My texture size is 800 by 800. Whereas the UI window size is different and depends on the screen.
I found the answer to this:
Changing myViewport.height() gives the required end in Y direction one wishes to set. Similarly, changing myViewport.width() gives the required end in X direction one wishes to set.
4 parameters in TextureNode's setRect can stretch & fit the texture in the way one wishes within a portion of the QQuickItem.

How to scale the contents of a QGraphicsView using the QPinchGesture?

I'm implementing an image viewer on an embedded platform. The hardware is a sort of tablet and has a touch screen as input device. The Qt version I'm using is 5.4.3.
The QGraphicsView is used to display a QGraphicsScene which contains a QGraphicsPixmapItem. The QGraphicsPixmapItem containts the pixmap to display.
The relevant part of the code is the following:
void MyGraphicsView::pinchTriggered(QPinchGesture *gesture)
{
QPinchGesture::ChangeFlags changeFlags = gesture->changeFlags();
if (changeFlags & QPinchGesture::ScaleFactorChanged) {
currentStepScaleFactor = gesture->totalScaleFactor();
}
if (gesture->state() == Qt::GestureFinished) {
scaleFactor *= currentStepScaleFactor;
currentStepScaleFactor = 1;
return;
}
// Compute the scale factor based on the current pinch level
qreal sxy = scaleFactor * currentStepScaleFactor;
// Get the pointer to the currently displayed picture
QList<QGraphicsItem *> listOfItems = items();
QGraphicsItem* item = listOfItems.at(0);
// Scale the picture
item.setScale(sxy);
// Adapt the scene to the scaled picture
setSceneRect(scene()->itemsBoundingRect());
}
As result of the pinch, the pixmap is scaled starting from the top-left corner of the view.
How to scale the pixmap respect to the center of the QPinchGesture?
From The Docs
The item is scaled around its transform origin point, which by default is (0, 0). You can select a different transformation origin by calling setTransformOriginPoint().
That function takes in a QPoint so you would need to find out your centre point first then set the origin point.
void QGraphicsItem::setTransformOriginPoint(const QPointF & origin)

Resizing and rotating a QGraphicsItem results in odd shape

I can't understand how scaling and rotation are applied to QGraphicsItem.
I need to be able to apply rotation and scaling (not necessarily keeping aspect ratio) and I get fully unexpected results.
Rotation must be around the item center. I seem to have no problem doing that - yet if I try to debug the bounding rectangle, I get seemingly wrong values.
If I don't keep aspect ratio, instead of rotation I get a very weird skew, and I have been struggling for quite a while to find the cause and correct it. I hope anybody can find a solution.
For many items - like rectangles - my solution was to give up on resize - and just replace the item with a new one of given size. I even did that for pixmap (though it probably will affect performance a lot).
But I don't know how to do that for text, or a few other types (svg...).
I am trying to understand how scaling is applied, on rotated items, and how to get it applied correctly.
The code below is an experiment where I scale and rotate a text item, and the result... see attached image
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsTextItem>
void experimentScaling(QGraphicsScene* s)
{
QGraphicsTextItem* ref = new QGraphicsTextItem(); // a reference, not resized
ref->setPlainText("hello world");
s->addItem(ref);
ref->setDefaultTextColor(Qt::red);
ref->setRotation(45);
QGraphicsTextItem* t = new QGraphicsTextItem(); // text item to be experimented on
t->setPlainText("hello world");
s->addItem(t);
QTransform transform; // scale
transform.scale(10, 1);
t->setTransform(transform);
t->update();
QPointF _center = t->boundingRect().center();
qDebug("%f %f %f %f", t->boundingRect().left(), t->boundingRect().top(), t->boundingRect().right(), t->boundingRect().bottom()); // seems to be unscaled...
t->setTransformOriginPoint(_center); // rotation must be around item center - and seems to work even though the bounding rect gives wrong values above
t->setRotation(45); // skewed
t->update();
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsScene s;
QGraphicsView view(&s);
s.setSceneRect(-20, -20, 800, 600);
view.show();
experimentScaling(&s);
return app.exec();
}
Reference (red) text rotated 45 degrees, text rotated 45 degrees and resized 10,1:
The resized (black) text should have the same height as the reference (red) - yet is much taller;
The bounding rectangle is no longer a rectangle - it is skewed;
The angle looks much smaller than 45;
Added a resized but not rotated reference as well:
Please help me understand why this behavior is happening and what can I do about it.
I have tried looking into QGraphicsRotation but I can't figure out how to apply it... All I get is a move instead of rotation.
As documented, the item's transformations are mathematically applied in a certain order - this is the order you'd be multiplying the transform matrices in and is, conceptually, the reverse of the order you'd normally think of.
The transform is applied. The origin point must be included in the transform itself, by applying translations during the transform.
The transformations are applied - each of them can specify its own center.
rotation then scale are applied, both relative to transformOriginPoint.
When you set transform to scaling, and set rotation, the rotation is performed before scaling. The scaling applies to the rotated result - it simply stretches the rotated version horizontally in your case.
You need to somehow enforce the reverse order of operations. The only two ways to do that are:
Stack the transforms in correct order and pass them to transform, or.
Pass a list of correct transformations to transformations.
I'll demonstrate how to do it either way, in an interactive fashion where you can adjust the transform parameters using sliders.
To obtain the correct result using transform:
QGraphicsItem * item = ....;
QTransform t;
QPointF xlate = item->boundingRect().center();
t.translate(xlate.x(), xlate.y());
t.rotate(angle);
t.scale(xScale, yScale);
t.translate(-xlate.x(), -xlate.y());
item->setTransform(t);
To obtain the correct result using transformations:
QGraphicsItem * item = ....;
QGraphicsRotation rot;
QGraphicsScale scale;
auto center = item->boundingRect().center();
rot.setOrigin(QVector3D(center));
scale.setOrigin(QVector3D(center()));
item->setTransformations(QList<QGraphicsTransform*>() << &rot << &scale);
Finally, the example:
// https://github.com/KubaO/stackoverflown/tree/master/questions/graphics-transform-32186798
#include <QtWidgets>
struct Controller {
public:
QSlider angle, xScale, yScale;
Controller(QGridLayout & grid, int col) {
angle.setRange(-180, 180);
xScale.setRange(1, 10);
yScale.setRange(1, 10);
grid.addWidget(&angle, 0, col + 0);
grid.addWidget(&xScale, 0, col + 1);
grid.addWidget(&yScale, 0, col + 2);
}
template <typename F> void connect(F && f) { connect(f, f, std::forward<F>(f)); }
template <typename Fa, typename Fx, typename Fy> void connect(Fa && a, Fx && x, Fy && y) {
QObject::connect(&angle, &QSlider::valueChanged, std::forward<Fa>(a));
QObject::connect(&xScale, &QSlider::valueChanged, std::forward<Fx>(x));
QObject::connect(&yScale, &QSlider::valueChanged, std::forward<Fy>(y));
}
QTransform xform(QPointF xlate) {
QTransform t;
t.translate(xlate.x(), xlate.y());
t.rotate(angle.value());
t.scale(xScale.value(), yScale.value());
t.translate(-xlate.x(), -xlate.y());
return t;
}
};
int main(int argc, char **argv)
{
auto text = QStringLiteral("Hello, World!");
QApplication app(argc, argv);
QGraphicsScene scene;
QWidget w;
QGridLayout layout(&w);
QGraphicsView view(&scene);
Controller left(layout, 0), right(layout, 4);
layout.addWidget(&view, 0, 3);
auto ref = new QGraphicsTextItem(text); // a reference, not resized
ref->setDefaultTextColor(Qt::red);
ref->setTransformOriginPoint(ref->boundingRect().center());
ref->setRotation(45);
scene.addItem(ref);
auto leftItem = new QGraphicsTextItem(text); // controlled from the left
leftItem->setDefaultTextColor(Qt::green);
scene.addItem(leftItem);
auto rightItem = new QGraphicsTextItem(text); // controlled from the right
rightItem->setDefaultTextColor(Qt::blue);
scene.addItem(rightItem);
QGraphicsRotation rot;
QGraphicsScale scale;
rightItem->setTransformations(QList<QGraphicsTransform*>() << &rot << &scale);
rot.setOrigin(QVector3D(rightItem->boundingRect().center()));
scale.setOrigin(QVector3D(rightItem->boundingRect().center()));
left.connect([leftItem, &left]{ leftItem->setTransform(left.xform(leftItem->boundingRect().center()));});
right.connect([&rot](int a){ rot.setAngle(a); },
[&scale](int s){ scale.setXScale(s); }, [&scale](int s){ scale.setYScale(s); });
right.angle.setValue(45);
right.xScale.setValue(3);
right.yScale.setValue(1);
view.ensureVisible(scene.sceneRect());
w.show();
return app.exec();
}
I was able to make that work by using two separate QTransforms and multiplying them together. Note that the order of transformations matter:
QTransform transform1;
transform1.scale(10, 1);
QTransform transform2;
transform2.rotate(45);
t->setTransform(transform1 * transform2);

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

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));
}
}

How to move a QGraphicsItem when scrolling the QGraphicsView?

I have a QGraphicsScene and a QGraphicsView that displays it. I need to add a QGraphicsItem to the scene and keep it at the same position even when I scroll the view. I tried overriding view's scrollContentsBy() method as follows but it didn't do the trick.
void FETimelineView::scrollContentsBy( int dx, int dy )
{
QGraphicsView::scrollContentsBy(dx, dy);
QRectF oRect = p_CatBar->rect();
p_CatBar->setPos( oRect.x() + dx, oRect.y() + dy );
}
Btw, FETimelineView is my QGraphicsView and p_CatBar is of type QGraphicsItem. Please help, thanks in advance.
Rather than moving it by the scrolled amount, You can get the position that you want it to be relative to the view and then set it directly according to that. So it would be something like this: -
// Assuming that the Graphics Item top left needs to be at 50,50
// and has a width and height of 30,20
void FETimelineView::scrollContentsBy( int dx, int dy )
{
QGraphicsView::scrollContentsBy(dx, dy);
// get the item's view position in scene coordinates
QRect scenePos = mapToScene(QRect(50, 50, 30, 20));
p_CatBar->setPos(scenePos);
}
I think the easier way it's actually the inverse of what you ask: try setting the flag ItemIgnoresTransformations in QGraphicsItem::GraphicsItemFlags.
There are other flags that could help you, see the docs.

Resources