深入理解Qt状态机的应用
Chapter1 深入理解Qt状态机的应用(一)
原文链接:https://blog.csdn.net/LeoLei8060/article/details/139777939
Qt的状态机框架提供了一种管理复杂系统状态的方法,它基于经典的有限状态机(FSM)理论。这种框架在开发涉及多种状态和状态之间需要明确转换的应用程序时特别有用,如用户界面交互、网络协议、游戏开发等场景。
什么是有限状态机?
有限状态机(finite-state machine)又称有限状态自动机(finite-state automaton),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。这个概念在计算机科学、数学、语言学、工程以及其他研究领域都有广泛的应用。有限状态机非常适合用于描述那些通过一系列的输入来转移状态的系统。
状态机的组成
有限状态机由以下几个关键部分组成:
- 状态(States):状态机包含有限个状态,这些状态是系统可能存在的不同情况或配置。
- 初始状态(Initial State):系统开始执行时所处的状态。
- 输入(Input):触发状态之间转换的外部事件或数据。
- 转换(Transitions):从一个状态到另一个状态的路径,这些转换是基于输入触发的。
- 最终状态(Final or Accepting States):在自动机中,这些状态可能表示任务完成或接受状态,在某些状态机中这类状态用来指示已达到某种期望的终点。
应用示例
交通信号控制灯系统
交通信号控制灯系统是最简单的一个有限状态机之一。
组成说明
- 状态:
红灯:表示禁止通行
绿灯:表示可以通行
黄灯:表示即将禁止通行 - 初始状态:
红灯:一般来说,为了安全,信号灯系统在启动时设置为红灯状态 - 输入:
计时器超时:在信号灯系统中,输入是内部计时器的超时事件。例如,红灯持续30秒,绿灯持续45秒,黄灯持续3秒。 - 转换:
从红灯状态到绿灯状态:当红灯的计时器超时后
从绿灯状态到黄灯状态:当绿灯的计时器超时后
从黄灯状态到红灯状态:当黄灯的计时器超时后 - 最终状态:
在信号灯系统中,通常不设定最终状态,因为信号灯的工作是无限循环的,不断重复各状态。
简单在线购物流程系统
组成说明
- 状态:
- 浏览商品状态:用户在网站上浏览商品
- 选择商品状态:用户选择特定商品并添加到购物车
- 结算状态:用户查看购物车,选择结算
- 支付状态:用户输入支付信息并提交支付
- 订单确认:系统验证支付信息,并确认订单成功
- 初始状态:
浏览商品状态 - 输入:
- 点击商品加入购物车
- 点击结算按钮
- 点击提交支付
- 支付确认:支付平台处理支付并返回结果
- 转换:
- 从浏览商品状态到选择商品状态:当用户添加商品到购物车
- 从选择商品状态到结算状态:当用户点击结算按钮
- 从结算状态到支付状态:当用户点击支付按钮
- 从支付状态到订单确认状态:支付被处理并确认
- 最终状态:
- 订单确认状态:当订单被系统确认并且支付成功,这个状态通常被视为一个流程的最终状态。
实际应用场景中的流程要更复杂,最终状态可能还会随着退货等操作发生改变,这里就不细化了
Qt状态机框架
Qt状态机框架组成
QStateMachine:这是状态机的主体,管理所有的状态和转换
QState:这是状态对象,管理状态属性
QFinalState:一个特殊的状态,表示最终状态,在满足某条件时进入该状态,触发完成信号
QHistoryState:用于记录状态机的历史,当从一个嵌套状态返回时可以恢复到之前的状态
QAbstractTransition:转换的基类,派生了QSignalTransition和QEventTransition两个类,主要是维护状态转换逻辑
常用接口说明
QStateMachine
addState()
void addState(QAbstractState *state)
添加状态对象到状态机。isRunning()
bool isRunning() const
获取状态机此时的运行状态。start()
void start()
启动状态机。状态机将重置为初始状态。stop()
void stop()
停止状态机。状态机将停止处理事件,然后发出stopped()信号。
QStateaddTransition()
添加转换源,可以是基于事件(QEventTransition)的,也可以是基于信号(QSignalTransition)的。assignProperty()
void assignProperty(QObject *object, const char *name, const QVariant &value)
当前状态下设置object对象的name属性值为value。
简单来说就是在进入该状态时,会触发各个object对象修改各种属性的属性值
比如信号灯:进入红灯状态时,需要把红灯对象的visible(是否可见)属性设为true,把绿灯和黄灯对象的visible属性设为false。
- setInitialState()
void setInitialState(QAbstractState *state)
将状态机的初始状态设置为state,state必须是状态机的子状态。
应用示例
还是以上面提到的交通信号灯系统为例。
效果
这里缩短了每个灯的定时器,黄灯为300毫秒,绿灯为3000毫秒,红灯为4500毫秒。
源码
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QState>
#include <QStateMachine>
#include <QTimer>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget* parent = nullptr);
~Widget();
void initTimer();
void initStateMachine();
private:
Ui::Widget* ui;
QStateMachine* m_stateMachine;
QTimer m_redTimer;
QTimer m_yellowTimer;
QTimer m_greenTimer;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "./ui_widget.h"
Widget::Widget(QWidget* parent)
: QWidget(parent)
, ui(new Ui::Widget)
, m_stateMachine(new QStateMachine(this))
{
ui->setupUi(this);
initTimer();
initStateMachine();
}
Widget::~Widget()
{
delete ui;
}
void Widget::initTimer()
{
m_redTimer.setInterval(3000);
m_redTimer.setSingleShot(true);
m_yellowTimer.setInterval(300);
m_yellowTimer.setSingleShot(true);
m_greenTimer.setInterval(4500);
m_greenTimer.setSingleShot(true);
}
void Widget::initStateMachine()
{
创建状态
QState* redState = new QState(m_stateMachine); // 红灯状态
QState* yellowState = new QState(m_stateMachine); // 黄灯状态
QState* greenState = new QState(m_stateMachine); // 绿灯状态
初始化状态的属性
// 红灯状态下,各个按钮显示的状态
redState->assignProperty(ui->redBtn, "visible", true);
redState->assignProperty(ui->yellowBtn, "visible", false);
redState->assignProperty(ui->greenBtn, "visible", false);
// 红灯状态下,各个按钮显示的状态
yellowState->assignProperty(ui->redBtn, "visible", false);
yellowState->assignProperty(ui->yellowBtn, "visible", true);
yellowState->assignProperty(ui->greenBtn, "visible", false);
// 红灯状态下,各个按钮显示的状态
greenState->assignProperty(ui->redBtn, "visible", false);
greenState->assignProperty(ui->yellowBtn, "visible", false);
greenState->assignProperty(ui->greenBtn, "visible", true);
初始化状态转换过程
// 红灯在红灯计时器超时后转换为绿灯
redState->addTransition(&m_redTimer, &QTimer::timeout, greenState);
// 在进入绿灯状态后,要启动绿灯定时器
connect(greenState, &QState::entered, [&]() { m_greenTimer.start(); });
// 绿灯在绿灯计时器超时后转换为黄灯
greenState->addTransition(&m_greenTimer, &QTimer::timeout, yellowState);
// 在进入黄灯状态后,要启动黄灯定时器
connect(yellowState, &QState::entered, [&]() { m_yellowTimer.start(); });
// 黄灯在黄灯计时器超时后转化为红灯
yellowState->addTransition(&m_yellowTimer, &QTimer::timeout, redState);
// 在进入红灯状态后,要启动红灯定时器
connect(redState, &QState::entered, [&]() { m_redTimer.start(); });
初始化状态机
// 设置状态机的初始状态
m_stateMachine->setInitialState(redState);
// 开启状态机
m_stateMachine->start();
}
Chapter2 深入理解Qt状态机的应用(二)
原文链接:https://blog.csdn.net/LeoLei8060/article/details/139808859
前文《深入理解Qt状态机的应用(一)》介绍了状态机的理论知识以及简单的状态机示例。
在实际应用场景中,状态机往往会比较复杂;本文将详细介绍分组状态、历史状态、并行状态以及其他技术。
通过分组状态共享转换
还是以交通信号灯系统为例,上一篇文章中已经实现了简单的信号灯状态机系统,但只是正常情况下的状态转换。
在实际应用场景中,信号灯除了红黄绿相互跳转外,还会存在一种情况是一直跳闪烁黄灯。我们要如何实现呢?
用按钮事件来模拟触发红黄绿三种状态转换为一直闪烁黄灯状态
// 添加闪烁黄灯状态
QState *flashYellowState = new QState(m_stateMachine);
// 闪烁黄灯状态下的各个对象状态
flashYellowState->assignProperty(ui->redBtn, "visible", false);
flashYellowState->assignProperty(ui->yellowBtn, "visible", true);
flashYellowState->assignProperty(ui->greenBtn, "visible", false);
// 添加其他状态在按钮事件触发时跳转到闪烁黄灯状态
redState->addTransition(ui->button, &QPushButton::clicked, flashYellowState);
yellowState->addTransition(ui->button, &QPushButton::clicked, flashYellowState);
greenState->addTransition(ui->button, &QPushButton::clicked, flashYellowState);
上面代码中因为红黄绿三个状态都在按钮事件的触发下转换为闪烁黄灯状态,这种重复的代码就会比较多。
我们考虑这个示例中一共有四个状态,其中红黄绿三个状态属于一组正常逻辑状态,而闪烁黄灯属于异常逻辑状态,信号灯要么是正常逻辑跳转,要么就是异常逻辑跳转。所以,这里可以用正常逻辑和异常逻辑两个状态作为顶层状态,而红黄绿作为正常逻辑状态的子状态,闪烁黄灯作为异常逻辑状态的子状态,代码如下:
QState *normalState = new QState(m_stateMachine); // 正常的状态组
QState *redState = new QState(normalState); // 红灯状态
QState *yellowState = new QState(normalState); // 黄灯状态
QState *greenState = new QState(normalState); // 绿灯状态
normalState->setInitialState(redState);
QState *abnormalState = new QState(m_stateMachine); // 异常的状态组
QState *flashYellowState = new QState(abnormalState); // 闪烁黄灯状态
abnormalState->setInitialState(flashYellowState);
...
初始化状态机
// 设置状态机的初始状态
m_stateMachine->setInitialState(normalState);
既然红黄绿状态作为了正常逻辑状态的子状态,那么这三个状态跳转到异常逻辑状态的转换代码可以改成:
normalState->addTransition(ui->button, &QPushButton::clicked, abnormalState);
// 解决重复代码问题:下面的代码就可以不用了
// redState->addTransition(ui->button, &QPushButton::clicked, flashYellowState);
// yellowState->addTransition(ui->button, &QPushButton::clicked, flashYellowState);
// greenState->addTransition(ui->button, &QPushButton::clicked, flashYellowState);
所以,通过状态分组,然后只需通过对父状态添加转换就能实现所有子状态的转换逻辑。
这就是分组状态共享转换。
注意:
一个状态只能成为一个父状态的子状态,不能同时存在多个父状态。
用历史状态保存和恢复当前状态
上面将信号灯分成了正常逻辑状态和异常逻辑状态两个顶层状态,将红黄绿作为正常逻辑状态的子状态,在故障按钮触发的情况下,正常逻辑状态转换为异常逻辑状态。
那现在假设在从正常逻辑转换为异常逻辑后,又要转换为正常逻辑状态时要求恢复成故障之前的正常逻辑状态。比如在绿灯的时候,发生故障,转换为异常逻辑状态,然后在修复故障后恢复回绿灯状态,而不是又从红灯开始转换。
QHistoryState
QHistoryState是一个伪状态,用来记录状态机退出某父状态时父状态所处的子状态。
用法
QHistoryState的用法主要包含以下流程:
- 创建历史状态并绑定复合状态(父状态)
QHistoryState *normalHisState = new QHistoryState(normalState); // 历史状态
- 添加转换(从同一个状态机的其他复合状态转换为历史状态)
abnormalState->addTransition(ui->button2, &QPushButton::clicked, normalHisState);
示例代码
void Widget::initStateMachine()
{
创建状态
QState *normalState = new QState(m_stateMachine); // 正常的状态组
QState *redState = new QState(normalState); // 红灯状态
QState *yellowState = new QState(normalState); // 黄灯状态
QState *greenState = new QState(normalState); // 绿灯状态
normalState->setInitialState(redState);
QHistoryState *normalHisState = new QHistoryState(normalState); // 历史状态
QState *abnormalState = new QState(m_stateMachine); // 异常的状态组
QState *flashYellowState = new QState(abnormalState); // 闪烁黄灯状态
abnormalState->setInitialState(flashYellowState);
初始化状态的属性
// 红灯状态下,各个按钮显示的状态
redState->assignProperty(ui->redBtn, "visible", true);
redState->assignProperty(ui->yellowBtn, "visible", false);
redState->assignProperty(ui->greenBtn, "visible", false);
// 黄灯状态下,各个按钮显示的状态
yellowState->assignProperty(ui->redBtn, "visible", false);
yellowState->assignProperty(ui->yellowBtn, "visible", true);
yellowState->assignProperty(ui->greenBtn, "visible", false);
// 绿灯状态下,各个按钮显示的状态
greenState->assignProperty(ui->redBtn, "visible", false);
greenState->assignProperty(ui->yellowBtn, "visible", false);
greenState->assignProperty(ui->greenBtn, "visible", true);
// 闪烁黄灯状态下,各个按钮显示的状态
flashYellowState->assignProperty(ui->redBtn, "visible", false);
flashYellowState->assignProperty(ui->yellowBtn, "visible", true);
flashYellowState->assignProperty(ui->greenBtn, "visible", false);
初始化状态转换过程
// 红灯在红灯计时器超时后转换为绿灯
redState->addTransition(&m_redTimer, &QTimer::timeout, greenState);
// 在进入绿灯状态后,要启动绿灯定时器
connect(greenState, &QState::entered, [&]() { m_greenTimer.start(); });
// 绿灯在绿灯计时器超时后转换为黄灯
greenState->addTransition(&m_greenTimer, &QTimer::timeout, yellowState);
// 在进入黄灯状态后,要启动黄灯定时器
connect(yellowState, &QState::entered, [&]() { m_yellowTimer.start(); });
// 黄灯在黄灯计时器超时后转化为红灯
yellowState->addTransition(&m_yellowTimer, &QTimer::timeout, redState);
// 在进入红灯状态后,要启动红灯定时器
connect(redState, &QState::entered, [&]() { m_redTimer.start(); });
normalState->addTransition(ui->button, &QPushButton::clicked, abnormalState);
abnormalState->addTransition(ui->button2, &QPushButton::clicked, normalHisState);
初始化状态机
// 设置状态机的初始状态
m_stateMachine->setInitialState(normalState);
// 开启状态机
m_stateMachine->start();
}
效果
并行组合复合状态
本节内容将通过一个手动建模画布的示例来介绍如何通过并行组合复合状态来解决复杂的状态逻辑问题。
并行关系指的是同级状态之间相互独立,互不影响。
示例需求说明
手动建模画布的功能包含:
- 画操作
1.1 画直线
1.2 画弧线
1.3 画圆
1.4 画矩形 - 选择模式
2.1 选择线模式
2.2 选择面模式 - 显示数据
3.1 显示背景网格
3.2 显示鼠标坐标
整理状态逻辑
在处于画操作状态时,用户切换选择模式会结束画操作,所以画操作和选择模式属于互斥关系。
而显示数据的切换是不影响前面两个功能的,所以显示数据和前面的操作属于并行关系。
这些状态的逻辑关系如下:
- 互斥关系1:
1.1 画直线
1.2 画弧线
1.3 画圆
1.4 画矩形
1.5 选择线模式
1.6 选择面模式 - 互斥关系2:
2.1 显示背景网格
2.2 不显示背景网格 - 互斥关系3:
3.1 显示鼠标坐标
3.2 不显示鼠标坐标 - 并行关系
4.1 显示背景网格
4.2 显示鼠标坐标
4.3 画操作&选择模式
整理清楚整体的状态逻辑关系非常重要。
实现
既然画操作和选择模式属于互斥关系,那么在它们的上一层用一个互斥状态对象(operatorState)来包装;显示数据和其他属于并行关系,所以可以在外层用并行状态(rootState)来包装。
注意:
状态机(QStateMachine)的ChildMode设置为并行模式(ParallelStates)会导致状态机无效。
它只能是互斥模式(ExclusiveStates)。
void Widget::initStateMachine()
{
// 根状态为并行状态,包装操作状态和显示数据状态属于并行关系
QState *rootState = new QState(QState::ParallelStates, m_stateMachine);
// 操作状态(包含画操作、选择模式操作)
{
QState *operatorState = new QState(rootState);
QState *paintOperState = new QState(operatorState);
QState *paintLineState = new QState(paintOperState);
QState *paintArcState = new QState(paintOperState);
QState *paintRectState = new QState(paintOperState);
QState *paintCircleState = new QState(paintOperState);
paintOperState->setInitialState(paintLineState);
operatorState->setInitialState(paintOperState);
QState *modelOperState = new QState(operatorState);
QState *selLineState = new QState(modelOperState);
QState *selFaceState = new QState(modelOperState);
modelOperState->setInitialState(selLineState);
paintLineState->assignProperty(ui->paintLabel, "text", GETLABELTEXT("画直线"));
paintArcState->assignProperty(ui->paintLabel, "text", GETLABELTEXT("画弧线"));
paintRectState->assignProperty(ui->paintLabel, "text", GETLABELTEXT("画矩形"));
paintCircleState->assignProperty(ui->paintLabel, "text", GETLABELTEXT("画圆"));
selLineState->assignProperty(ui->modelLabel, "text", GETLABELTEXT("选择线模式"));
selFaceState->assignProperty(ui->modelLabel, "text", GETLABELTEXT("选择面模式"));
struct bindTransitionObj
{
QPushButton *m_sender;
QState *m_state;
};
QVector<bindTransitionObj> bindTransitions;
bindTransitions.append({ui->paintLineBtn, paintLineState});
bindTransitions.append({ui->paintArcBtn, paintArcState});
bindTransitions.append({ui->paintCircleBtn, paintCircleState});
bindTransitions.append({ui->paintRectBtn, paintRectState});
bindTransitions.append({ui->selectLineBtn, selLineState});
bindTransitions.append({ui->selectFaceBtn, selFaceState});
for (auto fromObj : bindTransitions) {
for (auto toObj : bindTransitions) {
if (fromObj.m_state != toObj.m_state) {
addTransition(fromObj.m_state, toObj.m_sender, toObj.m_state);
}
}
}
}
{
// 显示状态,属于并行状态
QState *showState = new QState(QState::ParallelStates, rootState);
// 初始化显示背景网格状态
QState *gridState = new QState(showState);
QState *showGridState = new QState(gridState);
QState *notShowGridState = new QState(gridState);
// 初始化显示鼠标坐标状态
QState *positionState = new QState(showState);
QState *showPositionState = new QState(positionState);
QState *notShowPositionState = new QState(positionState);
gridState->setInitialState(notShowGridState);
positionState->setInitialState(notShowPositionState);
showGridState->assignProperty(ui->gridLabel, "text", GETLABELTEXT("显示背景网格"));
notShowGridState->assignProperty(ui->gridLabel, "text", GETLABELTEXT("不显示背景网格"));
showPositionState->assignProperty(ui->positionLabel, "text", GETLABELTEXT("显示鼠标坐标"));
notShowPositionState->assignProperty(ui->positionLabel, "text", GETLABELTEXT("不显示鼠标坐标"));
// 显示背景网格内部的状态转换
addTransition(showGridState, ui->showGridBtn, notShowGridState);
addTransition(notShowGridState, ui->showGridBtn, showGridState);
// 显示鼠标坐标内部的状态转换
addTransition(showPositionState, ui->showPositionBtn, notShowPositionState);
addTransition(notShowPositionState, ui->showPositionBtn, showPositionState);
}
初始化状态机
m_stateMachine->setInitialState(rootState);
m_stateMachine->start();
}
void Widget::addTransition(QState *fromState, QPushButton *btn, QState *toState)
{
fromState->addTransition(btn, &QPushButton::clicked, toState);
}