Interact with volatile graphics items

I hacked together some code today for interacting with a QGraphicsItem while its transformation changes.

The problem? QGraphicsItem's events are always delivered in local coordinates, and operations in response to such events that change item's transformation (scale it, or move it, or shear it) will leave its local coordinates intact, but might change its relative coordinates (position on scene compared to position in item, etc). For example, if you click on an item to "grab" and rotate or scale it, the next mouse move event you get will typically be... at the exact same position as the first one! Well that's because you rotated the item in response to the first mouse move, visually and effectively keeping the button-down position for the item under the cursor (relative to the item) intact after the rotation.

So how can you find the correct delta movement when the local mouse position keeps... staying the same?

Here's how to do it. This screenshot below shows an example of rotation and zooming for an item in response to the item's own local mouse events. You click on a corner of the item and drag the mouse to both zoom and rotate it.

The neat trick is to use the button-down scene position that comes with the event. That is, Graphics View stores the original mouse position in scene coordinates for when each button was pressed. You can at any time map that point back to the item's local coordinate system. That gives you the original button-down position in the item's new local coordinates, after the transformation. Apply some trigonometry, and there you go! :-)

void InteractiveItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
if (!grabbed)
return QGraphicsItem::mouseMoveEvent(event);

// Map the original button-down position back to local coordinates.
QPointF buttonDownPos = mapFromScene(event->buttonDownScenePos(Qt::LeftButton));

// Determine the "old" and "new" angles to the item's (1, 0) vector.
qreal oldAngle = (180 * angleForPos(buttonDownPos)) / pi;
qreal newAngle = (180 * angleForPos(event->pos())) / pi;

// Determine the item's new rotation and scaling
newRotation = rotation + oldAngle - newAngle;
newScale = scale * distanceToPoint(event->pos()) / distanceToPoint(buttonDownPos);

// Apply the new transformation
setMatrix(QMatrix().rotate(newRotation).scale(newScale, newScale));

The example stores the item's last known local scale and rotation, but it might as well have calculated that when the mouse was pressed. Oh, and you can get all the helper functions in the source code below.

The example also shows how to do animated fade effects, using QTimeLine. Good for the organic interfaces that Aaron's talking about?

You can download the sources here.

Blog Topics: