本页内容

Qt State Machine C++ 指南

状态机框架提供了用于创建和执行状态图的类。本页通过 C++ 语言演示了该框架的主要特性。

状态机框架中的 C++ 类

有关状态机框架中 C++ 类的完整列表,请参阅 Qt State Machine C++ Classes

一个简单的状态机

为了演示状态机 API 的核心功能,让我们来看一个小示例:一个包含三个状态的状态机,分别是s1 、s2 和s3 。该状态机由一个QPushButton 控件控制;当点击按钮时,状态机将转换到另一个状态。初始时,状态机处于s1 状态。该状态机的状态图如下所示:

点击按钮时,三个状态 s1、s2、s3 循环切换

以下代码片段展示了创建此类状态机所需的代码。首先,我们创建状态机及其状态:

    QStateMachine machine;
    QState *s1 = new QState();
    QState *s2 = new QState();
    QState *s3 = new QState();

然后,我们使用QState::addTransition()函数创建状态转换:

    s1->addTransition(button, &QPushButton::clicked, s2);
    s2->addTransition(button, &QPushButton::clicked, s3);
    s3->addTransition(button, &QPushButton::clicked, s1);

接下来,我们将状态添加到状态机中,并设置状态机的初始状态:

    machine.addState(s1);
    machine.addState(s2);
    machine.addState(s3);
    machine.setInitialState(s1);

最后,我们启动状态机:

    machine.start();

状态机以异步方式运行,即它将成为应用程序事件循环的一部分。

在进入和退出状态时执行有用的操作

上述状态机仅在不同状态之间进行转换,并未执行任何操作。QState::assignProperty() 函数可用于在进入某个状态时,让该状态设置QObject 的某个属性。在下面的代码片段中,为每个状态指定了应赋值给QLabel 的text属性的值:

    s1->assignProperty(label, "text", "In state s1");
    s2->assignProperty(label, "text", "In state s2");
    s3->assignProperty(label, "text", "In state s3");

当进入任意一种状态时,标签的文本将相应地发生变化。

进入状态时会发出QState::entered() 信号,退出状态时会发出QState::exited() 信号。在下面的代码片段中,当进入s3 状态时,将调用按钮的showMaximized() 槽;当退出s3 状态时,将调用按钮的showMinimized() 槽:

    QObject::connect(s3, &QState::entered, button, &QPushButton:showMaximized);
    QObject::connect(s3, &QState::exited, button, &QPushButton::showMinimized);

自定义状态可以重写 QAbstractState::onEntry() 和 QAbstractState::onExit()。

能够终止的状态机

上一节定义的状态机永远不会结束。为了使状态机能够结束,它需要有一个顶级终态(QFinalState 对象)。当状态机进入顶级终态时,该机将发出QStateMachine::finished()信号并停止运行。

要在状态图中引入终态,只需创建一个QFinalState 对象,并将其用作一个或多个转换的目标即可。

通过分组状态共享过渡

假设我们希望用户能够随时通过点击“退出”按钮来退出应用程序。为了实现这一点,我们需要创建一个终态,并将其设为与“退出”按钮的clicked() 信号相关联的转换的目标。 我们可以分别从s1 、s2 和s3 各添加一个转换;然而,这似乎有些冗余,而且还必须记得在未来添加每个新状态时都添加这样的转换。

通过将状态s1 、s2 和s3 进行分组,我们可以实现相同的行为(即无论状态机处于哪个状态,点击“退出”按钮都会退出状态机)。具体做法是创建一个新的顶级状态,并将三个原始状态设为该新状态的子状态。下图展示了新的状态机。

包含三个子状态 s11、s12、s13 的状态 s1,这些子状态在点击按钮时循环转换

三个原始状态已重命名为s11 、s12 和s13 ,以反映它们现在是新顶级状态s1 的子状态。子状态会隐式继承其父状态的转换。这意味着现在只需添加一条从s1 到终态s2 的转换即可。添加到s1 的新状态也将自动继承此转换。

要将状态分组,只需在创建状态时指定正确的父状态即可。你还需要指定哪个子状态是初始状态(即当父状态是过渡的目标时,状态机应进入哪个子状态)。

    QState *s1 = new QState();
    QState *s11 = new QState(s1);
    QState *s12 = new QState(s1);
    QState *s13 = new QState(s1);
    s1->setInitialState(s11);
    machine.addState(s1);
    QFinalState *s2 = new QFinalState();
    s1->addTransition(quitButton, &QPushButton::clicked, s2);
    machine.addState(s2);
    machine.setInitialState(s1);

    QObject::connect(&machine, &QStateMachine::finished,
                     QCoreApplication::instance(), &QCoreApplication::quit);

在此示例中,我们希望状态机运行完毕时应用程序退出,因此将状态机的finished()信号连接到应用程序的quit()槽。

子状态可以重写继承的转换。例如,以下代码添加了一个转换,当状态机处于s12 状态时,该转换会有效地使“退出”按钮被忽略。

    s12->addTransition(quitButton, &QPushButton::clicked, s12);

一个过渡可以将任何状态作为目标,即目标状态不必与源状态处于状态层次结构中的同一层级。

使用历史状态保存和恢复当前状态

假设我们想为上一节讨论的示例添加一个“中断”机制; 用户应能够通过点击按钮,让状态机执行一些无关的任务,之后状态机应恢复到之前的状态(即返回旧状态,在本例中为s11 、s12 和s13 中的任意一个)。

这种行为可以通过历史状态轻松建模。历史状态(QHistoryState 对象)是一种伪状态,它表示父状态上次退出时所处的子状态。

历史状态是作为我们要记录其当前子状态的该状态的子状态创建的;当状态机在运行时检测到此类状态的存在时,它会在父状态退出时自动记录当前(真实)的子状态。 向历史状态的转换实际上是向状态机先前保存的子状态的转换;状态机会自动将该转换“转发”到真实的子状态。

下图展示了添加中断机制后的状态机。

状态 s1 包含子状态和历史状态;按下中断按钮后状态转换至 s3,完成后将返回 s1 的历史状态

以下代码展示了其实现方法;在此示例中,当进入s3 时,我们仅显示一个消息框,然后立即通过历史状态返回至s1 的先前子状态。

    QHistoryState *s1h = new QHistoryState(s1);

    QState *s3 = new QState();
    s3->assignProperty(label, "text", "In s3");
    QMessageBox *mbox = new QMessageBox(mainWindow);
    mbox->addButton(QMessageBox::Ok);
    mbox->setText("Interrupted!");
    mbox->setIcon(QMessageBox::Information);
    QObject::connect(s3, &QState::entered, mbox, &QMessageBox::exec);
    s3->addTransition(s1h);
    machine.addState(s3);

    s1->addTransition(interruptButton, &QPushButton::clicked, s3);

使用并行状态避免状态组合爆炸

假设你想在单个状态机中建模一组汽车的互斥属性。假设我们关注的属性是“干净 vs 脏”和“行驶 vs 静止”。要表示所有可能的组合并在它们之间自由切换,需要四个互斥状态和八个转换。

四种状态:干净/静止、干净/移动、脏/静止、脏/移动,包含“启动”、“停止”、“清洁”和“弄脏”这四种状态转换

如果我们添加第三个属性(例如“红色”与“蓝色”),状态总数将翻倍,达到八个;如果再添加第四个属性(例如“封闭式”与“敞篷式”),状态总数将再次翻倍,达到 16 个。

使用并行状态时,随着属性数量的增加,状态和转换的总数呈线性增长,而非指数增长。此外,向并行状态中添加或移除状态时,不会影响其任何兄弟状态。

并行状态 s1,包含区域 s11(干净/脏)和 s12(移动/不移动)

要创建并行状态组,请向QState 构造函数传递QState::ParallelStates 。

    QState *s1 = new QState(QState::ParallelStates);
    // s11 and s12 will be entered in parallel
    QState *s11 = new QState(s1);
    QState *s12 = new QState(s1);

当进入一个并行状态组时,其所有子状态将同时被进入。各个子状态内部的转换将正常运行。但是,任何子状态都可能通过转换退出父状态。当这种情况发生时,父状态及其所有子状态都将被退出。

状态机框架中的并行性遵循交错语义。所有并行操作都将在事件处理的单个原子步骤中执行,因此没有任何事件可以中断并行操作。不过,由于状态机本身是单线程的,事件仍将按顺序处理。 举例说明:假设存在两个退出同一并行状态组的转换,且它们的条件同时成立。在这种情况下,最后处理的那个事件将不会产生任何影响,因为第一个事件已经导致机器退出了该并行状态。

检测复合状态是否已完成

子状态可以是最终状态(QFinalState 对象);当进入最终子状态时,父状态会发出QState::finished()信号。下图展示了一个复合状态s1 ,该状态在进入最终状态前会执行一些处理:

初始状态 s1 通过处理子状态过渡到最终状态,从而触发向 s2 的转换

当进入s1 的最终状态时,s1 将自动发出finished() 信号。我们使用信号转换来触发该事件以引发状态变化:

  s1->addTransition(s1, &QState::finished, s2);

在复合状态中使用最终状态非常有用,当你希望隐藏复合状态的内部细节时;也就是说,外部世界唯一能做的事情就是进入该状态,并在状态完成其工作时收到通知。 在构建复杂(深度嵌套)的状态机时,这是一种非常强大的抽象和封装机制。(在上例中,当然也可以直接从s1 的done 状态创建转换,而不是依赖于s1 的finished()信号,但这样会导致s1 的实现细节被暴露并成为依赖。)

对于并行状态组,当所有子状态均已进入最终状态时,将发出QState::finished()信号。

无目标转换

转换不必具有目标状态。无目标的转换可以像任何其他转换一样被触发;区别在于,当无目标转换被触发时,它不会引起任何状态变化。这使您能够在机器处于特定状态时对信号或事件做出反应,而无需离开该状态。示例:

QStateMachine machine;
QState *s1 = new QState(&machine);

QPushButton button;
QSignalTransition *trans = new QSignalTransition(&button, &QPushButton::clicked);
s1->addTransition(trans);

QMessageBox msgBox;
msgBox.setText("The button was clicked; carry on.");
QObject::connect(trans, QSignalTransition::triggered, &msgBox, &QMessageBox::exec);

machine.setInitialState(s1);

每次点击按钮时都会显示消息框,但状态机将保持在当前状态(s1)。然而,如果目标状态被显式设置为 s1,则每次都会退出并重新进入 s1 状态(例如,会发出QAbstractState::entered() 和QAbstractState::exited() 信号)。

事件、转换和守护

QStateMachine 运行其自身的事件循环。对于信号转换(QSignalTransition 对象),当QStateMachine 拦截到相应的信号时,会自动向自身发布一个QStateMachine::SignalEvent ;同样,对于QObject 事件转换(QEventTransition 对象),也会发布一个QStateMachine::WrappedEvent 。

您可以使用QStateMachine::postEvent()将自定义事件发布到状态机。

向状态机发布自定义事件时,通常还会存在一个或多个可由该类型事件触发的自定义转换。要创建此类转换,需继承QAbstractTransition 类并重写eventTest() 方法,在该方法中检查事件是否与您的事件类型匹配(并可选地检查其他条件,例如事件对象的属性)。

在此,我们定义了自己的自定义事件类型 `StringEvent`,用于向状态机发布字符串:

struct StringEvent : public QEvent
{
    StringEvent(const QString &val)
    : QEvent(QEvent::Type(QEvent::User+1)),
      value(val) {}

    QString value;
};

接下来,我们定义了一个仅在事件的字符串与特定字符串匹配时才会触发的转换(带条件的转换):

class StringTransition : public QAbstractTransition
{
    Q_OBJECT

public:
    StringTransition(const QString &value)
        : m_value(value) {}

protected:
    bool eventTest(QEvent *e) override
    {
        if (e->type() != QEvent::Type(QEvent::User+1)) // StringEvent
            return false;
        StringEvent *se = static_cast<StringEvent*>(e);
        return (m_value == se->value);
    }

    void onTransition(QEvent *) override {}

private:
    QString m_value;
};

在eventTest()的重写中,我们首先检查事件类型是否为所需类型;如果是,则将事件强制转换为StringEvent ,并执行字符串比较。

以下是一个使用自定义事件和转换的状态图:

使用 StringTransition 实现自定义事件处理的 s1 和 s2 状态

状态图的实现如下:

    QStateMachine machine;
    QState *s1 = new QState();
    QState *s2 = new QState();
    QFinalState *done = new QFinalState();

    StringTransition *t1 = new StringTransition("Hello");
    t1->setTargetState(s2);
    s1->addTransition(t1);
    StringTransition *t2 = new StringTransition("world");
    t2->setTargetState(done);
    s2->addTransition(t2);

    machine.addState(s1);
    machine.addState(s2);
    machine.addState(done);
    machine.setInitialState(s1);

状态机启动后,我们就可以向其发布事件了。

    machine.postEvent(new StringEvent("Hello"));
    machine.postEvent(new StringEvent("world"));

任何未被相关转换处理的事件都将被状态机静默处理。将状态分组并为这类事件提供默认处理方式会很有用;例如,如下面的状态图所示:

状态 s1,包含子状态 s11 和 s12,自定义事件转换,并在完成后庆祝该状态

对于嵌套较深的状态图,您可以在最合适的粒度级别添加此类“回退”过渡。

使用恢复策略自动恢复属性

在某些状态机中,将注意力集中在状态中的属性赋值上,而不是在状态不再活动时恢复属性,可能会很有用。 如果您知道,当机器进入一个未显式为该属性赋值的状态时,该属性应始终恢复为其初始值,则可以将全局恢复策略设置为 QStateMachine::RestoreProperties。

QStateMachine machine;
machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

设置此恢复策略后,状态机将自动恢复所有属性。如果进入了一个未设置特定属性的状态,它将首先搜索祖先层次结构,以查看该属性是否已在其中定义。 如果存在,该属性将被恢复为最近祖先所定义的值;如果不存在,则将其恢复为初始值(即在执行任何状态中的属性赋值操作之前的属性值)。

以以下代码为例:

    QStateMachine machine;
    machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

    QState *s1 = new QState();
    s1->assignProperty(object, "fooBar", 1.0);
    machine.addState(s1);
    machine.setInitialState(s1);

    QState *s2 = new QState();
    machine.addState(s2);

假设机器启动时属性fooBar 的值为0.0。当机器处于状态s1 时,该属性将变为1.0,因为该状态明确为其赋予了此值。当机器处于状态s2 时,该属性未被显式赋值,因此将隐式恢复为0.0。

如果使用嵌套状态,父状态为该属性定义的值将被所有未显式为其赋值的子状态继承。

    QStateMachine machine;
    machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

    QState *s1 = new QState();
    s1->assignProperty(object, "fooBar", 1.0);
    machine.addState(s1);
    machine.setInitialState(s1);

    QState *s2 = new QState(s1);
    s2->assignProperty(object, "fooBar", 2.0);
    s1->setInitialState(s2);

    QState *s3 = new QState(s1);

在此,s1 有两个子状态:s2 和s3 。当进入s2 时,属性fooBar 的值将为2.0,因为该值已为该状态显式定义。当状态机处于s3 状态时,该状态未定义该属性值,但s1 将该属性定义为1.0,因此fooBar 将被赋予该值。

动画与状态机

状态机 API 与动画框架相连,允许在状态中赋值时自动对属性进行动画处理。

状态机提供了一个可播放动画的特殊状态。QState 还可以在进入或退出该状态时设置属性,当提供QPropertyAnimation 时,该特殊动画状态将对这些值进行插值。

我们可以使用QSignalTransition 或QEventTransition 类,将一个或多个动画关联到状态之间的过渡中。这两个类都继承自QAbstractTransition ,该类定义了便捷函数addAnimation(),该函数允许追加一个或多个在过渡发生时触发的动画。

我们还可以将属性与状态关联起来,而无需手动设置起始值和结束值。

假设我们有以下代码:

    QState *s1 = new QState();
    QState *s2 = new QState();

    s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));
    s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100));

    s1->addTransition(button, &QPushButton::clicked, s2);

在此我们定义了用户界面的两个状态。在s1 中,button 的值较小;而在s2 中,该值较大。如果点击按钮从s1 过渡到s2 ,则在进入指定状态时,按钮的几何属性将立即被设置。但如果希望过渡过程更加平滑,我们只需创建一个QPropertyAnimation ,并将其添加到过渡对象中即可。

    QState *s1 = new QState();
    QState *s2 = new QState();

    s1->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));
    s2->assignProperty(button, "geometry", QRectF(0, 0, 100, 100));

    QSignalTransition *transition = s1->addTransition(button, &QPushButton::clicked, s2);
    transition->addAnimation(new QPropertyAnimation(button, "geometry"));

为相关属性添加动画意味着,当进入该状态时,属性的赋值将不再立即生效。取而代之的是,动画将在进入状态时开始播放,并平滑地完成属性的赋值。由于我们未显式设置动画的起始值和结束值,系统会自动隐式设定这些值。 动画的起始值将是动画开始时该属性的当前值,而结束值将根据该状态定义的属性赋值进行设置。

如果状态机的全局恢复策略设置为 QStateMachine::RestoreProperties,则还可以为属性恢复添加动画。

检测某个状态下所有属性是否已设置

当使用动画来赋值属性时,状态不再定义属性在机器处于给定状态时将具有的确切值。在动画运行期间,根据动画的不同,属性可能具有任何值。

在某些情况下,能够检测属性何时被实际赋予了状态定义的值会很有用。

假设我们有以下代码:

    QMessageBox *messageBox = new QMessageBox(mainWindow);
    messageBox->addButton(QMessageBox::Ok);
    messageBox->setText("Button geometry has been set!");
    messageBox->setIcon(QMessageBox::Information);

    QState *s1 = new QState();

    QState *s2 = new QState();
    s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));
    connect(s2, &QState::entered, messageBox, SLOT(exec()));

    s1->addTransition(button, &QPushButton::clicked, s2);

当点击button 时,机器将过渡到状态s2 ,该状态会设置按钮的几何属性,然后弹出一个消息框,通知用户几何属性已发生变化。

在未使用动画的正常情况下,这将按预期运行。但是,如果在从s1 过渡到s2 时,为button 的geometry 设置了动画,那么当进入s2 时,动画会开始播放,但geometry 属性在动画运行结束前实际上并不会达到其定义的值。 在这种情况下,消息框会在按钮的几何形状实际设置好之前弹出。

为了确保在几何形状实际达到最终值之前不弹出消息框,我们可以使用状态的propertiesAssigned()信号。当属性被赋值为最终值时,无论是在动画立即播放完成时还是在动画播放结束后,都会发出propertiesAssigned()信号。

    QMessageBox *messageBox = new QMessageBox(mainWindow);
    messageBox->addButton(QMessageBox::Ok);
    messageBox->setText("Button geometry has been set!");
    messageBox->setIcon(QMessageBox::Information);

    QState *s1 = new QState();

    QState *s2 = new QState();
    s2->assignProperty(button, "geometry", QRectF(0, 0, 50, 50));

    QState *s3 = new QState();
    connect(s3, &QState::entered, messageBox, SLOT(exec()));

    s1->addTransition(button, &QPushButton::clicked, s2);
    s2->addTransition(s2, &QState::propertiesAssigned, s3);

在此示例中,当点击button 时,状态机将进入s2 。它将保持在s2 状态,直到geometry 属性被设置为QRect(0, 0, 50, 50) 。随后,它将过渡到s3 。进入s3 时,消息框将弹出。如果过渡到s2 时geometry 属性有动画效果,则状态机将保持在s2 状态,直到动画播放完毕。 如果不存在此类动画,系统将直接设置该属性,并立即进入状态s3 。

无论哪种情况,当状态机处于状态s3 时,都可以确保属性geometry 已被赋予定义的值。

如果全局恢复策略设置为 QStateMachine::RestoreProperties,则在这些操作也执行完毕之前,该状态不会发出propertiesAssigned() 信号。

如果动画尚未完成就退出状态会发生什么

如果某个状态包含属性赋值,且进入该状态的过渡包含针对这些属性的动画,则在属性被赋值为该状态定义的值之前,该状态就可能被退出。正如上一节所述,当存在不依赖于propertiesAssigned() 信号的从该状态退出的过渡时,这种情况尤其可能发生。

状态机 API 保证由状态机赋值的属性必须满足以下条件之一:

  • 该属性已被显式赋值。
  • 当前正在通过动画过渡到已显式赋予该属性的值。

当在动画完成之前退出某个状态时,状态机的行为取决于过渡的目标状态。如果目标状态显式地为该属性赋值,则不会采取任何额外操作。该属性将被赋以目标状态定义的值。

如果目标状态未向该属性赋值,则有两种情况:默认情况下,该属性将被赋予其正在离开的状态所定义的值(即如果允许动画播放完毕,该属性本应被赋予的值)。 但是,如果设置了全局恢复策略,则该策略将优先适用,属性将按常规方式恢复。

默认动画

如前所述,您可以向过渡添加动画,以确保目标状态中的属性赋值具有动画效果。 如果您希望无论采用哪种过渡,都对给定属性使用特定的动画,可以将其作为默认动画添加到状态机中。当构建状态机时尚不清楚特定状态将赋予(或恢复)哪些属性时,此方法特别有用。

QState *s1 = new QState();
QState *s2 = new QState();

s2->assignProperty(object, "fooBar", 2.0);
s1->addTransition(s2);

QStateMachine machine;
machine.setInitialState(s1);
machine.addDefaultAnimation(new QPropertyAnimation(object, "fooBar"));

当状态机处于s2 状态时,由于该属性由s2 赋值,因此状态机将播放fooBar 属性的默认动画。

请注意,在状态转换中显式设置的动画将优先于该属性的任何默认动画。

状态机的嵌套

QStateMachine 是QState 的子类。这使得一个状态机可以成为另一个状态机的子状态。QStateMachine 重新实现了QState::onEntry()并调用QStateMachine::start(),因此当进入子状态机时,它将自动开始运行。

在状态机算法中,父状态机将子状态机视为一个原子状态。子状态机是自包含的;它维护自己的事件队列和配置。特别需要注意的是,子状态机的configuration()方法不属于父状态机的配置(只有子状态机本身属于)。

子状态机的状态不能作为父状态机中转换的目标;只有子状态机本身才能作为目标。反之,父状态机的状态也不能作为子状态机中转换的目标。子状态机的finished()信号可用于触发父状态机中的转换。

另请参阅 《Qt State Machine 概述》和《QML 指南:Qt State Machine 》。

© 2026 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.