前文《深入理解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:
- 画直线
- 画弧线
- 画圆
- 画矩形
- 选择线模式
- 选择面模式
- 互斥关系2:
- 显示背景网格
- 不显示背景网格
- 互斥关系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);
}
效果
源码链接
LeoLei8060@github
LeoLei8060@gitee