最终修改
我找出了问题,并在下面发布了一个解决方案作为答案。如果您在谷歌这里找到了通过ItemIsMovable
标志移动QGraphicsItems / QGraphicsObjects的好方法,同时避免与其他节点冲突,我在答案的最后包含了一个有效的itemChange
方法。
我的原始项目涉及将节点捕捉到任意网格,但这很容易删除,根本不需要此解决方案才能工作。效率方面,它不是最聪明地使用数据结构或算法,所以如果你最终在屏幕上有很多节点,你可能想尝试更聪明的东西。
原始问题
我有一个子类QGraphicsItem,Node,用于设置ItemIsMovable
和ItemSendsGeometryChanges
的标记。这些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);
}
我尝试了什么
我尝试存储delX
和delY
更改,如果我们输入上面的最后一个其他块,我尝试使用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()
的路径是错误的吗?我不确定这里出了什么问题,因为两个形状的比较似乎在移动完成后工作正常(即我之后可以准确地检测到碰撞)。如果有帮助,我可以上传整个代码。
答案 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()
函数的行,而只是使用原始值。如果您有任何问题,请随时发表评论。