串口助手基础版-QSerialPort类,QFile类和QFileDialog类练习
写在开头
本练习主要涉及QSerialPort类,QFile类,QFileDialog类和QReadWriteLock类的使用。
其中还包括线程/多线程同步,类之间通过信号传递参数,窗口退出事件/重写事件过滤器实现qlable的点击以及鼠标进入退出操作等。
代码构思
参考常见的串口助手,分析总结一个串口助手工具中应包含以下功能:
- 串口搜索功能:程序一起来就应该自动开始搜索串口,并且实时检测串口状态;
- 串口参数设置:设置串口的波特率、校验位、停止位和数据位等;
- 打开/关闭串口;
- 收发串口数据;
- 存储串口数据;
- 其他。
本练习代码思路
串口的搜索、串口参数的设置、串口打开和关闭以及数据的收发都围绕QSerialPort类进行,用户通过控制UI界面中的控件由信号与槽实现对QSerialPort类的相应操作。而数据存储由QFileDialog类来获取存储路径,通过QFile类来实现打开、读写和关闭文件的操作。
以下是本练习代码的具体思路:
- 串口搜索功能:程序运行后开启searththread线程,线程开启定时器以实现实时搜索串口的功能,并根据搜索的结果来更新相对应的全局变量;
- 串口参数设置:主界面点击设置按钮打开参数设置serialPortsettingform界面,打开后会初始化界面中对应的QCombox控件的Item项供用户选择,点击确定按钮后根据QComboxCurrent结果更新全局变量。
- 打开/关闭串口:主界面控制串口的打开/关闭,打开/关闭状态变化后,界面对应变化为不同样式并且更新状态标志位isSerialPortOpen。
- 收发串口数据:通过连接QSerialPort的readyRead()信号来显示串口数据,根据不同的标志位决定数据的显示位置(显示在哪个界面哪个控件)和状态(是否需要自动下拉/是否需要存储)等。
- 存储串口数据:QFileDialog类获取存储路径,通过QFile类实现打开、读写和关闭操作
- 其他:做了一个mark功能,可以直接在文件内插入标记方便快速定位位置。
代码详细设计
UI界面
1.mainwindow.ui
2.serialportsettingform.ui
3.readdataplusform.ui
代码部分
1.全局变量
单独把一些全局变量和共用的头文件放在appdata.h/.cpp 里面,这样在写的时候结构比较清晰;严谨一点的还可以创建一个类,把变量设置为私有的,再通过get/set去取值和修改。注意全局变量要加extern关键字并且提前需要初始化,否则每个类都会单独创建一个变量导致无法实现全局变量的效果;具体可以自己研究一下extern,static等关键字的作用并且做练习。
appdata.h
#ifndef APPDATA_H
#define APPDATA_H
//头文件
#include <QDesktopWidget>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QList>
#include <QDebug>
#include <QThread>
#include <QTime>
#include <QMutex>
#include <QReadWriteLock>
#include <QWaitCondition>
#include <QSerialPort>
#include <QEvent>
#include <QFont>
#include <QFileDialog>
//存储串口设置项的结构体
struct SerialportInfo
{
QString portName; //设备名
QSerialPort::BaudRate portBaud; //波特率
QSerialPort::StopBits portStopbits; //停止位
QSerialPort::DataBits portDatabits; //数据位
QSerialPort::Parity portParity; //校验位
QSerialPort::FlowControl portFlowcontrol;//流控制模式
//设置默认值:初始化的时候就不用额外写
SerialportInfo()
{
portName = "";
portBaud = QSerialPort::Baud921600;
portStopbits = QSerialPort::OneStop;
portDatabits = QSerialPort::Data8;
portParity = QSerialPort::NoParity;
portFlowcontrol = QSerialPort::NoFlowControl;
};
};
//枚举,用于信号中去传递不同情况的参数
enum markType
{
markBeginTime,
markEndTime,
markTime,
markEndFlag,
};
extern SerialportInfo checkedSerialportInfo; //存储当前被选定的串口:用于控制点击打开串口和设置串口的默认参数
extern QList<QSerialPortInfo> searthDeviceNameList; //搜到的设备的列表:这个只有设备名字,这个列表每一项都要存入SerialportList并且有默认值
extern QList<SerialportInfo> searthDeviceInfoList; //与当前存在的设备保持一致,与SerialportList不一定一致
extern QList<SerialportInfo> SerialportList; //存储所有本地获取过的串口的设置项:用于插入-拔掉-再插入的时候可以调出之前存储的参数
//extern QMutex mutex;
extern QReadWriteLock readWriteLock; //读写锁,用来防止checkedSerialportInfo读取和写入的时候冲突
//extern QWaitCondition isupdateList;
extern bool movetoEndl; //标志位:是否需要自动下拉
extern bool btn_revTextPlusOpen; //标志位:是否此时拓展界面是被打开的
extern QString dataSavePath; //串口数据的存储路径
extern bool saveStateFlag; //标志位:是否需要存储到文件里
class appdata
{
public:
appdata();
};
#endif // APPDATA_H
appdata.cpp 全局变量需要初始化
#include "appdata.h"
SerialportInfo checkedSerialportInfo;//存储当前被选定的串
QList<SerialportInfo> SerialportList;//存储所有已存在串口的设置项:存之前先判断一下之前是否已经存在数据
QList<QSerialPortInfo> searthDeviceNameList;
QList<SerialportInfo> searthDeviceInfoList;
QMutex mutex;
QReadWriteLock readWriteLock;
QWaitCondition isupdateList;//等待事件
bool movetoEndl = true;
bool btn_revTextPlusOpen = false;
QString dataSavePath = NULL;//存储文件的路径
bool saveStateFlag = false;
appdata::appdata()
{
}
2.searthportthread线程
所有的类里面,最先去写搜索线程searthportthread。因为搜索要做的事情比较单一且目的明确,与其他类的关联主要是全局变量的关联。
功能:
- 开启一个1s的定时器,重写timerEvent()事件。实际上线程开启定时器后就结束了,主要是靠定时器来跑,这一点需要理解。
- 搜索到的串口列表先传给searthDeviceNameList变量(该列表只有串口名),根据其去更新变量searthDeviceInfoList(搜索到的设备信息列表,该变量是携带串口参数信息的列表)。再去更新变量与SerialportList(本地储存的设备列表,该变量包含所有搜索过的串口信息,目的是为了实现当某个串口插入-拔出-再插入能记忆之前修改的参数的功能)
- 每次搜索完要检查上一次的checkedSerialportInfo串口是否还存在(该变量为:当前被选中的串口及其信息,用于主界面点击设置和打开按钮时候确定是打开哪一个串口而存在)
- 额外加一个检测用户是否更新设置参数,保持全局变量的参数一致。
searthportthread.h
#ifndef SEARTHPORTTHREAD_H
#define SEARTHPORTTHREAD_H
#include <QObject>
#include "appdata.h"
class searthportthread : public QThread
{
Q_OBJECT
public:
searthportthread();
void run() override;
void timerEvent(QTimerEvent *event);
void updateSerialportList();
void updatecheckedSerialportInfo();
void recoveryBySerialportList();
private:
int serathTimeIntervaltime; //控制搜索的间隔
signals:
void update_cmb_deviceList();
};
#endif // SEARTHPORTTHREAD_H
searthportthread.cpp
#include "searthportthread.h"
searthportthread::searthportthread()
{
serathTimeIntervaltime = startTimer(1000);//开启定时器
}
void searthportthread::run()
{
}
void searthportthread::timerEvent(QTimerEvent *event)
{
if(event->timerId() == serathTimeIntervaltime)
{
searthDeviceNameList = QSerialPortInfo::availablePorts();
updateSerialportList();
recoveryBySerialportList();
updatecheckedSerialportInfo();
}
}
//每次搜索完都要检查:
//更新全局变量中的三个关于设备的List
//1.searthDeviceNameList(当前搜到的设备名列表)与searthDeviceInfoList(上一次搜索的设备信息列表)是否有差异,并且更新searthDeviceInfoList
//2.更新后的searthDeviceInfoList(当前搜索到的设备列表)与SerialportList(本地储存的设备列表)是否有差异,并且更新SerialportList
void searthportthread::updateSerialportList()
{
//1.对比searthDeviceNameList(当前搜到的设备名列表)与searthDeviceInfoList(上一次搜索的设备信息列表)是否有差异
if(searthDeviceInfoList.isEmpty())
{
foreach(QSerialPortInfo i,searthDeviceNameList)
{
SerialportInfo temp;
temp.portName = i.portName();
searthDeviceInfoList.append(temp);
qDebug()<<"searth new port = "<<i.portName();
emit update_cmb_deviceList();//这个信号只有确认了串口变换后才发
}
}
else
{
//既要判断是否多了也要判断是否少了 需要两次嵌套循环
//发现不同就更新bool值,并且在当前循环的末尾去更新
//判断是否减少了串口
bool isNeedUpdate_reduce = true;
//这里用for循环是因为foreach只能遍历List不能修改
for(int index = 0;index<searthDeviceInfoList.size();index++)
{
SerialportInfo temp = searthDeviceInfoList[index];
foreach(QSerialPortInfo i,searthDeviceNameList)
{
if(i.portName() == temp.portName)
{
isNeedUpdate_reduce = false;
break;
}
else
{
isNeedUpdate_reduce = true;
}
}
if(isNeedUpdate_reduce)
{
qDebug()<<searthDeviceInfoList[index].portName<<"Exit!";
searthDeviceInfoList.removeAt(index);
emit update_cmb_deviceList();
}
}
//判断是否多了串口
//发现不同就更新bool值,并且在当前循环的末尾去更新
bool isNeedUpdate_add = false;
for(int index = 0;index<searthDeviceNameList.size();index++)
{
QSerialPortInfo temp = searthDeviceNameList[index];
foreach(SerialportInfo i,searthDeviceInfoList)
{
if(i.portName == temp.portName())
{
isNeedUpdate_add = false;
break;
}
else
{
isNeedUpdate_add = true;
}
}
if(isNeedUpdate_add)
{
SerialportInfo a;
a.portName = temp.portName();
searthDeviceInfoList.append(a);
emit update_cmb_deviceList();
qDebug()<<"searth new port = "<<temp.portName();
}
}
}
//2.对比更新后的searthDeviceInfoList(当前搜索到的设备列表)与SerialportList(本地储存的设备列表)是否有差异
//只加不减,因为本地储存要有一定的记忆功能
if(SerialportList.isEmpty() && !searthDeviceInfoList.isEmpty())
{
SerialportList = searthDeviceInfoList;
}
else
{
bool isNeedupdate = false;
//因为本地储存只需要直接append就好,不需要index值,所以使用foreach
foreach (SerialportInfo i, searthDeviceInfoList)
{
foreach(SerialportInfo j,SerialportList)
{
if(i.portName == j.portName)
{
isNeedupdate = false;
break;
}
else
{
isNeedupdate = true;
}
}
if(isNeedupdate)
{
SerialportInfo temp;
temp.portName = i.portName;
SerialportList.append(temp);
isNeedupdate = false;
}
}
}
}
//每次搜索完要检查上一次的checkedSerialportInfo串口是否还存在
//与searthDeviceInfoList对比:选定的值一定要是当前存在的串口
//1.如果当前searthDeviceInfoList为0,就将当前选定项置空--->无法置空---->恢复默认值
//2.如果由串口数量0到1,直接更新checkedSerialportInfo(可以直接归为第3点)
//3.如果有从N到M,先去判断当前确定的项是否还存在;是的话就不变,不是的话就默认变为SerialportList第一个(包括设备名和设备参数)
//原则:先去判断是否有变更,再统一去写新值 频繁写值容易造成读值错误 此处需要加读写锁
void searthportthread::updatecheckedSerialportInfo()
{
if(searthDeviceInfoList.isEmpty())
{
SerialportInfo temp;
checkedSerialportInfo = temp;
}
else
{
bool NeedUpdate = true;
foreach(SerialportInfo i,searthDeviceInfoList)
{
if(i.portName == checkedSerialportInfo.portName)//代表之前的串口还存在
{
NeedUpdate = false;
}
}
if(NeedUpdate)//不存在了就取第一个为当前的值
{
//这个是需要频繁去获取的参数,所以需要加锁
readWriteLock.lockForWrite();
checkedSerialportInfo = searthDeviceInfoList.at(0);
readWriteLock.unlock();
}
}
}
//保证与searthDeviceInfoList与SerialportList的参数一致
//用于当用户主动更新参数的情况
void searthportthread::recoveryBySerialportList()
{
for