| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the documentation of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:FDL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Free Documentation License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Free |
| ** Documentation License version 1.3 as published by the Free Software |
| ** Foundation and appearing in the file included in the packaging of |
| ** this file. Please review the following information to ensure |
| ** the GNU Free Documentation License version 1.3 requirements |
| ** will be met: https://www.gnu.org/licenses/fdl-1.3.html. |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| /*! |
| \example graphicsview/elasticnodes |
| \title Elastic Nodes Example |
| \ingroup examples-graphicsview |
| \brief Demonstrates how to interact with graphical items in a scene. |
| |
| The Elastic Nodes example shows how to implement edges between nodes in a |
| graph, with basic interaction. You can click to drag a node around, and |
| zoom in and out using the mouse wheel or the keyboard. Hitting the space |
| bar will randomize the nodes. The example is also resolution independent; |
| as you zoom in, the graphics remain crisp. |
| |
| \image elasticnodes-example.png |
| |
| Graphics View provides the QGraphicsScene class for managing and |
| interacting with a large number of custom-made 2D graphical items derived |
| from the QGraphicsItem class, and a QGraphicsView widget for visualizing |
| the items, with support for zooming and rotation. |
| |
| This example consists of a \c Node class, an \c Edge class, a \c |
| GraphWidget test, and a \c main function: the \c Node class represents |
| draggable yellow nodes in a grid, the \c Edge class represents the lines |
| between the nodes, the \c GraphWidget class represents the application |
| window, and the \c main() function creates and shows this window, and runs |
| the event loop. |
| |
| \section1 Node Class Definition |
| |
| The \c Node class serves three purposes: |
| |
| \list |
| \li Painting a yellow gradient "ball" in two states: sunken and raised. |
| \li Managing connections to other nodes. |
| \li Calculating forces pulling and pushing the nodes in the grid. |
| \endlist |
| |
| Let's start by looking at the \c Node class declaration. |
| |
| \snippet graphicsview/elasticnodes/node.h 0 |
| |
| The \c Node class inherits QGraphicsItem, and reimplements the two |
| mandatory functions \l{QGraphicsItem::boundingRect()}{boundingRect()} and |
| \l{QGraphicsItem::paint()}{paint()} to provide its visual appearance. It |
| also reimplements \l{QGraphicsItem::shape()}{shape()} to ensure its hit |
| area has an elliptic shape (as opposed to the default bounding rectangle). |
| |
| For edge management purposes, the node provides a simple API for adding |
| edges to a node, and for listing all connected edges. |
| |
| The \l{QGraphicsItem::advance()}{advance()} reimplementation is called |
| whenever the scene's state advances by one step. The calculateForces() |
| function is called to calculate the forces that push and pull on this node |
| and its neighbors. |
| |
| The \c Node class also reimplements |
| \l{QGraphicsItem::itemChange()}{itemChange()} to react to state changes (in |
| this case, position changes), and |
| \l{QGraphicsItem::mousePressEvent()}{mousePressEvent()} and |
| \l{QGraphicsItem::mouseReleaseEvent()}{mouseReleaseEvent()} to update the |
| item's visual appearance. |
| |
| We will start reviewing the \c Node implementation by looking at its |
| constructor: |
| |
| \snippet graphicsview/elasticnodes/node.cpp 0 |
| |
| In the constructor, we set the |
| \l{QGraphicsItem::ItemIsMovable}{ItemIsMovable} flag to allow the item to |
| move in response to mouse dragging, and |
| \l{QGraphicsItem::ItemSendsGeometryChanges}{ItemSendsGeometryChanges} to |
| enable \l{QGraphicsItem::itemChange()}{itemChange()} notifications for |
| position and transformation changes. We also enable |
| \l{QGraphicsItem::DeviceCoordinateCache}{DeviceCoordinateCache} to speed up |
| rendering performance. To ensure that the nodes are always stacked on top |
| of edges, we finally set the item's Z value to -1. |
| |
| \c Node's constructor takes a \c GraphWidget pointer and stores this as a |
| member variable. We will revisit this pointer later on. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 1 |
| |
| The addEdge() function adds the input edge to a list of attached edges. The |
| edge is then adjusted so that the end points for the edge match the |
| positions of the source and destination nodes. |
| |
| The edges() function simply returns the list of attached edges. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 2 |
| |
| There are two ways to move a node. The \c calculateForces() function |
| implements the elastic effect that pulls and pushes on nodes in the grid. |
| In addition, the user can directly move one node around with the mouse. |
| Because we do not want the two approaches to operate at the same time on |
| the same node, we start \c calculateForces() by checking if this \c Node is |
| the current mouse grabber item (i.e., QGraphicsScene::mouseGrabberItem()). |
| Because we need to find all neighboring (but not necessarily connected) |
| nodes, we also make sure the item is part of a scene in the first place. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 3 |
| |
| The "elastic" effect comes from an algorithm that applies pushing and |
| pulling forces. The effect is impressive, and surprisingly simple to |
| implement. |
| |
| The algorithm has two steps: the first is to calculate the forces that push |
| the nodes apart, and the second is to subtract the forces that pull the |
| nodes together. First we need to find all the nodes in the graph. We call |
| QGraphicsScene::items() to find all items in the scene, and then use |
| qgraphicsitem_cast() to look for \c Node instances. |
| |
| We make use of \l{QGraphicsItem::mapFromItem()}{mapFromItem()} to create a |
| temporary vector pointing from this node to each other node, in \l{The |
| Graphics View Coordinate System}{local coordinates}. We use the decomposed |
| components of this vector to determine the direction and strength of force |
| that should apply to the node. The forces accumulate for each node, and are |
| then adjusted so that the closest nodes are given the strongest force, with |
| rapid degradation when distance increases. The sum of all forces is stored |
| in \c xvel (X-velocity) and \c yvel (Y-velocity). |
| |
| \snippet graphicsview/elasticnodes/node.cpp 4 |
| |
| The edges between the nodes represent forces that pull the nodes together. |
| By visiting each edge that is connected to this node, we can use a similar |
| approach as above to find the direction and strength of all pulling forces. |
| These forces are subtracted from \c xvel and \c yvel. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 5 |
| |
| In theory, the sum of pushing and pulling forces should stabilize to |
| precisely 0. In practice, however, they never do. To circumvent errors in |
| numerical precision, we simply force the sum of forces to be 0 when they |
| are less than 0.1. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 6 |
| |
| The final step of \c calculateForces() determines the node's new position. |
| We add the force to the node's current position. We also make sure the new |
| position stays inside of our defined boundaries. We don't actually move the |
| item in this function; that's done in a separate step, from \c advance(). |
| |
| \snippet graphicsview/elasticnodes/node.cpp 7 |
| |
| The \c advance() function updates the item's current position. It is called |
| from \c GraphWidget::timerEvent(). If the node's position changed, the |
| function returns true; otherwise false is returned. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 8 |
| |
| The \c Node's bounding rectangle is a 20x20 sized rectangle centered around |
| its origin (0, 0), adjusted by 2 units in all directions to compensate for |
| the node's outline stroke, and by 3 units down and to the right to make |
| room for a simple drop shadow. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 9 |
| |
| The shape is a simple ellipse. This ensures that you must click inside the |
| node's elliptic shape in order to drag it around. You can test this effect |
| by running the example, and zooming far in so that the nodes are very |
| large. Without reimplementing \l{QGraphicsItem::shape()}{shape()}, the |
| item's hit area would be identical to its bounding rectangle (i.e., |
| rectangular). |
| |
| \snippet graphicsview/elasticnodes/node.cpp 10 |
| |
| This function implements the node's painting. We start by drawing a simple |
| dark gray elliptic drop shadow at (-7, -7), that is, (3, 3) units down and |
| to the right from the top-left corner (-10, -10) of the ellipse. |
| |
| We then draw an ellipse with a radial gradient fill. This fill is either |
| Qt::yellow to Qt::darkYellow when raised, or the opposite when sunken. In |
| sunken state we also shift the center and focal point by (3, 3) to |
| emphasize the impression that something has been pushed down. |
| |
| Drawing filled ellipses with gradients can be quite slow, especially when |
| using complex gradients such as QRadialGradient. This is why this example |
| uses \l{QGraphicsItem::DeviceCoordinateCache}{DeviceCoordinateCache}, a |
| simple yet effective measure that prevents unnecessary redrawing. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 11 |
| |
| We reimplement \l{QGraphicsItem::itemChange()}{itemChange()} to adjust the |
| position of all connected edges, and to notify the scene that an item has |
| moved (i.e., "something has happened"). This will trigger new force |
| calculations. |
| |
| This notification is the only reason why the nodes need to keep a pointer |
| back to the \c GraphWidget. Another approach could be to provide such |
| notification using a signal; in such case, \c Node would need to inherit |
| from QGraphicsObject. |
| |
| \snippet graphicsview/elasticnodes/node.cpp 12 |
| |
| Because we have set the \l{QGraphicsItem::ItemIsMovable}{ItemIsMovable} |
| flag, we don't need to implement the logic that moves the node according to |
| mouse input; this is already provided for us. We still need to reimplement |
| the mouse press and release handlers, though, to update the nodes' visual |
| appearance (i.e., sunken or raised). |
| |
| \section1 Edge Class Definition |
| |
| The \c Edge class represents the arrow-lines between the nodes in this |
| example. The class is very simple: it maintains a source- and destination |
| node pointer, and provides an \c adjust() function that makes sure the line |
| starts at the position of the source, and ends at the position of the |
| destination. The edges are the only items that change continuously as |
| forces pull and push on the nodes. |
| |
| Let's take a look at the class declaration: |
| |
| \snippet graphicsview/elasticnodes/edge.h 0 |
| |
| \c Edge inherits from QGraphicsItem, as it's a simple class that has no use |
| for signals, slots, and properties (compare to QGraphicsObject). |
| |
| The constructor takes two node pointers as input. Both pointers are |
| mandatory in this example. We also provide get-functions for each node. |
| |
| The \c adjust() function repositions the edge, and the item also implements |
| \l{QGraphicsItem::boundingRect()}{boundingRect()} and |
| \l{QGraphicsItem::paint()}{paint()}. |
| |
| We will now review its implementation. |
| |
| \snippet graphicsview/elasticnodes/edge.cpp 0 |
| |
| The \c Edge constructor initializes its \c arrowSize data member to 10 units; |
| this determines the size of the arrow which is drawn in |
| \l{QGraphicsItem::paint()}{paint()}. |
| |
| In the constructor body, we call |
| \l{QGraphicsItem::setAcceptedMouseButtons()}{setAcceptedMouseButtons(0)}. |
| This ensures that the edge items are not considered for mouse input at all |
| (i.e., you cannot click the edges). Then, the source and destination |
| pointers are updated, this edge is registered with each node, and we call |
| \c adjust() to update this edge's start end end position. |
| |
| \snippet graphicsview/elasticnodes/edge.cpp 1 |
| |
| The source and destination get-functions simply return the respective |
| pointers. |
| |
| \snippet graphicsview/elasticnodes/edge.cpp 2 |
| |
| In \c adjust(), we define two points: \c sourcePoint, and \c destPoint, |
| pointing at the source and destination nodes' origins respectively. Each |
| point is calculated using \l{The Graphics View Coordinate System}{local |
| coordinates}. |
| |
| We want the tip of the edge's arrows to point to the exact outline of the |
| nodes, as opposed to the center of the nodes. To find this point, we first |
| decompose the vector pointing from the center of the source to the center |
| of the destination node into X and Y, and then normalize the components by |
| dividing by the length of the vector. This gives us an X and Y unit delta |
| that, when multiplied by the radius of the node (which is 10), gives us the |
| offset that must be added to one point of the edge, and subtracted from the |
| other. |
| |
| If the length of the vector is less than 20 (i.e., if two nodes overlap), |
| then we fix the source and destination pointer at the center of the source |
| node. In practice this case is very hard to reproduce manually, as the |
| forces between the two nodes is then at its maximum. |
| |
| It's important to notice that we call |
| \l{QGraphicsItem::prepareGeometryChange()}{prepareGeometryChange()} in this |
| function. The reason is that the variables \c sourcePoint and \c destPoint |
| are used directly when painting, and they are returned from the |
| \l{QGraphicsItem::boundingRect()}{boundingRect()} reimplementation. We must |
| always call |
| \l{QGraphicsItem::prepareGeometryChange()}{prepareGeometryChange()} before |
| changing what \l{QGraphicsItem::boundingRect()}{boundingRect()} returns, |
| and before these variables can be used by |
| \l{QGraphicsItem::paint()}{paint()}, to keep Graphics View's internal |
| bookkeeping clean. It's safest to call this function once, immediately |
| before any such variable is modified. |
| |
| \snippet graphicsview/elasticnodes/edge.cpp 3 |
| |
| The edge's bounding rectangle is defined as the smallest rectangle that |
| includes both the start and the end point of the edge. Because we draw an |
| arrow on each edge, we also need to compensate by adjusting with half the |
| arrow size and half the pen width in all directions. The pen is used to |
| draw the outline of the arrow, and we can assume that half of the outline |
| can be drawn outside of the arrow's area, and half will be drawn inside. |
| |
| \snippet graphicsview/elasticnodes/edge.cpp 4 |
| |
| We start the reimplementation of \l{QGraphicsItem::paint()}{paint()} by |
| checking a few preconditions. Firstly, if either the source or destination |
| node is not set, then we return immediately; there is nothing to draw. |
| |
| At the same time, we check if the length of the edge is approximately 0, |
| and if it is, then we also return. |
| |
| \snippet graphicsview/elasticnodes/edge.cpp 5 |
| |
| We draw the line using a pen that has round joins and caps. If you run the |
| example, zoom in and study the edge in detail, you will see that there are |
| no sharp/square edges. |
| |
| \snippet graphicsview/elasticnodes/edge.cpp 6 |
| |
| We proceed to drawing one arrow at each end of the edge. Each arrow is |
| drawn as a polygon with a black fill. The coordinates for the arrow are |
| determined using simple trigonometry. |
| |
| \section1 GraphWidget Class Definition |
| |
| \c GraphWidget is a subclass of QGraphicsView, which provides the main |
| window with scrollbars. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.h 0 |
| |
| The class provides a basic constructor that initializes the scene, an \c |
| itemMoved() function to notify changes in the scene's node graph, a few |
| event handlers, a reimplementation of |
| \l{QGraphicsView::drawBackground()}{drawBackground()}, and a helper |
| function for scaling the view by using the mouse wheel or keyboard. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.cpp 0 |
| |
| \c GraphicsWidget's constructor creates the scene, and because most items |
| move around most of the time, it sets QGraphicsScene::NoIndex. The scene |
| then gets a fixed \l{QGraphicsScene::sceneRect}{scene rectangle}, and is |
| assigned to the \c GraphWidget view. |
| |
| The view enables QGraphicsView::CacheBackground to cache rendering of its |
| static, and somewhat complex, background. Because the graph renders a close |
| collection of small items that all move around, it's unnecessary for |
| Graphics View to waste time finding accurate update regions, so we set the |
| QGraphicsView::BoundingRectViewportUpdate viewport update mode. The default |
| would work fine, but this mode is noticably faster for this example. |
| |
| To improve rendering quality, we set QPainter::Antialiasing. |
| |
| The transformation anchor decides how the view should scroll when you |
| transform the view, or in our case, when we zoom in or out. We have chosen |
| QGraphicsView::AnchorUnderMouse, which centers the view on the point under |
| the mouse cursor. This makes it easy to zoom towards a point in the scene |
| by moving the mouse over it, and then rolling the mouse wheel. |
| |
| Finally we give the window a minimum size that matches the scene's default |
| size, and set a suitable window title. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.cpp 1 |
| |
| The last part of the constructor creates the grid of nodes and edges, and |
| gives each node an initial position. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.cpp 2 |
| |
| \c GraphWidget is notified of node movement through this \c itemMoved() |
| function. Its job is simply to restart the main timer in case it's not |
| running already. The timer is designed to stop when the graph stabilizes, |
| and start once it's unstable again. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.cpp 3 |
| |
| This is \c GraphWidget's key event handler. The arrow keys move the center |
| node around, the '+' and '-' keys zoom in and out by calling \c |
| scaleView(), and the enter and space keys randomize the positions of the |
| nodes. All other key events (e.g., page up and page down) are handled by |
| QGraphicsView's default implementation. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.cpp 4 |
| |
| The timer event handler's job is to run the whole force calculation |
| machinery as a smooth animation. Each time the timer is triggered, the |
| handler will find all nodes in the scene, and call \c |
| Node::calculateForces() on each node, one at a time. Then, in a final step |
| it will call \c Node::advance() to move all nodes to their new positions. |
| By checking the return value of \c advance(), we can decide if the grid |
| stabilized (i.e., no nodes moved). If so, we can stop the timer. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.cpp 5 |
| |
| In the wheel event handler, we convert the mouse wheel delta to a scale |
| factor, and pass this factor to \c scaleView(). This approach takes into |
| account the speed that the wheel is rolled. The faster you roll the mouse |
| wheel, the faster the view will zoom. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.cpp 6 |
| |
| The view's background is rendered in a reimplementation of |
| QGraphicsView::drawBackground(). We draw a large rectangle filled with a |
| linear gradient, add a drop shadow, and then render text on top. The text |
| is rendered twice for a simple drop-shadow effect. |
| |
| This background rendering is quite expensive; this is why the view enables |
| QGraphicsView::CacheBackground. |
| |
| \snippet graphicsview/elasticnodes/graphwidget.cpp 7 |
| |
| The \c scaleView() helper function checks that the scale factor stays |
| within certain limits (i.e., you cannot zoom too far in nor too far out), |
| and then applies this scale to the view. |
| |
| \section1 The main() Function |
| |
| In contrast to the complexity of the rest of this example, the \c main() |
| function is very simple: We create a QApplication instance, then create and |
| show an instance of \c GraphWidget. Because all nodes in the grid are moved |
| initially, the \c GraphWidget timer will start immediately after control |
| has returned to the event loop. |
| */ |