我有一个QGraphicsScene,它包含多个自定义QGraphicsItems。每个项都包含一个QGraphicsProxyWidget,它本身包含业务逻辑所需的任何小部件。代理应用了一个Qt::Window标志,这样它就有了一个标题栏来移动它。这一切都很好,除了在视图缩放后移动代理小部件时。
用户可以像谷歌地图一样在场景中移动,即缩小然后再放大到更远的地方。这是通过调用QGraphicsView::scale来完成的。无论缩放值如何,项目都应该始终可见,因此它们设置了QGraphicsItem::ItemIgnoresTransformations标志。
当视图被缩放时移动proxyWidget时,会发生的情况是,在第一次移动事件中,小部件将在正确拖动之前跳到某个位置。
我在Qt5.7.1中遇到了这个问题,可以用PyQt5复制它,因为它更容易复制和破解,请参阅下面的片段。
复制步骤:
- 移动小部件,没有发现任何异常
- 使用鼠标滚轮放大或缩小。绝对规模越大,对问题的影响就越大
- 单击小部件,并注意到它是如何在第一次移动鼠标时跳转的
代码段:
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_())
跳跃距离为:
- 与视图的缩放比例以及鼠标与场景原点的距离成比例
- 从场景位置(0,0)到鼠标位置(我认为)
- 可能是由于代理小部件未正确报告鼠标按下/移动所致。在查看了qgraphicProxy Widget.cpp(示例源)中的QGraphicProxy WidgetPrivate::mapToReceiver之后,我得到了这个诊断的提示,该程序似乎没有考虑场景缩放
我正在寻找
- 确认这是Qt的问题,我没有错误配置代理
- 关于如何修复代理给其子窗口小部件的鼠标位置的说明(在安装eventFilter之后)
- 任何其他解决方法
感谢
大约两年后,我再次回到这个问题上,并最终找到了解决方案。或者更确切地说是一种变通方法,但至少是一种简单的方法。事实证明,我可以很容易地避免陷入本地/场景/忽略变换的问题。
我没有将QGraphicsProxyWidget作为QGraphicsWidget的子级,也没有明确地将QWidget设置为代理目标,而是直接从QGraphicsScene获取代理,让它在包装器上设置窗口标志,并在代理上设置ItemIgnoresTransformations标志。然后(这里是解决方法)我在代理上安装一个事件过滤器,截取GraphicsSceneMouseMove事件,在该事件中我强制代理位置为currentPos+mouseDelta(均在场景坐标中)。
以下是上面的代码示例,使用该解决方案进行了修补:
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_())
希望这能帮助那些最终陷入同一条死胡同的人:)