Sur cette page

Exemple de nœuds élastiques

Démontre comment interagir avec des éléments graphiques dans une scène.

L'exemple des nœuds élastiques montre comment implémenter des arêtes entre les nœuds d'un graphe, avec une interaction de base. Vous pouvez cliquer sur un nœud pour le faire glisser, et effectuer un zoom avant et arrière à l'aide de la molette de la souris ou du clavier. Une pression sur la barre d'espacement rend les nœuds aléatoires. L'exemple est également indépendant de la résolution ; lorsque vous effectuez un zoom avant, les graphiques restent nets.

Application avec un graphe de nœuds et d'arêtes interactifs

Graphics View fournit la classe QGraphicsScene pour gérer et interagir avec un grand nombre d'éléments graphiques 2D personnalisés dérivés de la classe QGraphicsItem, ainsi qu'un widget QGraphicsView pour visualiser les éléments, avec prise en charge du zoom et de la rotation.

Cet exemple se compose d'une classe Node, d'une classe Edge, d'un test GraphWidget et d'une fonction main: la classe Node représente des nœuds jaunes glissants dans une grille, la classe Edge représente les lignes entre les nœuds, la classe GraphWidget représente la fenêtre de l'application et la fonction main() crée et affiche cette fenêtre et exécute la boucle d'événements.

Définition de la classe Node

La classe Node a trois fonctions :

  • Peindre une "boule" jaune en dégradé dans deux états : enfoncé et en relief.
  • Gérer les connexions avec d'autres nœuds.
  • Calculer les forces qui tirent et poussent les nœuds dans la grille.

Commençons par examiner la déclaration de la classe 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;
};

La classe Node hérite de QGraphicsItem et réimplémente les deux fonctions obligatoires boundingRect() et paint() pour fournir son apparence visuelle. Elle réimplémente également la fonction shape() pour s'assurer que sa zone d'impact a une forme elliptique (par opposition au rectangle de délimitation par défaut).

Pour la gestion des arêtes, le nœud fournit une API simple permettant d'ajouter des arêtes à un nœud et de répertorier toutes les arêtes connectées.

La réimplémentation advance() est appelée chaque fois que l'état de la scène avance d'un pas. La fonction calculateForces() est appelée pour calculer les forces qui poussent et tirent sur ce nœud et ses voisins.

La classe Node réimplémente également itemChange() pour réagir aux changements d'état (dans ce cas, les changements de position), et mousePressEvent() et mouseReleaseEvent() pour mettre à jour l'apparence visuelle de l'élément.

Nous commencerons par examiner l'implémentation de Node en nous penchant sur son constructeur :

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

Dans le constructeur, nous activons le drapeau ItemIsMovable pour permettre à l'élément de se déplacer en réponse au glissement de la souris, et ItemSendsGeometryChanges pour activer les notifications itemChange() pour les changements de position et de transformation. Nous activons également DeviceCoordinateCache pour accélérer les performances de rendu. Pour s'assurer que les nœuds sont toujours empilés sur les arêtes, nous fixons finalement la valeur Z de l'élément à -1.

NodeLe constructeur de la méthode prend un pointeur GraphWidget et le stocke en tant que variable membre. Nous reviendrons sur ce pointeur ultérieurement.

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

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

La fonction addEdge() ajoute l'arête d'entrée à une liste d'arêtes attachées. L'arête est ensuite ajustée de manière à ce que les points d'extrémité de l'arête correspondent aux positions des nœuds source et destination.

La fonction edges() renvoie simplement la liste des arêtes attachées.

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

Il existe deux façons de déplacer un nœud. La fonction calculateForces() met en œuvre l'effet élastique qui tire et pousse sur les nœuds de la grille. En outre, l'utilisateur peut déplacer directement un nœud à l'aide de la souris. Comme nous ne voulons pas que les deux approches opèrent en même temps sur le même nœud, nous démarrons calculateForces() en vérifiant si ce Node est l'élément actuel de la souris (c.-à-d. QGraphicsScene::mouseGrabberItem()). Comme nous devons trouver tous les nœuds voisins (mais pas nécessairement connectés), nous nous assurons également que l'élément fait partie d'une scène.

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

L'effet "élastique" provient d'un algorithme qui applique des forces de poussée et de traction. L'effet est impressionnant et étonnamment simple à mettre en œuvre.

L'algorithme comporte deux étapes : la première consiste à calculer les forces qui écartent les nœuds, et la seconde à soustraire les forces qui rapprochent les nœuds. Tout d'abord, nous devons trouver tous les nœuds du graphe. Nous appelons QGraphicsScene::items() pour trouver tous les éléments de la scène, puis nous utilisons qgraphicsitem_cast() pour rechercher les instances Node.

Nous utilisons mapFromItem() pour créer un vecteur temporaire pointant de ce nœud vers chaque autre nœud, en coordonnées locales. Nous utilisons les composantes décomposées de ce vecteur pour déterminer la direction et l'intensité de la force qui doit s'appliquer au nœud. Les forces s'accumulent pour chaque nœud et sont ensuite ajustées de manière à ce que les nœuds les plus proches reçoivent la force la plus forte, avec une dégradation rapide lorsque la distance augmente. La somme de toutes les forces est stockée sur xvel (vitesse X) et yvel (vitesse 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;
    }

Les arêtes entre les nœuds représentent les forces qui rapprochent les nœuds. En visitant chaque arête connectée à ce nœud, nous pouvons utiliser une approche similaire à celle décrite ci-dessus pour trouver la direction et l'intensité de toutes les forces d'attraction. Ces forces sont soustraites de xvel et yvel.

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

En théorie, la somme des forces de poussée et de traction devrait se stabiliser à 0 précisément, mais en pratique, ce n'est jamais le cas. Pour contourner les erreurs de précision numérique, nous forçons simplement la somme des forces à être égale à 0 lorsqu'elles sont inférieures à 0,1.

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

La dernière étape de calculateForces() détermine la nouvelle position du nœud. Nous ajoutons la force à la position actuelle du nœud. Nous nous assurons également que la nouvelle position reste à l'intérieur des limites définies. Nous ne déplaçons pas réellement l'élément dans cette fonction ; cela se fait dans une étape distincte, à partir de advance().

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

    setPos(newPos);
    return true;
}

La fonction advance() met à jour la position actuelle de l'élément. Elle est appelée à partir de GraphWidget::timerEvent(). Si la position du nœud a changé, la fonction renvoie true (vrai) ; sinon, elle renvoie false (faux).

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

Le rectangle de délimitation de Node est un rectangle de 20x20 centré sur son origine (0, 0), ajusté de 2 unités dans toutes les directions pour compenser le trait de contour du nœud, et de 3 unités vers le bas et la droite pour faire de la place à une ombre portée simple.

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

La forme est une simple ellipse. Vous devez donc cliquer à l'intérieur de la forme elliptique du nœud pour le faire glisser. Vous pouvez tester cet effet en exécutant l'exemple et en zoomant de manière à ce que les nœuds soient très grands. Sans réimplémenter shape(), la zone d'impact de l'objet serait identique à son rectangle de délimitation (c'est-à-dire rectangulaire).

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

Cette fonction implémente la peinture du nœud. Nous commençons par dessiner une simple ombre portée elliptique gris foncé à (-7, -7), c'est-à-dire à (3, 3) unités vers le bas et vers la droite à partir du coin supérieur gauche (-10, -10) de l'ellipse.

Nous dessinons ensuite une ellipse avec un remplissage à gradient radial. Ce remplissage est soit Qt::yellow à Qt::darkYellow lorsqu'il est élevé, soit l'inverse lorsqu'il est enfoncé. En cas d'enfoncement, nous déplaçons également le centre et le point focal de (3, 3) pour accentuer l'impression que quelque chose a été poussé vers le bas.

Dessiner des ellipses remplies avec des dégradés peut être assez lent, surtout lorsqu'on utilise des dégradés complexes tels que QRadialGradient. C'est pourquoi cet exemple utilise DeviceCoordinateCache, une mesure simple mais efficace qui évite de redessiner inutilement.

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

Nous réimplémentons itemChange() pour ajuster la position de toutes les arêtes connectées et pour informer la scène qu'un élément a été déplacé (c'est-à-dire que "quelque chose s'est produit"). Cela déclenchera de nouveaux calculs de force.

Cette notification est la seule raison pour laquelle les nœuds doivent conserver un pointeur vers GraphWidget. Une autre approche pourrait consister à fournir cette notification à l'aide d'un signal ; dans ce cas, Node devrait hériter de QGraphicsObject.

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

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

Comme nous avons activé l'indicateur ItemIsMovable, nous n'avons pas besoin d'implémenter la logique qui déplace le nœud en fonction de l'entrée de la souris ; cela est déjà prévu pour nous. Nous devons cependant réimplémenter les gestionnaires d'appui et de relâchement de la souris pour mettre à jour l'apparence visuelle des nœuds (c'est-à-dire enfoncés ou soulevés).

Définition de la classe Edge

La classe Edge représente les lignes fléchées entre les nœuds dans cet exemple. Cette classe est très simple : elle conserve un pointeur de nœud source et un pointeur de nœud de destination, et fournit une fonction adjust() qui garantit que la ligne commence à la position de la source et se termine à la position de la destination. Les arêtes sont les seuls éléments qui changent continuellement lorsque des forces tirent et poussent sur les nœuds.

Jetons un coup d'œil à la déclaration de la classe :

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 hérite de QGraphicsItem, car il s'agit d'une classe simple qui n'a pas besoin de signaux, d'emplacements et de propriétés (par rapport à QGraphicsObject).

Le constructeur prend deux pointeurs de nœuds en entrée. Les deux pointeurs sont obligatoires dans cet exemple. Nous fournissons également des fonctions d'obtention pour chaque nœud.

La fonction adjust() repositionne l'arête, et l'élément implémente également boundingRect() et paint().

Nous allons maintenant passer en revue son implémentation.

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

Le constructeur Edge initialise son membre de données arrowSize à 10 unités, ce qui détermine la taille de la flèche dessinée dans paint().

Dans le corps du constructeur, nous appelons setAcceptedMouseButtons(0). Cela garantit que les éléments du bord ne sont pas du tout pris en compte pour l'entrée de la souris (c'est-à-dire que vous ne pouvez pas cliquer sur les bords). Ensuite, les pointeurs de source et de destination sont mis à jour, cette arête est enregistrée avec chaque nœud et nous appelons adjust() pour mettre à jour la position de départ et d'arrivée de cette arête.

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

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

Les fonctions d'obtention de la source et de la destination renvoient simplement les pointeurs respectifs.

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

Dans adjust(), nous définissons deux points : sourcePoint et destPoint, qui pointent respectivement vers les origines des nœuds source et destination. Chaque point est calculé à l'aide de coordonnées locales.

Nous voulons que la pointe des flèches de l'arête pointe vers le contour exact des nœuds, plutôt que vers le centre des nœuds. Pour trouver ce point, nous décomposons d'abord le vecteur allant du centre de la source au centre du nœud de destination en X et Y, puis nous normalisons les composantes en les divisant par la longueur du vecteur. Nous obtenons ainsi un delta unitaire X et Y qui, multiplié par le rayon du nœud (qui est de 10), nous donne le décalage qui doit être ajouté à un point de l'arête et soustrait de l'autre.

Si la longueur du vecteur est inférieure à 20 (c'est-à-dire si deux nœuds se chevauchent), nous fixons le pointeur de source et de destination au centre du nœud source. En pratique, ce cas est très difficile à reproduire manuellement, car les forces entre les deux nœuds sont alors maximales.

Il est important de noter que nous appelons prepareGeometryChange() dans cette fonction. La raison en est que les variables sourcePoint et destPoint sont utilisées directement lors de la peinture et qu'elles sont renvoyées par la réimplémentation boundingRect(). Nous devons toujours appeler prepareGeometryChange() avant de modifier ce que boundingRect() renvoie, et avant que ces variables puissent être utilisées par paint(), pour que la comptabilité interne de Graphics View reste propre. Il est plus sûr d'appeler cette fonction une seule fois, immédiatement avant que l'une de ces variables ne soit modifiée.

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

Le rectangle de délimitation de l'arête est défini comme le plus petit rectangle qui inclut à la fois le point de départ et le point d'arrivée de l'arête. Comme nous dessinons une flèche sur chaque bord, nous devons également compenser en ajustant avec la moitié de la taille de la flèche et la moitié de la largeur du stylo dans toutes les directions. Le stylo est utilisé pour dessiner le contour de la flèche, et nous pouvons supposer que la moitié du contour peut être dessinée à l'extérieur de la zone de la flèche, et que l'autre moitié sera dessinée à l'intérieur.

void Edge::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    if (!source || !dest)
        return;

    QLineF line(sourcePoint, destPoint);
    if (qFuzzyCompare(line.length(), qreal(0.)))
        return;

Nous commençons la réimplémentation de paint() en vérifiant quelques conditions préalables. Tout d'abord, si le nœud source ou le nœud de destination n'est pas défini, nous retournons immédiatement ; il n'y a rien à dessiner.

En même temps, nous vérifions si la longueur de l'arête est approximativement égale à 0, et si c'est le cas, nous retournons également à la case départ.

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

Nous dessinons la ligne à l'aide d'un stylo dont les joints et les capuchons sont ronds. Si vous exécutez l'exemple, zoomez et étudiez le bord en détail, vous verrez qu'il n'y a pas d'arêtes vives ou carrées.

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

Nous dessinons une flèche à chaque extrémité de l'arête. Chaque flèche est dessinée comme un polygone avec un remplissage noir. Les coordonnées de la flèche sont déterminées à l'aide d'une simple trigonométrie.

Définition de la classe GraphWidget

GraphWidget est une sous-classe de QGraphicsView, qui fournit à la fenêtre principale des barres de défilement.

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

La classe fournit un constructeur de base qui initialise la scène, une fonction itemMoved() pour notifier les changements dans le graphe des nœuds de la scène, quelques gestionnaires d'événements, une réimplémentation de drawBackground() et une fonction d'aide pour la mise à l'échelle de la vue à l'aide de la molette de la souris ou du clavier.

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

GraphicsWidgetLe constructeur du logiciel crée la scène et, comme la plupart des éléments se déplacent la plupart du temps, il définit QGraphicsScene::NoIndex. La scène reçoit ensuite une valeur fixe scene rectangle, et est affectée à la vue GraphWidget.

La vue permet à QGraphicsView::CacheBackground de mettre en cache le rendu de son arrière-plan statique et quelque peu complexe. Étant donné que le graphique rend une collection étroite de petits éléments qui se déplacent tous, il n'est pas nécessaire que la vue graphique perde du temps à trouver des régions de mise à jour précises, c'est pourquoi nous définissons le mode de mise à jour de la vue QGraphicsView::BoundingRectViewportUpdate. Le mode par défaut fonctionnerait très bien, mais ce mode est nettement plus rapide pour cet exemple.

Pour améliorer la qualité du rendu, nous avons défini QPainter::Antialiasing.

L'ancre de transformation détermine la manière dont la vue doit défiler lorsque vous la transformez ou, dans notre cas, lorsque vous effectuez un zoom avant ou arrière. Nous avons choisi QGraphicsView::AnchorUnderMouse, qui centre la vue sur le point situé sous le curseur de la souris. Il est ainsi facile de zoomer sur un point de la scène en déplaçant la souris dessus, puis en faisant tourner la molette de la souris.

Enfin, nous donnons à la fenêtre une taille minimale correspondant à la taille par défaut de la scène, et définissons un titre de fenêtre approprié.

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

La dernière partie du constructeur crée la grille de nœuds et d'arêtes, et donne à chaque nœud une position initiale.

void GraphWidget::itemMoved()
{
    using namespace std::chrono_literals;

    if (!timer.isActive())
        timer.start(1000ms / 25, this);
}

GraphWidget est informé du mouvement des nœuds par l'intermédiaire de cette fonction itemMoved(). Son rôle est simplement de redémarrer la minuterie principale si elle n'est pas déjà en cours d'exécution. La minuterie est conçue pour s'arrêter lorsque le graphe se stabilise et pour redémarrer lorsqu'il est à nouveau instable.

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

Voici le gestionnaire d'événements des touches de GraphWidget. Les touches fléchées déplacent le nœud central, les touches '+' et '-' effectuent un zoom avant et arrière en appelant scaleView(), et les touches 'enter' et 'space' rendent aléatoire la position des nœuds. Tous les autres événements liés aux touches (par exemple, page précédente et page suivante) sont gérés par l'implémentation par défaut de 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();
}

Le gestionnaire d'événements de la minuterie a pour tâche d'exécuter l'ensemble de la machinerie de calcul de la force sous la forme d'une animation fluide. Chaque fois que la minuterie est déclenchée, le gestionnaire trouve tous les nœuds de la scène et appelle Node::calculateForces() sur chaque nœud, un à la fois. Puis, dans une dernière étape, il appellera Node::advance() pour déplacer tous les nœuds vers leur nouvelle position. En vérifiant la valeur de retour de advance(), nous pouvons déterminer si la grille s'est stabilisée (c'est-à-dire qu'aucun nœud n'a été déplacé). Si c'est le cas, nous pouvons arrêter le minuteur.

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

Dans le gestionnaire d'événement de la roue, nous convertissons le delta de la roue de la souris en un facteur d'échelle et nous transmettons ce facteur à scaleView(). Cette approche tient compte de la vitesse à laquelle la roue est actionnée. Plus vous faites tourner la molette rapidement, plus le zoom sur la vue est rapide.

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

L'arrière-plan de la vue est rendu dans une réimplémentation de QGraphicsView::drawBackground(). Nous dessinons un grand rectangle rempli d'un dégradé linéaire, nous ajoutons une ombre portée, puis nous rendons le texte par-dessus. Le texte est rendu deux fois pour un simple effet d'ombre portée.

Ce rendu en arrière-plan est assez coûteux ; c'est pourquoi la vue active 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);
}

La fonction d'aide scaleView() vérifie que le facteur d'échelle reste dans certaines limites (c'est-à-dire que vous ne pouvez pas zoomer trop loin, ni trop loin), puis applique cette échelle à la vue.

La fonction main()

Contrairement à la complexité du reste de cet exemple, la fonction main() est très simple : Nous créons une instance de QApplication, puis nous créons et affichons une instance de GraphWidget. Comme tous les nœuds de la grille sont déplacés initialement, le minuteur de GraphWidget démarre immédiatement après le retour du contrôle dans la boucle d'événements.

Exemple de projet @ code.qt.io

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