Dragging a QGraphicsWidget with ItemIgnoresTransformations flag, after the QGraphicsView has been scaled (zoomed)
-
Hi all,
TLDR: jump to steps to reproduce.
My QGraphicsScene contains multiple custom QGraphicsItems. Some of those items contain a QGraphicsProxyWidget which itself contains whatever widgets are needed by the business logic. The proxy has a Qt::Window flag applied to it, so that it has a title bar to move it around the view. This is all working well, except when moving a proxy widget after the view has been scaled.
The user can move around the scene à la google maps, ie by zooming out then zooming in back a little farther away. This is done with calls to QGraphicsView::scale. Items should always be visible no matter the zoom value, so they have the QGraphicsItem::ItemIgnoresTransformations flag set.
What happens when moving a proxyWidget while the view has been scaled is that on the first move event the widget will jump to some location before properly being dragged. I wan not able to figure out how Qt gets to that first move's location and why it makes the widget do such a "jump".
I have had this issue since Qt5.7.1 (currenlty using 5.11.1) but I can't postpone anymore. I could reproduce the problem with PyQt5 as it is simpler to reproduce and hack around. Please see the snippet below, but note that the issue is with both c++ and python, so Qt itself, not the bindings.
Steps to reproduce:
- move the widget around, notice nothing unusual
- use the mouse wheel to zoom in or out. The higher the absolute scale, the higher the effect on the issue.
- click on the widget in order to move it around, and notice how it jumps on the first moving of the mouse.
I expect the widget to be dragged seamlessly, as when no scaling is applied to the scene.
Snippet:
import sys import PyQt5 from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsProxyWidget, QGraphicsWidget, QGraphicsObject global view global scaleLabel def scaleScene(event): delta = 1.0015**event.angleDelta().y() view.scale(delta, delta) scaleLabel.setPlainText("scale: %.2f"%view.transform().m11()) view.update() if __name__ == '__main__': app = QApplication(sys.argv) # create main widget w = QWidget() w.resize(800, 600) layout = QVBoxLayout() w.setLayout(layout) w.setWindowTitle('Example') w.show() # rescale view on mouse wheel, notice how when view.transform().m11() is not 1, # dragging the subwindow is not smooth on the first mouse move event w.wheelEvent = scaleScene # create scene and view scene = QGraphicsScene() scaleLabel = scene.addText("scale: 1") view = QGraphicsView(scene) layout.addWidget(view) view.show(); # create item in which the proxy lives item = QGraphicsWidget() scene.addItem(item) item.setFlag(PyQt5.QtWidgets.QGraphicsItem.ItemIgnoresTransformations) item.setAcceptHoverEvents(True) # create proxy with window and dummy content proxy = QGraphicsProxyWidget(item, Qt.Window) button = QPushButton('dummy') proxy.setWidget(button) # start app sys.exit(app.exec_())
It seems like the jump distance is:
- proportional to the scaling of the view , and to the distance of the mouse from the scene origin
- goes from scene position (0,0) towards the mouse position
- might be caused by the proxy widget not reporting the mouse press/move properly. I'm hinted at this diagnostic after looking at QGraphicsProxyWidgetPrivate::mapToReceiver in qgraphicsproxywidget.cpp (sample source), which does not seem to take scene scaling into account.
I am looking for either
- confirmation that this is an issue with Qt and I did not misconfigured the proxy or anything else. I don't want to report a non-issue to Qt devs.
- any hint on how to fix the mouse location given by the proxy to its children widgets (after installing a eventFilter for instance)
Thanks :)
edit: typos -
Good news everyone!
I found a solution. Or rather a workaround, but a simple one at least. It turns out I can easily avoid getting into the issue with local/scene/ignored transforms in the first place. I'm kind of ashamed I did not think about this earlier, but hey at least I can move forward with my project now. Here's the fix.Instead of parenting the QGraphicsProxyWidget to a QGraphicsWidget, and explicitly setting the QWidget as proxy target, I get the proxy directly from the QGraphicsScene, letting it set the window flag on the wrapper, and set the ItemIgnoresTransformations flag on the proxy. Then (and here's the workaround) I install an event filter on the proxy, intercept the GraphicsSceneMouseMove event where I force the proxy position to currentPos+mouseDelta (both in scene coordinates).
Here's the code sample from above, patched with that solution:
import sys import PyQt5 from PyQt5.QtCore import Qt from PyQt5.QtWidgets import * global view global scaleLabel def scaleScene(event): delta = 1.0015**event.angleDelta().y() view.scale(delta, delta) scaleLabel.setPlainText("scale: %.2f"%view.transform().m11()) view.update() class ItemFilter(PyQt5.QtWidgets.QGraphicsItem): def __init__(self, target): super(ItemFilter, self).__init__() self.target = target def boundingRect(self): return self.target.boundingRect() def paint(self, *args, **kwargs): pass def sceneEventFilter(self, watched, event): if watched != self.target: return False if event.type() == PyQt5.QtCore.QEvent.GraphicsSceneMouseMove: self.target.setPos(self.target.pos()+event.scenePos()-event.lastScenePos()) event.setAccepted(True) return True return super(ItemFilter, self).sceneEventFilter(watched, event) if __name__ == '__main__': app = QApplication(sys.argv) # create main widget w = QWidget() w.resize(800, 600) layout = QVBoxLayout() w.setLayout(layout) w.setWindowTitle('Example') w.show() # rescale view on mouse wheel, notice how when view.transform().m11() is not 1, # dragging the subwindow is not smooth on the first mouse move event w.wheelEvent = scaleScene # create scene and view scene = QGraphicsScene() scaleLabel = scene.addText("scale: 1") view = QGraphicsView(scene) layout.addWidget(view) view.show(); button = QPushButton('dummy') proxy = scene.addWidget(button, Qt.Window) proxy.setFlag(PyQt5.QtWidgets.QGraphicsItem.ItemIgnoresTransformations) itemFilter = ItemFilter(proxy) scene.addItem(itemFilter) proxy.installSceneEventFilter(itemFilter) # start app sys.exit(app.exec_())
It solves the isue just as wekll in c++.
Hoping this may help someone who's ended up in the same dead end I was :)