移动方块

移动方块示例演示了如何使用自定义转换的 QGraphicsSceneQStateMachine 来动画化项目。

该示例动画化了您在上图中可以看到的蓝色方块。动画会将方块移动到四个预设位置之间。

该示例包含以下类

  • StateSwitcher 继承自 QState 并且可以将 StateSwitchTransition 添加到其他状态中。当进入时,它将随机转换到这些状态之一。
  • StateSwitchTransition 是一种在 StateSwitchEvent 触发时触发的自定义转换。
  • StateSwitchEvent 是一种触发 StateSwitchTransitionQEvent
  • QGraphicsRectWidget 是一个 QGraphicsWidget,它仅仅用纯蓝色填充其背景。

方块是 QGraphicsRectWidget 的实例,并在 QGraphicsScene 中动画化。我们这样做是通过构建一个状态图,并将动画插入到状态图中。然后,我们可以在这个状态图中执行一个 QStateMachine。所有这些都在 main() 中完成。让我们首先看看 main() 函数。

main() 函数

在初始化 QApplication 之后,我们使用 QGraphicsRectWidget 设置 QGraphicsScene

    auto button1 = new QGraphicsRectWidget;
    auto button2 = new QGraphicsRectWidget;
    auto button3 = new QGraphicsRectWidget;
    auto button4 = new QGraphicsRectWidget;
    button2->setZValue(1);
    button3->setZValue(2);
    button4->setZValue(3);
    QGraphicsScene scene(0, 0, 300, 300);
    scene.setBackgroundBrush(Qt::black);
    scene.addItem(button1);
    scene.addItem(button2);
    scene.addItem(button3);
    scene.addItem(button4);

在将场景添加到 QGraphicsView 之后,是时候构建状态图了。让我们首先看看我们试图构建的状态图。

请注意,group 有七个子状态,但我们只在图中包含了三个。将逐行检查构建此图的代码,并展示图如何工作。首先,我们构建 group 状态

    QStateMachine machine;

    auto group = new QState;
    group->setObjectName("group");
    QTimer timer;
    timer.setInterval(1250);
    timer.setSingleShot(true);
    QObject::connect(group, &QState::entered, &timer, QOverload<>::of(&QTimer::start));

定时器用于在移动方块之间添加延迟。当进入 group 时启动定时器。稍后我们将看到,group 有一个在计时器超时时返回到 StateSwitcher 的转换。group 是机器中的初始状态,因此当示例启动时将安排动画。

    auto 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);
    ...
    auto 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);

createGeometryState() 返回一个 QState,它将在进入时会设置我们的项目几何形状。它还把 group 作为此状态的父状态。

插入到转换中的 QPropertyAnimation 将使用分配给 QState 的值(即通过 QState::assignProperty()),即动画将在属性的当前值和目标状态中的值之间进行插值。我们将在稍后添加到状态图的动画转换中。

    QParallelAnimationGroup animationGroup;

    auto anim = new QPropertyAnimation(button4, "geometry");
    anim->setDuration(1000);
    anim->setEasingCurve(QEasingCurve::OutElastic);
    animationGroup.addAnimation(anim);

我们将项目并行移动。每个项目都添加到animationGroup中,这是插入到转换中的动画。

    auto subGroup = new QSequentialAnimationGroup(&animationGroup);
    subGroup->addPause(100);
    anim = new QPropertyAnimation(button3, "geometry");
    anim->setDuration(1000);
    anim->setEasingCurve(QEasingCurve::OutElastic);
    subGroup->addAnimation(anim);

顺序动画组subGroup帮助我们为每个项目的动画插入延迟。

    auto stateSwitcher = new StateSwitcher(&machine);
    stateSwitcher->setObjectName("stateSwitcher");
    group->addTransition(&timer, &QTimer::timeout, stateSwitcher);
    stateSwitcher->addState(state1, &animationGroup);
    stateSwitcher->addState(state2, &animationGroup);
    ...
    stateSwitcher->addState(state7, &animationGroup);

我们在StateSwitcher::addState()中将StateSwitchTransition添加到状态切换器中。我们也在这个函数中添加了动画。由于QPropertyAnimation使用状态中的值,我们可以将相同的QPropertyAnimation实例插入所有StateSwitchTransition中。

如前所述,我们添加了一个在计时器超时时触发的转换到状态切换器中。

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

最后,我们可以创建状态机,添加我们的初始状态,并开始状态图的执行。

函数createGeometryState()

createGeometryState()中,我们为每个图形项目设置几何形状。

static QState *createGeometryState(QObject *w1, const QRect &rect1, QObject *w2, const QRect &rect2,
                                   QObject *w3, const QRect &rect3, QObject *w4, const QRect &rect4,
                                   QState *parent)
{
    auto result = new QState(parent);
    result->assignProperty(w1, "geometry", rect1);
    result->assignProperty(w2, "geometry", rect2);
    result->assignProperty(w3, "geometry", rect3);
    result->assignProperty(w4, "geometry", rect4);

    return result;
}

如前所述,QAbstractTransition将使用assignProperty设置的属性值来设置使用addAnimation添加的动画。

StateSwitcher

StateSwitcher具有到我们使用createGeometryState()创建的每个QState的状态切换转换。它的任务是当进入时随机转换到这些状态之一。

StateSwitcher中的所有函数都是内联的。我们将逐步查看它的定义。

class StateSwitcher : public QState
{
    Q_OBJECT
public:
    explicit StateSwitcher(QStateMachine *machine) : QState(machine) { }

StateSwitcher是为特定目的而设计的状态,并且总是顶层状态。我们使用m_stateCount来跟踪我们要管理的状态数量,以及使用m_lastIndex来记住我们最后转换到的状态。

    void onEntry(QEvent *) override
    {
        int n;
        while ((n = QRandomGenerator::global()->bounded(m_stateCount) + 1) == m_lastIndex)
        { }
        m_lastIndex = n;
        machine()->postEvent(new StateSwitchEvent(n));
    }
    void onExit(QEvent *) override {}

我们选择将要转换到的下一个状态,并发布一个StateSwitchEvent,我们知道这将触发所选状态的StateSwitchTransition

    void addState(QState *state, QAbstractAnimation *animation) {
        auto trans = new StateSwitchTransition(++m_stateCount);
        trans->setTargetState(state);
        addTransition(trans);
        trans->addAnimation(animation);
    }

魔法在这里发生。我们为每个添加的状态分配一个数字。这个数字同时赋予StateSwitchTransition和StateSwitchEvents。正如我们所见,状态切换事件将使用相同的数字触发转换。

StateSwitchTransition

StateSwitchTransition继承自QAbstractTransition,在StateSwitchEvent上触发。它只包含内联函数,因此让我们看看它的eventTest()函数,这是我们唯一定义的函数。

    bool eventTest(QEvent *event) override
    {
        return (event->type() == QEvent::Type(StateSwitchEvent::StateSwitchType))
            && (static_cast<StateSwitchEvent *>(event)->rand() == m_rand);
    }

eventTest是通过QStateMachine检查是否应该触发转换时调用的,一个返回true的值意味着它会这样做。我们只是检查我们的分配数字是否等于事件的数字(在这种情况下,我们将发射)。

StateSwitchEvent

StateSwitchEvent继承自QEvent,并持有由StateSwitcher分配给状态和状态切换转换的数字。我们已经在StateSwitcher中看到它如何用来触发StateSwitchTransition

class StateSwitchEvent: public QEvent
{
public:
    explicit StateSwitchEvent(int rand) : QEvent(StateSwitchType), m_rand(rand) { }

    static constexpr QEvent::Type StateSwitchType = QEvent::Type(QEvent::User + 256);

    int rand() const { return m_rand; }

private:
    int m_rand;
};

此类中只有内联函数,因此查看其定义即可。

QGraphicsRectWidget 类

QGraphicsRectWidget 继承自 QGraphicsWidget,简单地将其 rect() 画成蓝色。我们内联了 paintEvent(),这是我们定义的唯一函数。以下是 QGraphicsRectWidget 类的定义

class QGraphicsRectWidget : public QGraphicsWidget
{
public:
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
    {
        painter->fillRect(rect(), Qt::blue);
    }
};

继续前行

本例中展示的技术对所有的 QPropertyAnimation 都同样有效。只要要动画化的值是一个 Qt 属性,你就可以将其动画插入到状态图中。

QState::addAnimation() 接受一个 QAbstractAnimation,因此可以将任何类型的动画插入到图中。

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd. 本文档的贡献者对其各自的贡献享有版权。提供的文档根据 Free Software Foundation 发布的 GNU Free Documentation License 版本 1.3 的条款授权。Qt 及其相关标志是 The Qt Company Ltd. 在芬兰和/或其他国家的商标。所有其他商标均属于其各自的所有者。