传送门:
Qt 状态机框架:The State Machine Framework (一)
Qt 状态机框架:The State Machine Framework (二)
一、什么是状态机框架
状态机框架提供了用于创建和执行状态图/表[1]的类。这些概念和表示法基于Harel的Statecharts
:一种复杂系统的可视化形式,也是UML
状态图的基础。状态机执行的语义是基于状态图XML(SCXML)
的。
状态图提供了一种图形化的方式来建模 系统对刺激的反应。这是通过定义系统可能处于的状态,以及系统如何从一种状态移动到另一种状态(状态之间的转换)来实现的。事件驱动系统(如Qt应用程序)的一个关键特征是,行为通常不仅取决于上一个或当前事件,还取决于之前的事件。使用状态图,这些信息很容易表达。
Qt
状态机框架提供了一个API和执行模型,可用于在Qt
应用程序中有效地嵌入状态图的元素和语义。该框架与Qt的元对象系统紧密集成;例如,状态之间的转换可以由信号触发,并且可以将状态配置为在{QObject}s
上设置属性和调用方法。Qt的事件系统用于驱动状态机。
状态机框架中的状态图是分层的。状态可以嵌套在其他状态中,状态机的当前配置由当前活动的一组状态组成。状态机的有效配置中的所有状态都将具有一个共同的祖先。
二、状态机框架中的核心类
下面这些类被Qt用来创建基于事件驱动的状态机。
Class | Description |
---|---|
QAbstractState | The base class of states of a QStateMachine |
QAbstractTransition | The base class of transitions between QAbstractState objects |
QEventTransition | QObject-specific transition for Qt events |
QFinalState | Final state |
QHistoryState | Means of returning to a previously active substate |
QKeyEventTransition | Transition for key events |
QMouseEventTransition | Transition for mouse events |
QSignalTransition | Transition based on a Qt signal |
QState | General-purpose state for QStateMachine |
QStateMachine | Hierarchical finite state machine |
QStateMachine::SignalEvent | Represents a Qt signal event |
QStateMachine::WrappedEvent | Inherits QEvent and holds a clone of an event associated with a QObject |
三、一些状态机的使用示例
3.1 简单3态循环状态机
有一个状态机包含3个状态:s1
、s2
、s3
,这个状态机由一个按钮控制。当按钮被点击,状态机移动到另一个状态。初始状态被设置为s1
.
【状态移动图】:
这台机器的状态转移图如下所示:
【codes】:
#include <QApplication>
#include <QPushButton>
#include <QStateMachine>
#include <QState>
#include <QDebug>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QPushButton* button = new QPushButton(QStringLiteral("状态机测试"),&w);
QLabel* label = new QLabel(QStringLiteral("状态提示"),&w);
button->setGeometry(20,20,200,100);
label->setGeometry(20,130,200,100);
QStateMachine machine;
QObject::connect(&machine,&QStateMachine::runningChanged,
[&](bool running){
qDebug() << "the machine is running? " << running;
});
QState* s1 = new QState();
QState* s2 = new QState();
QState* s3 = new QState();
// 设置不同状态下,指定对象的属性值
s1->assignProperty(label,"text","In state s1");
s2->assignProperty(label,"text","In state s2");
s3->assignProperty(label,"text","In state s3");
// 绑定指定状态的进入和退出动作
QObject::connect(s3, &QState::entered, button, [&](){w.showMaximized();});
QObject::connect(s3, &QState::exited, button, [&](){w.showNormal();});
// 状态转移
s1->addTransition(button,SIGNAL(clicked(bool)),s2);
s2->addTransition(button,SIGNAL(clicked(bool)),s3);
s3->addTransition(button,SIGNAL(clicked(bool)),s1);
// 为状态机添加状态
machine.addState(s1);
machine.addState(s2);
machine.addState(s3);
// 初始化状态机
machine.setInitialState(s1);
// 启动状态机
machine.start();
w.show();
return a.exec();
}
【运行效果】:
3.2 含中止状态的 嵌套状态机 示例
假使我们需要在点击[退出]按钮时,能立即退出应用程序,不用管当前应用处于那种状态。
三个原始状态已被重命名为s11
、s12
和s13
,以反映它们现在是新的顶级状态s1
的子级。子状态隐式继承其父状态的转换。这意味着现在添加从s1
到最终状态s2
的单个转换就足够了。添加到s1
的新状态也将自动继承此转换。
【codes】:
#include "widget.h"
#include <QApplication>
#include <QState>
#include <QStateMachine>
#include <QFinalState>
#include <QPushButton>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QPushButton* button = new QPushButton(QStringLiteral("状态机测试"),&w);
QPushButton* quitButton = new QPushButton(QStringLiteral("退出"),&w);
QLabel* label = new QLabel(QStringLiteral("状态提示"),&w);
button->setGeometry(20,20,200,100);
quitButton->setGeometry(20,130,200,100);
label->setGeometry(20,260,200,100);
QStateMachine machine;
QState* s1 = new QState();
{ /// s1 子状态
QState* s11 = new QState(s1);
QState* s12 = new QState(s1);
QState* s13 = new QState(s1);
/// 状态转移
s11->addTransition(button,SIGNAL(clicked(bool)),s12);
s12->addTransition(button,SIGNAL(clicked(bool)),s13);
s13->addTransition(button,SIGNAL(clicked(bool)),s11);
s11->assignProperty(label,"text","In state s11");
s12->assignProperty(label,"text","In state s12");
s13->assignProperty(label,"text","In state s13");
// 绑定指定状态的进入和退出动作
QObject::connect(s13, &QState::entered, button, [&](){w.showMaximized();});
QObject::connect(s13, &QState::exited, button, [&](){w.showNormal();});
// 指定s1 的初始子状态时s11
s1->setInitialState(s11);
}
QFinalState* s2 = new QFinalState();
// 当退出按钮被点击时,顶层状态移动到 s2——结束状态
s1->addTransition(quitButton,SIGNAL(clicked(bool)),s2);
// 当移动到结束状态时,状态机会发送finished信号,绑定信号和退出应用的槽函数,达到点击退出按钮,程序结束的目的
QObject::connect(&machine, SIGNAL(finished()),
QApplication::instance(), SLOT(quit()));
//! 将top-level state :s1和s2 添加到状态机
machine.addState(s1);
machine.addState(s2);
machine.setInitialState(s1);
machine.start();
w.show();
return a.exec();
}
【运行效果】:
3.3 含中断的状态机 示例
想象一下,我们想在上一节讨论的例子中添加一个“中断”机制;用户应该能够点击按钮以使状态机执行一些不相关的任务,之后状态机应该恢复它之前正在做的任何事情(即,返回到旧状态,在这种情况下是s11
、s12
和s13
中的一个)。
这样的行为可以很容易地使用历史状态进行建模。历史状态(QHistoryState
对象)是一种伪状态,表示父状态上次退出时所处的子状态。
历史状态被创建为我们希望记录当前子状态的状态的子状态;当状态机在运行时检测到这种状态的存在时,它会在父状态退出时自动记录当前(真实)子状态。向历史状态的转换实际上是向状态机先前保存的子状态的转换;状态机自动将转换“转发”到真正的子状态。
下图显示了添加中断机制后的状态机。
【codes】:
#include "widget.h"
#include <QApplication>
#include <QState>
#include <QStateMachine>
#include <QFinalState>
#include <QHistoryState>
#include <QPushButton>
#include <QLabel>
#include <QMessageBox>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QPushButton* button = new QPushButton(QStringLiteral("状态机测试"),&w);
QPushButton* quitButton = new QPushButton(QStringLiteral("退出"),&w);
QPushButton* interruptButton = new QPushButton(QStringLiteral("中断"),&w);
QLabel* label = new QLabel(QStringLiteral("状态提示"),&w);
button->setGeometry(20,20,200,100);
quitButton->setGeometry(20,130,200,100);
interruptButton->setGeometry(20,260,200,100);
label->setGeometry(20,400,200,100);
QStateMachine machine;
QState* s1 = new QState();
{ /// s1 子状态
QState* s11 = new QState(s1);
QState* s12 = new QState(s1);
QState* s13 = new QState(s1);
/// 状态转移
s11->addTransition(button,SIGNAL(clicked(bool)),s12);
s12->addTransition(button,SIGNAL(clicked(bool)),s13);
s13->addTransition(button,SIGNAL(clicked(bool)),s11);
s11->assignProperty(label,"text","In state s11");
s12->assignProperty(label,"text","In state s12");
s13->assignProperty(label,"text","In state s13");
// 绑定指定状态的进入和退出动作
QObject::connect(s13, &QState::entered, button, [&](){w.showMaximized();});
QObject::connect(s13, &QState::exited, button, [&](){w.showNormal();});
// 指定s1 的初始子状态时s11
s1->setInitialState(s11);
}
// 当退出按钮被点击时,顶层状态移动到 s2——结束状态
QFinalState* s2 = new QFinalState();
s1->addTransition(quitButton,SIGNAL(clicked(bool)),s2);
QObject::connect(&machine, SIGNAL(finished()),
QApplication::instance(), SLOT(quit()));
/// 中断状态---------------------------------------------------
QHistoryState *s1h = new QHistoryState(s1);
QState *s3 = new QState();
s3->assignProperty(label, "text", "In s3");
QMessageBox *mbox = new QMessageBox;
mbox->addButton(QMessageBox::Ok);
mbox->setText("Interrupted!");
mbox->setIcon(QMessageBox::Information);
QObject::connect(s3, SIGNAL(entered()), mbox, SLOT(exec()));
// 由中断状态转移到历史状态
s3->addTransition(s1h);
// 当中断按钮被点击时,状态转移到s3 中断状态
s1->addTransition(interruptButton, SIGNAL(clicked()), s3);
//------------------------------------------------------------
machine.addState(s1); // 运行状态
machine.addState(s2); // 结束状态
machine.addState(s3); // 中止状态
// 设置状态机初始状态s1
machine.setInitialState(s1);
// 启动状态机
machine.start();
w.show();
return a.exec();
}
【运行效果】:
四、从源码角度剖析几个关键的函数
*!
\fn template <typename PointerToMemberFunction> QState::addTransition(const QObject *sender, PointerToMemberFunction signal, QAbstractState *target);
\since 5.5
\overload
Adds a transition associated with the given \a signal of the given \a sender
object, and returns the new QSignalTransition object. The transition has
this state as the source, and the given \a target as the target state.
*/
/*!
Adds a transition associated with the given \a signal of the given \a sender
object, and returns the new QSignalTransition object. The transition has
this state as the source, and the given \a target as the target state.
*/
QSignalTransition *QState::addTransition(const QObject *sender, const char *signal,
QAbstractState *target)
{
if (!sender) {
qWarning("QState::addTransition: sender cannot be null");
return 0;
}
if (!signal) {
qWarning("QState::addTransition: signal cannot be null");
return 0;
}
if (!target) {
qWarning("QState::addTransition: cannot add transition to null state");
return 0;
}
int offset = (*signal == '0'+QSIGNAL_CODE) ? 1 : 0;
const QMetaObject *meta = sender->metaObject();
if (meta->indexOfSignal(signal+offset) == -1) {
if (meta->indexOfSignal(QMetaObject::normalizedSignature(signal+offset)) == -1) {
qWarning("QState::addTransition: no such signal %s::%s",
meta->className(), signal+offset);
return 0;
}
}
QSignalTransition *trans = new QSignalTransition(sender, signal);
trans->setTargetState(target);
addTransition(trans);
return trans;
}
/*!
Adds the given \a state to this state machine. The state becomes a top-level
state and the state machine takes ownership of the state.
If the state is already in a different machine, it will first be removed
from its old machine, and then added to this machine.
\sa removeState(u), setInitialState()
*/
void QStateMachine::addState(QAbstractState *state)
{
if (!state) {
qWarning("QStateMachine::addState: cannot add null state");
return;
}
if (QAbstractStatePrivate::get(state)->machine() == this) {
qWarning("QStateMachine::addState: state has already been added to this machine");
return;
}
state->setParent(this);
}
五、总结
本篇尚有状态机的部分使用示例没有展示,特别是并行状态机及其他更复杂的状态转换等。不过楼太高,看着会累。话说笔者也已经累了,遂欲起下篇。有兴趣者,请君移步。
参考文档:
1、https://blog.csdn.net/qq_35629971/article/details/125988152
2、https://www.wisdom.weizmann.ac.il/~dharel/SCANNED.PAPERS/Statecharts.pdf