Exemple de nœuds élastiques
Montre comment interagir avec les éléments graphiques d'une scène.
L'exemple « Nœuds élastiques » montre comment implémenter des arêtes entre des nœuds dans un graphe, avec des interactions de base. Vous pouvez cliquer pour faire glisser un nœud, et zoomer ou dézoomer à l'aide de la molette de la souris ou du clavier. Appuyer sur la barre d'espace permet de réorganiser les nœuds de manière aléatoire. L'exemple est également indépendant de la résolution ; lorsque vous zoomez, les graphiques restent nets.

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 ` permettant de visualiser ces é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 déplaçables 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, puis exécute la boucle d’événements.
Définition de la classe Node
La classe « Node » remplit trois fonctions :
- Afficher une « boule » avec un dégradé jaune dans deux états : enfoncée et surélevée.
- Gérer les connexions avec les 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 définir son apparence visuelle. Elle réimplémente également ` shape()` pour garantir que sa zone de collision ait une forme elliptique (par opposition au rectangle de délimitation par défaut).
À des fins de 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 de la méthode advance() est appelée chaque fois que l’état de la scène avance d’une étape. 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), ainsi que ` mousePressEvent()` et ` mouseReleaseEvent()` pour mettre à jour l’apparence visuelle de l’élément.
Nous allons commencer à 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 de la méthode itemChange() en cas de changements de position et de transformation. Nous activons également « DeviceCoordinateCache » pour accélérer les performances de rendu. Pour garantir que les nœuds soient toujours superposés aux arêtes, nous définissons enfin la valeur Z de l’élément sur -1.
NodeLe constructeur de prend un pointeur GraphWidget et le stocke en tant que variable membre. Nous reviendrons sur ce pointeur plus tard.
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 associées. L’arête est ensuite ajustée de manière à ce que ses extrémités correspondent aux positions des nœuds source et de 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() ` implémente l’effet élastique qui tire et pousse les nœuds dans la grille. De plus, l’utilisateur peut déplacer directement un nœud à l’aide de la souris. Comme nous ne souhaitons pas que ces deux approches agissent simultanément sur le même nœud, nous commençons l’ calculateForces() ion en vérifiant si cet Node est l’élément actuellement sélectionné par la souris (c’est-à-dire 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 bien 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 repoussent les nœuds les uns des autres, et la seconde à soustraire les forces qui les rapprochent. Nous devons d’abord trouver tous les nœuds du graphe. Nous appelons QGraphicsScene::items() pour trouver tous les éléments de la scène, puis utilisons qgraphicsitem_cast() pour rechercher les instances de type « 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, puis sont ajustées de manière à ce que les nœuds les plus proches subissent la force la plus forte, celle-ci diminuant rapidement à mesure que la distance augmente. La somme de toutes les forces est stockée dans 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 des forces qui rapprochent les nœuds. En parcourant chaque arête reliée à ce nœud, on peut utiliser une approche similaire à celle décrite ci-dessus pour déterminer la direction et l’intensité de toutes les forces de traction. Ces forces sont soustraites de xvel et yvel.
En théorie, la somme des forces de poussée et de traction devrait s’équilibrer exactement à 0. Dans la pratique, cependant, 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’elle est inférieure à 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 la fonction 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 que nous avons définies. Nous ne déplaçons pas réellement l’élément dans cette fonction ; cela se fait lors d’une étape distincte, à partir de la fonction 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 depuis ` GraphWidget::timerEvent()`. Si la position du nœud a changé, la fonction renvoie `true` ; sinon, elle renvoie `false`.
QRectF Node::boundingRect() const
{
qreal adjust = 2;
return QRectF( -10 - adjust, -10 - adjust, 23 + adjust, 23 + adjust);
}Le rectangle de délimitation de la fonction ` Node` est un rectangle de 20x20 centré sur son origine (0, 0), décalé 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 vers la droite afin de laisser de la place pour 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. Cela garantit que vous devez cliquer à l’intérieur de la forme elliptique du nœud pour pouvoir le faire glisser. Vous pouvez tester cet effet en exécutant l’exemple et en effectuant un fort zoom avant afin que les nœuds apparaissent très grands. Sans réimplémenter la fonction ` shape()`, la zone de collision de l’élément 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 le dessin du nœud. Nous commençons par dessiner une simple ombre portée elliptique gris foncé en (-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 à dégradé radial. Ce remplissage va soit de Qt::yellow à Qt::darkYellow lorsque le nœud est en relief, soit dans le sens inverse lorsqu’il est en creux. À l’état en creux, nous décalons également le centre et le point focal de (3, 3) afin de renforcer l’impression que quelque chose a été enfoncé.
Le dessin d’ellipses remplies avec des dégradés peut s’avérer assez lent, en particulier 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 les redessinages inutiles.
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 la méthode ` itemChange()` pour ajuster la position de toutes les arêtes connectées et pour signaler à la scène qu’un élément s’est déplacé (c’est-à-dire « qu’il s’est passé quelque chose »). Cela déclenchera de nouveaux calculs de force.
Cette notification est la seule raison pour laquelle les nœuds doivent conserver un pointeur vers la classe ` GraphWidget`. Une autre approche consisterait à fournir cette notification à l’aide d’un signal ; dans ce cas, la classe ` 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 des entrées de la souris ; cela est déjà pris en charge pour nous. Nous devons toutefois réimplémenter les gestionnaires de clic et de relâchement de la souris afin de mettre à jour l’apparence visuelle des nœuds (c’est-à-dire enfoncés ou surélevés).
Définition de la classe Edge
La classe ` Edge ` représente les flèches reliant les nœuds dans cet exemple. Cette classe est très simple : elle gère un pointeur vers le nœud source et un autre vers le nœud de destination, et fournit une fonction ` adjust() ` qui garantit que la ligne commence à la position du nœud source et se termine à celle du nœud de destination. Les arêtes sont les seuls éléments qui changent en continu à mesure que des forces tirent et poussent sur les nœuds.
Examinons 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'utilise ni signaux, ni slots, ni propriétés (à comparer avec ` QGraphicsObject`).
Le constructeur prend deux pointeurs de nœud en entrée. Ces deux pointeurs sont obligatoires dans cet exemple. Nous fournissons également des fonctions de récupération 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 ; cela détermine la taille de la flèche qui est dessinée dans paint().
Dans le corps du constructeur, nous appelons setAcceptedMouseButtons(0). Cela garantit que les éléments de l’arête ne sont absolument pas pris en compte pour les entrées de la souris (c’est-à-dire qu’il est impossible de cliquer sur les arêtes). Ensuite, les pointeurs source et destination sont mis à jour, cette arête est enregistrée auprès de chaque nœud, et nous appelons adjust() pour mettre à jour les positions 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 get de source et de 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`, pointant respectivement vers les origines des nœuds source et de 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, et non vers leur centre. Pour trouver ce point, nous décomposons d’abord le vecteur pointant du centre du nœud source vers le centre du nœud de destination en composantes X et Y, puis nous normalisons ces composantes en les divisant par la longueur du vecteur. Cela nous donne un delta unitaire en X et en Y qui, multiplié par le rayon du nœud (qui est égal à 10), nous fournit le décalage à ajouter à l’une des extrémités de l’arête et à soustraire de l’autre.
Si la longueur du vecteur est inférieure à 20 (c'est-à-dire si deux nœuds se chevauchent), nous fixons alors les pointeurs source et 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 à leur maximum.
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 du traçage, et qu’elles sont renvoyées par la réimplémentation de ` boundingRect()`. Nous devons toujours appeler prepareGeometryChange() avant de modifier la valeur renvoyée par boundingRect(), et avant que ces variables puissent être utilisées par paint(), afin de préserver la cohérence de la gestion interne de Graphics View. Le plus sûr est d’appeler cette fonction une seule fois, immédiatement avant toute modification de ces variables.
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 englobant d’une arête est défini comme le plus petit rectangle incluant à 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 arête, nous devons également compenser en ajustant la taille de la flèche et la largeur du stylo de la moitié dans toutes les directions. Le stylo sert à tracer le contour de la flèche, et nous pouvons supposer que la moitié de ce contour se trouve à l'extérieur de la zone de la flèche, tandis que l'autre moitié se trouve à 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 la fonction ` 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.
Parallèlement, nous vérifions si la longueur de l’arête est approximativement égale à 0 ; si c’est le cas, nous quittons également la fonction.
// 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 doté de jonctions et d’extrémités arrondies. Si vous exécutez l’exemple, zoomez et examinez l’arête 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 procédons ensuite au tracé d’une flèche à chaque extrémité de l’arête. Chaque flèche est dessinée sous la forme d’un polygone rempli de noir. Les coordonnées de la flèche sont déterminées à l’aide de la trigonométrie élémentaire.
Définition de la classe GraphWidget
GraphWidget est une sous-classe de ` QGraphicsView`, qui fournit la fenêtre principale avec 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 signaler les modifications apportées au graphe de nœuds de la scène, quelques gestionnaires d’événements, une réimplémentation de ` drawBackground()` et une fonction d’aide permettant de mettre à l’échelle 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 de la classe 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 se voit alors attribuer une valeur fixe pour ` 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 relativement complexe. Étant donné que le graphe affiche un ensemble dense de petits éléments qui se déplacent tous, il n’est pas nécessaire que Graphics View perde du temps à rechercher des zones de mise à jour précises ; nous définissons donc le mode de mise à jour de la fenêtre d’affichage QGraphicsView::BoundingRectViewportUpdate. Le mode par défaut fonctionnerait correctement, mais ce mode est nettement plus rapide pour cet exemple.
Pour améliorer la qualité du rendu, nous avons défini QPainter::Antialiasing.
L’ancrage 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 l’option « QGraphicsView::AnchorUnderMouse », qui centre la vue sur le point situé sous le curseur de la souris. Cela permet de zoomer facilement sur un point de la scène en déplaçant la souris dessus, puis en faisant tourner la molette.
Enfin, nous attribuons à 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 attribue à 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 déplacement des nœuds via cette fonction itemMoved(). Son rôle consiste simplement à redémarrer le chronomètre principal s’il n’est pas déjà en cours d’exécution. Le chronomètre est conçu pour s’arrêter lorsque le graphe se stabilise, et redémarrer dès qu’il redevient 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);
}
}Il s’agit du gestionnaire d’événements clavier principal de ` GraphWidget`. Les touches fléchées déplacent le nœud central, les touches « + » et « - » permettent de zoomer et dézoomer en appelant ` scaleView()`, tandis que les touches Entrée et Espace randomisent les positions des nœuds. Tous les autres événements clavier (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 rôle du gestionnaire d’événements du minuteur est d’exécuter l’ensemble du mécanisme de calcul des forces sous la forme d’une animation fluide. À chaque déclenchement du minuteur, le gestionnaire identifie tous les nœuds de la scène et appelle la méthode ` Node::calculateForces() ` sur chacun d’entre eux, un par un. Enfin, dans une dernière étape, il appelle la fonction ` Node::advance() ` pour déplacer tous les nœuds vers leurs nouvelles positions. En vérifiant la valeur renvoyée par ` advance()`, nous pouvons déterminer si la grille s’est stabilisée (c’est-à-dire si aucun nœud n’a bougé). Si tel 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 molette, nous convertissons le delta de la molette de souris en un facteur d’échelle, puis nous transmettons ce facteur à scaleView(). Cette approche tient compte de la vitesse à laquelle la molette est actionnée. Plus vous faites défiler rapidement la molette de la souris, plus la vue effectuera un zoom 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, ajoutons une ombre portée, puis affichons du texte par-dessus. Le texte est affiché deux fois pour créer un effet d'ombre portée simple.
Ce rendu d’arrière-plan est assez coûteux ; c’est pourquoi la vue active la fonction ` 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 qu’il n’est pas possible de zoomer ni de dézoomer excessivement), 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 créons et affichons une instance de ` GraphWidget`. Comme tous les nœuds de la grille sont déplacés au départ, le minuteur ` GraphWidget ` démarre immédiatement après le retour du contrôle à la boucle d’événements.
© 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.