Qt:QStateMachine 状态机框架
添加模块
| CMake | find_package(Qt6 REQUIRED COMPONENTS StateMachine)target_link_libraries(mytarget PRIVATE Qt6::StateMachine) |
| qmake | QT += statemachine |
了解
- 分层有限状态机(分层即 有主状态机 且 每种状态下可能有子状态机)
- 执行算法基于状态图 XML(SCXML)算法。
知识点
- 状态类 QAbstractState
- QState 基础状态
- QHistoryState 历史态
- QFinalState 最终态
- 状态转换类 QAbstractTransition
- 无信号,立即转移:addTransition(QAbstractState *target)
- 触发了信号才会转移:
addTransition(const QObject *sender, PointerToMemberFunction signal,QAbstractState *target); - 无目标的转移 QSignalTransition
- 添加/删除状态 addState()/removeState()
- 必须设置初始状态 setInitialState(QAbstractState *state) 再 start()状态机
- 如果遇到错误,状态机会进入错误状态,除非遇到未定义的错误,状态机才会停止。
———— ● ———— ● ———— ● ———— ● ————
练习1:掌握最基础的框架——三态循环
目标:总共有3种状态:s1,s2,s3;状态循环:s1->s2->s3->s1;实现按下next按钮,切换下一个状态;QLabel显示当前状态。
//UI QPushButton *nextButton = new QPushButton("next",this); QLabel *label = new QLabel(this); //1.创建状态机 QStateMachine *machine = new QStateMachine(this); //2.创建基础状态 指定父对象避免泄露也更清晰 QState *s1 = new QState(machine); QState *s2 = new QState(machine); QState *s3 = new QState(machine); //3.给每个状态分配修改属性的工作 s1->assignProperty(label,"text","当前处于s1状态"); s2->assignProperty(label,"text","当前处于s2状态"); s3->assignProperty(label,"text","当前处于s3状态"); //4.按下next按钮是触发状态切换的信号 s1->addTransition(nextButton,&QPushButton::clicked,s2); s2->addTransition(nextButton,&QPushButton::clicked,s3); s3->addTransition(nextButton,&QPushButton::clicked,s1); //5.把状态放进状态机 第2步已经指定了状态机了,这步可省略 /* machine->addState(s1); machine->addState(s2); machine->addState(s3); */ //6.设置初始状态 machine->setInitialState(s1); //7.启动状态机 machine->start();
练习2:QAbstractState的3个信号:进入、退出、是否激活
connect(s1,&QState::entered,this,[](){qDebug()<< "进入 s1";}); connect(s1,&QState::exited,this,[](){qDebug()<< "退出 s1";}); connect(s1,&QState::activeChanged,this,[](bool active){qDebug()<< " s1 是否是激活状态:" << active;}); ////////////////结果////////////////// 进入 s1 s1 是否是激活状态: true s1 是否是激活状态: false 退出 s1 进入 s2 s2 是否是激活状态: true
练习3:最终态 QFinalState
目标:增加一个Quit 退出应用按钮,点击后将当前状态转移到最终态,使得状态机发出finished()信号。
//UI QPushButton *quitButton = new QPushButton("Quit",this); //新建最终态 QFinalState *sf = new QFinalState(machine); //进入最终态会发送QStateMachine::finished信号 //接收到信号做退出应用操作 connect(machine,&QStateMachine::finished,QCoreApplication::instance(),&QCoreApplication::quit); //添加每个状态可以切换到最终态的转移过程 s1->addTransition(quitButton,&QPushButton::clicked,sf); s2->addTransition(quitButton,&QPushButton::clicked,sf); s3->addTransition(quitButton,&QPushButton::clicked,sf); //把最终态放进状态机 machine->addState(sf);
引申:如果最终态在子状态机里(QFinalState *sf1 = new QFinalState(s1);),那么又怎么关闭应用?
当s11->s12->sf1时,触发的是s1的finished信号,所以还需要加上当s1接收到finished信号则转换到最终态,才能通知到machine层
练习4:多层状态机
目标:用多层状态机思想改进练习3,且 让其中一个状态忽略最终态
思考:如果有几十个状态,所有状态都要支持切换到最终态,那岂不是要添加几十行转移过程的代码(单层状态机)
要点:子状态继承父状态的转移过程;子状态可以覆盖继承的转换
//创建状态机 QStateMachine *machine = new QStateMachine(this); //父状态 QState *s1 = new QState(machine); //子状态 QState *s11 = new QState(s1); QState *s12 = new QState(s1); QState *s13 = new QState(s1); //标签显示当前子状态 s11->assignProperty(label,"text","当前状态 s11"); s12->assignProperty(label,"text","当前状态 s12"); s13->assignProperty(label,"text","当前状态 s13"); //最终态 QFinalState *sf = new QFinalState(machine); //进入最终态退出应用 connect(machine,&QStateMachine::finished,QCoreApplication::instance(),&QCoreApplication::quit); //只给父类挂载最终态的转移 s1->addTransition(quitButton,&QPushButton::clicked,sf); //子状态的转移 s11->addTransition(nextButton,&QPushButton::clicked,s12); s12->addTransition(nextButton,&QPushButton::clicked,s13); s13->addTransition(nextButton,&QPushButton::clicked,s11); //子状态可以重写来覆盖父状态的转换 s12->addTransition(quitButton,&QPushButton::clicked,s12); //设置各个状态机的初始状态 machine->setInitialState(s1); s1->setInitialState(s11); //启动状态机 machine->start();
练习5:历史状态 QHistoryState
目标:QLabel显示“当前处于状态X”,增加一个中断按钮,当按下这个按钮,会进入中断状态,3s之后自动回到上一状态。
要点:QHistoryState没有QState那样可以安排属性任务,安排跳转的功能,它只是记录了上一次状态位置。
QHistoryState只能记录自己父类及其子孙的状态,所以要在谁的子状态机种使用,父类就是谁。
QState *s1 = new QState(machine); QState *s2 = new QState(machine); QState *s11 = new QState(s1); QState *s12 = new QState(s1); QHistoryState *sh = new QHistoryState(s1); sh->setDefaultState(s11); s11->assignProperty(label, "text", "当前处于状态s11"); s12->assignProperty(label, "text", "当前处于状态s12"); //按下next按钮:s11->s12->s11 按下interrupt按钮:s1->s2 //s11 s12 是 s1 的子类,所以会继承按下interrupt按钮,跳转到s2 s11->addTransition(nextButton, &QPushButton::clicked, s12); s12->addTransition(nextButton, &QPushButton::clicked, s11); s1->addTransition(interruptButton, &QPushButton::clicked, s2); //实现进入中断状态后改变标签显示文字,3s后回到上一状态 QTimer *backTimer = new QTimer; backTimer->setSingleShot(true); connect(s2,&QState::entered,this,[=](){ label->setText("当前处于中断状态"); backTimer->start(3000); //延时3s }); connect(s2,&QState::exited,this,[=](){ backTimer->stop(); }); //重点!!!用QState去承载中断要做的任务,任务完成后怎么返回上一状态呢 //上一个状态是谁,被sh记录了 s2->addTransition(backTimer, &QTimer::timeout,sh); machine->setInitialState(s1); s1->setInitialState(s11); machine->start();
练习6:并行状态 QState::ParallelStates
要点:父类声明的时候指定并行状态,则子类会同时运行
QState *s1 = new QState(QState::ParallelStates,machine); QState *s11 = new QState(s1); QState *s12 = new QState(s1); connect(s11,&QState::entered,this,[](){qDebug()<<"entered s11";}); connect(s12,&QState::entered,this,[](){qDebug()<<"entered s12";}); machine->setInitialState(s1); s1->setInitialState(s11); machine->start(); /////////////////结果///////////////// 同时输出 entered s11 和 entered s12
练习7:目标为空的转移
思考:之前已经写了多次添加转移的方法了(eg. s1->addTransition(s1,&QState::finished,sf);)第3个参数指定了转移目标,所以目标为空的转移指不指定转移目标。那么为什么会有这种需求呢?
要点:无目标转移类 QSignalTransition
没有转移目标,自然也没有目标信号,用 addTransition(QAbstractTransition *transition) 添加转移,注意和无信号立即转移的参数区别:addTransition(QAbstractState *target)
QStateMachine *machine = new QStateMachine(this); QState *s1 = new QState(machine); QState *s11 = new QState(s1); QState *s12 = new QState(s1); s11->addTransition(nextButton,&QPushButton::clicked,s12); s12->addTransition(nextButton,&QPushButton::clicked,s11); s11->assignProperty(label,"text","当前是s11状态"); s12->assignProperty(label,"text","当前是s12状态"); //无目标的转移 QSignalTransition *t = new QSignalTransition(actButton, &QPushButton::clicked); //添加转移 s12->addTransition(t); connect(t, &QAbstractTransition::triggered, this, [=]{ QMessageBox::information(this, "动作", "收到事件:执行动作,但不换态(仍在 s12)"); }); machine->setInitialState(s1); s1->setInitialState(s11); machine->start();
现在公布思考题的答案:示例实现了当处于s12状态时,按下act按钮,就会显示消息框。这个功能似乎直接绑定actButton的信号槽也能实现?区别在于,直接绑定按钮的信号槽,那么所有状态下都能触发。而使用无目标转移,只有在添加了这个转移的目标状态下才能触发。
练习8:自定义事件 + 自定义迁移 配合
要点:
- 事件类型
- QStateMachine::SignalEvent :当你用 QSignalTransition *addTransition(const QObject *sender, const char *signal, QAbstractState *target);时,状态机会把信号包装成这种事件
- QStateMachine::WrappedEvent:用
QEventTransition监听某个QObject的原始 Qt 事件(例如QEvent::KeyPress、QEvent::MouseButtonPress) - 自定义事件
- 继承QEvent自定义事件
- 用
machine->postEvent()或machine->sendEvent()发给状态机 - 自定义迁移:继承
QAbstractTransition,重写eventTest()决定是否触发,重写onTransition()执行动作
- 内部事件:状态机自己在运行过程中发的,比如
QState::finished
- 守卫:在迁移触发前加一个条件判断
- 信号迁移和事件迁移都能加守卫:继承
QSignalTransition或QEventTransition,重写eventTest()
- 信号迁移和事件迁移都能加守卫:继承
练习 8.1 把练习1改成检测到按下键盘按键就切换到下一状态
111111111111
———— ● ———— ● ———— ● ———— ● ————

浙公网安备 33010602011771号