エラスティック・ノードの例

シーン内のグラフィカルアイテムとインタラクションする方法を示します。

Elastic Nodes のサンプルは、基本的なインタラクションで、グラフのノード間にエッジを実装する方法を示しています。クリックしてノードをドラッグしたり、マウスホイールやキーボードを使ってズームインやズームアウトができます。スペースバーを押すと、ノードがランダムに表示されます。この例では、解像度に依存せず、拡大してもグラフィックは鮮明なままです。

Graphics Viewは、QGraphicsItem クラスから派生した多数のカスタムメイドの2Dグラフィカル・アイテムを管理・操作するためのQGraphicsScene クラスと、アイテムを視覚化するためのQGraphicsView ウィジェットを提供し、ズームと回転をサポートします。

この例は、Node クラス、Edge クラス、GraphWidget テスト、main 関数で構成されています。Node クラスはグリッド内のドラッグ可能な黄色いノードを表し、Edge クラスはノード間の線を表し、GraphWidget クラスはアプリケーション・ウィンドウを表し、main() 関数はこのウィンドウを作成して表示し、イベント・ループを実行します。

ノード・クラスの定義

Node クラスには3つの役割があります:

  • 黄色いグラデーションの "ボール "を、沈んだ状態と盛り上がった状態の2種類で描く。
  • 他のノードとの接続を管理する。
  • グリッド内のノードを引っ張ったり押したりする力を計算する。

まず、Node クラスの宣言を見てみましょう。

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 クラスはQGraphicsItem を継承し、boundingRect() とpaint() の2つの必須関数を再実装して、見た目を整えます。また、shape ()を再実装し、ヒット領域が(デフォルトの外接矩形ではなく)楕円形状になるようにしています。

エッジ管理のために、ノードにエッジを追加したり、接続されているエッジをすべてリストするためのシンプルな API を提供します。

advance() の再実装は、シーンの状態が1ステップ進むたびに呼び出されます。calculateForces()関数は、このノードとその近傍ノードを押したり引いたりする力を計算するために呼び出されます。

また、Node クラスは、itemChange() を再実装して状態の変化(この場合は位置の変化)に反応し、mousePressEvent() とmouseReleaseEvent() を再実装してアイテムの外観を更新します。

Node の実装を、コンストラクタから見ていくことにする:

Node::Node(GraphWidget *graphWidget)
    : graph(graphWidget)
{
    setFlag(ItemIsMovable);
    setFlag(ItemSendsGeometryChanges);
    setCacheMode(DeviceCoordinateCache);
    setZValue(-1);
}

コンストラクタでは、ItemIsMovable フラグを設定し、マウスのドラッグに応じてアイテムが移動するようにします。また、ItemSendsGeometryChanges を設定し、位置と変形の変更に対するitemChange() 通知を有効にします。また、DeviceCoordinateCache を有効にして、レンダリングのパフォーマンスを高速化します。ノードが常にエッジの上にスタックされるように、最後にアイテムの Z 値を -1 に設定します。

NodeのコンストラクタはGraphWidget ポインタを取り、これをメンバ変数として格納します。このポインタについては、後ほど説明します。

void Node::addEdge(Edge *edge)
{
    edgeList << edge;
    edge->adjust();
}

QList<Edge *> Node::edges() const
{
    return edgeList;
}

addEdge()関数は、入力されたエッジをエッジのリストに追加します。その後、エッジの端点がソースノードとデスティネーションノードの位置に一致するようにエッジが調整されます。

edges()関数は、単にアタッチされたエッジのリストを返します。

void Node::calculateForces()
{
    if (!scene() || scene()->mouseGrabberItem() == this) {
        newPos = pos();
        return;
    }

ノードの移動には2つの方法があります。calculateForces() 関数は、グリッド内のノードを引っ張ったり押したりする弾性効果を実装しています。さらに、ユーザーがマウスを使ってノードを直接動かすこともできます。この2つのアプローチが同じノードで同時に動作することは避けたいので、このNode が現在のマウスグラバーアイテム(つまり、QGraphicsScene::mouseGrabberItem())であるかどうかをチェックすることでcalculateForces() を開始します。隣接する(必ずしも接続されている必要はない)ノードをすべて見つける必要があるため、アイテムがそもそもシーンの一部であることも確認します。

    // Sum up all forces pushing this item away
    qreal xvel = 0;
    qreal yvel = 0;
    const QList<QGraphicsItem *> items = scene()->items();
    for (QGraphicsItem *item : 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;
        }
    }

伸縮性」効果は、押したり引いたりする力を適用するアルゴリズムから生まれます。この効果は印象的で、驚くほど簡単に実装できます。

アルゴリズムには2つのステップがあります:1つ目はノードを押し広げる力を計算すること、2つ目はノードを引き寄せる力を引くことです。まず、グラフのすべてのノードを見つける必要がある。QGraphicsScene::items ()を呼び出してシーン内のすべてのアイテムを見つけ、qgraphicsitem_cast ()を使用してNode インスタンスを探します。

mapFromItem() を使って、このノードから他の各ノードを指す一時的なベクトルをローカル座標で作成する。このベクトルの分解された成分を使用して、ノードにかかる力の方向と強さを決定します。力は各ノードごとに蓄積され、最も近いノードに最も強い力がかかるように調整されます。すべての力の合計は、xvel (X-速度)とyvel (Y-速度)に格納されます。

    // Now subtract all forces pulling items together
    double weight = (edgeList.size() + 1) * 10;
    for (const Edge *edge : std::as_const(edgeList)) {
        QPointF vec;
        if (edge->sourceNode() == this)
            vec = mapToItem(edge->destNode(), 0, 0);
        else
            vec = mapToItem(edge->sourceNode(), 0, 0);
        xvel -= vec.x() / weight;
        yvel -= vec.y() / weight;
    }

ノード間のエッジは、ノードを引き寄せる力を表します。このノードに接続されている各エッジを訪問することで、上記と同様のアプローチを使用して、すべての引っ張る力の方向と強さを求めることができます。これらの力はxvelyvel から差し引かれる。

    if (qAbs(xvel) < 0.1 && qAbs(yvel) < 0.1)
        xvel = yvel = 0;

理論的には、押す力と引く力の和は正確に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));
}

calculateForces() の最後のステップでは、ノードの新しい位置を決定します。ノードの現在位置に力を加えます。また、新しい位置が定義した境界の内側にとどまるようにします。この関数では実際にアイテムを移動させません。それはadvance() の別のステップで行います。

bool Node::advancePosition()
{
    if (newPos == pos())
        return false;

    setPos(newPos);
    return true;
}

advance() 関数はアイテムの現在位置を更新します。GraphWidget::timerEvent()ノードの位置が変更された場合、この関数は true を返し、そうでない場合は false を返します。

QRectF Node::boundingRect() const
{
    qreal adjust = 2;
    return QRectF( -10 - adjust, -10 - adjust, 23 + adjust, 23 + adjust);
}

Node の外接矩形は、原点 (0, 0) を中心とした 20 x 20 の大きさの矩形で、ノードのアウトラインストロークを補正するために全方向に 2 単位調整され、単純なドロップシャドウのためのスペースを作るために下と右に 3 単位調整されます。

QPainterPath Node::shape() const
{
    QPainterPath path;
    path.addEllipse(-10, -10, 20, 20);
    return path;
}

形状は単純な楕円です。このため、ドラッグするにはノードの楕円形の内側をクリックする必要があります。この効果は、サンプルを実行し、ノードが非常に大きくなるようにズームインすることで試すことができます。shape() を再実装しなければ、アイテムのヒット領域はその境界矩形(つまり長方形)と同じになります。

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).lighter(120));
        gradient.setColorAt(0, QColor(Qt::darkYellow).lighter(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);
}

この関数はノードのペイントを実装します。まず、(-7, -7)、つまり楕円の左上隅(-10, -10)から(3, 3)単位下と右に、単純な濃いグレーの楕円ドロップシャドウを描きます。

次に、放射状グラデーションの塗りつぶしで楕円を描きます。この塗りは、盛り上がるとQt::yellow からQt::darkYellow になり、沈むとその逆になります。また、沈んだ状態では、中心と焦点を (3, 3) だけずらして、何かが押し下げられたような印象を強調します。

グラデーションによる塗りつぶし楕円の描画は、特にQRadialGradient のような複雑なグラデーションを使う場合、かなり時間がかかることがあります。このためこの例では、不要な再描画を防ぐシンプルで効果的な手段であるDeviceCoordinateCache を使用しています。

QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value)
{
    switch (change) {
    case ItemPositionHasChanged:
        for (Edge *edge : std::as_const(edgeList))
            edge->adjust();
        graph->itemMoved();
        break;
    default:
        break;
    };

    return QGraphicsItem::itemChange(change, value);
}

itemChange() を再実装して、すべての接続されたエッジの位置を調整し、アイテムが移動した(つまり「何かが起こった」)ことをシーンに通知します。これにより、新しい力の計算がトリガーされます。

この通知は、ノードがGraphWidget へのポインタを保持する必要がある唯一の理由です。別のアプローチとして、シグナルを使用してこのような通知を提供することもできます。その場合、NodeQGraphicsObject を継承する必要があります。

void Node::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    update();
    QGraphicsItem::mousePressEvent(event);
}

void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    update();
    QGraphicsItem::mouseReleaseEvent(event);
}

ItemIsMovable フラグを設定したので、マウス入力に応じてノードを移動するロジックを実装する必要はない。しかし、ノードの視覚的な外観(沈んでいるか、盛り上がっているか)を更新するために、マウスを押したり離したりするハンドラを再実装する必要があります。

エッジ・クラスの定義

Edge クラスは、この例のノード間の矢印線を表します。このクラスは非常にシンプルで、ソースとデスティネーションのノード・ポインタを保持し、線がソースの位置で始まり、デスティネーションの位置で終わることを確認するadjust() 関数を提供します。エッジは、力がノードを引っ張ったり押したりすることで連続的に変化する唯一のアイテムである。

クラス宣言を見てみよう:

class Edge : public QGraphicsItem
{
public:
    Edge(Node *sourceNode, Node *destNode);

    Node *sourceNode() const;
    Node *destNode() const;

    void adjust();

    enum { Type = UserType + 2 };
    int type() const override { return Type; }

protected:
    QRectF boundingRect() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;

private:
    Node *source, *dest;

    QPointF sourcePoint;
    QPointF destPoint;
    qreal arrowSize = 10;
};

Edge このクラスはシグナル、スロット、プロパティを使用しないシンプルなクラスなので、 を継承しています( と比較してください)。QGraphicsItem QGraphicsObject

コンストラクタは2つのノード・ポインタを入力として受け取る。この例ではどちらのポインタも必須です。また、各ノードの get 関数も提供します。

adjust() 関数はエッジを再配置し、アイテムはboundingRect() とpaint() も実装する。

次にその実装をレビューします。

Edge::Edge(Node *sourceNode, Node *destNode)
    : source(sourceNode), dest(destNode)
{
    setAcceptedMouseButtons(Qt::NoButton);
    source->addEdge(this);
    dest->addEdge(this);
    adjust();
}

Edge コンストラクタはarrowSize データ・メンバを 10 単位に初期化します。これはpaint() で描画される矢印のサイズを決定します。

コンストラクタ本体では、setAcceptedMouseButtons(0)を呼び出す。これにより、エッジ・アイテムはマウス入力としてまったく考慮されない(つまり、エッジをクリックできない)。次に、ソースとデスティネーションのポインタを更新し、このエッジを各ノードに登録し、adjust() を呼び出してこのエッジの開始位置と終了位置を更新します。

Node *Edge::sourceNode() const
{
    return source;
}

Node *Edge::destNode() const
{
    return dest;
}

ソースとデスティネーションのget関数は単にそれぞれのポインタを返します。

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();
    }
}

adjust() では、2つの点を定義する:sourcePointdestPoint の2点を定義します。それぞれの点はローカル座標を使って計算される。

エッジの矢印の先端は、ノードの中心ではなく、ノードの正確な輪郭を指すようにしたい。この点を見つけるために、まず、始点ノードの中心から終点ノードの中心を指すベクトルをXとYに分解し、ベクトルの長さで割って成分を正規化する。これでXとYの単位デルタが得られ、これにノードの半径(10)を掛けると、エッジの一点に加えなければならないオフセットが得られ、もう一点から引かなければならないオフセットが得られる。

ベクトルの長さが20より小さい場合(つまり、2つのノードが重なっている場合)、ソースとデスティネーションのポインタをソース・ノードの中心に固定します。実際には、2つのノード間の力が最大になるため、このケースを手動で再現するのは非常に難しい。

この関数でprepareGeometryChange() を呼び出していることに注目することが重要である。なぜなら、sourcePointdestPoint という変数はペイント時に直接使用され、boundingRect() の再実装から返されるからです。boundingRect() が返す変数を変更する前に、またpaint() がこれらの変数を使用する前に、常にprepareGeometryChange() を呼び出す必要があります。そのような変数が変更される直前に、この関数を一度だけ呼び出すのが最も安全です。

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;

paint()の再実装は、いくつかの前提条件をチェックすることから始める。まず、ソース・ノードかデスティネーション・ノードのどちらかが設定されていなければ、直ちにリターンする。

同時に、辺の長さがおよそ0であるかどうかをチェックし、0であればリターンする。

    // Draw the line itself
    painter->setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    painter->drawLine(line);

丸い接合部とキャップを持つペンを使って線を描く。例題を実行して拡大し、エッジを詳しく調べると、鋭角や四角のエッジがないことがわかるだろう。

    // Draw the arrows
    double 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);
}

エッジの両端に1つずつ矢印を描きます。各矢印は黒の塗りつぶしで多角形として描かれます。矢印の座標は簡単な三角法を用いて決定される。

GraphWidgetクラスの定義

GraphWidget は のサブクラスで、メインウィンドウにスクロールバーを提供します。QGraphicsView

class GraphWidget : public QGraphicsView
{
    Q_OBJECT

public:
    GraphWidget(QWidget *parent = nullptr);

    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;
#endif
    void drawBackground(QPainter *painter, const QRectF &rect) override;

    void scaleView(qreal scaleFactor);

private:
    int timerId = 0;
    Node *centerNode;
};

このクラスは、シーンを初期化する基本的なコンストラクタ、シーンのノード・グラフの変更を通知するitemMoved() 関数、いくつかのイベント・ハンドラ、drawBackground() の再実装、マウス・ホイールまたはキーボードを使用してビューをスケーリングするためのヘルパー関数を提供します。

GraphWidget::GraphWidget(QWidget *parent)
    : QGraphicsView(parent)
{
    QGraphicsScene *scene = new QGraphicsScene(this);
    scene->setItemIndexMethod(QGraphicsScene::NoIndex);
    scene->setSceneRect(-200, -200, 400, 400);
    setScene(scene);
    setCacheMode(CacheBackground);
    setViewportUpdateMode(BoundingRectViewportUpdate);
    setRenderHint(QPainter::Antialiasing);
    setTransformationAnchor(AnchorUnderMouse);
    scale(qreal(0.8), qreal(0.8));
    setMinimumSize(400, 400);
    setWindowTitle(tr("Elastic Nodes"));

GraphicsWidgetコンストラクタはシーンを作成し、ほとんどのアイテムはほとんどの時間動き回るので、QGraphicsScene::NoIndex を設定します。その後、シーンは固定のscene rectangle を取得し、GraphWidget ビューに割り当てられる。

このビューによって、QGraphicsView::CacheBackground 、静的でやや複雑な背景のレンダリングがキャッシュされる。グラフは、動き回る小さなアイテムの密接なコレクションをレンダリングするため、グラフィック・ビューが正確な更新領域を見つけるのに時間を浪費する必要はないため、QGraphicsView::BoundingRectViewportUpdate ビューポート更新モードを設定します。デフォルトでも問題ありませんが、この例ではこのモードの方が明らかに高速です。

レンダリング品質を向上させるために、QPainter::Antialiasing を設定します。

変換アンカーは、ビューを変換するとき、つまりこの例ではズームインまたはズームアウトするときに、ビューがどのようにスクロールするかを決定します。私たちは、QGraphicsView::AnchorUnderMouse を選択しました。これは、マウスカーソルの下にある点をビューの中心にします。これにより、マウスをその上に移動させ、マウスホイールを回転させることで、シーンのポイントに向かって簡単にズームすることができます。

最後に、シーンのデフォルトサイズに合う最小サイズをウィンドウに与え、適切なウィンドウタイトルを設定します。

    Node *node1 = new Node(this);
    Node *node2 = new Node(this);
    Node *node3 = new Node(this);
    Node *node4 = new Node(this);
    centerNode = new Node(this);
    Node *node6 = new Node(this);
    Node *node7 = new Node(this);
    Node *node8 = new Node(this);
    Node *node9 = new Node(this);
    scene->addItem(node1);
    scene->addItem(node2);
    scene->addItem(node3);
    scene->addItem(node4);
    scene->addItem(centerNode);
    scene->addItem(node6);
    scene->addItem(node7);
    scene->addItem(node8);
    scene->addItem(node9);
    scene->addItem(new Edge(node1, node2));
    scene->addItem(new Edge(node2, node3));
    scene->addItem(new Edge(node2, centerNode));
    scene->addItem(new Edge(node3, node6));
    scene->addItem(new Edge(node4, node1));
    scene->addItem(new Edge(node4, centerNode));
    scene->addItem(new Edge(centerNode, node6));
    scene->addItem(new Edge(centerNode, node8));
    scene->addItem(new Edge(node6, node9));
    scene->addItem(new Edge(node7, node4));
    scene->addItem(new Edge(node8, node7));
    scene->addItem(new Edge(node9, node8));

    node1->setPos(-50, -50);
    node2->setPos(0, -50);
    node3->setPos(50, -50);
    node4->setPos(-50, 0);
    centerNode->setPos(0, 0);
    node6->setPos(50, 0);
    node7->setPos(-50, 50);
    node8->setPos(0, 50);
    node9->setPos(50, 50);
}

コンストラクタの最後の部分では、ノードとエッジのグリッドを作成し、各ノードに初期位置を与えます。

void GraphWidget::itemMoved()
{
    if (!timerId)
        timerId = startTimer(1000 / 25);
}

GraphWidget itemMoved() 、ノードの移動が通知されます。この関数の仕事はメイン・タイマーを再起動させることです。タイマーはグラフが安定したら停止し、また不安定になったら開始するように設計されている。

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);
    }
}

これはGraphWidget のキー・イベント・ハンドラである。矢印キーは中央のノードを移動させ、'+'キーと'-'キーはscaleView() を呼び出すことで拡大縮小し、エンターキーとスペースキーはノードの位置をランダムにする。その他のキー・イベント(ページ・アップやページ・ダウンなど)はすべて、QGraphicsView のデフォルト実装で処理される。

void GraphWidget::timerEvent(QTimerEvent *event)
{
    Q_UNUSED(event);

    QList<Node *> nodes;
    const QList<QGraphicsItem *> items = scene()->items();
    for (QGraphicsItem *item : items) {
        if (Node *node = qgraphicsitem_cast<Node *>(item))
            nodes << node;
    }

    for (Node *node : std::as_const(nodes))
        node->calculateForces();

    bool itemsMoved = false;
    for (Node *node : std::as_const(nodes)) {
        if (node->advancePosition())
            itemsMoved = true;
    }

    if (!itemsMoved) {
        killTimer(timerId);
        timerId = 0;
    }
}

タイマーイベントハンドラの仕事は、力計算機構全体を滑らかなアニメーションとして実行することです。タイマーがトリガーされるたびに、ハンドラはシーン内のすべてのノードを見つけ、Node::calculateForces() を各ノードで1つずつ呼び出します。そして最後のステップでは、Node::advance() を呼び出して、すべてのノードを新しい位置に移動します。advance() の戻り値をチェックすることで、グリッドが安定したかどうか(ノードが移動しなかったかどうか)を判断できます。そうであれば、タイマーを停止することができる。

void GraphWidget::wheelEvent(QWheelEvent *event)
{
    scaleView(pow(2., -event->angleDelta().y() / 240.0));
}

ホイールイベントハンドラーでは、マウスホイールのデルタをスケールファクターに変換し、このファクターをscaleView() に渡します。 このアプローチでは、ホイールの回転速度を考慮します。マウスホイールの回転が速ければ速いほど、ビューのズームも速くなります。

void GraphWidget::drawBackground(QPainter *painter, const QRectF &rect)
{
    Q_UNUSED(rect);

    // Shadow
    QRectF 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);

    // Fill
    QLinearGradient 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);

    // Text
    QRectF 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);
}

ビューの背景はQGraphicsView::drawBackground() の再実装でレンダリングされる。線形グラデーションで塗りつぶされた大きな矩形を描画し、ドロップシャドウを追加して、その上にテキストをレンダリングします。テキストは単純なドロップシャドウ効果のために2回レンダリングされます。

この背景のレンダリングにはかなりのコストがかかります。そのため、ビューではQGraphicsView::CacheBackground を有効にしています。

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);
}

scaleView() ヘルパー関数は、スケールファクターが一定の範囲内に収まっていることをチェックし(つまり、ズームインしすぎてもズームアウトしすぎてもいけない)、このスケールをビューに適用する。

main()関数

この例の他の部分の複雑さとは対照的に、main() 関数は非常にシンプルです:QApplication のインスタンスを作成し、GraphWidget のインスタンスを作成して表示します。グリッド内のすべてのノードは最初に移動されるため、GraphWidget タイマーは、制御がイベントループに戻った直後に開始されます。

プロジェクト例 @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。