禁止可移动的QGraphics物品与其他物品发生冲突

Prohibit collision of a movable QGraphicsItem with other items

本文关键字:冲突 其他 QGraphics 可移动 禁止      更新时间:2023-10-16

Final Edit

我想出了问题所在,并在下面发布了一个解决方案作为答案。如果你从谷歌偶然来到这里,寻找一种体面的方式来通过ItemIsMovable标志移动QGraphicsItems/QGraphicsObjects,同时避免与其他节点发生冲突,我在答案的最后包含一个工作itemChange方法。

我最初的项目处理将节点捕捉到任意网格,但这很容易删除,并且根本不需要此解决方案工作。效率方面,它不是数据结构或算法最聪明的使用,所以如果你最终在屏幕上有很多节点,你可能想尝试一些更智能的东西。


原始问题

我有一个子类化的QGraphicsItem,Node,它为ItemIsMovableItemSendsGeometryChanges设置标志。这些 Node 对象具有重写的shape()boundingRect()函数,这些函数为它们提供了一个可以移动的基本矩形。

使用鼠标移动节点会按预期调用itemChange()函数,我可以调用一个强制移动节点始终与网格对齐的snapPoint()函数。(这是一个简单的函数,它基于给定的参数返回一个新的 QPointF,其中新点被限制为实际捕捉网格上的最近坐标)。这就像现在一样完美地工作。

我最大的问题是我想避免捕捉到与其他节点碰撞的位置。也就是说,如果我将节点移动到结果(对齐网格)位置,我希望collidingItems()列表完全为空。

我可以准确地检测到ItemPositionHasChanged标志被抛入itemChange()后的collidingItems,但不幸的是,这是在节点已经移动之后。更重要的是,我发现这个冲突列表在节点从初始itemChange()返回之前没有正确更新(这很糟糕,因为节点已经被移动到与其他一些节点重叠的无效位置)。

这是我到目前为止获得的itemChange()函数:

QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value)
{
if ( change == ItemPositionChange && scene() )
{
// Snap the value to the grid
QPointF pt = value.toPointF();
QPointF snapped = snapPoint(pt);
// How much we've moved by
qreal delX = snapped.x() - pos().x();
qreal delY = snapped.y() - pos().y();
// We didn't move enough to justify a new snapped position
// Visually, this results in no changes and no collision testing
// is required, so we can quit early
if ( delX == 0 && delY == 0 )
return snapped;
// If we're here, then we know the Node's position has actually
// updated, and we know exactly how far it's moved based on delX
// and delY (which is constrained to the snap grid dimensions)
// for example: delX may be -16 and delY may be 0 if we've moved 
// left on a grid of 16px size squares
// Ideally, this is the location where I'd like to check for
// collisions before returning the snapped point. If I can detect
// if there are any nodes colliding with this one now, I can avoid
// moving it at all and avoid the ItemPositionHasChanged checks
// below
return snapped;
}
else if ( change == ItemPositionHasChanged && scene())
{
// This gets fired after the Node actually gets moved (once return
// snapped gets called above)
// The collidingItems list is now validly set and can be compared
// but unfortunately its too late, as we've already moved into a
// potentially offending position that overlaps other Nodes
if ( collidingItems().empty() )
{
// This is okay
}
else
{
// This is bad! This is what we wanted to avoid
}
}
return QGraphicsItem::itemChange(change, value);
}

我尝试过什么

我尝试存储delXdelY更改,如果我们进入上面的最后一个 else 块,我尝试用moveBy(-lastDelX, -lastDelY)来反转之前的移动。这产生了一些可怕的结果,因为当鼠标仍然被按下并移动节点本身时,它会被调用(最终在尝试绕过碰撞时多次移动节点,通常导致节点飞离屏幕一侧非常快)。

我还尝试存储以前的(已知的有效)点,并在位置上调用 setX 和 setY 以在发现碰撞时恢复,但这也有与上述方法类似的问题。

结论

如果我能在移动发生之前找到一种方法来准确检测collidingItems(),我将能够完全避免ItemPositionHasChanged块,确保ItemPositionChange只返回有效的点,从而避免任何移动到先前有效位置的逻辑。

感谢您抽出宝贵时间,如果您需要更多代码或示例来更好地说明问题,请告诉我。

编辑 1:

我想我偶然发现了一种可能构成实际解决方案基础的技术,但我仍然需要一些帮助,因为它并不完全有效。

在最后一个return snapped;之前的itemChange()函数中,我添加了以下代码:

// Make a copy of the shape and translate it correctly
// This should be equivalent to the shape formed after a successful
// move (i.e. after return snapped;)
QPainterPath path = shape();
path.translate(delX, delY);
// Examine all the other items to see if they collide with the desired
// shape....
for (QGraphicsItem* other : canvas->items() )
{
if ( other == this ) // don't care about this node, obviously
continue;
if ( other->collidesWithPath(path) )
{
// We should hopefully only reach this if another
// item collides with the desired path. If that happens,
// we don't want to move this node at all. Unfortunately,
// we always seem to enter this block even when the nodes
// shouldn't actually collide.
qDebug() << "Found collision?";
return pos();
}
}
// Should only reach this if no collisions found, so we're okay to move
return snapped;

。但这也行不通。任何新节点总是进入collidesWithPath块,即使它们彼此不靠近。上面代码中的 canvas 对象只是保存所有这些 Node 对象的 QGraphicsView,以防万一也不清楚。

任何想法如何使collidesWithPath正常工作?复制shape()的路径是错误的吗?我不确定这里出了什么问题,因为在移动完成后,两种形状的比较似乎工作正常(即我可以在之后准确地检测到碰撞)。如果有帮助,我可以上传整个代码。

我想通了。我确实在shape()呼叫修改方面走在正确的轨道上。我没有针对完整形状对象本身(这是一个路径对象,其碰撞测试可能更复杂且效率低下)进行实际测试,而是基于此线程编写了一个额外的函数来比较两个QRectF(这本质上是形状调用返回的内容):

bool rectsCollide(QRectF a, QRectF b)
{
qreal ax1, ax2, ay1, ay2;
a.getCoords(&ax1, &ay1, &ax2, &ay2);
qreal bx1, bx2, by1, by2;
b.getCoords(&bx1, &by1, &bx2, &by2);
return ( ax1 < bx2 &&
ax2 > bx1 &&
ay1 < by2 &&
ay2 > by1 );
}

然后,在与原始帖子编辑相同的区域中:

QRectF myTranslatedRect = getShapeRect();
myTranslatedRect.translate(delX, delY);
for (QGraphicsItem* other : canvas->items() )
{
if ( other == this )
continue;
Node* n = (Node*)other;
if ( rectsCollide( myTranslatedRect, n->getShapeRect() ) )
return pos();
}
return snapped;

这适用于一个小的辅助函数getShapeRect()它基本上只返回覆盖shape()函数中使用的相同矩形,但以QRectF形式而不是QPainterPath

。这个解决方案似乎效果很好。节点可以自由移动(同时仍受捕捉网格约束),除非它可能与另一个节点重叠。在这种情况下,不会显示任何移动。

编辑 1:

我花了一些时间研究这个问题以获得稍微好一点的结果,我想我会分享,因为我觉得在碰撞中移动节点可能是很常见的事情。在上面的代码中,将节点"拖动"到与当前拖动的节点碰撞的长节点/节点集会产生一些不良影响;也就是说,由于我们只是默认在任何碰撞时返回pos(),我们根本不移动 - 即使有可能在X或Y方向上成功移动。这发生在对碰撞体的对角线拖动上(有点难以解释,但如果你正在使用上面的代码构建一个类似的项目,当你尝试将一个节点与另一个节点一起拖动时,你可以看到它的实际效果)。我们可以通过一些细微的更改轻松检测到这种情况:

我们可以有两个单独的矩形,而不是对照单个平移的矩形检查rectsCollide();一个在 X 方向上平移,另一个在 Y 方向上平移。for 循环现在进行检查以确保任何 X 移动独立于 Y 移动都有效。在 for 循环之后,我们可以对照这些情况进行检查以确定最终的走势。

下面是可以在代码中自由使用的最终itemChange()方法:

QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value)
{
if ( change == ItemPositionChange && scene() )
{
// Figure out the new point
QPointF snapped = snapPoint(value.toPointF());
// deltas to store amount of change
qreal delX = snapped.x() - pos().x();
qreal delY = snapped.y() - pos().y();
// No changes
if ( delX == 0 && delY == 0 )
return snapped;
// Check against all the other items for collision
QRectF translateX = getShapeRect();
QRectF translateY = getShapeRect();
translateX.translate(delX, 0);
translateY.translate(0, delY);
bool xOkay = true;
bool yOkay = true;
for ( QGraphicsItem* other : canvas->items())
{
if (other == this)
continue;
if ( rectsCollide(translateX, ((Node*)other)->getShapeRect()) )
xOkay = false;
if ( rectsCollide(translateY, ((Node*)other)->getShapeRect()) )
yOkay = false;
}
if ( xOkay && yOkay ) // Both valid, so move to the snapped location
return snapped;
else if ( xOkay ) // Move only in the X direction
{
QPointF r = pos();
r.setX(snapped.x());
return r;
}
else if ( yOkay ) // Move only in the Y direction
{
QPointF r = pos();
r.setY(snapped.y());
return r;
}
else // Don't move at all
return pos();
}
return QGraphicsItem::itemChange(change, value);
}

对于上面的代码,如果您希望自由移动不受网格限制,那么避免任何类型的捕捉行为实际上是非常简单的。解决方法只是更改调用snapPoint()函数的行,并改用原始值。如果您有任何问题,请随时发表评论。

相关文章: