禁止可移动的 QGraphicsItem 与其他项目发生碰撞
Prohibit collision of a movable QGraphicsItem with other items
最终编辑
我发现了问题并在下面发布了解决方案作为答案。如果您从 google 在这里偶然发现了一种通过 ItemIsMovable
标志移动 QGraphicsItems / QGraphicsObjects 同时避免与其他节点发生冲突的好方法,我在最后包含了一个有效的 itemChange
方法我的回答。
我的原始项目涉及将节点捕捉到任意网格,但这很容易删除并且根本不需要此解决方案的工作。效率方面,这不是数据结构或算法的最巧妙使用,所以如果您最终在屏幕上看到很多节点,您可能想尝试更智能的东西。
原题
我有一个子类 QGraphicsItem,Node,它设置了 ItemIsMovable
和 ItemSendsGeometryChanges
的标志。这些 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);
}
我试过的
我尝试存储 delX
和 delY
更改,如果我们进入上面最后的 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()
函数的行,并只使用原始值。如有任何问题,请随时发表评论。
最终编辑
我发现了问题并在下面发布了解决方案作为答案。如果您从 google 在这里偶然发现了一种通过 ItemIsMovable
标志移动 QGraphicsItems / QGraphicsObjects 同时避免与其他节点发生冲突的好方法,我在最后包含了一个有效的 itemChange
方法我的回答。
我的原始项目涉及将节点捕捉到任意网格,但这很容易删除并且根本不需要此解决方案的工作。效率方面,这不是数据结构或算法的最巧妙使用,所以如果您最终在屏幕上看到很多节点,您可能想尝试更智能的东西。
原题
我有一个子类 QGraphicsItem,Node,它设置了 ItemIsMovable
和 ItemSendsGeometryChanges
的标志。这些 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);
}
我试过的
我尝试存储 delX
和 delY
更改,如果我们进入上面最后的 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()
函数的行,并只使用原始值。如有任何问题,请随时发表评论。