我在使用鼠标在 QGraphicsScene 周围移动 QGraphicsItemGroup 的子类时遇到问题。该场景有一个网格,我希望该组在移动时捕捉到该网格。为了实现这一点,我在子类中重新实现了 itemChange 函数,如下所示:
from os import environ
environ["QT_ENABLE_HIGHDPI_SCALING"] = "0"
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
app = QApplication([])
class GridSnappingQGIG(QGraphicsItemGroup):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges, True)
def boundingRect(self):
return self.childrenBoundingRect()
def itemChange(self, change, value):
scene = self.scene()
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and scene:
scene = self.scene()
x, y = scene.snap_to_grid(value.x(), value.y(), scene.grid_size)
bounding_rect = self.boundingRect()
x, y = min(8192 - bounding_rect.width(), max(0, x)), min(8192 - bounding_rect.height(), max(0, y))
return QPointF(x, y)
return super().itemChange(change, value)
class Scene(QGraphicsScene):
def __init__(self, *args):
super().__init__(*args)
self.grid_size = 32
def snap_to_grid(self, x, y, grid_size):
return [grid_size * round(x / grid_size), grid_size * round(y / grid_size)]
scene = Scene(0, 0, 8192, 8192)
rect1 = QGraphicsRectItem(0, 0, 32, 32)
scene.addItem(rect1)
rect1.setPos(4000,4000)
rect2 = QGraphicsRectItem(0, 0, 128, 256)
scene.addItem(rect2)
rect2.setPos(3680,3680)
group = GridSnappingQGIG()
scene.addItem(group)
group.addToGroup(rect1)
group.addToGroup(rect2)
view = QGraphicsView(scene)
view.showMaximized()
app.exec()
snap_to_grid 是我的场景类中的一个函数,它查找 scene.grid_size x scene.grid_size 网格上距离输入点 (x, y) 最近的点。整个场景为 8192x8192 像素,因此最小/最大操作可确保项目不会移出场景。
调用 scene.addToItem(ItemGroup) 后,我可以移动该组,但只能向右或向下移动,不能向上或向左移动。如果boundingRectangle的左上角是(0, 0)(在场景坐标中),这就是我期望的行为。我实现了虚拟boundingRect()函数来返回childrenBoundingRect(),但这并没有解决问题。我想也许我需要在将组添加到场景后调整位置。我尝试通过在边界矩形的左上角调用 mapToScene 然后调用 setPos(x, y) 来获取场景坐标中组的左上角 (x, y),但随后该组被推入当我试图移动它时,它就向右下方向移动。
我尝试注释掉 itemChange 函数,在这种情况下,移动的行为符合预期(当然,没有网格捕捉)。网格捕捉实现的行为也符合自定义 QGraphicsItems 子类的预期。
一些系统信息:
OS Name: Microsoft Windows 10 Home
OS Version: 10.0.19045 N/A Build 19045
Python 3.10.2
PyQt6 6.4.2
pyqt6-plugins 6.4.2.2.3
PyQt6-Qt6 6.4.3
PyQt6-sip 13.5.2
pyqt6-tools 6.4.2.3.3
qt6-applications 6.4.3.2.3
qt6-tools 6.4.3.1.3
注意:最初的问题是误导性的,导致人们相信任何下/右运动都是不可能的;实际上,问题在于无法将项目移动到超出其初始位置的顶部/左侧。
问题主要是由对 QGraphicsItems 的常见误解引起的:项目的 position 并不总是与其 contents 匹配。正如 QGraphicsScene 文档经常重申的那样,项目位置always初始化为
(0, 0)
,这是相对于其父级的(如果没有设置父级,则可能是场景)。
例如,
scene.addRect(50, 50, 100, 100)
将在(50, 50)
处创建一个100x100的正方形shown,但QGraphicsRectItemposition仍将是
(0, 0)
。
考虑以下因素:
rect1 = scene.addRect(50, 50, 100, 100)
rect2 = scene.addRect(0, 0, 100, 100)
rect2.setPos(50, 50)
以上将产生两个“视觉上”相同的正方形。但他们的位置并不匹配。 QGraphicsItemGroup 也会发生同样的情况。
当您创建一个组时,其原始位置将是上面提到的
(0, 0)
,并且其项目仍将位于其原始
pos()
。当项目添加到组中时,它们的位置实际上没有改变,根据其场景坐标并与组的父项相关来保留原始位置(是的,我知道,这不是那么直观,但这是有道理的)。这意味着,如果您想将该组的项目限制在场景区域内,则不能使用组位置作为参考:您需要检查translated边界矩形是否在场景限制内,并最终调整位置基于受边界限制的翻译差异。 不幸的是,这带来了由相对几何形状引起的许多其他方面:我
假设您的项目始终具有尊重捕捉尺寸的几何形状,但如果情况并非如此,则实现可能会变得更加复杂。问题来自于这样一个事实:您必须决定用于捕捉的参考几何体:假设 grid_size
为 32,如果某个项目位于 15(x 或 y)并且其组被移动,会发生什么?
boundingRect()
始终是场景从根本上用于绘图目的的内容,其中包括绘图所需的一切:如果您创建一个基本的
(0, 0, 10, 10)
矩形,其边界矩形实际上将是 (-0.5, -0.5, 11, 11)
,因为它包括笔宽度。但是,在您的情况下,您实际上需要考虑矩形的实际几何形状,因此使用该组的 boundingRect()
(或其 childrenBoundingRect()
)会不一致。如您所见,虽然并非不可能,但项目几何管理可能会变得相当复杂,除非您了解它们的行为。所有这些都显示了图形视图框架的强大功能和复杂性。
不过,如果您有一个独立的系统,可以完全控制上述方面,则有相对简单的解决方案。
在下面的示例中,我使用较小的场景和项目尺寸,并使用一个同时考虑场景“捕捉”及其边界的函数。
class GridSnappingQGIG(QGraphicsItemGroup):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.setFlags(
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
def itemChange(self, change, value):
scene = self.scene()
if scene and change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
# create the "real bounding rect" based on geometries of child
# items, using their actual coordinates
childrenRect = QRectF()
for child in self.childItems():
if isinstance(child, (QGraphicsRectItem, QGraphicsEllipseItem)):
childRect = child.rect()
elif isinstance(child, QGraphicsPathItem):
childRect = child.path().boundingRect()
elif isinstance(child, QGraphicsLineItem):
childRect = QRectF(child.line().p1(), child.line().p2())
else:
# further implementation required for other item types, in
# order to avoid issues based on pen widths, margins, etc.
childRect = child.boundingRect()
# adjust the children rect with the child position translated
# to the *current* position (not the new one!)
childrenRect |= childRect.translated(child.pos() + self.pos())
# get the new possible geometry, based on the position difference
adjustedRect = scene.adjusted_grid_rect(
childrenRect.translated(value - self.pos()))
diff = adjustedRect.topLeft() - childrenRect.topLeft()
# return the difference between the computed moved rectangle and the
# current one, allowing proper "snapping"
return self.pos() + diff
return super().itemChange(change, value)
class Scene(QGraphicsScene):
grid_size = 32
def adjusted_grid_rect(self, rect):
x = round(rect.x() / self.grid_size) * self.grid_size
y = round(rect.y() / self.grid_size) * self.grid_size
newRect = QRectF(x, y, rect.width(), rect.height())
sceneRect = self.sceneRect()
if newRect.right() > sceneRect.right():
newRect.moveRight(sceneRect.right())
if newRect.x() < sceneRect.x():
newRect.moveLeft(sceneRect.x())
if newRect.bottom() > sceneRect.bottom():
newRect.moveBottom(sceneRect.bottom())
if newRect.y() < sceneRect.y():
newRect.moveTop(sceneRect.y())
return newRect
def drawBackground(self, qp, rect):
# for testing purposes, draw the snap grid
gs = self.grid_size
sceneRect = self.sceneRect()
qp.fillRect(rect & sceneRect, QColor(127, 127, 127, 63))
qp.setPen(Qt.GlobalColor.lightGray)
left, top, width, height = (rect & sceneRect).getRect()
right = left + width
bottom = top + height
x = left // gs * gs
if x == sceneRect.x():
x += gs
while x < right:
qp.drawLine(QLineF(x, top, x, bottom))
x += gs
y = top // gs * gs
if y == sceneRect.y():
y += gs
while y < bottom:
qp.drawLine(QLineF(left, y, right, y))
y += gs
app = QApplication([])
scene = Scene(0, 0, 640, 480)
rect1 = QGraphicsRectItem(0, 0, 32, 32)
scene.addItem(rect1)
rect1.setPos(128, 128)
rect2 = QGraphicsRectItem(0, 0, 64, 128)
scene.addItem(rect2)
rect2.setPos(256, 256)
group = GridSnappingQGIG()
scene.addItem(group)
group.addToGroup(rect1)
group.addToGroup(rect2)
view = QGraphicsView(scene)
view.setRenderHint(QPainter.RenderHint.Antialiasing)
view.resize(700, 700)
view.show()
QTimer.singleShot(10, lambda:
view.fitInView(scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio))
app.exec()
请注意,该示例没有考虑单击原点,因此其结果可能并不完全直观(结果根据鼠标按下事件的原点而变化)。考虑这个方面以及场景大小和最终的缩放。为了供将来参考,请考虑这些方面,因为创建一个大场景可能会在调试和用户体验方面产生问题。