我试图在具有碰撞检测的 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_())
但是,正如您所看到的,可拖动的项目(下一个)不会在不碰撞的轴上移动,只是简单地停止,而且如果用户拖动得足够快,它就会在碰撞对象附近停止。有什么办法可以解决这两个问题吗?如有任何帮助,我们将不胜感激。
代码还使用项目列表并对所有项目应用相同的移动,这样,如果一个项目发生碰撞并且选择了许多项目,它们都不会移动并保持在一起。
您的问题与碰撞检测问题有关,该问题因其复杂性而广为人知。
即使在处理正交对象(矩形,没有旋转)时,实现相交算法也很困难,特别是在处理运动和期望一致性时。
限制项目在封闭的正交空间(凸空间)内的移动相对容易,因为我们只需要根据其边界矩形来阻止坐标:例如,如果顶部边缘更新垂直位置到顶部限制高于它,或者如果底部边缘低于该高度,则为底部限制减去可用高度。
基于其他凸物体限制运动会产生非凸限制,这意味着我们不能仅仅基于左/右/上/下的简单限制来“调整”坐标。我们需要定义一个可能的“邻近”实现,并决定避免碰撞的“最近”坐标如何满足我们的需求。
如果限制基于两个以上的对象,则会进一步增加复杂性:我们显然无法检查每个可能的坐标,我们必须找到一种给出“更好”结果的算法,而且问题还在于找出“更好”的程度决定了。
上面仅考虑了正交对象(正交对齐的矩形,显然包括正方形),但是当使用其他形状和旋转时,问题的复杂性会上升(请参阅密切相关的打包问题)。
有很多方法可以实现所要求的。其中一些问题可以通过外部库来解决,并且已经在 StackOverflow 上进行了讨论(请参阅 this 和 this 优秀帖子)。
尽管如此,请考虑一下“运动”的概念实际上是一个抽象概念,尤其是在数字领域:每个“运动”实际上都是位置的绝对变化,无论需要多少“时间”;鼠标移动实际上只是一个新位置。
由于我们正在处理简单的矩形,这可能会有点简单,“暴力”方法可能是合理的。
这种方法显然是次优的,因为它不是很有效:它只是“扩大”可能位置的范围,直到找到不相交的形状(请参阅维基百科关于碰撞检测的文章中的各种阶段方法,以获得更高效的结果)实现这一目标的方法)。
不过,考虑到简单的情况,这可能是可以接受的。
在下面的示例中,我更改了 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_())
最后,请注意,上述内容严格基于项目几何形状始终基于整数的假设。如果视图缩放或项目几何形状使用非整数几何形状,结果可能会出乎意料。