传送门:
Qt 状态机框架:The State Machine Framework (一)
Qt 状态机框架:The State Machine Framework (二)
1、利用并行态避免态的组合爆炸
假设您想在单个状态机中对汽车的一组互斥属性进行建模。假设我们感兴趣的属性是干净与肮脏,以及移动与不移动。需要四个相互排斥的状态和八个转换才能表示所有可能的组合并在它们之间自由移动。
如果我们增加第三个属性(比如红色与蓝色),状态的总数将翻一番,达到8个;如果我们增加第四个属性(比如,封闭与可转换),状态的总数将再次翻倍,达到16。
使用并行状态,状态和转换的总数随着我们添加更多属性而线性增长,而不是指数增长。此外,状态可以添加到并行状态或从并行状态中删除,而不会影响其任何兄弟状态。
【codes】:
#include <QApplication>
#include <QWidget>
#include <QState>
#include <QStateMachine>
#include <QPushButton>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QPushButton* controlButton_0 = new QPushButton(QStringLiteral("雨刷"),&w);
QPushButton* controlButton_1 = new QPushButton(QStringLiteral("刹车"),&w);
QLabel* label_0 = new QLabel(&w);
QLabel* label_1 = new QLabel(&w);
controlButton_0->setGeometry(0,0,200,60);
controlButton_1->setGeometry(0,70,200,60);
label_0->setGeometry(200,0,200,60);
label_1->setGeometry(200,70,200,60);
QStateMachine machine;
QState* s1 = new QState(QState::ParallelStates);
{
QState* s11 = new QState(s1);
{
QState* s11_clean = new QState(s11);
QState* s11_dirty = new QState(s11);
s11_clean->assignProperty(label_0,"text","clean");
s11_dirty->assignProperty(label_0,"text","dirty");
s11_clean->addTransition(controlButton_0,SIGNAL(clicked(bool)),s11_dirty);
s11_dirty->addTransition(controlButton_0,SIGNAL(clicked(bool)),s11_clean);
s11->setInitialState(s11_clean);
}
QState* s12 = new QState(s1);
{
QState* s12_notMoving = new QState(s12);
QState* s12_moving = new QState(s12);
s12_notMoving->assignProperty(label_1,"text","not moving");
s12_moving->assignProperty(label_1,"text","moving");
s12_notMoving->addTransition(controlButton_1,SIGNAL(clicked(bool)),s12_moving);
s12_moving->addTransition(controlButton_1,SIGNAL(clicked(bool)),s12_notMoving);
s12->setInitialState(s12_notMoving);
}
}
machine.addState(s1);
machine.setInitialState(s1);
machine.start();
w.show();
return a.exec();
}
【运行效果】:
假设汽车已经启动。开/关雨刷和是否踩住刹车是两个并行的,互不干扰的状态。
2、状态结束状态检测
在示例1的基础上,我们对汽车的 【启动->运行->结束】这一完整的过程进行简单的状态模拟。
在实际生活中,我们踩住刹车同时按住【一键启动】按钮,汽车点火,进入s1
启动状态。当汽车熄火,进入s2
状态。当汽车驻车和锁闭车门后,进入s3
状态。生活中的car 启停和运行过程中的操作状态切换模型,远比我们下图中所绘制的要复杂的多。本例对模型进行适当的简化。
在启动状态下,我们可以对汽车进行一些操作,有些操作状态时串行的,也有一些操作状态是并行的。 并行的状态譬如我们在示例1或者如下状态表所示的 开关雨刷与控制汽车的行驶和停止;串行的状态,譬如我们汽车点火和熄火两个状态,他不能同时存在,类似这样的状态我们认为是串行的。不过,串行的状态还可以分为循环状态和 可中止状态。 譬如我们打开车门,在主驾驶座位上可以循环重复【打火启动】->【停车熄火】这两个状态的切换;但是如果我们已经下车,在【熄火】状态下,我们可以将车门重【未锁车】状态切换到【锁车】终止状态。
当进入到终止状态(QFinalState
)后,会发送 finished
信号
【codes】:
#include <QApplication>
#include <QWidget>
#include <QState>
#include <QStateMachine>
#include <QFinalState>
#include <QPushButton>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QPushButton* fire_button = new QPushButton(QStringLiteral("点火/熄火"),&w);
QPushButton* controlButton_0 = new QPushButton(QStringLiteral("雨刷"),&w);
QPushButton* controlButton_1 = new QPushButton(QStringLiteral("刹车"),&w);
QPushButton* lock_button = new QPushButton(QStringLiteral("锁车"),&w);
QLabel* label_0 = new QLabel(&w);
QLabel* label_1 = new QLabel(&w);
fire_button->setGeometry(0,300,200,60);
lock_button->setGeometry(0,400,200,60);
controlButton_0->setGeometry(0,0,200,60);
controlButton_1->setGeometry(0,70,200,60);
label_0->setGeometry(200,0,200,60);
label_1->setGeometry(200,70,200,60);
QStateMachine machine;
QState* car = new QState();
QState* s1 = new QState(QState::ParallelStates,car);
{
QState* s11 = new QState(s1);
{
QState* s11_clean = new QState(s11);
QState* s11_dirty = new QState(s11);
s11_clean->assignProperty(label_0,"text","clean");
s11_dirty->assignProperty(label_0,"text","dirty");
s11_clean->addTransition(controlButton_0,SIGNAL(clicked(bool)),s11_dirty);
s11_dirty->addTransition(controlButton_0,SIGNAL(clicked(bool)),s11_clean);
s11->setInitialState(s11_clean);
}
QState* s12 = new QState(s1);
{
QState* s12_notMoving = new QState(s12);
QState* s12_moving = new QState(s12);
s12_notMoving->assignProperty(label_1,"text","not moving");
s12_moving->assignProperty(label_1,"text","moving");
s12_notMoving->addTransition(controlButton_1,SIGNAL(clicked(bool)),s12_moving);
s12_moving->addTransition(controlButton_1,SIGNAL(clicked(bool)),s12_notMoving);
s12->setInitialState(s12_notMoving);
}
}
QState* s2 = new QState(car);
{
QState* s21 = new QState(s2);
QFinalState* finalState = new QFinalState(s2);
s21->addTransition(lock_button,SIGNAL(clicked(bool)),finalState);
s2->setInitialState(s21);
}
s1->addTransition(fire_button,SIGNAL(clicked(bool)),s2);
s2->assignProperty(label_0,"text",QStringLiteral("已熄火"));
s2->assignProperty(label_1,"text",u8"已熄火");
QState* s3 = new QState(car);
s2->addTransition(s2, SIGNAL(finished()), s3);
s3->assignProperty(label_0,"text",u8"已熄火锁车");
s3->assignProperty(label_1,"text",u8"已熄火锁车");
car->setInitialState(s1);
machine.addState(car);
machine.setInitialState(car);
machine.start();
w.show();
return a.exec();
}
【运行效果】:
解释一下代码演示的流程:
程序启动,状态机启动,进入到默认的car.s1
状态(点火状态),s1
是一个并行的子状态机,包括雨刷和刹车这两个对象的状态控制,所以如图演示的,我们点击雨刷和刹车按钮,可以自由切换两个对象的状态,互补干扰;
当我们点击【点火/熄火】按钮,car
的状态切换到car.s2
(熄火状态),s2
是一个串行的状态机,默认状态是[熄火/未锁车],当我们点击【锁车】按钮时,car
的状态切换到car.s2.finalState
,s2
状态结束,发送一个finished
的信号,car
切换到s3
状态。
至此,一个相对复杂一点儿的状态机模型我们演示完成。当然实际的应用场景往往比这个要复杂的更多。但是我们只要保证一下几个步骤/原则,无论多复杂的状态转换模型,都可以轻而易举的完成。
* 1、状态模块划分(先父后子)
* 2、区分并行or串行
* 3、状态是否要中断
* 4、是否有结束状态
* 5、父状态需添加默认子状态
* 6、状态机需设置默认顶层状态
3、无目标状态的状态转移
有时候,我们会有这样的场景,当前状态(假设是s1
)在接收到某个信号或者事件之后,转移动作被触发。我们可以捕获转移 对象(QAbstractTransition
或者其子对象)发出的 triggered
信号,并在槽函数中做一些业务处理。当结束业务处理逻辑时,有趣的是,当前状态仍是s1
。这样,一个动作就可以反复的react
,就很适合重复性的状态转换了。
【codes】:
#include <QApplication>
#include <QState>
#include <QStateMachine>
#include <QSignalTransition>
#include <QSignalMapper>
#include <QPushButton>
#include <QMessageBox>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QStateMachine machine;
QState* s1 = new QState();
machine.addState(s1);
QPushButton* button = new QPushButton(QStringLiteral("触发"),&w);
button->setGeometry(150,150,100,60);
QSignalTransition* trans = new QSignalTransition(button,SIGNAL(clicked(bool)));
s1->addTransition(trans); // 无目标状态转移
QMessageBox box;
box.setText(QStringLiteral("The button was clicked, carry on."));
QObject::connect(trans,SIGNAL(triggered()),&box,SLOT(exec()));
machine.setInitialState(s1);
machine.start();
w.show();
return a.exec();
}
【运行效果】:
4、事件、转换和防护
QStateMachine
运行自己的事件循环。对于信号转换(QSignalTransition
对象),QStateMachine
在截获相应信号时会自动向自身发布QStateMachine::SignalEvent
;类似地,对于QObject
事件转换(QEventTransition
对象),会发布一个QStateMachine::WrappedEvent
。
您可以使用QStateMachine::postEvent( )
将自己的事件发布到状态机。
将自定义事件发布到状态机时,通常还会有一个或多个自定义转换,这些转换可以由该类型的事件触发。要创建这样的转换,您可以将QAbstractTransition
子类化并重新实现QAbstract transition::eventTest( )
,在其中检查事件是否与您的事件类型(以及可选的其他条件,例如事件对象的属性)匹配。
在下面的例子里,我们定义了自己的自定义事件类型StringEvent,用于将字符串发布到状态机:
【codes】:
```cpp
#include <QApplication>
#include <QState>
#include <QStateMachine>
#include <QSignalTransition>
#include <QFinalState>
#include <QDebug>
#include <QSignalMapper>
#include <QPushButton>
#include <QMessageBox>
#include <QLineEdit>
#include <QLabel>
#include <QHBoxLayout>
#include <QVBoxLayout>
struct StringEvent : public QEvent
{
StringEvent(const QString& val)
: QEvent(QEvent::Type(QEvent::User+1)),value(val)
{
}
QString value;
};
struct StringTransition : public QAbstractTransition
{
Q_OBJECT
public:
StringTransition(const QString& value) : m_value(value)
{}
protected:
bool eventTest(QEvent *e){
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* e) override { Q_UNUSED(e);}
private:
QString m_value;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
QLineEdit* s1_ldt = new QLineEdit;
QLineEdit* s2_ldt = new QLineEdit;
QLabel* s_lab = new QLabel;
QHBoxLayout* s1_hlayout = new QHBoxLayout;
s1_hlayout->addWidget(new QLabel("s1:"));
s1_hlayout->addWidget(s1_ldt);
QHBoxLayout* s2_hlayout = new QHBoxLayout;
s2_hlayout->addWidget(new QLabel("s2:"));
s2_hlayout->addWidget(s2_ldt);
QVBoxLayout* vlayout = new QVBoxLayout;
vlayout->addLayout(s1_hlayout);
vlayout->addLayout(s2_hlayout);
vlayout->addWidget(s_lab);
w.setLayout(vlayout);
QStateMachine machine;
QState* s1 = new QState();
QState* s2 = new QState();
QFinalState* done = new QFinalState();
s1->assignProperty(s_lab,"text","state in s1");
s2->assignProperty(s_lab,"text","state in s2");
QObject::connect(s1_ldt,&QLineEdit::textChanged,[&](){
machine.postEvent( new StringEvent(s1_ldt->text().trimmed()));
});
QObject::connect(s2_ldt,&QLineEdit::textChanged,[&](){
machine.postEvent(new StringEvent(s2_ldt->text().trimmed()));
});
QObject::connect(done,&QFinalState::entered,[&](){
s_lab->setText("state in final state.");
qDebug() << "state in final state.";
});
QObject::connect(done,&QFinalState::exited,[&](){
s_lab->setText("final state has exited.");
});
// s1->s2->done
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.start();
w.show();
return a.exec();
}
#include "main.moc"
【运行效果】:
[参考文档]:
1、https://blog.csdn.net/qq_35629971/article/details/125988152
2、https://www.wisdom.weizmann.ac.il/~dharel/SCANNED.PAPERS/Statecharts.pdf
3、Qt Assistant 5.14.2