Qt State Machine C++ 指南
状态机框架为创建和执行状态图提供了类。本页用 C++ 说明了该框架的主要功能。
状态机框架中的 C++ 类
有关状态机框架中 C++ 类的完整列表,请参阅 Qt State Machine C++ Classes
简单的状态机
为了演示状态机 API 的核心功能,我们来看一个小例子:状态机有三个状态:s1
、s2
和s3
。状态机由一个QPushButton 控制;当点击按钮时,状态机会转换到另一个状态。最初,状态机处于s1
状态。该机器的状态图如下:
下面的代码段显示了创建这样一个状态机所需的代码。首先,我们创建状态机和状态:
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 的文本属性的值:
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 object)。当状态机进入顶层终态时,状态机将发出QStateMachine::finished() 信号并停止。
要在图中引入最终状态,只需创建一个QFinalState 对象,并将其作为一个或多个转换的目标。
通过分组状态共享转换
假设我们希望用户可以随时点击 "退出 "按钮退出应用程序。为此,我们需要创建一个最终状态,并将其作为与退出按钮的clicked() 信号相关联的过渡的目标。我们可以从s1
、s2
和s3
中各添加一个过渡;但这似乎是多余的,而且我们还必须记住从将来添加的每个新状态中添加这样一个过渡。
我们可以通过将s1
、s2
和s3
的状态分组来实现相同的行为(即无论状态机处于哪个状态,单击 "退出 "按钮都会退出状态机)。具体做法是创建一个新的顶层状态,并使原来的三个状态成为新状态的子状态。下图显示了新的状态机。
原来的三个状态被重新命名为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 对象)是一种伪状态,代表父状态最后一次退出时的子状态。
历史状态是作为我们希望记录当前子状态的状态的子状态创建的;当状态机在运行时检测到存在这样一个状态时,它会在父状态退出时自动记录当前(真实)的子状态。向历史状态的转换实际上是向状态机先前保存的子状态的转换;状态机会自动 "转发 "到真实的子状态。
下图显示了添加中断机制后的状态机。
下面的代码展示了如何实现该机制;在本例中,我们只需在输入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 蓝色),状态总数将翻倍,达到 8 个;如果我们增加第四个属性(例如,封闭 vs 可转换),状态总数将再次翻倍,达到 16 个。
使用并行状态后,随着属性的增加,状态和转换的总数将呈线性增长,而不是指数增长。此外,在并行状态中添加或删除状态,不会影响任何同级状态。
要创建并行状态组,请将QState::ParallelStates 传递给QState 构造函数。
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
进入最终状态时,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
并执行字符串比较。
下面是一个使用自定义事件和转换的状态图:
下面是状态图的实现过程:
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"));
未被任何相关转换处理的事件将被状态机静默消耗。对状态进行分组并提供对此类事件的默认处理方式可能很有用,例如下面的状态图:
对于深度嵌套的状态图,你可以在最合适的粒度级别添加这种 "回退 "转换。
使用还原策略自动还原属性
在某些状态机中,将注意力集中在分配状态中的属性上,而不是在状态不再活动时还原这些属性,是非常有用的。如果你知道当机器进入一个没有显式赋予属性值的状态时,某个属性应始终恢复到其初始值,你可以将全局还原策略设置为 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 与动画框架(The Animation Framework)相连接,可以在属性分配到状态时自动为其添加动画。
状态机提供了一个可以播放动画的特殊状态。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
时,机器将播放属性fooBar
的默认动画,因为该属性是由s2
分配的。
请注意,在转换时明确设置的动画将优先于给定属性的默认动画。
嵌套状态机
QStateMachine 是 的子类。它允许一个状态机成为另一个状态机的子状态。 重新实现 () 并调用 () ,这样当进入子状态机时,它会自动开始运行。QState QStateMachine QState::onEntry QStateMachine::start
在状态机算法中,父状态机将子状态机视为原子态。子状态机是独立的,它维护自己的事件队列和配置。特别要注意的是,子机器的configuration() 并不是父机器配置的一部分(只有子机器本身是)。
子状态机的状态不能被指定为父状态机的转换目标,只有子状态机本身可以。反之,父状态机的状态也不能指定为子状态机的转换目标。子状态机的finished() 信号可用于触发父状态机的转换。
© 2025 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.