深入理解Qt状态机的应用(二)

前文《深入理解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的用法主要包含以下流程:

  1. 创建历史状态并绑定复合状态(父状态)

QHistoryState *normalHisState = new QHistoryState(normalState); // 历史状态

  1. 添加转换(从同一个状态机的其他复合状态转换为历史状态)

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();
}

效果

Video_2024-06-19_110551.gif

并行组合复合状态

本节内容将通过一个手动建模画布的示例来介绍如何通过并行组合复合状态来解决复杂的状态逻辑问题。

并行关系指的是同级状态之间相互独立,互不影响。

示例需求说明

手动建模画布的功能包含:

  1. 画操作
    1. 画直线
    2. 画弧线
    3. 画圆
    4. 画矩形
  2. 选择模式
    1. 选择线模式
    2. 选择面模式
  3. 显示数据
    1. 显示背景网格
    2. 显示鼠标坐标

整理状态逻辑

在处于画操作状态时,用户切换选择模式会结束画操作,所以画操作和选择模式属于互斥关系。
而显示数据的切换是不影响前面两个功能的,所以显示数据和前面的操作属于并行关系。
这些状态的逻辑关系如下:

  1. 互斥关系1:
    1. 画直线
    2. 画弧线
    3. 画圆
    4. 画矩形
    5. 选择线模式
    6. 选择面模式
  2. 互斥关系2:
    1. 显示背景网格
    2. 不显示背景网格
  3. 互斥关系3:
    1. 显示鼠标坐标
    2. 不显示鼠标坐标
  4. 并行关系
    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);
}

效果

image.png

源码链接

LeoLei8060@github
LeoLei8060@gitee

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/725552.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【计算机毕业设计】211校园约拍微信小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

进入docker容器内部操作mysql数据库

文章目录 1、查询docker容器2、进入mysql容器内部3、连接mysql数据库4、查询mysql所有的数据库5、使用某个数据库6、展示数据库中所有的表7、查询某张表8、断开mysql9、退出mysql容器 1、查询docker容器 [rootlocalhost ~]# docker ps CONTAINER ID IMAGE …

java.lang.ClassNotFoundException: javafx.util.Pair的问题解决与原因详解

先说解决办法: 1、引入依赖 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.12</version> </dependency>2、更换代码依赖地址&#xff1a; 原来依赖地址&#xff1a; import j…

YOLOv10改进 | 注意力篇 | YOLOv10引入EMAttention(EMA)注意力

1. EMA介绍 1.1 摘要:在各种计算机视觉任务中说明了通道或空间注意机制在产生更可辨别的特征表示方面的显着有效性。 然而,通过通道降维来建模跨通道关系可能会给提取深度视觉表示带来副作用。 本文提出了一种新型高效的多尺度注意力(EMA)模块。 着眼于保留每个通道的信息…

springMVC的bug

写SpringMVC时&#xff0c;配置视图解析器路径中少写了个“/”导致url拼接错误&#xff0c;无法返回视图

机器学习参数寻优:方法、实例与分析

机器学习参数寻优:方法、实例与分析 机器学习模型的性能很大程度上依赖于其参数的选择。参数寻优(Hyperparameter Tuning)是提升模型表现的关键步骤之一。本文将详细介绍主流的参数寻优方法,包括网格搜索(Grid Search)、随机搜索(Random Search)、贝叶斯优化(Bayesia…

开发基于Java语言的SaaS(Software-as-a-Service,软件即服务)模式的HIS系统详解 HIS系统源码 支持二开

开发基于Java语言的SaaS&#xff08;Software-as-a-Service&#xff0c;软件即服务&#xff09;模式的HIS系统详解 HIS系统源码 支持二开 开发基于Java语言的SaaS&#xff08;Software-as-a-Service&#xff0c;软件即服务&#xff09;模式的HIS&#xff08;Hospital Informat…

SpringCloud Alibaba Sentinel 流量控制之流控模式实践总结

官网文档&#xff1a;https://sentinelguard.io/zh-cn/docs/flow-control.html wiki地址&#xff1a;https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6 本文版本&#xff1a;spring-cloud-starter-alibaba&#xff1a;2.2.0.RELEASE 如下图所…

STM32单片机I2C通信详解

文章目录 1. I2C通信概述 2. 硬件电路 3. I2C时序基本单元 4. I2C时序 4.1 指定地址写 4.2 当前地址读 4.3 指定地址读 5. I2C外设 6. I2C框图 7. I2C基本结构 8. 主机发送 9. 主机接收 10. 软件和硬件波形对比 11. 代码示例 1. I2C通信概述 I2C(Inter-Integrat…

arm-linux-strip 指令的作用

指令作用 arm-linux-strip 是一个用于从目标文件&#xff08;如可执行文件或对象文件&#xff09;中移除符号信息的工具。这些符号信息&#xff08;如函数名、变量名等&#xff09;在开发过程中很有用&#xff0c;因为它们允许调试器&#xff08;如 GDB&#xff09;确定内存地址…

【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【11】ElasticSearch

持续学习&持续更新中… 守破离 【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【11】ElasticSearch 简介基本概念ElasticSearch概念-倒排索引安装基本命令ik 分词器SpringBoot整合测试存储数据&#xff1a;测试复杂检索同步与异步调用 参考 简介 Elasticsearch 是一…

华为云与AWS负载均衡服务深度对比:性能、成本与可用性

随着云计算的迅速发展&#xff0c;企业对于云服务提供商的选择变得越来越关键。在选择云服务提供商时&#xff0c;负载均衡服务是企业关注的重点之一。我们九河云将深入比较两大知名云服务提供商华为云和AWS的负载均衡服务&#xff0c;从性能、成本和可用性等方面进行对比。 AW…

数据库并发控制技术

1.数据库中为什么要采用并发控制&#xff1f;并发控制技术能保证事务的哪些特性&#xff1f; 因为多个事务的并发操作会对数据库产生影响&#xff0c;当多个事务同时访问一个数据时就会互相干扰。并发控制技术能保证事务的一致性&#xff0c;隔离性。一致性是指事务要么全部运…

ARM服务器虚拟化手机,云手机推流应用案例

大家都知道&#xff0c;ARM 服务器虚拟化手机和云手机推流技术可算是热门话题&#xff0c;不止是企业&#xff0c;个人卖家也会通过云手机推流来获得更多的客源&#xff0c;实现经济自由&#xff0c;但是针对云手机的推流&#xff0c;很多人还是不知道有哪些应用场景~我们可以展…

三十分钟学会RabbitMQ

1、初识MQ 1.1 MQ是什么&#xff1f; MQ(message queue)&#xff0c;从字面意思上看&#xff0c;本质是个队列&#xff0c;FIFO先入先出&#xff0c;只不过队列中存放的内容是message而已&#xff0c;还是一种跨进程的通信机制&#xff0c;用于上下游传递消息。在互联网架构中…

SpringCloud Alibaba Sentinel基础入门与安装

GitHub地址&#xff1a;https://github.com/alibaba/Sentinel 中文文档&#xff1a;https://sentinelguard.io/zh-cn/docs/introduction.html 下载地址&#xff1a;https://github.com/alibaba/Sentinel/releases Spring Cloud Alibaba 官方说明文档&#xff1a;Spring Clou…

前端根据环境变量配置网页的title和favicon

前端根据环境变量配置网页的title和favicon 前言流程步骤一、设置environment文件二、在入口文件中配置三、删除index.html中的title和 icon link四、使用对应的打包命令进行部署 注意事项一、angular中&#xff0c;需要在angular.json添加favicon.ico额外的构建 前言 有些项目…

大学物理绪论组收集和分析

目录 ​编辑 随机误差的估计 算术平均值的标准偏差 不确定度&#xff08;Uncertainty&#xff09;是测量学中的一个重要概念&#xff0c;用于表示测量结果的可靠程度。它反映了测量值可能偏离真值&#xff08;即被测量的客观真实值&#xff09;的程度。 A类不确定度的计算方…

Reverse-Proxy微软开源:高效构建HTTP反向代理的利器

Reverse-Proxy&#xff1a; 简化你的网络架构&#xff0c;用微软的反向代理加速你的服务。- 精选真开源&#xff0c;释放新价值。 概览 微软的reverse-proxy项目是一个高性能的HTTP反向代理应用程序开发工具包。它提供了一种灵活的方式来构建能够处理大量并发连接的代理服务。…

centos 7.8 安装sql server 2019

1.系统环境 centos 7.8 2.数据库安装文件准备 下载 SQL Server 2019 (15.x) Red Hat 存储库配置文件 sudo curl -o /etc/yum.repos.d/mssql-server.repo https://packages.microsoft.com/config/rhel/7/mssql-server-2019.repo 采用yum源进行不安装下载,这时yum 会自动检测…