Move Blocks Example

The Move Blocks example shows how to animate items in a QGraphicsScene using a QStateMachine with a custom transition.

Move Blocks Screenshot

Download this example

# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""PySide6 port of the examples/statemachine/moveblocks example from Qt v6.x"""

import sys

from PySide6.QtCore import (QAbstractAnimation, QEasingCurve, QEvent, QObject,
                            QParallelAnimationGroup, QPropertyAnimation,
                            QRandomGenerator, QRect, QSequentialAnimationGroup,
                            Qt, QTimer)
from PySide6.QtGui import QPainter, QResizeEvent
from PySide6.QtWidgets import (QApplication, QGraphicsView, QGraphicsScene,
                               QGraphicsWidget, QStyleOptionGraphicsItem,
                               QWidget)
from PySide6.QtStateMachine import (QAbstractTransition, QState, QStateMachine)


StateSwitchType = QEvent.Type(QEvent.Type.User + 256)


class StateSwitchEvent(QEvent):
    def __init__(self, rand: int = 0) -> None:
        super().__init__(StateSwitchType)
        self._rand = rand

    def rand(self) -> int:
        return self._rand


class QGraphicsRectWidget(QGraphicsWidget):
    def __init__(self):
        super().__init__()

    def paint(self, painter: QPainter,
              option: QStyleOptionGraphicsItem, widget: QWidget | None = None):
        painter.fillRect(self.rect(), Qt.blue)


class StateSwitchTransition(QAbstractTransition):
    def __init__(self, rand: int = 0) -> None:
        super().__init__()
        self._rand = rand

    def eventTest(self, event: QEvent) -> bool:
        return event.type() == StateSwitchType and event.rand() == self._rand

    def onTransition(self, event: QEvent):
        pass


class StateSwitcher(QState):
    def __init__(self, machine: QStateMachine) -> None:
        super().__init__(machine)
        self._state_count = 0
        self._last_index = 0
        self.rg = QRandomGenerator.global_()

    def onEntry(self, event: QEvent) -> None:
        while True:
            n = int(self.rg.bounded(self._state_count)) + 1
            if n != self._last_index:
                break
        self._last_index = n
        self.event = StateSwitchEvent(n)
        self.machine().postEvent(self.event)

    def onExit(self, event: QEvent) -> None:
        pass

    def addState(self, state: QState, animation: QAbstractAnimation) -> None:
        self._state_count += 1
        trans = StateSwitchTransition(self._state_count)
        trans.setTargetState(state)
        self.addTransition(trans)
        trans.addAnimation(animation)


def createGeometryState(w1: QObject, rect1: QRect,
                        w2: QObject, rect2: QRect,
                        w3: QObject, rect3: QRect,
                        w4: QObject, rect4: QRect, parent: QState) -> QState:
    result = QState(parent)
    result.assignProperty(w1, "geometry", rect1)
    result.assignProperty(w2, "geometry", rect2)
    result.assignProperty(w3, "geometry", rect3)
    result.assignProperty(w4, "geometry", rect4)

    return result


class GraphicsView(QGraphicsView):
    def __init__(self, scene: QGraphicsScene, parent: QWidget | None = None):
        super().__init__(scene, parent)

    def resizeEvent(self, event: QResizeEvent) -> None:
        self.fitInView(self.sceneRect())
        super().resizeEvent(event)


if __name__ == '__main__':
    app = QApplication(sys.argv)

    button1, button2 = QGraphicsRectWidget(), QGraphicsRectWidget()
    button3, button4 = QGraphicsRectWidget(), QGraphicsRectWidget()

    button2.setZValue(1)
    button3.setZValue(2)
    button4.setZValue(3)

    scene = QGraphicsScene(0, 0, 300, 300)
    scene.setBackgroundBrush(Qt.black)
    scene.addItem(button1)
    scene.addItem(button2)
    scene.addItem(button3)
    scene.addItem(button4)

    window = GraphicsView(scene)
    window.setFrameStyle(0)
    window.setAlignment(Qt.AlignLeft | Qt.AlignTop)
    window.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    window.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    machine = QStateMachine()

    group = QState()
    group.setObjectName("group")
    timer = QTimer()
    timer.setInterval(1250)
    timer.setSingleShot(True)

    group.entered.connect(timer.start)

    state1, state2, state3 = QState(), QState(), QState()
    state4, state5, state6 = QState(), QState(), QState()
    state7 = QState()

    state1 = createGeometryState(button1, QRect(100, 0, 50, 50),
                                 button2, QRect(150, 0, 50, 50),
                                 button3, QRect(200, 0, 50, 50),
                                 button4, QRect(250, 0, 50, 50),
                                 group)
    state2 = createGeometryState(button1, QRect(250, 100, 50, 50),
                                 button2, QRect(250, 150, 50, 50),
                                 button3, QRect(250, 200, 50, 50),
                                 button4, QRect(250, 250, 50, 50),
                                 group)
    state3 = createGeometryState(button1, QRect(150, 250, 50, 50),
                                 button2, QRect(100, 250, 50, 50),
                                 button3, QRect(50, 250, 50, 50),
                                 button4, QRect(0, 250, 50, 50),
                                 group)
    state4 = createGeometryState(button1, QRect(0, 150, 50, 50),
                                 button2, QRect(0, 100, 50, 50),
                                 button3, QRect(0, 50, 50, 50),
                                 button4, QRect(0, 0, 50, 50),
                                 group)
    state5 = createGeometryState(button1, QRect(100, 100, 50, 50),
                                 button2, QRect(150, 100, 50, 50),
                                 button3, QRect(100, 150, 50, 50),
                                 button4, QRect(150, 150, 50, 50),
                                 group)
    state6 = createGeometryState(button1, QRect(50, 50, 50, 50),
                                 button2, QRect(200, 50, 50, 50),
                                 button3, QRect(50, 200, 50, 50),
                                 button4, QRect(200, 200, 50, 50),
                                 group)
    state7 = createGeometryState(button1, QRect(0, 0, 50, 50),
                                 button2, QRect(250, 0, 50, 50),
                                 button3, QRect(0, 250, 50, 50),
                                 button4, QRect(250, 250, 50, 50),
                                 group)
    group.setInitialState(state1)

    animation_group = QParallelAnimationGroup()
    sub_group = QSequentialAnimationGroup()

    anim = QPropertyAnimation(button4, b"geometry")
    anim.setDuration(1000)
    anim.setEasingCurve(QEasingCurve.OutElastic)
    animation_group.addAnimation(anim)

    sub_group = QSequentialAnimationGroup(animation_group)
    sub_group.addPause(100)
    anim = QPropertyAnimation(button3, b"geometry")
    anim.setDuration(1000)
    anim.setEasingCurve(QEasingCurve.OutElastic)
    sub_group.addAnimation(anim)

    sub_group = QSequentialAnimationGroup(animation_group)
    sub_group.addPause(150)
    anim = QPropertyAnimation(button2, b"geometry")
    anim.setDuration(1000)
    anim.setEasingCurve(QEasingCurve.OutElastic)
    sub_group.addAnimation(anim)

    sub_group = QSequentialAnimationGroup(animation_group)
    sub_group.addPause(200)
    anim = QPropertyAnimation(button1, b"geometry")
    anim.setDuration(1000)
    anim.setEasingCurve(QEasingCurve.OutElastic)
    sub_group.addAnimation(anim)

    state_switcher = StateSwitcher(machine)
    state_switcher.setObjectName("state_switcher")
    group.addTransition(timer.timeout, state_switcher)
    state_switcher.addState(state1, animation_group)
    state_switcher.addState(state2, animation_group)
    state_switcher.addState(state3, animation_group)
    state_switcher.addState(state4, animation_group)
    state_switcher.addState(state5, animation_group)
    state_switcher.addState(state6, animation_group)
    state_switcher.addState(state7, animation_group)

    machine.addState(group)
    machine.setInitialState(group)
    machine.start()

    window.resize(300, 300)
    window.show()

    sys.exit(app.exec())