Elastic Nodes Example 翻译及学习整理
文章目錄
- Elastic Nodes Example 翻譯及學習整理
- 題記:
- 簡介:
- Node Class Definition
- Edge Class Definition
- GraphWidget Class Definition
- The main() Function
Elastic Nodes Example 翻譯及學習整理
題記:
因為最近的一個項目需要實現圖像交互,好在Qt有現成的一些示例。示例是全英文的,還是翻譯整理一遍,這樣印象會更深刻些。
簡介:
該示例演示了如何實現在場景中的圖形交互。
具體有一下幾個方面:
1、在交互(如鼠標拖放鍵盤敲擊等)過程中如何實現節點之間的連線。
2、實現基本的一些交互,鼠標單擊、拖放節點。鼠標滾輪縮放視圖。空格鍵讓節點位置隨機變化。
我們知道,QGraphicsView 幫助GraphicsScene c類更好的實現圖形項的交互,如縮放和旋轉。
本示例的程序文檔結構很簡單,主要由 一個Node class, 一個 Edge class,還有 GraphWidget組成。
其中 Node class 實現的是小圓球的節點,Edge class實現的是節點之間的連線,GraphWidge class 實現的是一個窗體。main()函數實現窗體的顯示,以及事件的循環。
Node Class Definition
該類實現的三個主要目標:
1、繪制具有極性漸變填充的小圓球。
2、實現與其他小球的交互。
3、計算拖拽的拉力,從而拉動各個節點。
以下是該類的聲明部分
class Node : public QGraphicsItem{public:Node(GraphWidget *graphWidget);void addEdge(Edge *edge);QList<Edge *> edges() const;enum { Type = UserType + 1 };int type() const override { return Type; }void calculateForces();bool advancePosition();QRectF boundingRect() const override;QPainterPath shape() const override;void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;protected:QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;void mousePressEvent(QGraphicsSceneMouseEvent *event) override;void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;private:QList<Edge *> edgeList;QPointF newPos;GraphWidget *graph;};首先,Node class 繼承自QGraphicsItem,從而重寫兩個強制的函數boundingRect() and paint() 來實現圖形繪制。 通過重寫shape() 來確保碰撞檢測。
為了實現節點間連線的管理控制,該類同時提供了API來增加節點間的連線,以及列表的方式來管理各個相互連接的節點。
advancePosition() 來實現節點的步進。
calculateForces() 來計算拖拽的力度,從而使得臨近的節點發生相應的移動。
itemChange() 來相應狀態的變化(如節點位置發生變化時),mousePressEvent() and mouseReleaseEvent() 來更新圖像項的顯示。
接著,我們來看下Node 的構造函數:
Node::Node(GraphWidget *graphWidget): graph(graphWidget){setFlag(ItemIsMovable);setFlag(ItemSendsGeometryChanges);setCacheMode(DeviceCoordinateCache);setZValue(-1);}在構造函數中,
旗標ItemIsMovable 設置圖形項是可以被移動。
旗標ItemSendsGeometryChanges 確保itemChange() 通知位置發生移動。
DeviceCoordinateCache用來加速圖形的渲染。
為了確保節點始終顯示在“連線”的上層(碰撞檢測時有用),這里我們設置圖形項的Z值為-1.
在構造函數里還設置了一個GraphWidget 指針,用來存儲“this”指針,因為我們過會訪問到。
void Node::addEdge(Edge *edge){edgeList << edge;edge->adjust();}QList<Edge *> Node::edges() const{return edgeList;}```addEdge() 添加與圖形相關的連線。如果當節點的位置發生改變是,連接也會相應改變。edges() 返回節點相關聯的連線列表。```cppvoid Node::calculateForces(){if (!scene() || scene()->mouseGrabberItem() == this) {newPos = pos();return;}有兩種方式來實現節點的移動。calculateForces()計算拖拽的彈力。另外,用戶可以直接拖拽鼠標點中的節點。
// Sum up all forces pushing this item awayqreal xvel = 0;qreal yvel = 0;foreach (QGraphicsItem *item, scene()->items()) {Node *node = qgraphicsitem_cast<Node *>(item);if (!node)continue;QPointF vec = mapToItem(node, 0, 0);qreal dx = vec.x();qreal dy = vec.y();double l = 2.0 * (dx * dx + dy * dy);if (l > 0) {xvel += (dx * 150.0) / l;yvel += (dy * 150.0) / l;}}彈力的計算是通過一個算法來實現。
該算法有兩個步驟:
1、計算使得節點分離的力。
2、減去使得節點聚集的力。
首先,我們需要查找所有的節點。
接著我們用mapToItem(),來記錄每個節點的在場景坐標系中的位置。這個位置信息將用來計算拖拽彈力的大小和方向。我們計算每個節點之間力的總和,然后調整分配,最近的節點獲得的力最大。力度的總和記錄在兩個變量 xvel,yvel
接著我們計算節點間聚集的力
// Now subtract all forces pulling items togetherdouble weight = (edgeList.size() + 1) * 10;foreach (Edge *edge, edgeList) {QPointF vec;if (edge->sourceNode() == this)vec = mapToItem(edge->destNode(), 0, 0);elsevec = mapToItem(edge->sourceNode(), 0, 0);xvel -= vec.x() / weight;yvel -= vec.y() / weight;}節點間連線的長度決定聚集的力的大小。通過遍歷與當前節點相連的每個連線,我們可以使用類似上面的方法來計算得到聚集里的大小和方向。該力從xvel ,yvel減去得到。
if (qAbs(xvel) < 0.1 && qAbs(yvel) < 0.1)xvel = yvel = 0;從物理理論上來講,拖拽的分離的力,和節點間聚合的力會趨向平衡的。由于計算的精度引起的誤差,我們這里設置當<0.1 時候,就認為是0;
QRectF sceneRect = scene()->sceneRect();newPos = pos() + QPointF(xvel, yvel);newPos.setX(qMin(qMax(newPos.x(), sceneRect.left() + 10), sceneRect.right() - 10));newPos.setY(qMin(qMax(newPos.y(), sceneRect.top() + 10), sceneRect.bottom() - 10));}最后我們決定節點的新位置。同時我們保證新的坐標位置仍然在我們設定的邊界內。我們這里沒有移動圖形項,移動的功能實現交給了advancePosition()
bool Node::advancePosition(){if (newPos == pos())return false;setPos(newPos);return true;}advancePosition()實現了節點位置的更新,通過調用GraphWidget::timerEvent()來實現。
QRectF Node::boundingRect() const{qreal adjust = 2;return QRectF( -10 - adjust, -10 - adjust, 23 + adjust, 23 + adjust);}節點的邊界矩形的大小是20x20,修正值2,是為了補償邊框的粗細大小。3 個單位的值會了在繪制底部的陰影。
QPainterPath Node::shape() const
{
QPainterPath path;
path.addEllipse(-10, -10, 20, 20);
return path;
}
節點的形狀是一個簡單的橢圓。確保圖中拽時,單擊的是節點的內部。
void Node::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *){painter->setPen(Qt::NoPen);painter->setBrush(Qt::darkGray);painter->drawEllipse(-7, -7, 20, 20);QRadialGradient gradient(-3, -3, 10);if (option->state & QStyle::State_Sunken) {gradient.setCenter(3, 3);gradient.setFocalPoint(3, 3);gradient.setColorAt(1, QColor(Qt::yellow).light(120));gradient.setColorAt(0, QColor(Qt::darkYellow).light(120));} else {gradient.setColorAt(0, Qt::yellow);gradient.setColorAt(1, Qt::darkYellow);}painter->setBrush(gradient);painter->setPen(QPen(Qt::black, 0));painter->drawEllipse(-10, -10, 20, 20);}該函數實現的是節點的繪制。一開始我們我們繪制一個灰黑的陰影。
然后我們繪制一個有極性漸變填充的圓。帶有渲染填充會比較慢,這就是所以我們一開始就設置DeviceCoordinateCache的原因。該設置可以有效確保不必要的重繪功能。
QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value){switch (change) {case ItemPositionHasChanged:foreach (Edge *edge, edgeList)edge->adjust();graph->itemMoved();break;default:break;};return QGraphicsItem::itemChange(change, value);}itemChange() 會調整所有連接的節點的位置,這同時也將觸發計算新的force calculations.
這里就是我們為什么要留一個GraphWidget的指針。還有一種同樣實現的方式,那就是運用信號槽關聯,但是這樣的話,Node 需要繼承QGraphicsObject.
void Node::mousePressEvent(QGraphicsSceneMouseEvent *event){update();QGraphicsItem::mousePressEvent(event);}void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent *event){update();QGraphicsItem::mouseReleaseEvent(event);}因為我們已經設置的圖形是可移動的旗標,所以我們不需要記錄實現鼠標交互時候的坐標,因為它已經提供給我們了,我們只需要重寫該函數的句柄就可以了。
Edge Class Definition
該類實現了帶箭頭的連線。
它包含了指向源節點和目標節點的指針。
提供了adjust() 來保證始終從源節點到目標節點的連線。
下面來看該類的聲明:
Edge 繼承了QGraphicsItem,它非常簡單,沒有信號 與槽,沒有屬性。它的構造函數里有兩個指針。我們還提供了提取該兩個指針的函數。
Edge::Edge(Node *sourceNode, Node *destNode): arrowSize(10){setAcceptedMouseButtons(0);source = sourceNode;dest = destNode;source->addEdge(this);dest->addEdge(this);adjust();}構造函數中初始化了arrowSize 這個變量。
setAcceptedMouseButtons(0). 不接受鼠標的按鈕事件。
更新連線的兩個指針,并通過adjust()來更新連線的起點和終點位置。
Node *Edge::sourceNode() const
{
return source;
}
Node *Edge::destNode() const
{
return dest;
}
返回當前連線的節點的指針。
void Edge::adjust(){if (!source || !dest)return;QLineF line(mapFromItem(source, 0, 0), mapFromItem(dest, 0, 0));qreal length = line.length();prepareGeometryChange();if (length > qreal(20.)) {QPointF edgeOffset((line.dx() * 10) / length, (line.dy() * 10) / length);sourcePoint = line.p1() + edgeOffset;destPoint = line.p2() - edgeOffset;} else {sourcePoint = destPoint = line.p1();}}這里為了箭頭在節點的輪廓上,而不是節點的中心,我們這里做了edgeOffset補償。
如果vector 小于20,比如節點重合了。這里我們讓兩個指針夠指向同一個源節點。實際上,這是難易發生的。
prepareGeometryChange() 為了使得返回boundingRect()
QRectF Edge::boundingRect() const{if (!source || !dest)return QRectF();qreal penWidth = 1;qreal extra = (penWidth + arrowSize) / 2.0;return QRectF(sourcePoint, QSizeF(destPoint.x() - sourcePoint.x(),destPoint.y() - sourcePoint.y())).normalized().adjusted(-extra, -extra, extra, extra);}邊界矩形的定義。
void Edge::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *){if (!source || !dest)return;QLineF line(sourcePoint, destPoint);if (qFuzzyCompare(line.length(), qreal(0.)))return;繪制連線時,我們這里設置了兩個異常的返回。
// Draw the line itselfpainter->setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));painter->drawLine(line); // Draw the arrowsdouble angle = std::atan2(-line.dy(), line.dx());QPointF sourceArrowP1 = sourcePoint + QPointF(sin(angle + M_PI / 3) * arrowSize,cos(angle + M_PI / 3) * arrowSize);QPointF sourceArrowP2 = sourcePoint + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,cos(angle + M_PI - M_PI / 3) * arrowSize);QPointF destArrowP1 = destPoint + QPointF(sin(angle - M_PI / 3) * arrowSize,cos(angle - M_PI / 3) * arrowSize);QPointF destArrowP2 = destPoint + QPointF(sin(angle - M_PI + M_PI / 3) * arrowSize,cos(angle - M_PI + M_PI / 3) * arrowSize);painter->setBrush(Qt::black);painter->drawPolygon(QPolygonF() << line.p1() << sourceArrowP1 << sourceArrowP2);painter->drawPolygon(QPolygonF() << line.p2() << destArrowP1 << destArrowP2);}繪制連線和箭頭
GraphWidget Class Definition
GraphWidget 是QGraphicsView的子類。class GraphWidget : public QGraphicsView{Q_OBJECTpublic:GraphWidget(QWidget *parent = 0);void itemMoved();public slots:void shuffle();void zoomIn();void zoomOut();protected:void keyPressEvent(QKeyEvent *event) override;void timerEvent(QTimerEvent *event) override;#if QT_CONFIG(wheelevent)void wheelEvent(QWheelEvent *event) override;#endifvoid drawBackground(QPainter *painter, const QRectF &rect) override;void scaleView(qreal scaleFactor);private:int timerId;Node *centerNode;};該類,初始化了場景。
提供了itemMoved() 來告知場景中的圖形發生了變化。一些事件的重載。以及背景的繪制,視圖的縮放。
這里初始化了場景,還有設置了場景的大小。
setCacheMode(CacheBackground); 緩存靜態背景。
setViewportUpdateMode(BoundingRectViewportUpdate); 設置視圖的更新模式。
setRenderHint(QPainter::Antialiasing);抗鋸齒。
setTransformationAnchor(AnchorUnderMouse);縮放時以鼠標為中心。
初始化節點和連線。
void GraphWidget::itemMoved(){if (!timerId)timerId = startTimer(1000 / 25);}GraphWidget 會監測節點的移動。通過定時器事件來更新節點的位置。
void GraphWidget::keyPressEvent(QKeyEvent *event){switch (event->key()) {case Qt::Key_Up:centerNode->moveBy(0, -20);break;case Qt::Key_Down:centerNode->moveBy(0, 20);break;case Qt::Key_Left:centerNode->moveBy(-20, 0);break;case Qt::Key_Right:centerNode->moveBy(20, 0);break;case Qt::Key_Plus:zoomIn();break;case Qt::Key_Minus:zoomOut();break;case Qt::Key_Space:case Qt::Key_Enter:shuffle();break;default:QGraphicsView::keyPressEvent(event);}}鍵盤事件的實現。
void GraphWidget::timerEvent(QTimerEvent *event){Q_UNUSED(event);QList<Node *> nodes;foreach (QGraphicsItem *item, scene()->items()) {if (Node *node = qgraphicsitem_cast<Node *>(item))nodes << node;}foreach (Node *node, nodes)node->calculateForces();bool itemsMoved = false;foreach (Node *node, nodes) {if (node->advancePosition())itemsMoved = true;}if (!itemsMoved) {killTimer(timerId);timerId = 0;}}每次只要計時器開啟,該句柄會查找所有的節點,通過計算拖拽的力,然后更新節點的位置。通過advance()的返回值,若果是false,則停止定時器。
void GraphWidget::wheelEvent(QWheelEvent *event){scaleView(pow((double)2, -event->delta() / 240.0));}鼠標的滾輪縮放。
void GraphWidget::drawBackground(QPainter *painter, const QRectF &rect){Q_UNUSED(rect);// ShadowQRectF sceneRect = this->sceneRect();QRectF rightShadow(sceneRect.right(), sceneRect.top() + 5, 5, sceneRect.height());QRectF bottomShadow(sceneRect.left() + 5, sceneRect.bottom(), sceneRect.width(), 5);if (rightShadow.intersects(rect) || rightShadow.contains(rect))painter->fillRect(rightShadow, Qt::darkGray);if (bottomShadow.intersects(rect) || bottomShadow.contains(rect))painter->fillRect(bottomShadow, Qt::darkGray);// FillQLinearGradient gradient(sceneRect.topLeft(), sceneRect.bottomRight());gradient.setColorAt(0, Qt::white);gradient.setColorAt(1, Qt::lightGray);painter->fillRect(rect.intersected(sceneRect), gradient);painter->setBrush(Qt::NoBrush);painter->drawRect(sceneRect);// TextQRectF textRect(sceneRect.left() + 4, sceneRect.top() + 4,sceneRect.width() - 4, sceneRect.height() - 4);QString message(tr("Click and drag the nodes around, and zoom with the mouse ""wheel or the '+' and '-' keys"));QFont font = painter->font();font.setBold(true);font.setPointSize(14);painter->setFont(font);painter->setPen(Qt::lightGray);painter->drawText(textRect.translated(2, 2), message);painter->setPen(Qt::black);painter->drawText(textRect, message);}背景的繪制,
void GraphWidget::scaleView(qreal scaleFactor){qreal factor = transform().scale(scaleFactor, scaleFactor).mapRect(QRectF(0, 0, 1, 1)).width();if (factor < 0.07 || factor > 100)return;scale(scaleFactor, scaleFactor);}滾輪幅度的計算。
The main() Function
int main(int argc, char **argv) {QApplication app(argc, argv);GraphWidget *widget = new GraphWidget;QMainWindow mainWindow;mainWindow.setCentralWidget(widget);mainWindow.show();return app.exec(); }main()函數很簡單,創建了QApplication、mainWindows、GraphWidget,開啟了事件循環。
ps:花了一天的時間閱讀完了,真的是一段段翻譯下來。這個過程還是很耗精力的。移動算法部分是本應用程序的精妙之處,和物理學上的力的合成和分解有關,這里用到定時器來模擬橡筋的力的傳遞過程,非常有意思。算法部分還是有一定難度,等過后慢慢調試理解吧。
總結
以上是生活随笔為你收集整理的Elastic Nodes Example 翻译及学习整理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Qt 2D绘图功能简单总结
- 下一篇: QML 编程之旅 -- QML程序的基本