通过网格捕捉移动 QGraphicsItemGroup 时遇到问题

问题描述 投票:0回答:1

我在使用鼠标在 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
python qgraphicsscene pyqt6 qgraphicsitemgroup
1个回答
0
投票

注意:最初的问题是误导性的,导致人们相信任何下/右运动都是不可能的;实际上,问题在于无法将项目移动到超出初始位置的顶部/左侧。

问题主要是由对 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()

请注意,该示例没有考虑单击原点,因此其结果可能并不完全直观(结果根据鼠标按下事件的原点而变化)。考虑这个方面以及场景大小和最终的缩放。为了供将来参考,请考虑这些方面,因为创建一个大场景可能会在调试和用户体验方面产生问题。

© www.soinside.com 2019 - 2024. All rights reserved.