我想放大 QGraphicsView,以便鼠标下的场景位置保持在鼠标下。以下代码实现了这一点:
from PySide6.QtCore import QPoint
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QApplication
import math
class MyGraphicsView(QGraphicsView):
def wheelEvent(self, event):
self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse)
self.setResizeAnchor(self.ViewportAnchor.AnchorUnderMouse)
if event.angleDelta().y() > 0:
self.scale(1.1, 1.1)
elif event.angleDelta().y() < 0:
self.scale(1 / 1.1, 1 / 1.1)
self.setTransformationAnchor(self.ViewportAnchor.NoAnchor)
self.setResizeAnchor(self.ViewportAnchor.NoAnchor)
class MyTestScene(QGraphicsScene):
def drawBackground(self, painter, rect, PySide6_QtCore_QRectF=None, PySide6_QtCore_QRect=None):
left = int(math.floor(rect.left()))
top = int(math.floor(rect.top()))
right = int(math.ceil(rect.right()))
bottom = int(math.ceil(rect.bottom()))
first_left = left - (left % 100)
first_top = top - (top % 100)
for x in range(first_left, right + 1, 100):
for y in range(first_top, bottom + 1, 100):
painter.drawEllipse(QPoint(x, y), 2.5, 2.5)
painter.drawText(x, y, f"{x},{y}")
if __name__ == '__main__':
app = QApplication([])
scene = MyTestScene()
scene.setSceneRect(-2000, -2000, 4000, 4000)
view = MyGraphicsView()
view.setScene(scene)
view.setGeometry(100, 100, 900, 600)
view.setVisible(True)
# view.setInteractive(False)
app.exec()
此代码存在以下问题:
- 如果用户在第一次尝试使用滚轮缩放之前单击了视图(并且该视图是“交互式的”),则一切都会按预期工作。
- 如果不是,在第一个滚轮事件中,场景会“跳跃”(除了正确缩放之外,还会平移),以便 (0, 0) 位于鼠标下方。此后,一切都按预期运行。
- 如果视图设置为“非交互式”,场景就会“跳跃”,使得每次滚轮事件发生时 (0, 0) 都位于鼠标下方。
有人能解释一下这种行为吗?我遗漏了什么吗?或者这是 Qt 中的一个错误?
我尝试在 python 3.12.4 下使用 PySide 6.7.2,并在 python 3.13.1 下使用 PySide 6.8.1(均在 Windows 上),结果相同。
这只是一个部分的、间接的“错误”,是由您的特定方法引起的。
setTransformationAnchor()
使用 时,和都会setResizeAnchor()
自动启用mouseTracking
视口AnchorUnderMouse
,这是必需的,以便让视图始终跟踪最后已知的鼠标位置,以便正确缩放/调整大小。请注意,再次更改锚点不会禁用鼠标跟踪。由于您仅在 内启用鼠标的调整大小/变换锚点
wheelEvent()
,因此尚未存储“最后已知的鼠标位置”。单击后,它会起作用,因为您恰好在单击的同一点滚动滚轮,但如果您单击某个点,然后将鼠标移到其他位置并第一次滚动滚轮,您仍然会得到不一致的行为,因为锚点被放置在最后已知位置(您单击的位置)。只要您在第一次滚动后移动鼠标(锚点第一次改变),它就会按预期工作。
简单的解决方法是默认启用鼠标跟踪,但请记住,它必须在视口上完成,而不是在图形视图上完成,因为 Qt 滚动区域的所有输入事件始终在视口上接收,然后“重新路由”到相关的事件处理程序。
不幸的是,如果视图不是交互式的,这还不够,因为在这种情况下没有跟踪鼠标位置。
为了解决这个问题,解决方案是临时设置交互模式,根据当前位置发送虚假鼠标移动事件,然后取消设置模式。这种方法也可以处理鼠标跟踪(但我们不必恢复它,因为设置锚点无论如何都会覆盖它,如上所述)。
请注意,通常最好使用,但如果您在视图上实现时没有调用超类的函数,
QApplication.sendEvent()
它将再次失效。在这种情况下,您可以考虑用 替换该行。mouseMoveEvent()
super().mouseMoveEvent(ev)
无论如何,调用超级函数
mouseMoveEvent()
总是必要的,因为鼠标位置最终只存储在 QGraphicsView 鼠标移动处理程序的原始实现中。另一种方法
除了上面提到的实施要求之外,考虑到改变锚点的影响以及这里未提及的其他方面,我建议根据 Qt 的实际操作(以及它需要了解先前鼠标位置的原因)采用更直接的方法。
QGraphicsView 在缩放和考虑鼠标位置时实际执行的操作是:
centerOn()
通过将上述偏移量添加到初始位置,使视图居中(使用);因此,我们可以轻松地实现我们自己的“缩放到鼠标”功能:
结果基本相同,但实现更为精确。
请注意,添加的 QPointF 偏移量
oldPos
是为了考虑到鼠标位置是基于整数的事实,这显然会导致左/上偏移,尤其是在放大时。添加一个0.5
偏移量乘以当前变换矩阵(在进一步缩放之前)应该会导致所需中心位置的更精确的偏移量。更准确的方法应该考虑两种不同缩放比例之间的关系,但我将把这个留给读者最终去实现。