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::KeyPressQEvent::MouseButtonPress
    • 自定义事件
      • 继承QEvent自定义事件
      • machine->postEvent()machine->sendEvent() 发给状态机
      • 自定义迁移:继承 QAbstractTransition,重写 eventTest() 决定是否触发,重写 onTransition() 执行动作
    • 内部事件:状态机自己在运行过程中发的,比如 QState::finished
  • 守卫:在迁移触发前加一个条件判断
    • 信号迁移和事件迁移都能加守卫:继承 QSignalTransitionQEventTransition,重写 eventTest()

练习 8.1 把练习1改成检测到按下键盘按键就切换到下一状态

 

111111111111

———— ● ————  ● ————  ● ————  ● ————

posted @ 2025-08-14 12:21  番茄玛丽  阅读(341)  评论(0)    收藏  举报