禁止可移动的QGraphicsItem与其他项目冲突
最终编辑
我解决了这个问题,并在下面发布了一个解决方案作为答案。如果您无意中从Google找到了一种合适的方式来通过ItemIsMovable
标志移动QGraphicsItems/QGraphicsObject,同时避免与其他节点发生冲突,我在答案的末尾提供了一个有效的itemChange
方法。
我最初的项目涉及将节点捕捉到任意网格,但这很容易删除,并且根本不是此解决方案工作所必需的。就效率而言,这不是对数据结构或算法的最巧妙使用,因此,如果屏幕上有很多节点,您可能想尝试更智能的方法。
原始问题
我有一个子类QGraphicsItem Node,它为ItemIsMovable
和ItemSendsGeometryChanges
设置标志。这些节点对象重写了shape()
和boundingRect()
函数,这些函数为它们提供了一个可四处移动的基本矩形。
itemChange()
函数,我可以调用一个snapPoint()
函数来强制移动的节点始终捕捉到网格。(这是一个简单的函数,它基于给定的参数返回新的QPointF,其中新点被限制在实际捕捉栅格上最接近的坐标)。这与现在一样工作正常。
我最大的问题是,我希望避免捕捉到与其他节点冲突的位置。也就是说,如果我将一个节点移动到一个结果(捕捉到网格)位置,我希望collidingItems()
列表完全为空。
ItemPositionHasChanged
标志被抛入itemChange()
之后准确地检测到collidingItems
,但不幸的是,这是在Node已经移动之后。更重要的是,我发现此冲突列表在节点从初始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和Sty以恢复,但这也存在与上面的方法类似的问题。
结论
如果我能找到一种方法,在移动发生之前就准确地检测到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
块,即使它们彼此相距很远。上面代码中的画布对象只是保存所有这些Node对象的QGraphicsView,如果这一点也不清楚的话。
collidesWithPath
正常工作吗?复制shape()
的路径是否错误?我不确定这里出了什么问题,因为在移动完成后,两个形状的比较似乎工作得很好(即,我可以在之后准确地检测到碰撞)。如果有帮助,我可以上传整个代码。
解决方案
我想出来了。通过修改shape()
调用,我确实走上了正确的道路。我并没有实际测试完整的Shape对象本身,而是编写了一个基于this thread的额外函数来比较两个QRectF
(这实际上就是Shape调用返回的内容):
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方向上移动。这发生在对撞机上的对角拖动上(这有点难解释,但如果您正在使用上面的代码构建类似的项目,当您尝试将一个节点拖到另一个节点旁边时,您可以看到它的作用)。我们可以通过几个微小的更改轻松地检测到这种情况:
下面是您可以在代码中自由使用的最后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()
函数的行,并使用原始值。如果您有任何问题,请随时发表意见。
相关文章