summaryrefslogtreecommitdiff
path: root/doc/src/examples/elasticnodes.qdoc
blob: 5b610df3e17050ee0499f5eb7853d2d4f01b7c50 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/****************************************************************************
**
** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the documentation of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:FDL$
** GNU Free Documentation License
** 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.
**
** Other Usage
** Alternatively, this file may be used in accordance with the terms
** and conditions contained in a signed written agreement between you
** and Nokia.
**
**
**
**
** $QT_END_LICENSE$
**
****************************************************************************/

/*!
    \example graphicsview/elasticnodes
    \title Elastic Nodes Example

    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
    \o Painting a yellow gradient "ball" in two states: sunken and raised.
    \o Managing connections to other nodes.
    \o Calculating forces pulling and pushing the nodes in the grid.
    \endlist

    Let's start by looking at the \c Node class declaration.

    \snippet examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/graphicsview/elasticnodes/edge.cpp 1

    The source and destination get-functions simply return the respective
    pointers.

    \snippet examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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 examples/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, seed the
    randomizer using qsrand(), and 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.
*/