Colliding Mice Example¶
Demonstrates how to animate items on a graphics view.
The Colliding Mice example shows how to use the Graphics View framework to implement animated items and detect collision between items.
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.
The example consists of an item class and a main function: the Mouse
class represents the individual mice extending QGraphicsItem
, and the main()
function provides the main application window.
We will first review the Mouse
class to see how to animate items and detect item collisions, and then we will review the main()
function to see how to put the items into a scene and how to implement the corresponding view.
Mouse Class Definition¶
The mouse
class inherits from QGraphicsItem
. The QGraphicsItem
class is the base class for all graphical items in the Graphics View framework, and provides a light-weight foundation for writing your own custom items.
class Mouse(QGraphicsItem): # public Mouse() boundingRect = QRectF() shape = QPainterPath() def paint(painter, option,): widget) = QWidget() protected: def advance(step): # private angle = 0 speed = 0 mouseEyeDirection = 0 color = QColor()
When writing a custom graphics item, you must implement QGraphicsItem
‘s two pure virtual public functions: boundingRect()
, which returns an estimate of the area painted by the item, and paint()
, which implements the actual painting. In addition, we reimplement the shape()
and advance()
. We reimplement shape()
to return an accurate shape of our mouse item; the default implementation simply returns the item’s bounding rectangle. We reimplement advance()
to handle the animation so it all happens on one update.
Mouse Class Definition¶
When constructing a mouse item, we first ensure that all the item’s private variables which were no yet initialized directly in the class are properly initialized:
Mouse.Mouse() : color(QRandomGenerator.global().bounded(256), QRandomGenerator.global().bounded(256), QRandomGenerator.global().bounded(256)) setRotation(QRandomGenerator.global().bounded(360 * 16))
To calculate the various components of the mouse’s color, we use QRandomGenerator
.
Then we call the setRotation()
function inherited from QGraphicsItem
. Items live in their own local coordinate system. Their coordinates are usually centered around (0, 0), and this is also the center for all transformations. By calling the item’s setRotation()
function we alter the direction in which the mouse will start moving.
When the QGraphicsScene
decides to advance the scene by a frame, it will call advance()
on each of the items. This enables us to animate our mouse using our reimplementation of the advance() function.
def advance(self, step): if (not step) return lineToCenter = QLineF(QPointF(0, 0), mapFromScene(0, 0)) if (lineToCenter.length() > 150) { angleToCenter = std::atan2(lineToCenter.dy(), lineToCenter.dx()) angleToCenter = normalizeAngle((Pi - angleToCenter) + Pi / 2) if (angleToCenter < Pi and angleToCenter > Pi / 4) { # Rotate left angle += (angle < -Pi / 2) if 0.25 else -0.25 } else if (angleToCenter >= Pi and angleToCenter < (Pi + Pi / 2 + Pi / 4)) { # Rotate right angle += (angle < Pi / 2) if 0.25 else -0.25 } else if (.sin(angle) < 0) { angle += 0.25 } else if (.sin(angle) > 0) { angle -= 0.25
First, we don’t bother doing any advance if the step is 0
. This is because advance() is called twice: once with step == 0
, indicating that items are about to advance, and then with step == 1
for the actual advance. We also ensure that the mouse stays within a circle with a radius of 150 pixels.
Note the mapFromScene()
function provided by QGraphicsItem
. This function maps a position given in scene coordinates, to the item’s coordinate system.
> dangerMice = scene().items(QPolygonF() << mapToScene(0, 0) << mapToScene(-30, -50) << mapToScene(30, -50)) for item in dangerMice: if (item == self) continue lineToMouse = QLineF(QPointF(0, 0), mapFromItem(item, 0, 0)) angleToMouse = std::atan2(lineToMouse.dy(), lineToMouse.dx()) angleToMouse = normalizeAngle((Pi - angleToMouse) + Pi / 2) if (angleToMouse >= 0 and angleToMouse < Pi / 2) { # Rotate right angle += 0.5 } else if (angleToMouse <= TwoPi and angleToMouse > (TwoPi - Pi / 2)) { # Rotate left angle -= 0.5 if (dangerMice.size() > 1 and QRandomGenerator.global().bounded(10) == 0) { if (QRandomGenerator.global().bounded(1)) angle += QRandomGenerator.global().bounded(1 / 500.0) else: angle -= QRandomGenerator.global().bounded(1 / 500.0)
Then we try to avoid colliding with other mice.
speed += (-50 + QRandomGenerator.global().bounded(100)) / 100.0 dx = ::sin(angle) * 10 mouseEyeDirection = (qAbs(dx / 5) < 1) if 0 else dx / 5 setRotation(rotation() + dx) setPos(mapToParent(0, -(3 + sin(speed) * 3)))
Finally, we calculate the mouse’s speed and its eye direction (for use when painting the mouse), and set its new position.
The position of an item describes its origin (local coordinate (0, 0)) in the parent coordinates. The setPos()
function sets the position of the item to the given position in the parent’s coordinate system. For items with no parent, the given position is interpreted as scene coordinates. QGraphicsItem
also provides a mapToParent()
function to map a position given in item coordinates to the parent’s coordinate system. If the item has no parent, the position will be mapped to the scene’s coordinate system instead.
Then it is time to provide an implementation for the pure virtual functions inherited from QGraphicsItem
. Let’s first take a look at the boundingRect()
function:
def boundingRect(self): adjust = 0.5 return QRectF(-18 - adjust, -22 - adjust, 36 + adjust, 60 + adjust)
The boundingRect()
function defines the outer bounds of the item as a rectangle. Note that the Graphics View framework uses the bounding rectangle to determine whether the item requires redrawing, so all painting must be done inside this rectangle.
def paint(self, painter, arg__0, arg__1): # Body painter.setBrush(color) painter.drawEllipse(-10, -20, 20, 40) # Eyes painter.setBrush(Qt.white) painter.drawEllipse(-10, -17, 8, 8) painter.drawEllipse(2, -17, 8, 8) # Nose painter.setBrush(Qt.black) painter.drawEllipse(QRectF(-2, -22, 4, 4)) # Pupils painter.drawEllipse(QRectF(-8.0 + mouseEyeDirection, -17, 4, 4)) painter.drawEllipse(QRectF(4.0 + mouseEyeDirection, -17, 4, 4)) # Ears painter.setBrush(scene().collidingItems(self).isEmpty() if Qt.darkYellow else Qt.red) painter.drawEllipse(-17, -12, 16, 16) painter.drawEllipse(1, -12, 16, 16) # Tail path = QPainterPath(QPointF(0, 20)) path.cubicTo(-5, 22, -5, 22, 0, 25) path.cubicTo(5, 27, 5, 32, 0, 30) path.cubicTo(-5, 32, -5, 42, 0, 35) painter.setBrush(Qt.NoBrush) painter.drawPath(path)
The Graphics View framework calls the paint()
function to paint the contents of the item; the function paints the item in local coordinates.
Note the painting of the ears: whenever a mouse item collides with other mice items its ears are filled with red; otherwise they are filled with dark yellow. We use the collidingItems()
function to check if there are any colliding mice. The actual collision detection is handled by the Graphics View framework using shape-shape intersection. All we have to do is to ensure that the shape()
function returns an accurate shape for our item:
def shape(self): path = QPainterPath() path.addRect(-10, -20, 20, 40) return path
Because the complexity of arbitrary shape-shape intersection grows with an order of magnitude when the shapes are complex, this operation can be noticably time consuming. An alternative approach is to reimplement the collidesWithItem()
function to provide your own custom item and shape collision algorithm.
This completes the Mouse
class implementation; it is now ready for use. Let’s take a look at the main()
function to see how to implement a scene for the mice and a view for displaying the contents of the scene.
The Main() Function¶
The main()
function provides the main application window, as well as creating the items, their scene, and a corresponding view.
if __name__ == "__main__": app = QApplication([])
First, we create an application object and create the scene:
scene = QGraphicsScene() scene.setSceneRect(-300, -300, 600, 600)
The QGraphicsScene
class serves as a container for QGraphicsItems. It also provides functionality that lets you efficiently determine the location of items as well as determining which items are visible within an arbitrary area on the scene.
When creating a scene it is recommended to set the scene’s rectangle; the rectangle that defines the extent of the scene. It is primarily used by QGraphicsView
to determine the view’s default scrollable area, and by QGraphicsScene
to manage item indexing. If not explicitly set, the scene’s default rectangle will be the largest bounding rectangle of all the items on the scene since the scene was created. This means that the rectangle will grow when items are added or moved in the scene, but it will never shrink.
scene.setItemIndexMethod(QGraphicsScene.NoIndex)
The item index function is used to speed up item discovery. NoIndex
implies that item location is of linear complexity, as all items on the scene are searched. Adding, moving and removing items, however, is done in constant time. This approach is ideal for dynamic scenes, where many items are added, moved or removed continuously. The alternative is BspTreeIndex
, which makes use of a binary search to achieve item location algorithms that are of an order closer to logarithmic complexity.
for i in range(0, MouseCount): mouse = Mouse() mouse.setPos(::sin((i * 6.28) / MouseCount) * 200, ::cos((i * 6.28) / MouseCount) * 200) scene.addItem(mouse)
Then we add the mice to the scene.
view = QGraphicsView(scene) view.setRenderHint(QPainter.Antialiasing) view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"))
To be able to view the scene, we must also create a QGraphicsView
widget. The QGraphicsView
class visualizes the contents of a scene in a scrollable viewport. We also ensure that the contents are rendered using antialiasing, and we create the cheese background by setting the view’s background brush.
The image used for the background is stored as a binary file in the application’s executable using Qt’s resource system . The QPixmap
constructor accepts both file names that refer to actual files on disk and file names that refer to the application’s embedded resources.
view.setCacheMode(QGraphicsView.CacheBackground) view.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate) view.setDragMode(QGraphicsView.ScrollHandDrag)
Then we set the cache mode; QGraphicsView
can cache pre-rendered content in a pixmap, which is then drawn onto the viewport. The purpose of such caching is to speed up the total rendering time for areas that are slow to render, for example: texture, gradient, and alpha blended backgrounds. The CacheMode
property holds which parts of the view are cached, and the CacheBackground
flag enables caching of the view’s background.
By setting the dragMode
property, we define what should happen when the user clicks on the scene background and drags the mouse. The ScrollHandDrag
flag makes the cursor change into a pointing hand, and dragging the mouse around will scroll the scrollbars.
view.setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Colliding Mice")) view.resize(400, 300) view.show() timer = QTimer() QObject.connect(timer, QTimer.timeout, scene, QGraphicsScene.advance) timer.start(1000 / 33) sys.exit(app.exec())
In the end, we set the application window’s title and size before we enter the main event loop using the exec()
function.
Finally, we create a QTimer
and connect its timeout() signal to the advance() slot of the scene. Every time the timer fires, the scene will advance one frame.
We then tell the timer to fire every 1000/33 milliseconds. This will give us a frame rate of 30 frames a second, which is fast enough for most animations. Doing the animation with a single timer connection to advance the scene ensures that all the mice are moved at one point and, more importantly, only one update is sent to the screen after all the mice have moved.
© 2022 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.