Elastic Nodes の例
シーン内のグラフィカルな要素とどのようにやり取りするかを実演します。
「Elastic Nodes」のサンプルでは、グラフ内のノード間にエッジを実装し、基本的な操作を行う方法を示しています。ノードをクリックしてドラッグしたり、マウスホイールやキーボードを使ってズームイン・ズームアウトしたりできます。スペースキーを押すと、ノードの位置がランダムに配置されます。また、このサンプルは解像度に依存しないため、ズームインしてもグラフィックは鮮明なままです。

Graphics View では、QGraphicsItem クラスを継承した多数のカスタム 2D グラフィック アイテムを管理および操作するための `QGraphicsScene ` クラスと、ズームや回転をサポートしてアイテムを視覚化する `QGraphicsView ` ウィジェットを提供しています。
この例は、Node クラス、Edge クラス、GraphWidget テスト、およびmain 関数で構成されています。Node クラスはグリッド上のドラッグ可能な黄色いノードを表し、Edge クラスはノード間の線を表し、GraphWidget クラスはアプリケーションウィンドウを表し、main() 関数はこのウィンドウを作成・表示し、イベントループを実行します。
Nodeクラスの定義
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 を継承しており、視覚的な外観を定義するために、2つの必須関数boundingRect()とpaint()を再実装しています。また、ヒット領域が(デフォルトのバウンディング矩形とは異なり)楕円形になるように、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 ` フラグを設定し、位置や変換の変更に対する `itemChange()` 通知を有効にするために `ItemSendsGeometryChanges ` を設定します。また、レンダリングパフォーマンスを向上させるために `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() 関数は、グリッド上のノードを引っ張ったり押したりする弾性効果を実装しています。さらに、ユーザーはマウスを使って1つのノードを直接移動させることもできます。 同じノードに対してこれら2つの処理が同時に実行されることを防ぐため、calculateForces() の処理開始時に、このNode が現在のマウスグラバー項目(つまり、QGraphicsScene::mouseGrabberItem ())であるかどうかを確認します。また、隣接する(必ずしも接続されているとは限らない)すべてのノードを見つける必要があるため、その項目がそもそもシーンの一部であることも確認します。
// 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;
}ノード間のエッジは、ノード同士を引き寄せる力を表しています。このノードに接続されている各エッジを順にたどることで、上記と同様の手法を用いて、すべての引力の向きと大きさを求めることができます。これらの力は、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));
}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×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 へのポインタを保持する必要がある唯一の理由です。別のアプローチとして、シグナルを使用してこのような通知を行うことも考えられます。その場合、Node はQGraphicsObject から継承する必要があります。
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;
}sourceおよびdestinationの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() では、sourcePoint とdestPoint の 2 つの点を定義し、それぞれ始点ノードと終点ノードの原点を指すようにします。各点はローカル座標を用いて計算されます。
エッジの矢印の先端は、ノードの中心ではなく、ノードの正確な輪郭を指すようにしたい。この点を見つけるために、まず、送信元ノードの中心から宛先ノードの中心を指すベクトルを X と Y に分解し、次にベクトルの長さで割って成分を正規化する。 これにより、XおよびY方向の単位デルタが得られ、これをノードの半径(10)で乗算することで、エッジの片方の端点に加算し、もう一方の端点から減算すべきオフセットが算出されます。
ベクトルの長さが 20 未満の場合(つまり、2 つのノードが重なっている場合)、送信元と宛先のポインタを送信元ノードの中心に固定します。実際には、このケースを手動で再現するのは非常に困難です。なぜなら、この状態では 2 つのノード間の力が最大になるからです。
この関数内でprepareGeometryChange()を呼び出すことに注意することが重要です。その理由は、変数sourcePoint とdestPoint が描画時に直接使用され、これらはboundingRect()の再実装から返されるためです。 Graphics Viewの内部管理を適切に保つため、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 であるかどうかも確認し、その場合は同様に戻ります。
// 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:
QBasicTimer timer;
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 を使用して、静的でやや複雑な背景のレンダリングをキャッシュできるようにしています。グラフは、互いに近く配置された多数の小さなアイテムをレンダリングし、それらがすべて移動するため、Graphics View が正確な更新領域を見つけるために時間を浪費する必要はありません。そこで、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()
{
using namespace std::chrono_literals;
if (!timer.isActive())
timer.start(1000ms / 25, this);
}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() を呼び出してズームイン・ズームアウトを行い、Enterキーとスペースキーでノードの位置をランダム化します。その他のすべてのキーイベント(Page UpやPage Downなど)は、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)
timer.stop();
}タイマーイベントハンドラの役割は、力学計算の仕組み全体を滑らかなアニメーションとして実行することです。タイマーがトリガーされるたびに、ハンドラはシーン内のすべてのノードを検索し、各ノードに対して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 タイマーが開始されます。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.