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:

    1. move the widget around, notice nothing unusual
    2. use the mouse wheel to zoom in or out. The higher the absolute scale, the higher the effect on the issue.
    3. 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 :)


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.