6
votes

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:

reference 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:

enter image description here

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.

2
Is that transform.scale(10, 1) correct? Shouldn't it be transform.scale(10, 10) ?juzzlin
@juzzlin: I don't want to keep aspect ratio. I am trying to stretch the text in one direction or bothThalia

2 Answers

9
votes

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.

  1. The transform is applied. The origin point must be included in the transform itself, by applying translations during the transform.
  2. The transformations are applied - each of them can specify its own center.
  3. 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:

  1. Stack the transforms in correct order and pass them to transform, or.

  2. 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:

screenshot

// 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();
}
2
votes

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