QGraphicsItem::setFlag(ItemIsMovable) is a wonderful feature. With one line of code, an item becomes dragable with the mouse for repositioning. Unfortunately, things get a bit tricky when reparenting during drags.
Let’s say you have two QGraphicsItem called plate1 and plate2, in a subclass called PlateGraphicsItem. Inside plate1 and plate2, you have a few child items, food1, food2, food3, food4, etc, in a subclass called FoodGraphicsItem. Foods are inside of plates. What if we want to move one piece of food from one plate to another, smoothly dragging it there? Intuitively, we should be able to do this (after remembering to run setFlag(ItemSendsGeometryChanges)):
QVariant FoodGraphicsItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && scene()) { QRectF newRect = mapRectToScene(boundingRect()); newRect.moveTo(parentItem()->mapToScene(value.toPointF())); foreach (QGraphicsItem *item, scene()->items()) { if (item != parentItem() && qgraphicsitem_cast< PlateGraphicsItem* >(item) && item->mapRectToScene(item->boundingRect()).contains(newRect)) { setParentItem(item); break; } } } return QGraphicsItem::itemChange(change, value); } |
As the user drags the food, we see if it’s boundingbox fits inside the bounding box of a different plate. If it does, we switch parents. But this doesn’t happen. As soon as the item is reparented, it leaps across the screen to the same relative position it was in while inside of the old plate. If it was on the bottom-left corner of the old plate, it moves to the bottom-left corner of the new plate. Not what we want. So it seems like we should be able to modify the inside of that loop like this:
QPointF oldPos = scenePos(); setParentItem(item); return QVariant(item->mapFromScene(oldPos)); |
But alas, this does not work either. As soon as you move the mouse again (while still dragging), the item leaps to where it was before the above fix. And remember: this leap puts the item far away from underneath the mouse cursor. So it works initially, but the next time Qt’s MouseMove event handler is called, we’re SOL.
After digging through the Qt source for quite a bit of time, it turns out that Qt keeps track of where the mouse was pressed originally, in the scene coordinates, and uses this to compute the new location relative to the old parent coordinates. Not good. Mixing coordinate systems like this is not okay once the item is reparented. It turns out, we have to simulate a mouse release event to clear Qt’s internal state. But this doesn’t completely do it, because the mouse event still has the scene coordinate of where the button was pressed, which means we also need to intercept and tamper this information before Qt sees it. Essentially what this amounts to is reversing Qt’s coordinate equation, solving for zero, and adding back the location that we want. And this has to happen every time the user moves the mouse, but it should only happen once the item has been reparented. And if the item is reparented back to the original plate after a series of drags, we still have to do this transformation, since we have already cleared Qt’s internal state, which means we need to store the click location once we reparent.
So all and all, this horrible hack amounts to adding this (and making the above modification):
void FoodGraphicsItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { //BIG PHAT UGLY HACK! if (event->buttonDownScenePos(Qt::LeftButton) == m_manualMouseReleaseAt || !parentItem()->contains(parentItem()->mapFromScene(event->buttonDownScenePos(Qt::LeftButton)))) { m_manualMouseReleaseAt = event->buttonDownScenePos(Qt::LeftButton); QGraphicsSceneMouseEvent mouseRelease(QEvent::GraphicsSceneMouseRelease); QGraphicsItem::mouseReleaseEvent(&mouseRelease); event->setButtonDownScenePos(Qt::LeftButton, parentItem()->mapToScene(pos() + transform().map(event->buttonDownPos(Qt::LeftButton)))); } QGraphicsItem::mouseMoveEvent(event); } |
This works, but it’s awful. Really awful. And it could break in future Qt versions. But for now, it works.
So I’m wondering — is this behavior by design, or have I uncovered an odd Qt bug?
Update: It looks like others have encountered the same problem. Well, here’s a work around. But is there a more correct solution?
I’m doing lot of reparenting when moving QGraphicsItems in my project (playgrund/base/plasma/containments/groupingdesktop) and all i need to do is:
QPointF pos = item->scenePos();
item->setParentItem(newParent);
item->setPos(newParent->mapFromScene(pos));
the first thing i wondered is why you aren’t reparenting on mouse release? you can certainly check what plate is under the food item while it is moving, and even have the plate respond visually in some way (or even “reject” the item, e.g. if it was “full”) .. but maybe just reparent when the movement actually stops?
@giucam
This works for updating the position as far as reparenting goes, but the mouse event is still tied to the original position, so the item is moved to the correct place initially, but as soon as you move the mouse again after it’s been reparented, it jumps all over.
@aseigo
Good thinking. But I don’t think I really want to do this. The way my application is setup, I have some bounds checking to ensure that child rectangles are never moved outside their parent until they’re reparented when the drag destination is fully enclosed in the new parent. This makes dragging rectangles from one place to another “stick” at the border between them until the user has deliberately dragged it far enough in.
Put more simply, doing something like that is incompatible with this:
QRectF transformed = transform().mapRect(boundingRect());
if (newPoint.x() < -transformed.x())
newPoint.setX(-transformed.x());
if (newPoint.y() < transformed.y())
newPoint.setY(-transformed.y());
if (newPoint.x() > parentItem()->boundingRect().width() – transformed.width() – transformed.x())
newPoint.setX(parentItem()->boundingRect().width() – transformed.width() – transformed.x());
if (newPoint.y() > parentItem()->boundingRect().height() – transformed.height() – transformed.y())
newPoint.setY(parentItem()->boundingRect().height() – transformed.height() – transformed.y());
return newPoint;
Probably you can simulate a mouse press on reparenting. (and use timer with 0 timeout just to ensure you are doing things in a fresh event loop iteration)
I see this post is old, but I’m experiencing the same problem. As I move around some QGraphicsItems, I want them to “snap” to some other items, and it works visually, but when I try to click on them after moving, it doesn’t work.
Did you ever figure out a nicer solution?