Beispiel für elastische Knoten
Veranschaulicht die Interaktion mit grafischen Elementen in einer Szene.
Das Beispiel „Elastische Knoten“ zeigt, wie Kanten zwischen Knoten in einem Graphen mit grundlegenden Interaktionsmöglichkeiten implementiert werden. Sie können einen Knoten per Klick verschieben und mit dem Mausrad oder der Tastatur hinein- und herauszoomen. Durch Drücken der Leertaste werden die Knoten zufällig angeordnet. Das Beispiel ist zudem auflösungsunabhängig; beim Heranzoomen bleiben die Grafiken scharf.

Graphics View stellt die Klasse „ QGraphicsScene “ zur Verwaltung und Interaktion mit einer großen Anzahl benutzerdefinierter 2D-Grafikelemente bereit, die von der Klasse „ QGraphicsItem “ abgeleitet sind, sowie das Widget „ QGraphicsView “ zur Visualisierung der Elemente mit Unterstützung für Zoomen und Drehen.
Dieses Beispiel besteht aus einer „ Node “-Klasse, einer „ Edge “-Klasse, einem „ GraphWidget “-Test und einer „ main “-Funktion: Die „ Node “-Klasse repräsentiert verschiebbare gelbe Knoten in einem Raster, die „ Edge “-Klasse repräsentiert die Linien zwischen den Knoten, die „ GraphWidget “-Klasse repräsentiert das Anwendungsfenster und die „ main() “-Funktion erstellt und zeigt dieses Fenster an sowie führt die Ereignisschleife aus.
Definition der Klasse „Node“
Die Klasse „ Node “ erfüllt drei Zwecke:
- Zeichnen einer gelben „Kugel“ mit Farbverlauf in zwei Zuständen: eingedrückt und angehoben.
- Verwaltung der Verbindungen zu anderen Knoten.
- Berechnung der Kräfte, die die Knoten im Raster ziehen und schieben.
Schauen wir uns zunächst die Deklaration der Klasse „ Node “ an.
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;
};Die Klasse „ Node “ erbt von „ QGraphicsItem “ und implementiert die beiden obligatorischen Funktionen „ boundingRect()“ und „ paint()“ neu, um ihr visuelles Erscheinungsbild festzulegen. Außerdem implementiert sie „ shape()“ neu, um sicherzustellen, dass ihr Trefferbereich eine elliptische Form hat (im Gegensatz zum standardmäßigen Begrenzungsrechteck).
Zur Verwaltung der Kanten bietet der Knoten eine einfache API zum Hinzufügen von Kanten zu einem Knoten sowie zum Auflisten aller verbundenen Kanten.
Die Neuimplementierung von ` advance()` wird aufgerufen, sobald sich der Zustand der Szene um einen Schritt weiterentwickelt. Die Funktion `calculateForces()` wird aufgerufen, um die Kräfte zu berechnen, die auf diesen Knoten und seine Nachbarn wirken.
Die Klasse „ Node “ implementiert außerdem die Methoden „ itemChange()“, um auf Zustandsänderungen (in diesem Fall Positionsänderungen) zu reagieren, sowie „ mousePressEvent()“ und „ mouseReleaseEvent()“, um das visuelle Erscheinungsbild des Elements zu aktualisieren.
Wir beginnen die Betrachtung der „ Node “-Implementierung mit einem Blick auf den Konstruktor:
Node::Node(GraphWidget *graphWidget)
: graph(graphWidget)
{
setFlag(ItemIsMovable);
setFlag(ItemSendsGeometryChanges);
setCacheMode(DeviceCoordinateCache);
setZValue(-1);
}Im Konstruktor setzen wir das Flag „ ItemIsMovable “, damit sich das Element beim Ziehen mit der Maus verschieben lässt, sowie „ ItemSendsGeometryChanges “, um Benachrichtigungen über itemChange() bei Positions- und Transformationsänderungen zu aktivieren. Außerdem aktivieren wir „ DeviceCoordinateCache “, um die Rendering-Leistung zu beschleunigen. Um sicherzustellen, dass die Knoten stets über den Kanten liegen, setzen wir schließlich den Z-Wert des Elements auf -1.
NodeDer Konstruktor von `` nimmt einen ` GraphWidget `-Zeiger entgegen und speichert diesen als Member-Variable. Wir werden später noch auf diesen Zeiger zurückkommen.
void Node::addEdge(Edge *edge)
{
edgeList << edge;
edge->adjust();
}
QList<Edge *> Node::edges() const
{
return edgeList;
}Die Funktion `addEdge()` fügt die eingegebene Kante einer Liste der angehängten Kanten hinzu. Die Kante wird anschließend so angepasst, dass die Endpunkte der Kante mit den Positionen des Quell- und des Zielknotens übereinstimmen.
Die Funktion `edges()` gibt einfach die Liste der verbundenen Kanten zurück.
void Node::calculateForces()
{
if (!scene() || scene()->mouseGrabberItem() == this) {
newPos = pos();
return;
}Es gibt zwei Möglichkeiten, einen Knoten zu verschieben. Die Funktion ` calculateForces() ` implementiert den elastischen Effekt, der Knoten im Raster zieht und schiebt. Darüber hinaus kann der Benutzer einen Knoten direkt mit der Maus verschieben. Da wir nicht möchten, dass beide Ansätze gleichzeitig auf denselben Knoten angewendet werden, prüfen wir zu Beginn von „ calculateForces() “, ob dieser Node das aktuell mit der Maus ausgewählte Element ist (d. h. QGraphicsScene::mouseGrabberItem()). Da wir alle benachbarten (aber nicht unbedingt verbundenen) Knoten finden müssen, stellen wir außerdem zunächst sicher, dass das Element Teil einer Szene ist.
// 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;
}
}Der „elastische“ Effekt entsteht durch einen Algorithmus, der Schub- und Zugkräfte anwendet. Der Effekt ist beeindruckend und überraschend einfach zu implementieren.
Der Algorithmus besteht aus zwei Schritten: Zunächst werden die Kräfte berechnet, die die Knoten auseinanderdrücken, und anschließend werden die Kräfte subtrahiert, die die Knoten zusammenziehen. Zunächst müssen wir alle Knoten im Graphen ermitteln. Wir rufen ` QGraphicsScene::items()` auf, um alle Elemente in der Szene zu finden, und verwenden dann ` qgraphicsitem_cast()` zur Suche nach ` Node `-Instanzen.
Wir nutzen ` mapFromItem()` (), um einen temporären Vektor zu erstellen, der von diesem Knoten zu jedem anderen Knoten in lokalen Koordinaten zeigt. Wir verwenden die zerlegten Komponenten dieses Vektors, um die Richtung und Stärke der Kraft zu bestimmen, die auf den Knoten wirken soll. Die Kräfte summieren sich für jeden Knoten und werden anschließend so angepasst, dass die nächstgelegenen Knoten die stärkste Kraft erhalten, wobei die Kraft mit zunehmendem Abstand rapide abnimmt. Die Summe aller Kräfte wird in „ xvel “ (X-Geschwindigkeit) und „ yvel “ (Y-Geschwindigkeit) gespeichert.
// 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;
}Die Kanten zwischen den Knoten stellen Kräfte dar, die die Knoten zueinander ziehen. Indem wir jede mit diesem Knoten verbundene Kante durchlaufen, können wir mit einem ähnlichen Ansatz wie oben die Richtung und Stärke aller Zugkräfte ermitteln. Diese Kräfte werden von xvel und yvel abgezogen.
Theoretisch sollte sich die Summe aus Schub- und Zugkräften genau auf 0 einpendeln. In der Praxis ist dies jedoch nie der Fall. Um Fehler in der numerischen Genauigkeit zu umgehen, setzen wir die Summe der Kräfte einfach auf 0, wenn sie kleiner als 0,1 ist.
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));
}Der letzte Schritt von ` calculateForces() ` bestimmt die neue Position des Knotens. Wir addieren die Kraft zur aktuellen Position des Knotens. Außerdem stellen wir sicher, dass die neue Position innerhalb der von uns definierten Grenzen bleibt. In dieser Funktion verschieben wir das Element noch nicht tatsächlich; dies geschieht in einem separaten Schritt über ` advance()`.
bool Node::advancePosition()
{
if (newPos == pos())
return false;
setPos(newPos);
return true;
}Die Funktion „ advance() “ aktualisiert die aktuelle Position des Objekts. Sie wird von „ GraphWidget::timerEvent() “ aufgerufen. Hat sich die Position des Knotens geändert, gibt die Funktion „true“ zurück; andernfalls wird „false“ zurückgegeben.
QRectF Node::boundingRect() const
{
qreal adjust = 2;
return QRectF( -10 - adjust, -10 - adjust, 23 + adjust, 23 + adjust);
}Das Begrenzungsrechteck von „ Node “ ist ein Rechteck der Größe 20×20, das um seinen Ursprung (0, 0) zentriert ist und in alle Richtungen um 2 Einheiten versetzt wurde, um den Umrissstrich des Knotens auszugleichen, sowie um 3 Einheiten nach unten und nach rechts, um Platz für einen einfachen Schlagschatten zu schaffen.
QPainterPath Node::shape() const
{
QPainterPath path;
path.addEllipse(-10, -10, 20, 20);
return path;
}Die Form ist eine einfache Ellipse. Dadurch wird sichergestellt, dass man innerhalb der elliptischen Form des Knotens klicken muss, um ihn zu verschieben. Sie können diesen Effekt testen, indem Sie das Beispiel ausführen und stark hineinzoomen, sodass die Knoten sehr groß erscheinen. Ohne die Neuimplementierung von ` shape()` wäre der Trefferbereich des Elements identisch mit seinem Begrenzungsrechteck (d. h. rechteckig).
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);
}Diese Funktion implementiert die Darstellung des Knotens. Wir beginnen damit, einen einfachen dunkelgrauen elliptischen Schlagschatten bei (-7, -7) zu zeichnen, d. h. (3, 3) Einheiten nach unten und nach rechts von der oberen linken Ecke (-10, -10) der Ellipse.
Anschließend zeichnen wir eine Ellipse mit einer radialen Farbverlaufsfüllung. Diese Füllung verläuft entweder von Qt::yellow nach Qt::darkYellow, wenn der Knoten angehoben ist, oder umgekehrt, wenn er abgesenkt ist. Im abgesenkten Zustand verschieben wir zudem den Mittelpunkt und den Brennpunkt um (3, 3), um den Eindruck zu verstärken, dass etwas nach unten gedrückt wurde.
Das Zeichnen gefüllter Ellipsen mit Farbverläufen kann recht langsam sein, insbesondere bei komplexen Farbverläufen wie „ QRadialGradient “. Aus diesem Grund verwendet dieses Beispiel „ DeviceCoordinateCache “ – eine einfache, aber effektive Maßnahme, die unnötiges Neuzeichnen verhindert.
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);
}Wir implementieren „ itemChange()“ neu, um die Position aller verbundenen Kanten anzupassen und der Szene mitzuteilen, dass sich ein Element bewegt hat (d. h. „etwas ist passiert“). Dies löst neue Kraftberechnungen aus.
Diese Benachrichtigung ist der einzige Grund, warum die Knoten einen Zeiger zurück auf ` GraphWidget` führen müssen. Ein anderer Ansatz könnte darin bestehen, eine solche Benachrichtigung über ein Signal zu übermitteln; in diesem Fall müsste ` Node ` von ` QGraphicsObject` erben.
void Node::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
update();
QGraphicsItem::mousePressEvent(event);
}
void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
update();
QGraphicsItem::mouseReleaseEvent(event);
}Da wir das Flag „ ItemIsMovable “ gesetzt haben, müssen wir die Logik, die den Knoten entsprechend der Mauseingabe verschiebt, nicht implementieren; diese wird uns bereits bereitgestellt. Wir müssen jedoch weiterhin die Handler für das Drücken und Loslassen der Maustaste neu implementieren, um das visuelle Erscheinungsbild der Knoten (d. h. eingedrückt oder angehoben) zu aktualisieren.
Definition der Edge-Klasse
Die Klasse „ Edge “ repräsentiert in diesem Beispiel die Pfeillinien zwischen den Knoten. Die Klasse ist sehr einfach aufgebaut: Sie verwaltet einen Zeiger auf den Quell- und einen auf den Zielknoten und stellt eine Funktion „ adjust() “ bereit, die sicherstellt, dass die Linie an der Position des Quellknotens beginnt und an der Position des Zielknotens endet. Die Kanten sind die einzigen Elemente, die sich kontinuierlich verändern, während Kräfte an den Knoten ziehen und drücken.
Werfen wir einen Blick auf die Klassendeklaration:
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 erbt von ` QGraphicsItem`, da es sich um eine einfache Klasse handelt, die keine Signale, Slots und Eigenschaften benötigt (vergleiche mit ` QGraphicsObject`).
Der Konstruktor nimmt zwei Knotenzeiger als Eingabe entgegen. Beide Zeiger sind in diesem Beispiel obligatorisch. Außerdem stellen wir für jeden Knoten Get-Funktionen bereit.
Die Funktion ` adjust() ` verschiebt die Kante, und das Element implementiert außerdem ` boundingRect()` und ` paint()`.
Wir werden nun die Implementierung näher betrachten.
Edge::Edge(Node *sourceNode, Node *destNode)
: source(sourceNode), dest(destNode)
{
setAcceptedMouseButtons(Qt::NoButton);
source->addEdge(this);
dest->addEdge(this);
adjust();
}Der Konstruktor von ` Edge ` initialisiert sein Datenelement ` arrowSize ` auf 10 Einheiten; dies bestimmt die Größe des Pfeils, der in ` paint()` gezeichnet wird.
Im Konstruktorkörper rufen wir ` setAcceptedMouseButtons(0)` auf. Dies stellt sicher, dass die Kantenelemente bei der Mauseingabe überhaupt nicht berücksichtigt werden (d. h., man kann nicht auf die Kanten klicken). Anschließend werden die Quell- und Zielzeiger aktualisiert, diese Kante wird bei jedem Knoten registriert, und wir rufen ` adjust() ` auf, um die Start- und Endposition dieser Kante zu aktualisieren.
Node *Edge::sourceNode() const
{
return source;
}
Node *Edge::destNode() const
{
return dest;
}Die Get-Funktionen für Quelle und Ziel geben einfach die jeweiligen Zeiger zurück.
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();
}
}In ` adjust()` definieren wir zwei Punkte: ` sourcePoint` und ` destPoint`, die jeweils auf die Ursprünge des Quell- und des Zielknotens zeigen. Jeder Punkt wird anhand lokaler Koordinaten berechnet.
Wir möchten, dass die Spitzen der Kantenpfeile auf den exakten Umriss der Knoten zeigen und nicht auf deren Mittelpunkt. Um diesen Punkt zu ermitteln, zerlegen wir zunächst den Vektor, der vom Mittelpunkt des Quellknotens zum Mittelpunkt des Zielknotens zeigt, in X- und Y-Komponenten und normalisieren diese anschließend, indem wir sie durch die Länge des Vektors dividieren. Dadurch erhalten wir ein X- und ein Y-Einheiten-Delta, das, wenn es mit dem Radius des Knotens (der 10 beträgt) multipliziert wird, den Versatz ergibt, der zu einem Punkt der Kante addiert und vom anderen subtrahiert werden muss.
Ist die Länge des Vektors kleiner als 20 (d. h., wenn sich zwei Knoten überlappen), legen wir den Quell- und Zielzeiger auf die Mitte des Quellknotens fest. In der Praxis lässt sich dieser Fall manuell nur sehr schwer reproduzieren, da die Kräfte zwischen den beiden Knoten dann ihr Maximum erreichen.
Es ist wichtig zu beachten, dass wir in dieser Funktion ` prepareGeometryChange()` aufrufen. Der Grund dafür ist, dass die Variablen ` sourcePoint ` und ` destPoint ` direkt beim Zeichnen verwendet werden und sie von der Neuimplementierung von ` boundingRect()` zurückgegeben werden. Wir müssen immer „ prepareGeometryChange()“ aufrufen, bevor wir den Rückgabewert von „ boundingRect()“ ändern und bevor diese Variablen von „ paint()“ verwendet werden können, um die interne Verwaltung der Graphics View sauber zu halten. Am sichersten ist es, diese Funktion einmalig unmittelbar vor der Änderung einer solchen Variablen aufzurufen.
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);
}Das Begrenzungsrechteck der Kante ist definiert als das kleinste Rechteck, das sowohl den Start- als auch den Endpunkt der Kante enthält. Da wir auf jeder Kante einen Pfeil zeichnen, müssen wir dies durch eine Anpassung um die Hälfte der Pfeilgröße und die Hälfte der Stiftbreite in alle Richtungen kompensieren. Der Stift wird zum Zeichnen des Umrisses des Pfeils verwendet, und wir können davon ausgehen, dass die Hälfte des Umrisses außerhalb des Pfeilbereichs und die andere Hälfte innerhalb gezeichnet wird.
void Edge::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
if (!source || !dest)
return;
QLineF line(sourcePoint, destPoint);
if (qFuzzyCompare(line.length(), qreal(0.)))
return;Wir beginnen die Neuimplementierung von ` paint()` mit der Überprüfung einiger Vorbedingungen. Erstens: Wenn entweder der Quell- oder der Zielknoten nicht gesetzt ist, kehren wir sofort zurück; es gibt nichts zu zeichnen.
Gleichzeitig prüfen wir, ob die Länge der Kante ungefähr 0 ist; ist dies der Fall, kehren wir ebenfalls zurück.
// Draw the line itself
painter->setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
painter->drawLine(line);Wir zeichnen die Linie mit einem Stift, der abgerundete Verbindungsstellen und Enden hat. Wenn du das Beispiel ausführst, heranzoomst und die Kante im Detail betrachtest, wirst du sehen, dass es keine scharfen/eckigen Kanten gibt.
// 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);
}Anschließend zeichnen wir an jedem Ende der Kante einen Pfeil. Jeder Pfeil wird als Polygon mit schwarzer Füllung gezeichnet. Die Koordinaten für den Pfeil werden mithilfe einfacher Trigonometrie bestimmt.
Definition der Klasse „GraphWidget“
GraphWidget ist eine Unterklasse von ` QGraphicsView`, die das Hauptfenster mit Bildlaufleisten bereitstellt.
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;
};Die Klasse stellt einen Basis-Konstruktor zur Initialisierung der Szene, eine ` itemMoved() `-Funktion zur Benachrichtigung über Änderungen im Knotengraf der Szene, einige Ereignisbehandler, eine Neuimplementierung von ` drawBackground()` sowie eine Hilfsfunktion zur Skalierung der Ansicht mithilfe des Mausrads oder der Tastatur bereit.
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"));GraphicsWidgetDer Konstruktor erstellt die Szene, und da sich die meisten Elemente die meiste Zeit über bewegen, setzt er ` QGraphicsScene::NoIndex`. Die Szene erhält dann eine feste ` scene rectangle` und wird der Ansicht ` GraphWidget ` zugewiesen.
Die Ansicht ermöglicht es „ QGraphicsView::CacheBackground “, das Rendern ihres statischen und etwas komplexen Hintergrunds zwischenzuspeichern. Da der Graph eine dichte Ansammlung kleiner Elemente rendert, die sich alle bewegen, ist es für „Graphics View“ unnötig, Zeit mit der Suche nach genauen Aktualisierungsbereichen zu verschwenden; daher setzen wir den Viewport-Aktualisierungsmodus auf „ QGraphicsView::BoundingRectViewportUpdate “. Der Standardmodus würde zwar gut funktionieren, aber dieser Modus ist in diesem Beispiel spürbar schneller.
Um die Darstellungsqualität zu verbessern, setzen wir „ QPainter::Antialiasing “.
Der Transformationsanker legt fest, wie die Ansicht scrollen soll, wenn Sie die Ansicht transformieren – in unserem Fall also beim Vergrößern oder Verkleinern. Wir haben „ QGraphicsView::AnchorUnderMouse “ gewählt, wodurch die Ansicht auf den Punkt unter dem Mauszeiger zentriert wird. So lässt sich ganz einfach auf einen Punkt in der Szene heranzoomen, indem man die Maus darüber bewegt und dann das Mausrad dreht.
Schließlich weisen wir dem Fenster eine Mindestgröße zu, die der Standardgröße der Szene entspricht, und legen einen passenden Fenstertitel fest.
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);
}Der letzte Teil des Konstruktors erstellt das Gitter aus Knoten und Kanten und weist jedem Knoten eine Anfangsposition zu.
void GraphWidget::itemMoved()
{
using namespace std::chrono_literals;
if (!timer.isActive())
timer.start(1000ms / 25, this);
}GraphWidget wird über die Funktion ` itemMoved() ` über Knotenbewegungen informiert. Ihre Aufgabe besteht lediglich darin, den Haupttimer neu zu starten, falls dieser noch nicht läuft. Der Timer ist so ausgelegt, dass er stoppt, wenn sich der Graph stabilisiert, und erneut startet, sobald er wieder instabil ist.
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);
}
}Dies ist der zentrale Ereignis-Handler von ` GraphWidget`. Die Pfeiltasten verschieben den zentralen Knoten, die Tasten „+“ und „-“ zoomen durch Aufruf von ` scaleView()` hinein und heraus, und die Eingabetaste sowie die Leertaste randomisieren die Positionen der Knoten. Alle anderen Tastenereignisse (z. B. Bild auf und Bild ab) werden von der Standardimplementierung von ` QGraphicsView` verarbeitet.
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();
}Die Aufgabe des Timer-Ereignis-Handlers besteht darin, den gesamten Mechanismus der Kraftberechnung als flüssige Animation auszuführen. Jedes Mal, wenn der Timer ausgelöst wird, ermittelt der Handler alle Knoten in der Szene und ruft nacheinander für jeden Knoten die Funktion „ Node::calculateForces() “ auf. Anschließend ruft er in einem letzten Schritt „ Node::advance() “ auf, um alle Knoten an ihre neuen Positionen zu verschieben. Anhand des Rückgabewerts von „ advance() “ können wir feststellen, ob sich das Gitter stabilisiert hat (d. h., ob sich keine Knoten mehr bewegt haben). Ist dies der Fall, können wir den Timer anhalten.
void GraphWidget::wheelEvent(QWheelEvent *event)
{
scaleView(pow(2., -event->angleDelta().y() / 240.0));
}Im Ereignis-Handler für das Mausrad wandeln wir die Mausrad-Differenz in einen Skalierungsfaktor um und übergeben diesen Faktor an ` scaleView()`. Dieser Ansatz berücksichtigt die Geschwindigkeit, mit der das Mausrad gedreht wird. Je schneller Sie das Mausrad drehen, desto schneller wird die Ansicht gezoomt.
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);
}Der Hintergrund der Ansicht wird in einer Neuimplementierung von ` QGraphicsView::drawBackground()` gerendert. Wir zeichnen ein großes Rechteck, das mit einem linearen Farbverlauf gefüllt ist, fügen einen Schlagschatten hinzu und rendern dann Text darüber. Der Text wird zweimal gerendert, um einen einfachen Schlagschatteneffekt zu erzielen.
Diese Hintergrunddarstellung ist recht rechenintensiv; deshalb aktiviert die Ansicht ` 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);
}Die Hilfsfunktion ` scaleView() ` prüft, ob der Skalierungsfaktor innerhalb bestimmter Grenzen bleibt (d. h., man kann weder zu stark hinein- noch herauszoomen), und wendet diese Skalierung dann auf die Ansicht an.
Die Funktion `main()`
Im Gegensatz zur Komplexität des restlichen Beispiels ist die Funktion „ main() “ sehr einfach: Wir erstellen eine Instanz von „ QApplication “, anschließend eine Instanz von „ GraphWidget “ und zeigen diese an. Da alle Knoten im Raster zunächst verschoben werden, startet der Timer „ GraphWidget “ sofort, nachdem die Kontrolle an die Ereignisschleife zurückgegeben wurde.
© 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.