记:Qt 开发上位机软件心得

软件开发 · 2023-03-14

最近使用 Qt 开发了一款应用于工业控制的上位机软件,特此撰写一篇文章记录开发过程中的心得体会。

  1. 开发环境

开发环境是 x86 架构的 Windows 和 Ubuntu,安装 Qt 5.12 用于开发,期间我还使用了 Jetbrain CLion 来替代 Qt 自带的 IDE(Qt Creator),CLion 的代码补全、修改函数、提取方法、数据库服务、集成 Git 等特性都很好用。但是有一说一,在调试和界面设计的时候,还是得用回 Qt Creator。

  1. 部署环境

软件会被部署在一台基于瑞芯微 RK3566 平台的工控机上。工控机配备了一块 7 英寸的触摸屏,具有 RJ45 网络接口、USB 接口、RS485 通信接口,运行的是 Ubuntu 18.04,但是桌面环境不是默认的 Gnome,而是 LXDE。部署软件时,Qt Creator 可以通过 SSH 连接到工控机并进行部署和调试,还算是方便。工控机通过 RS485 接口与下位机通信。

  1. 系统设计

拿到需求文档之后,先要分析需求并进行系统设计。这时遇到一个比较有争议的点,需求中提到需要使用数据库来进行数据存取,但是据我所知,一般情况下工控机都是用 csv 文件来读写数据,因为上位机软件存储的数据规模小,也没有并发场景,用数据库属于杀鸡用牛刀。但是我还是试着设计了一个数据库,一个表用来存储密码,一个表用来存储工艺信息,再用两个表存储工艺中包含的工位参数。

  1. 系统实现

这个阶段就是一个踩坑的过程。在 Qt 项目中,main.cpp 一般就是用来初始化 QApplication 对象的,所以不会在这里面写什么东西。除了它之外,软件的架构类似 MVC,写了一个类用于连接和操作数据库,一个类用于串口通信,两个类用于实现两个界面的业务逻辑,另外 Qt Widget Application 的界面是用 ui 文件来描述的,在 Qt Creator 中拖拉控件就可以完成界面设计。以下是开发过程中遇到的一些问题和解决方案。

  • 需要一个全局变量

这属于 C++ 的知识,可以在随便一个类(例如 MainWindow)里面声明一个静态变量,其它类要访问这个静态变量的话,引入“MainWindow”这个类的头文件就可以调用静态变量了。注意:静态变量要在类内声明,类外初始化。

  • 应用需要全屏显示

一行代码就可以解决:setWindowState(Qt::WindowFullScreen);

  • 提取数据库文件至本地

这个问题比较复杂。在 Windows 下比较简单,可以先将数据库文件添加到资源文件(qrc 文件)中,然后就可以使用 QFile 类的 copy 方法把数据库文件复制到指定的路径。但是在 Linux 下这招行不通,QFile 对象也没有输出有用的错误信息。最后决定另辟蹊径,用脚本、安装包等方式把数据库文件放到指定的路径下。

  • 对输入的文本做正则表达式匹配

可以使用 Qt 提供的 QRegularExpression 类,匹配纯数字的代码如下:

auto *re = new QRegularExpression("\\d+");
auto *match = new QRegularExpressionMatch(re->match(string));
bool isMatch = match->hasMatch();
QString matchString = match->captured(0);

其中 isMatch 变量的值就是匹配的结果,matchString 中存放的是匹配通过的字符,可以用这个字符串来判断是不是所有字符都通过了匹配。

  • 使用容器临时存放数据

因为界面上需要具备修改参数的功能,而且是要在用户点击“保存”按钮之后才写入数据库,所以需要一个临时的空间来存储用户修改的数据。一开始使用的是 QMap 来存放数据的键值对,但是发现有个坑:QMap 是基于红黑树来实现的,它是按照键值的升序对元素排序,而不是插入的顺序。后续的解决方案是使用哈希表和链表相结合的方式来实现。哈希表本身是无序的,它可以根据哈希值快速查找元素,所以还需要另外用一个表维护插入顺序。哈希表用来存放数据的键值对,链表用于维护插入哈希表的顺序,后续只要遍历链表元素就可以按插入顺序去遍历哈希表了。

// 哈希表
QHash<QString, QList<QString>> hm;
// 链表
QList<QString> list;
// 哈希表插入元素
hm.insert(key, value);
// 链表插入元素
list.append(key);
// 哈希表删除元素
hm.remove(key);
// 链表删除元素
list.erase(key);
  • 利用迭代器遍历容器

上文的链表引出了另一个问题:要遍历一个容器,很容易想到用下标,但如果是链表这种不支持随机读取的数据结构呢?可以利用迭代器来实现。代码如下:

for(auto it = list.begin(); it != list.end(); ++it)
{
    // it 就是链表中存放的值
    // code here
}
  • 在切换选项卡时弹出对话框

生成 tabWidget 的槽函数 currentChanged,参数是新选项卡的索引。可以通过索引的值判断用户想切换到哪个选项卡,并进行相应的处理,例如弹出对话框等等。

  • 设定计时器

如果我们在开发过程中需要实现这几种功能,就可以利用计时器配合信号槽实现:

1. 间隔一段时间后,执行特定代码
2. 等待一段时间后再执行代码

我们可以在源代码文件中引入QTimer模块,这样就可以使用Qt提供的计时器了。
#include <QTimer>
之后创建一个计时器对象。
QTimer testTimer;
我们需要设置这个计时器的间隔,否则它就不知道要等待多久了,这里的单位是毫秒。
testTimer.setInterval(1000);
默认情况下,这个计时器会每隔1000毫秒发送一次信号,如果我们只希望计时一次,可以将其设置为单次触发模式。
testTimer.setSingleShot(true);
好了,现在我们可以连接计时器到时的槽函数,这样计时器到时就会发送timeout信号,执行对应的槽函数。
connect(&testTimer, QTimer::timeout, this, &MainWindow::testFunc);
现在可以在我们需要的时候开启这个计时器了。
testTimer.start();
当然也可以在有需要的时候停止它:
testTimer.stop();
Qt提供的计时器使用简单,功能也强大,即使在单线程的应用程序中,也可以利用异步执行的信号槽机制实现类似多线程应用程序的功能。

Qt C++
Theme Jasmine by Kent Liao 粤ICP备2021153836号