Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moving a QGraphicsProxyWidget with ItemIgnoresTransformations after changing QGraphicsView scale

Tags:

qt5

pyqt5

I have a QGraphicsScene that contains multiple custom QGraphicsItems. Each item contains 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. This is all working well, except when moving a proxy widget when 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 had this issue with Qt5.7.1, and could reproduce it with PyQt5 as it is simpler to reproduce and hack around, please see the snippet below.

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, and notice how it jumps on the first moving of the mouse.

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_())

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 (I think)
  • 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.
  • an explanation on how fix the mouse location given by the proxy to its children widgets (after installing a eventFilter)
  • any other workaround

Thanks

like image 653
onzelin Avatar asked Dec 20 '25 11:12

onzelin


1 Answers

Almost 2 years later I got back to this issue again, and finally 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.

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_())

Hoping this may help someone who's ended up in the same dead end I was :)

like image 76
onzelin Avatar answered Dec 23 '25 14:12

onzelin