禁止可移动QGraphicsItem与其他项目

时间:2017-05-20 15:21:42

标签: c++ qt collision qgraphicsitem

最终修改

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

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

原始问题

我有一个子类QGraphicsItem,Node,用于设置ItemIsMovableItemSendsGeometryChanges的标记。这些Node对象具有覆盖shape()boundingRect()函数,这些函数为它们提供了一个基本的矩形来移动。

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

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

我可以在collidingItems中抛出ItemPositionHasChanged标志后准确检测到itemChange(),但不幸的是,这是在节点已经移动之后。更重要的是,我发现此冲突列表没有正确更新直到节点从初始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更改,如果我们输入上面的最后一个其他块,我尝试使用moveBy(-lastDelX, -lastDelY)撤消之前的移动。这有一些可怕的结果,因为当鼠标仍被按下并且移动节点本身时(这最终在尝试移动碰撞时移动节点多次)被调用,通常导致节点飞离屏幕的一侧很快)。

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

结论

如果我能在移动发生之前找到准确检测collidingItems()的方法,我就可以完全避开ItemPositionHasChanged阻止,确保仅ItemPositionChange返回有效的点,从而避免任何移动到先前有效位置的逻辑。

感谢您的时间,如果您需要更多代码或示例来更好地说明问题,请与我们联系。

修改1:

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

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

// 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()的路径是错误的吗?我不确定这里出了什么问题,因为两个形状的比较似乎在移动完成后工作正常(即我之后可以准确地检测到碰撞)。如果有帮助,我可以上传整个代码。

1 个答案:

答案 0 :(得分:0)

我明白了。通过shape()调用修改,我确实在正确的轨道上。而不是实际测试完整形状对象本身,这是一个路径对象,其碰撞测试可能更复杂,效率更低,我写了一个基于this thread的额外函数,比较两个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(),而是可以有两个单独的rects;一个在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()函数的行,而只是使用原始值。如果您有任何问题,请随时发表评论。