禁止可移动的 QGraphicsItem 与其他项目发生碰撞

Prohibit collision of a movable QGraphicsItem with other items

最终编辑

我发现了问题并在下面发布了解决方案作为答案。如果您从 google 在这里偶然发现了一种通过 ItemIsMovable 标志移动 QGraphicsItems / QGraphicsObjects 同时避免与其他节点发生冲突的好方法,我在最后包含了一个有效的 itemChange 方法我的回答。

我的原始项目涉及将节点捕捉到任意网格,但这很容易删除并且根本不需要此解决方案的工作。效率方面,这不是数据结构或算法的最巧妙使用,所以如果您最终在屏幕上看到很多节点,您可能想尝试更智能的东西。


原题

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

用鼠标四处移动节点会按预期调用 itemChange() 函数,我可以调用 snapPoint() 函数强制移动的节点始终捕捉到网格。 (这是一个简单的函数,returns 基于给定参数的新 QPointF,其中新点被限制在实际捕捉网格上最近的坐标)。这与现在一样完美。

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

itemChange() 中抛出 ItemPositionHasChanged 标志后,我可以准确检测到 collidingItems,但不幸的是,这是在节点已经移动之后。更重要的是,我发现这个冲突列表没有正确更新 直到 节点 returns 从初始 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 只有 returns 个点是有效的,因此避免了移动到先前有效位置的任何逻辑。

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

编辑 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;

...但这也不起作用。任何新节点 always 进入 collidesWithPath 块,即使它们彼此相距甚远。上面代码中的 canvas 对象只是包含所有这些 Node 对象的 QGraphicsView,以防万一也不清楚。

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

我想通了。我确实在 shape() 调用修改的正确轨道上。我没有实际测试完整形状对象本身,它是一个路径对象,其碰撞测试可能更复杂和低效,我写了一个基于 this thread 的额外函数来比较两个 QRectF(本质上是这个形状叫什么 returns):

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 );
}

然后,在与原编辑相同的区域post:

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() 一起工作,它基本上只是 return 与覆盖 shape() 函数中使用的相同的矩形,但是以 QRectF 形式代替QPainterPath.

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

编辑 1:

我花了更多的时间来处理这个以获得更好的结果,我想我会分享,因为我觉得移动节点并发生碰撞是很常见的事情。在上面的代码中,"dragging" 一个节点与一个长节点/一组节点会与当前拖动的节点发生碰撞,这会产生一些不良影响;也就是说,因为我们只是在任何碰撞时默认为 return 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() 函数的行,并只使用原始值。如有任何问题,请随时发表评论。