Elastic Nodes Beispiel

Zeigt, wie man mit grafischen Elementen in einer Szene interagieren kann.

Das Beispiel Elastic Nodes zeigt, wie man Kanten zwischen Knoten in einem Graphen implementiert, mit grundlegender Interaktion. Sie können klicken, um einen Knoten zu verschieben, und mit dem Mausrad oder der Tastatur hinein- und herauszoomen. Wenn Sie die Leertaste drücken, werden die Knoten zufällig angeordnet. Das Beispiel ist auch auflösungsunabhängig; wenn Sie hineinzoomen, bleibt die Grafik scharf.

Graphics View bietet die Klasse QGraphicsScene für die Verwaltung und Interaktion mit einer großen Anzahl von benutzerdefinierten 2D-Grafikelementen, die von der Klasse QGraphicsItem abgeleitet sind, und ein Widget QGraphicsView für die 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 ziehbare 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 und führt die Ereignisschleife aus.

Definition der Knotenklasse

Die Klasse Node dient drei Zwecken:

  • Sie malt einen gelben "Ball" mit Farbverlauf in zwei Zuständen: gesenkt und gehoben.
  • Sie verwaltet die Verbindungen zu anderen Knotenpunkten.
  • Berechnung der Kräfte, die die Knoten im Gitter 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 QGraphicsItem und implementiert die beiden obligatorischen Funktionen boundingRect() und paint() neu, um ihr visuelles Erscheinungsbild zu gewährleisten. Sie implementiert auch shape() neu, um sicherzustellen, dass ihr Trefferbereich eine elliptische Form hat (im Gegensatz zum standardmäßigen begrenzenden Rechteck).

Für die Kantenverwaltung bietet der Knoten eine einfache API zum Hinzufügen von Kanten zu einem Knoten und zum Auflisten aller verbundenen Kanten.

Die Neuimplementierung advance() wird immer dann aufgerufen, wenn der Zustand der Szene um einen Schritt fortschreitet. Die Funktion calculateForces() wird aufgerufen, um die Kräfte zu berechnen, die auf diesen Knoten und seine Nachbarn drücken und ziehen.

Die Klasse Node implementiert auch 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 Überprüfung der Implementierung von Node 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 als Reaktion auf das Ziehen mit der Maus bewegen kann, und ItemSendsGeometryChanges, um itemChange() Benachrichtigungen für Positions- und Transformationsänderungen zu aktivieren. Wir aktivieren auch DeviceCoordinateCache, um die Rendering-Leistung zu beschleunigen. Um sicherzustellen, dass die Knoten immer über die Kanten gestapelt werden, setzen wir den Z-Wert des Elements auf -1.

NodeDer Konstruktor des Elements nimmt einen Zeiger auf GraphWidget und speichert diesen als Mitgliedsvariable. Wir werden später 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 zu einer Liste von angehängten Kanten hinzu. Die Kante wird dann so angepasst, dass die Endpunkte der Kante mit den Positionen der Quell- und Zielknoten übereinstimmen.

Die Funktion edges() gibt einfach die Liste der angehängten 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 an Knoten im Gitter zieht und drückt. Darüber hinaus kann der Benutzer einen Knoten direkt mit der Maus verschieben. Da wir nicht wollen, dass die beiden Ansätze gleichzeitig auf denselben Knoten wirken, starten wir calculateForces(), indem wir prüfen, ob Node das aktuelle Element ist, das mit der Maus gegriffen wird (d. h. QGraphicsScene::mouseGrabberItem()). Da wir alle benachbarten (aber nicht notwendigerweise verbundenen) Knoten finden müssen, stellen wir auch sicher, dass das Element überhaupt 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 drückende und ziehende Kräfte anwendet. Der Effekt ist beeindruckend und erstaunlich einfach zu implementieren.

Der Algorithmus besteht aus zwei Schritten: Im ersten werden die Kräfte berechnet, die die Knoten auseinander drücken, und im zweiten werden die Kräfte abgezogen, die die Knoten zusammenziehen. Zuerst müssen wir alle Knoten im Graphen finden. Wir rufen QGraphicsScene::items() auf, um alle Elemente in der Szene zu finden, und verwenden dann qgraphicsitem_cast(), um nach Node Instanzen zu suchen.

Mit mapFromItem() erstellen wir einen temporären Vektor, 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 akkumulieren sich für jeden Knoten und werden dann so angepasst, dass die nächstgelegenen Knoten die stärkste Kraft erhalten, wobei die Kraft mit zunehmender Entfernung schnell 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 zusammenziehen. Indem wir jede Kante besuchen, die mit diesem Knoten verbunden ist, können wir einen ähnlichen Ansatz wie oben verwenden, um die Richtung und Stärke aller ziehenden Kräfte zu finden. Diese Kräfte werden von xvel und yvel subtrahiert.

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

Theoretisch sollte sich die Summe der Druck- und Zugkräfte auf genau 0 einpendeln, was in der Praxis jedoch nie der Fall ist. Um Fehler in der numerischen Präzision zu umgehen, zwingen wir die Summe der Kräfte einfach zu 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 zu der aktuellen Position des Knotens. Wir stellen auch sicher, dass die neue Position innerhalb der definierten Grenzen bleibt. Das Element wird in dieser Funktion nicht wirklich verschoben; das wird in einem separaten Schritt von advance() erledigt.

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. Wenn sich die Position des Knotens geändert hat, 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 20x20 großes Rechteck, das um seinen Ursprung (0, 0) zentriert ist und um 2 Einheiten in alle Richtungen angepasst wurde, um den Umriss des Knotens auszugleichen, sowie um 3 Einheiten nach unten und 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 Sie innerhalb der elliptischen Form des Knotens klicken müssen, um ihn zu verschieben. Sie können diesen Effekt testen, indem Sie das Beispiel ausführen und weit hineinzoomen, so dass die Knoten sehr groß sind. Ohne die Neuimplementierung von shape() wäre der Trefferbereich des Objekts 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 das Malen des Knotens. Wir beginnen mit dem Zeichnen eines einfachen dunkelgrauen elliptischen Schlagschattens bei (-7, -7), d. h. (3, 3) Einheiten nach unten und rechts von der oberen linken Ecke (-10, -10) der Ellipse.

Dann zeichnen wir eine Ellipse mit einer radialen Gradientenfüllung. Diese Füllung ist entweder Qt::yellow bis Qt::darkYellow, wenn sie erhöht ist, oder das Gegenteil, wenn sie gesenkt ist. Im abgesenkten Zustand verschieben wir außerdem den Mittelpunkt und den Brennpunkt um (3, 3), um den Eindruck zu verstärken, dass etwas nach unten gedrückt wurde.

Das Zeichnen von gefüllten Ellipsen mit Farbverläufen kann recht langsam sein, vor allem wenn man komplexe Farbverläufe wie QRadialGradient verwendet. Aus diesem Grund wird in diesem Beispiel DeviceCoordinateCache verwendet, 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 reimplementieren itemChange(), um die Position aller verbundenen Kanten anzupassen und um 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 behalten müssen. Ein anderer Ansatz könnte darin bestehen, eine solche Benachrichtigung über ein Signal bereitzustellen; 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, brauchen wir die Logik, die den Knoten entsprechend der Mauseingabe bewegt, nicht zu implementieren; dies ist bereits für uns vorgesehen. Wir müssen jedoch die Handler für das Drücken und Loslassen der Maus neu implementieren, um das visuelle Erscheinungsbild des Knotens zu aktualisieren (d. h. versenkt oder angehoben).

Definition der Klasse Edge

Die Klasse Edge stellt die Pfeillinien zwischen den Knoten in diesem Beispiel dar. Die Klasse ist sehr einfach: Sie verwaltet einen Quell- und einen Zielknotenzeiger und bietet eine adjust() Funktion, die sicherstellt, dass die Linie an der Position der Quelle beginnt und an der Position des Ziels endet. Die Kanten sind die einzigen Elemente, die sich kontinuierlich ändern, wenn 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 Verwendung für Signale, Slots und Eigenschaften hat (vgl. QGraphicsObject).

Der Konstruktor nimmt zwei Knotenzeiger als Eingabe entgegen. Beide Zeiger sind in diesem Beispiel obligatorisch. Wir stellen auch get-Funktionen für jeden Knoten bereit.

Die Funktion adjust() positioniert die Kante neu, und das Element implementiert auch boundingRect() und paint().

Wir werden nun ihre Implementierung überprüfen.

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

Der Edge Konstruktor initialisiert sein arrowSize Datenmitglied auf 10 Einheiten; dies bestimmt die Größe des Pfeils, der in paint() gezeichnet wird.

Im Körper des Konstruktors rufen wir setAcceptedMouseButtons(0) auf. Damit stellen wir sicher, dass die Kantenelemente für die Mauseingabe überhaupt nicht berücksichtigt werden (d. h. Sie können die Kanten nicht anklicken). Dann 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 entsprechenden 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 auf die Ursprünge der Quell- bzw. Zielknoten zeigen. Jeder Punkt wird mit lokalen Koordinaten berechnet.

Wir möchten, dass die Spitze der Pfeile der Kante genau auf die Umrisse der Knoten zeigt und nicht auf den Mittelpunkt der Knoten. Um diesen Punkt zu finden, zerlegen wir zunächst den Vektor, der vom Zentrum der Quelle zum Zentrum des Zielknotens zeigt, in X und Y und normalisieren dann die Komponenten, indem wir sie durch die Länge des Vektors teilen. Auf diese Weise erhalten wir ein X- und Y-Einheitsdelta, das, wenn es mit dem Radius des Knotens (10) multipliziert wird, den Versatz ergibt, der zu einem Punkt der Kante addiert und von dem anderen subtrahiert werden muss.

Ist die Länge des Vektors kleiner als 20 (d.h. wenn sich zwei Knoten überschneiden), dann fixieren wir den Quell- und den Zielzeiger auf den Mittelpunkt des Quellknotens. In der Praxis ist dieser Fall manuell nur sehr schwer zu reproduzieren, da die Kräfte zwischen den beiden Knoten dann am größten sind.

Es ist wichtig zu beachten, dass wir prepareGeometryChange() in dieser Funktion aufrufen. Der Grund dafür ist, dass die Variablen sourcePoint und destPoint direkt beim Malen verwendet werden und von der Reimplementierung boundingRect() zurückgegeben werden. Wir müssen immer prepareGeometryChange() aufrufen, bevor wir ändern, was boundingRect() zurückgibt, und bevor diese Variablen von paint() verwendet werden können, um die interne Buchhaltung von Graphics View sauber zu halten. Am sichersten ist es, diese Funktion einmal aufzurufen, unmittelbar bevor eine solche Variable geändert wird.

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 einschließt. Da wir an jeder Kante einen Pfeil zeichnen, müssen wir auch einen Ausgleich schaffen, indem wir die halbe Pfeilgröße und die halbe Stiftbreite in alle Richtungen einstellen. Der Stift wird verwendet, um den Umriss des Pfeils zu zeichnen, und wir können davon ausgehen, dass die Hälfte des Umrisses außerhalb des Pfeilbereichs und die andere Hälfte innerhalb gezeichnet werden kann.

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 annähernd 0 ist, und wenn das der Fall ist, 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 runde Fugen und Kappen hat. Wenn Sie das Beispiel ausführen, heranzoomen und die Kante im Detail betrachten, werden Sie sehen, dass es keine scharfen/quadratischen 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);
}

Wir fahren fort und zeichnen einen Pfeil an jedem Ende der Kante. Jeder Pfeil wird als Polygon mit einer schwarzen Füllung gezeichnet. Die Koordinaten für den Pfeil werden mit einfacher Trigonometrie bestimmt.

GraphWidget Klassendefinition

GraphWidget ist eine Unterklasse von QGraphicsView, die das Hauptfenster mit Rollbalken versieht.

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

Die Klasse bietet einen einfachen Konstruktor, der die Szene initialisiert, eine itemMoved() Funktion, um Änderungen im Knotengraphen der Szene mitzuteilen, einige Ereignishandler, eine Reimplementierung von drawBackground() und eine Hilfsfunktion zum Skalieren der Ansicht mit dem Mausrad oder der Tastatur.

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 der Szene erstellt die Szene, und da sich die meisten Elemente die meiste Zeit 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 Rendering des statischen und etwas komplexen Hintergrunds zwischenzuspeichern. Da das Diagramm eine dichte Sammlung von kleinen Objekten rendert, die sich alle bewegen, ist es für die Grafikansicht unnötig, Zeit mit der Suche nach genauen Aktualisierungsbereichen zu verschwenden, also setzen wir den QGraphicsView::BoundingRectViewportUpdate viewport update mode. Die Standardeinstellung würde auch funktionieren, aber dieser Modus ist für dieses Beispiel deutlich schneller.

Um die Rendering-Qualität zu verbessern, stellen wir QPainter::Antialiasing ein.

Der Transformationsanker bestimmt, wie die Ansicht beim Transformieren der Ansicht oder in unserem Fall beim Vergrößern oder Verkleinern gescrollt werden soll. Wir haben QGraphicsView::AnchorUnderMouse gewählt, wodurch die Ansicht auf den Punkt unter dem Mauszeiger zentriert wird. Dies erleichtert das Zoomen zu einem bestimmten Punkt in der Szene, indem man die Maus darüber bewegt und dann das Mausrad dreht.

Schließlich geben wir dem Fenster eine Mindestgröße, die der Standardgröße der Szene entspricht, und setzen einen passenden Fenstertitel.

    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 gibt jedem Knoten eine Anfangsposition.

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

GraphWidget wird durch diese itemMoved() Funktion über Knotenbewegungen informiert. Ihre Aufgabe besteht einfach darin, den Hauptzeitgeber neu zu starten, falls er noch nicht läuft. Der Timer ist so konzipiert, dass er stoppt, wenn sich der Graph stabilisiert, und 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 GraphWidget's key event handler. Die Pfeiltasten verschieben den mittleren Knoten, die '+'- und '-'-Tasten vergrößern und verkleinern den Graphen durch den Aufruf von scaleView(), und die Eingabe- und Leertaste verändern die Position der Knoten zufällig. Alle anderen Tastenereignisse (z.B. Seite nach oben und Seite nach unten) werden von der Standardimplementierung von QGraphicsView behandelt.

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

Die Aufgabe des Timer-Ereignishandlers ist es, die gesamte Kraftberechnungsmaschinerie in einer flüssigen Animation ablaufen zu lassen. Jedes Mal, wenn der Timer ausgelöst wird, findet der Handler alle Knoten in der Szene und ruft Node::calculateForces() für jeden Knoten einzeln auf. In einem letzten Schritt ruft er dann 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. keine Knoten verschoben wurden). Wenn ja, können wir den Timer stoppen.

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

In der Rad-Ereignishandhabung wandeln wir das Mausrad-Delta in einen Skalierungsfaktor um und übergeben diesen Faktor an scaleView(). Dieser Ansatz berücksichtigt die Geschwindigkeit, mit der das Rad gerollt 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 mit 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 den Text darüber. Der Text wird zweimal gerendert, um einen einfachen Schlagschatteneffekt zu erzielen.

Dieses Rendering des Hintergrunds ist ziemlich teuer; deshalb ermöglicht 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 liegt (d. h. Sie können weder zu weit hinein- noch zu weit herauszoomen), und wendet dann diese Skalierung auf die Ansicht an.

Die main()-Funktion

Im Gegensatz zur Komplexität des restlichen Beispiels ist die Funktion main() sehr einfach: Wir erstellen eine Instanz von QApplication, dann erstellen wir eine Instanz von GraphWidget und zeigen sie an. Da alle Knoten im Raster zunächst verschoben werden, startet der Timer von GraphWidget sofort, nachdem die Kontrolle zur Ereignisschleife zurückgekehrt ist.

Beispielprojekt @ code.qt.io

© 2025 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.