Ejemplo de nodos elásticos
Demuestra cómo interactuar con elementos gráficos en una escena.
El ejemplo de Nodos Elásticos muestra cómo implementar aristas entre nodos en un gráfico, con interacción básica. Puede hacer clic para arrastrar un nodo, y acercar y alejar utilizando la rueda del ratón o el teclado. Si pulsas la barra espaciadora, los nodos aparecerán aleatoriamente. El ejemplo también es independiente de la resolución; a medida que aumentas el zoom, los gráficos siguen siendo nítidos.

Graphics View proporciona la clase QGraphicsScene para gestionar e interactuar con un gran número de elementos gráficos 2D personalizados derivados de la clase QGraphicsItem, y un widget QGraphicsView para visualizar los elementos, con soporte para zoom y rotación.
Este ejemplo consta de una clase Node, una clase Edge, una prueba GraphWidget, y una función main: la clase Node representa nodos amarillos arrastrables en una cuadrícula, la clase Edge representa las líneas entre los nodos, la clase GraphWidget representa la ventana de la aplicación, y la función main() crea y muestra esta ventana, y ejecuta el bucle de eventos.
Definición de la clase Node
La clase Node sirve para tres propósitos:
- Pintar una "bola" de gradiente amarillo en dos estados: hundida y elevada.
- Gestionar las conexiones con otros nodos.
- Calcular las fuerzas que tiran y empujan de los nodos en la rejilla.
Empecemos por ver la declaración de la clase 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 clase Node hereda de QGraphicsItem, y reimplementa las dos funciones obligatorias boundingRect() y paint() para proporcionar su aspecto visual. También reimplementa shape() para garantizar que su área de impacto tenga una forma elíptica (a diferencia del rectángulo delimitador por defecto).
Para la gestión de aristas, el nodo proporciona una API sencilla para añadir aristas a un nodo y para listar todas las aristas conectadas.
La reimplementación advance() es llamada cada vez que el estado de la escena avanza un paso. La función calculateForces() es llamada para calcular las fuerzas que empujan y tiran de este nodo y sus vecinos.
La clase Node también reimplementa itemChange() para reaccionar a los cambios de estado (en este caso, cambios de posición), y mousePressEvent() y mouseReleaseEvent() para actualizar la apariencia visual del elemento.
Comenzaremos revisando la implementación de Node observando su constructor:
Node::Node(GraphWidget *graphWidget) : graph(graphWidget) { setFlag(ItemIsMovable); setFlag(ItemSendsGeometryChanges); setCacheMode(DeviceCoordinateCache); setZValue(-1); }
En el constructor, establecemos la bandera ItemIsMovable para permitir que el elemento se mueva en respuesta al arrastre del ratón, y ItemSendsGeometryChanges para habilitar las notificaciones itemChange() para cambios de posición y transformación. También activamos DeviceCoordinateCache para acelerar el rendimiento del renderizado. Para asegurarnos de que los nodos siempre se apilan sobre los bordes, finalmente establecemos el valor Z del elemento en -1.
NodeEl constructor de 's toma un puntero GraphWidget y lo almacena como variable miembro. Volveremos sobre este puntero más adelante.
void Node::addEdge(Edge *edge) { edgeList << edge; edge->adjust(); } QList<Edge *> Node::edges() const { return edgeList; }
La función addEdge() añade la arista de entrada a una lista de aristas adjuntas. La arista se ajusta para que los puntos finales de la arista coincidan con las posiciones de los nodos de origen y destino.
La función edges() simplemente devuelve la lista de aristas adjuntas.
void Node::calculateForces() { if (!scene() || scene()->mouseGrabberItem() == this) { newPos = pos(); return; }
Hay dos formas de mover un nodo. La función calculateForces() implementa el efecto elástico que tira y empuja de los nodos en la malla. Además, el usuario puede mover directamente un nodo con el ratón. Como no queremos que los dos enfoques operen al mismo tiempo sobre el mismo nodo, iniciamos calculateForces() comprobando si este Node es el elemento actual de agarre del ratón (es decir, QGraphicsScene::mouseGrabberItem()). Como tenemos que encontrar todos los nodos vecinos (pero no necesariamente conectados), también nos aseguramos de que el elemento forma parte de una escena en primer lugar.
// 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; } }
El efecto "elástico" procede de un algoritmo que aplica fuerzas de empuje y tracción. El efecto es impresionante y sorprendentemente sencillo de aplicar.
El algoritmo consta de dos pasos: el primero consiste en calcular las fuerzas que separan los nodos y el segundo en restar las fuerzas que los juntan. Primero tenemos que encontrar todos los nodos del grafo. Llamamos a QGraphicsScene::items() para encontrar todos los elementos de la escena, y luego usamos qgraphicsitem_cast() para buscar instancias de Node.
Hacemos uso de mapFromItem() para crear un vector temporal que apunte desde este nodo a cada uno de los otros nodos, en coordenadas locales. Utilizamos los componentes descompuestos de este vector para determinar la dirección y la fuerza que debe aplicarse al nodo. Las fuerzas se acumulan para cada nodo, y luego se ajustan para que los nodos más cercanos reciban la fuerza más fuerte, con una rápida degradación cuando aumenta la distancia. La suma de todas las fuerzas se almacena en xvel (velocidad X) y yvel (velocidad 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; }
Las aristas entre los nodos representan las fuerzas que los unen. Visitando cada arista que está conectada a este nodo, podemos utilizar un enfoque similar al anterior para encontrar la dirección y la fuerza de todas las fuerzas de tracción. Estas fuerzas se restan de xvel y yvel.
En teoría, la suma de las fuerzas de empuje y tracción debería estabilizarse exactamente en 0. En la práctica, sin embargo, nunca lo hacen. Para evitar errores de precisión numérica, simplemente forzamos la suma de fuerzas a ser 0 cuando son menores que 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)); }
El último paso de calculateForces() determina la nueva posición del nodo. Añadimos la fuerza a la posición actual del nodo. También nos aseguramos de que la nueva posición se mantiene dentro de nuestros límites definidos. En realidad no movemos el elemento en esta función; eso se hace en un paso separado, desde advance().
bool Node::advancePosition() { if (newPos == pos()) return false; setPos(newPos); return true; }
La función advance() actualiza la posición actual del elemento. Se llama desde GraphWidget::timerEvent(). Si la posición del nodo ha cambiado, la función devuelve true; en caso contrario, devuelve false.
QRectF Node::boundingRect() const { qreal adjust = 2; return QRectF( -10 - adjust, -10 - adjust, 23 + adjust, 23 + adjust); }
El rectángulo delimitador de Node es un rectángulo de 20x20 centrado alrededor de su origen (0, 0), ajustado en 2 unidades en todas las direcciones para compensar el trazo del contorno del nodo, y en 3 unidades hacia abajo y hacia la derecha para dejar espacio para una simple sombra.
QPainterPath Node::shape() const { QPainterPath path; path.addEllipse(-10, -10, 20, 20); return path; }
La forma es una simple elipse. De este modo, deberá hacer clic dentro de la forma elíptica del nodo para arrastrarlo. Puede probar este efecto ejecutando el ejemplo y haciendo zoom para que los nodos sean muy grandes. Sin reimplementar shape(), el área de impacto del elemento sería idéntica a su rectángulo delimitador (es decir, rectangular).
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); }
Esta función implementa el pintado del nodo. Comenzamos dibujando una simple sombra elíptica gris oscura en (-7, -7), es decir, (3, 3) unidades hacia abajo y hacia la derecha desde la esquina superior izquierda (-10, -10) de la elipse.
A continuación, dibujamos una elipse con un relleno de gradiente radial. Este relleno es Qt::yellow a Qt::darkYellow cuando está elevado, o lo contrario cuando está hundido. En estado hundido también desplazamos el centro y el punto focal en (3, 3) para acentuar la impresión de que algo ha sido empujado hacia abajo.
Dibujar elipses rellenas con degradados puede ser bastante lento, especialmente cuando se utilizan degradados complejos como QRadialGradient. Por eso este ejemplo utiliza DeviceCoordinateCache, una medida sencilla pero eficaz que evita redibujar innecesariamente.
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); }
Reimplementamos itemChange() para ajustar la posición de todas las aristas conectadas, y para notificar a la escena que un elemento se ha movido (es decir, "algo ha pasado"). Esto desencadenará nuevos cálculos de fuerza.
Esta notificación es la única razón por la que los nodos necesitan mantener un puntero de vuelta a GraphWidget. Otro enfoque podría ser proporcionar dicha notificación utilizando una señal; en tal caso, Node necesitaría heredar de QGraphicsObject.
void Node::mousePressEvent(QGraphicsSceneMouseEvent *event) { update(); QGraphicsItem::mousePressEvent(event); } void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { update(); QGraphicsItem::mouseReleaseEvent(event); }
Dado que hemos establecido la bandera ItemIsMovable, no necesitamos implementar la lógica que mueve el nodo según la entrada del ratón; esto ya está previsto para nosotros. Sin embargo, aún necesitamos reimplementar los manejadores de pulsación y liberación del ratón para actualizar la apariencia visual de los nodos (es decir, hundidos o elevados).
Definición de la clase Edge
La clase Edge representa las líneas-flecha entre los nodos de este ejemplo. La clase es muy simple: mantiene un puntero de nodo de origen y de destino, y proporciona una función adjust() que se asegura de que la línea comienza en la posición del origen, y termina en la posición del destino. Las aristas son los únicos elementos que cambian continuamente a medida que las fuerzas tiran y empujan de los nodos.
Echemos un vistazo a la declaración de la clase:
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 hereda de QGraphicsItem, ya que es una clase simple que no utiliza señales, ranuras ni propiedades (en comparación con QGraphicsObject).
El constructor toma dos punteros de nodo como entrada. Ambos punteros son obligatorios en este ejemplo. También proporcionamos funciones get para cada nodo.
La función adjust() reposiciona la arista, y el elemento también implementa boundingRect() y paint().
A continuación revisaremos su implementación.
Edge::Edge(Node *sourceNode, Node *destNode) : source(sourceNode), dest(destNode) { setAcceptedMouseButtons(Qt::NoButton); source->addEdge(this); dest->addEdge(this); adjust(); }
El constructor Edge inicializa su miembro de datos arrowSize a 10 unidades; esto determina el tamaño de la flecha que se dibuja en paint().
En el cuerpo del constructor, llamamos a setAcceptedMouseButtons(0). Esto asegura que los elementos de los bordes no son considerados para la entrada del ratón en absoluto (es decir, no se puede hacer clic en los bordes). A continuación, se actualizan los punteros de origen y destino, se registra esta arista con cada nodo y se llama a adjust() para actualizar la posición inicial y final de esta arista.
Node *Edge::sourceNode() const { return source; } Node *Edge::destNode() const { return dest; }
Las funciones get de origen y destino simplemente devuelven los punteros respectivos.
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(); } }
En adjust(), definimos dos puntos: sourcePoint, y destPoint, que apuntan a los orígenes de los nodos de origen y destino respectivamente. Cada punto se calcula utilizando coordenadas locales.
Queremos que la punta de las flechas de la arista apunte al contorno exacto de los nodos, y no al centro de los nodos. Para encontrar este punto, primero descomponemos el vector que apunta desde el centro del nodo de origen al centro del nodo de destino en X e Y, y luego normalizamos los componentes dividiéndolos por la longitud del vector. Esto nos da un delta unitario X e Y que, multiplicado por el radio del nodo (que es 10), nos da el desplazamiento que hay que sumar a un punto de la arista, y restar al otro.
Si la longitud del vector es inferior a 20 (es decir, si dos nodos se solapan), entonces fijamos el puntero de origen y destino en el centro del nodo de origen. En la práctica, este caso es muy difícil de reproducir manualmente, ya que entonces las fuerzas entre los dos nodos son máximas.
Es importante notar que llamamos a prepareGeometryChange() en esta función. La razón es que las variables sourcePoint y destPoint se utilizan directamente al pintar, y se devuelven desde la reimplementación de boundingRect(). Siempre debemos llamar a prepareGeometryChange() antes de cambiar lo que devuelve boundingRect(), y antes de que estas variables puedan ser utilizadas por paint(), para mantener limpia la contabilidad interna de la Vista Gráfica. Es más seguro llamar a esta función una vez, inmediatamente antes de que cualquier variable sea modificada.
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); }
El rectángulo delimitador de la arista se define como el rectángulo más pequeño que incluye tanto el punto inicial como el final de la arista. Como dibujamos una flecha en cada arista, también necesitamos compensar ajustando con la mitad del tamaño de la flecha y la mitad del ancho de la pluma en todas las direcciones. La pluma se utiliza para dibujar el contorno de la flecha, y podemos suponer que la mitad del contorno se puede dibujar fuera del área de la flecha, y la otra mitad se dibujará dentro.
void Edge::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (!source || !dest) return; QLineF line(sourcePoint, destPoint); if (qFuzzyCompare(line.length(), qreal(0.))) return;
Comenzamos la reimplementación de paint() comprobando algunas condiciones previas. En primer lugar, si el nodo origen o destino no están definidos, entonces regresamos inmediatamente; no hay nada que dibujar.
Al mismo tiempo, comprobamos si la longitud de la arista es aproximadamente 0, y si lo es, entonces también devolvemos.
// Draw the line itself painter->setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); painter->drawLine(line);
Dibujamos la línea usando un bolígrafo que tiene juntas y tapas redondas. Si ejecutas el ejemplo, haces zoom y estudias la arista en detalle, verás que no hay aristas vivas/cuadradas.
// 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); }
Procedemos a dibujar una flecha en cada extremo de la arista. Cada flecha se dibuja como un polígono con relleno negro. Las coordenadas para la flecha se determinan utilizando trigonometría simple.
Definición de la clase GraphWidget
GraphWidget es una subclase de QGraphicsView, que proporciona barras de desplazamiento a la ventana principal.
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 clase proporciona un constructor básico que inicializa la escena, una función itemMoved() para notificar cambios en el gráfico de nodos de la escena, algunos manejadores de eventos, una reimplementación de drawBackground(), y una función de ayuda para escalar la vista utilizando la rueda del ratón o el teclado.
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"));
GraphicsWidgetEl constructor de la escena crea la escena, y como la mayoría de los elementos se mueven la mayor parte del tiempo, establece QGraphicsScene::NoIndex. A continuación, la escena recibe un scene rectangle fijo y se asigna a la vista GraphWidget.
La vista permite a QGraphicsView::CacheBackground almacenar en caché la representación de su fondo estático y, en cierto modo, complejo. Debido a que el gráfico renderiza una estrecha colección de pequeños elementos que se mueven alrededor, es innecesario que la Vista Gráfica pierda tiempo encontrando regiones de actualización precisas, por lo que establecemos el modo de actualización de la vista QGraphicsView::BoundingRectViewportUpdate. Por defecto funcionaría bien, pero este modo es notablemente más rápido para este ejemplo.
Para mejorar la calidad de renderizado, establecemos QPainter::Antialiasing.
El ancla de transformación decide cómo debe desplazarse la vista cuando la transformas, o en nuestro caso, cuando acercamos o alejamos el zoom. Hemos elegido QGraphicsView::AnchorUnderMouse, que centra la vista en el punto situado bajo el cursor del ratón. Esto hace que sea fácil hacer zoom hacia un punto de la escena moviendo el ratón sobre él, y luego girando la rueda del ratón.
Finalmente damos a la ventana un tamaño mínimo que coincide con el tamaño por defecto de la escena, y establecemos un título de ventana adecuado.
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 última parte del constructor crea la rejilla de nodos y aristas, y da a cada nodo una posición inicial.
void GraphWidget::itemMoved() { using namespace std::chrono_literals; if (!timer.isActive()) timer.start(1000ms / 25, this); }
GraphWidget es notificado del movimiento de los nodos a través de esta función itemMoved(). Su trabajo es simplemente reiniciar el temporizador principal en caso de que no se esté ejecutando ya. El temporizador está diseñado para detenerse cuando el gráfico se estabiliza, y comenzar una vez que es inestable de nuevo.
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); } }
Este es el manejador de eventos de teclas de GraphWidget. Las teclas de flecha mueven el nodo central, las teclas '+' y '-' hacen zoom llamando a scaleView(), y las teclas enter y espacio aleatorizan las posiciones de los nodos. El resto de eventos de teclado (por ejemplo, avanzar y retroceder página) se gestionan mediante la implementación por defecto 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(); }
El trabajo del manejador de eventos del temporizador es ejecutar toda la maquinaria de cálculo de fuerza como una animación suave. Cada vez que el temporizador se activa, el manejador encontrará todos los nodos en la escena, y llamará a Node::calculateForces() en cada nodo, uno a la vez. Luego, en un paso final llamará a Node::advance() para mover todos los nodos a sus nuevas posiciones. Comprobando el valor de retorno de advance(), podemos decidir si la cuadrícula se ha estabilizado (es decir, no se ha movido ningún nodo). Si es así, podemos detener el temporizador.
void GraphWidget::wheelEvent(QWheelEvent *event) { scaleView(pow(2., -event->angleDelta().y() / 240.0)); }
En el manejador de eventos de la rueda, convertimos el delta de la rueda del ratón en un factor de escala, y pasamos este factor a scaleView(). Este enfoque tiene en cuenta la velocidad a la que se rueda la rueda. Cuanto más rápido gire la rueda del ratón, más rápido se ampliará la vista.
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); }
El fondo de la vista se renderiza en una reimplementación de QGraphicsView::drawBackground(). Dibujamos un rectángulo grande relleno con un gradiente lineal, añadimos una sombra y luego renderizamos el texto encima. El texto se renderiza dos veces para conseguir un simple efecto de sombra.
Este renderizado de fondo es bastante costoso; por eso la vista habilita 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 función de ayuda scaleView() comprueba que el factor de escala se mantiene dentro de ciertos límites (es decir, no puede acercarse ni alejarse demasiado), y luego aplica esta escala a la vista.
La función main()
En contraste con la complejidad del resto de este ejemplo, la función main() es muy sencilla: Creamos una instancia de QApplication, luego creamos y mostramos una instancia de GraphWidget. Como todos los nodos de la rejilla se mueven inicialmente, el temporizador de GraphWidget se iniciará inmediatamente después de que el control haya vuelto al bucle de eventos.
© 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.