在 QGraphicsScene 中移动带有碰撞的项目

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

我试图在具有碰撞检测的 QGraphicsView 中移动多个或单个选定项目(一个矩形),以便它们不会相互移动,而是捕捉到任一端而不阻止另一个轴上的移动,这里是代码:

import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class Circle(QGraphicsRectItem):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.orgpos = None
        self.lastpos = None

class Scene(QGraphicsScene):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        circ = Circle(0,0,100,50)
        circ.setPos(500,500)
        self.addItem(circ)
        self.selected = [circ]
        circ2 = Circle(0,0,100,50)
        circ2.setPos(0,100)
        self.addItem(circ2)

    def mousePressEvent(self, event):
        self.offset = event.scenePos()
        for x in self.selected:
            x.orgpos = x.pos()

    def mouseMoveEvent(self, event):
        for item in self.selected:
            item.setPos(event.scenePos()-self.offset+item.orgpos)
            if len(item.collidingItems(Qt.IntersectsItemShape))>0:
                for x in self.selected:
                    x.setPos(x.lastpos)
                break
            item.lastpos = item.pos()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    scene = Scene(0, 0, 1000, 800)
    view = QGraphicsView(scene, renderHints=QPainter.Antialiasing)
    view.show()
    sys.exit(app.exec_())

但是,正如您所看到的,可拖动的项目(下一个)不会在不碰撞的轴上移动,只是简单地停止,而且如果用户拖动得足够快,它就会在碰撞对象附近停止。有什么办法可以解决这两个问题吗?如有任何帮助,我们将不胜感激。

代码还使用项目列表并对所有项目应用相同的移动,这样,如果一个项目发生碰撞并且选择了许多项目,它们都不会移动并保持在一起。

python pyqt pyqt5 qgraphicsview qgraphicsscene
1个回答
0
投票

您的问题与碰撞检测问题有关,该问题因其复杂性而广为人知。

即使在处理正交对象(矩形,没有旋转)时,实现相交算法也很困难,特别是在处理运动和期望一致性时。

限制项目在封闭的正交空间(凸空间)内的移动相对容易,因为我们只需要根据其边界矩形来阻止坐标:例如,如果顶部边缘更新垂直位置到顶部限制高于它,或者如果底部边缘低于该高度,则为底部限制减去可用高度。

基于其他凸物体限制运动会产生非凸限制,这意味着我们不能仅仅基于左/右/上/下的简单限制来“调整”坐标。我们需要定义一个可能的“邻近”实现,并决定避免碰撞的“最近”坐标如何满足我们的需求。

如果限制基于两个以上的对象,则会进一步增加复杂性:我们显然无法检查每个可能的坐标,我们必须找到一种给出“更好”结果的算法,而且问题还在于找出“更好”的程度决定了。

上面仅考虑了正交对象(正交对齐的矩形,显然包括正方形),但是当使用其他形状和旋转时,问题的复杂性会上升(请参阅密切相关的打包问题)。

有很多方法可以实现所要求的。其中一些问题可以通过外部库来解决,并且已经在 StackOverflow 上进行了讨论(请参阅 thisthis 优秀帖子)。
尽管如此,请考虑一下“运动”的概念实际上是一个抽象概念,尤其是在数字领域:每个“运动”实际上都是位置的绝对变化,无论需要多少“时间”;鼠标移动实际上只是一个新位置。 由于我们正在处理简单的矩形,这可能会有点简单,“暴力”方法可能是合理的。

这个概念相对简单:根据当前项目位置,检查新的鼠标位置是否导致交叉。如果没有,则“运动”被接受。如果不是,则根据感知到的移动考虑合理数量的可能可接受的位置,并选择最接近新鼠标位置的位置。


这种方法显然是次优的,因为它不是很有效:它只是“扩大”可能位置的范围,直到找到不相交的形状(请参阅维基百科关于碰撞检测的文章中的各种阶段方法,以获得更高效的结果)实现这一目标的方法)。

不过,考虑到简单的情况,这可能是可以接受的。

在下面的示例中,我更改了 OP 实现,以便遵循正确的 QGraphicsItem 实现(包括项目选择)。它工作得

几乎

很好:有一些罕见的情况是由快速鼠标移动引起的“错误捕捉”:我会考虑进一步挖掘这个问题,但一旦用户将鼠标在屏幕上移动足够远,它就会以相对直观的方式工作。对轴。 from random import randrange from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * class RectItem(QGraphicsRectItem): def __init__(self, x, y, w, h): super().__init__(0, 0, w, h) self.setPos(x, y) self.setFlag(self.ItemIsSelectable) self.setPen(QColor.fromHsv( randrange(360), randrange(127, 255), randrange(63, 191) )) class Scene(QGraphicsScene): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for i in range(4): self.addItem(RectItem(i * 100, i * 100, 100, 50)) def checkCollision(self, event): if not self.selected or not self.others: return downPos = event.buttonDownScenePos(Qt.LeftButton) scenePos = event.scenePos() downDelta = scenePos - downPos if self.fixedPaths.intersects(self.movingPaths.translated(downDelta)): # local function references created for simplicity checkFunc = self.fixedPaths.intersects transFunc = self.movingPaths.translated deltas = [self.lastLegal] if downDelta.x(): # horizontally "expand" the possible deltas until no # intersection is found tempLeft = QPointF(downDelta) tempRight = QPointF(downDelta) deltas.extend((tempLeft, tempRight)) while checkFunc(transFunc(tempLeft)): tempLeft.setX(tempLeft.x() - 1) while checkFunc(transFunc(tempRight)): tempRight.setX(tempRight.x() + 1) if downDelta.y(): # as above, for vertical "expand" tempUp = QPointF(downDelta) tempDown = QPointF(downDelta) deltas.extend((tempUp, tempDown)) while checkFunc(transFunc(tempUp)): tempUp.setY(tempUp.y() + 1) while checkFunc(transFunc(tempDown)): tempDown.setY(tempDown.y() - 1) # find the closest "legal" point deltas.sort(key=lambda p: QLineF(downDelta, p).length()) self.lastLegal = downDelta = deltas[0] # using start positions allows for x/y "reset" based on the start # mouse position in case geometries are adjusted for item, startPos in self.startPositions.items(): item.setPos(startPos + downDelta) def mousePressEvent(self, event): super().mousePressEvent(event) if event.button() == Qt.LeftButton: # initialize variables that may be used frequently self.startPositions = {} self.lastLegal = QPointF() self.selected = self.selectedItems() self.others = [] if self.selected: # create paths for future collision detection self.movingPaths = QPainterPath() self.fixedPaths = QPainterPath() for item in self.items(): if item in self.selected: self.startPositions[item] = item.pos() self.movingPaths.addPath( item.shape().translated(item.pos())) else: self.others.append(item) self.fixedPaths.addPath( item.shape().translated(item.pos())) def mouseMoveEvent(self, event): super().mouseMoveEvent(event) if event.buttons() == Qt.LeftButton: self.checkCollision(event) if __name__ == '__main__': import sys app = QApplication(sys.argv) scene = Scene(0, 0, 800, 450) view = QGraphicsView(scene, renderHints=QPainter.Antialiasing) view.show() sys.exit(app.exec_())

最后,请注意,上述内容严格基于项目几何形状始终基于整数的假设。如果视图缩放或项目几何形状使用非整数几何形状,结果可能会出乎意料。

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