2017-05-20 37 views
0

最後編輯禁止可移動的QGraphicsItem的碰撞與其他項目

我想通了這個問題,並公佈瞭解決方案如下回答。如果你在谷歌尋找一種體面的方式來通過ItemIsMovable標誌移動QGraphicsItems/QGraphicsObjects,同時避免與其他節點發生衝突,那麼我在我的答案最後加上了一個工作itemChange方法。

我原來的項目處理捕捉到任意網格的節點,但這很容易被刪除,並且根本不需要這個解決方案的工作。效率方面,它並不是數據結構或算法最聰明的用法,所以如果屏幕上出現大量節點,您可能想嘗試更智能的方法。


原始的問題

我有一個子類的QGraphicsItem,節點,即設置標誌爲ItemIsMovableItemSendsGeometryChanges。這些節點對象覆蓋了shape()boundingRect()函數,爲它們提供了一個基本的矩形來移動。

用鼠標移動一個節點,按照預期調用itemChange()函數,我可以調用一個snapPoint()函數,該函數強制移動的節點始終對齊網格。 (這是一個簡單的函數,它返回基於給定參數的新QPointF,其中新點限制在實際捕捉網格上最接近的座標上)。這與現在完全一樣。

我的大問題是,我想避免陷入與其他節點相撞的位置。也就是說,如果我將一個節點移動到結果(對齊網格)位置,我希望collidingItems()列表完全爲空。

我可以在ItemPositionHasChanged標誌在itemChange()中拋出後準確地檢測到collidingItems,但不幸的是這是在節點已經移動之後。更重要的是,我發現這個碰撞列表沒有正確更新,直到節點從最初的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只返回那些點有效,從而避免任何邏輯移到先前有效的位置。

感謝您的時間,請讓我知道是否需要更多代碼或示例來更好地說明問題。

編輯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塊,即使它們彼此之間沒有距離。上面的代碼中的畫布對象只是QGraphicsView,它包含所有這些Node對象,以防它們也不清楚。

任何想法如何使collidesWithPath正常工作?複製shape()錯誤的路徑是?我不確定這裏出了什麼問題,因爲兩個形狀的比較似乎在移動完成後正常工作(即我可以在之後準確檢測到碰撞)。如果有幫助,我可以上傳整個代碼。

回答

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()函數的行,而只是使用原始值。如果您有任何問題,歡迎發表評論。

相關問題