前言
前一篇文章讲到了利用C#窗体应用程序来设计上位机的界面软件,但是这样的界面软件还不够通用,只能定制化运行,图形界面的代码和上位机初步处理数据的代码耦合性较高。所以这篇文章讲了利用QT Creator将上位机部分的代码编写成QT类库,从而和图形界面解耦合,如果使用不同的软件开发图形界面,都可以使用到这个类库。
创建一个QT程序
- 首先我们需要创建一个QT程序来当做这个类库的主窗体,打开Qt Creator后,选择工具栏"文件"->“新建文件或项目”->“项目”->“Application”->“Qt Widgets Application”;
- 单击"Choose"后,重新命名,选择文件存放路径,点击"下一步";
- 根据自己需要选择"Kits",点击"下一步";然后命名各个文件名和基类名,点击"下一步";最后点击"完成"就可以生成一个Qt主程序了。
创建QT类库
- 首先右键项目栏中的QT项目名,选择"添加新文件",接着选择"C++"->“C++ Class”,点击"Choose";
- 给类名取名,选择要包含的基类,点击"下一步";
- 可以看到该C++类就被添加到主项目里了,最后点击"完成"。头文件就被放到主项目的Headers文件夹下,.cpp文件就放到Sources文件夹下了。
编写QT类库
- 我们在刚创建的头文件的类里创建各个变量和函数,以光开关模块为例,代码如下所示。
#ifndef BOTDR_H
#define BOTDR_H
#include <QMainWindow>
#include <QObject> //Qt链接函数所用
#include <QtSerialPort/QSerialPort> //Qt串口类
#include <QtSerialPort/QSerialPortInfo>
#include <QDebug> //qDebug所用
#include <QtDebug>
#include <QThread> //Qt多进程所用
class BOTDR : public QThread
{
Q_OBJECT
public:
explicit BOTDR();
~BOTDR();
bool ComOpen(int BaudRate); //Qt串口打开函数
bool ComClose(); //Qt串口关闭函数
bool SendData(int *frame, int length); //Qt串口发送数据函数
bool OpticalSwitchSetting(int ChannelNumber); //光开关下发命令函数
int CalBIP8(int *data); //异或校验码函数
struct QTARGC //用来显示下位机参数的结构体
{
int Module; //各个模块的标识码
int Optical_Rx; //存放下位机返回的通道号参数
bool IsSuccess;
bool IsUpdate;
int Error;
int Warning;
int ErrorExtense;
};
QTARGC QtArgc;
signals:
void readyRead(); //信号函数
void DataRead();
public slots:
void ReceivedData(); //Qt串口接收数据函数,也是事件函数,由信号函数触发
private:
QSerialPort* MySerial; //实例化一个私有串口
QByteArray txdata; //串口接收的QByteArray
const int OpticalSwitch = 0x03;
protected:
void run();
};
#endif // BOTDR_H
- 接着我们在创建的.cpp文件里编写类先关的函数。
#include "botdr.h"
BOTDR::BOTDR()
{
//构造函数初始化串口和参数
MySerial = new QSerialPort();
QtArgc.Optical_Rx = 0;
}
BOTDR::~BOTDR()
{
//析构函数关闭串口
BOTDR::ComClose();
}
bool BOTDR::ComOpen(int BaudRate)
{
//查询当前PC内有多少可用的串口,全部放入列表Ports
QList<QSerialPortInfo> Ports = QSerialPortInfo::availablePorts();
int a = Ports.length();
if (a>1)
{
//如果有新插入的设备则MySerial选择对应的端口
MySerial->setPortName(Ports[a-1].portName());
}
else
{
MySerial->setPortName(Ports[0].portName()) ; //默认条件下则选择端口1
}
MySerial->setBaudRate(BaudRate,QSerialPort::AllDirections);//设置波特率和读写方向
MySerial->setDataBits(QSerialPort::Data8); //数据位为8位
MySerial->setFlowControl(QSerialPort::NoFlowControl);//无流控制
MySerial->setParity(QSerialPort::NoParity); //无校验位
MySerial->setStopBits(QSerialPort::OneStop); //一位停止位
//按照以上设置打开MySerial串口
if (MySerial->open(QIODevice::ReadWrite))
{
qDebug() <<MySerial->portName()<<"串口打开成功!";
return true;
}
else
{
qDebug() <<"串口打开失败!";
return false;
}
}
bool BOTDR::ComClose()
{
if(MySerial->isOpen()==true)
{
//清除串口数据并关闭
MySerial->clear();
MySerial->close();
}
return true;
}
bool BOTDR::SendData(int* frame, int length)
{
//将原本的int数组内的数据转换到QByteArray内
for(int i=0;i<length;i++)
{
txdata[i] = frame[i];
}
//串口发送数据
if(MySerial->write(txdata)== length)
{
qDebug()<<"命令发送成功!";
//串口发出数据后,串口如果准备完毕,就链接到数据接收函数,时刻监听串口
QObject::connect(MySerial,SIGNAL(readyRead()), this,SLOT(ReceivedData()));
return true;
}
else
return false;
}
void BOTDR::ReceivedData()
{
QtArgc.IsSuccess =false;
QtArgc.IsUpdate = false;
QtArgc.Module = 0;
QByteArray data = MySerial->readAll(); //将串口缓冲区内所有数据放入QByteArray类型的data内
int length = data.length();
if(length>0) //当数据不为空时
{
//数据转化为无符号十六进制(因为QByteArray会把每个字节数据当成有符号的byte型,但我这里接收到数据是无符号的byte,所以要做个转换,不然接收到的数据就是错误的;所以请根据自行需求加不加这段代码)
int buff[30] ={0};
for(int i=0;i<length;i++)
{
if(data[i]&128)
{
buff[i]= 128+(data[i]&127);
}
else
{
buff[i] = data[i];
}
}
//根据接收到的数据进行初步的处理
if((buff[0]-65+'A') =='D')
{
//标识本次数据更新属于哪个模块
QtArgc.Module = buff[1];
if(buff[1]==OpticalSwitch)
{
QtArgc.Optical_Rx = buff[2];
QtArgc.IsUpdate = true;
}
}
else if((buff[0]-65+'A')=='S')
{
QtArgc.IsSuccess = true;
}
else if((buff[0]-65+'A')=='W')
{
QtArgc.IsSuccess = false;
QtArgc.Warning = buff[1];
}
else if((buff[0]-65+'A')=='E')
{
QtArgc.IsSuccess = false;
QtArgc.Error = buff[1];
if(length > 2)
{
QtArgc.ErrorExtense = buff[2];
}
}
}
// else
// qDebug()<<"数据接收失败!";
}
//光开关命令下发函数
bool BOTDR::OpticalSwitchSetting(int ChannelNumber)
{
if(MySerial->isOpen()) //检查串口是否打开
{
if(ChannelNumber>=0 && ChannelNumber<5)
{
int frame[8] = { 0xfb, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xfe };
frame[4] = ChannelNumber;
frame[1] = BOTDR::CalBIP8(frame);
//发送数据命令帧
if(BOTDR::SendData(frame,8)==true)
{
return true;
}
else
return false;
}
else
return false;
}
else
return false;
}
//异或校验函数
int BOTDR::CalBIP8(int *data)
{
int bip16 = char(data[0] ^ (data[1] & 0x00) ^ data[2] ^ data[3] ^ data[4] ^ data[5] ^ data[6] ^ data[7]);
int bip8 = char(((bip16 & 0xf0) >> 4) ^ (bip16 & 0x0f));
return bip8;
}
void BOTDR::run()
{
//只有当更新和成功标志位置true后才发射信号函数
while(QtArgc.IsUpdate!=true && QtArgc.IsSuccess !=true)
{}
emit DataRead(); //发射信号函数,进而触发读取通道号的函数
}
调用QT类库
- 类库里的函数和变量设置好了之后,我们就可以使用类库了,这里我们不在UI界面做设计,只在mainwindow.cpp里简单调用类库。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//调用类库
com = new BOTDR(); //示例化类
connect(com, SIGNAL(DataRead()),SLOT(DisplayArgc()));
com->start();
//串口打开,光开关设置参数
if(com->ComOpen(9600) == true)
{
com->OpticalSwitchSetting(3);
}
}
void MainWindow::DisplayArgc()
{
if(com->QtArgc.IsSuccess == true)
{
qDebug()<<"本次操作成功";
}
else if(com->QtArgc.IsUpdate == true)
{
switch(com->QtArgc.Module)
{
case 0x03:
{
//OpticalSwitch
qDebug()<<"当前通道号为"<<com->QtArgc.Optical_Rx;
break;
}
default:
break;
}
}
else
{
qDebug()<<"差错代码:"<<com->QtArgc.Error;
qDebug()<<"差错代码扩展:"<<com->QtArgc.ErrorExtense;
qDebug()<<"告警代码:"<<com->QtArgc.Warning;
}
}
- 串口打开后,光开关设置参数,调用类库里相应的函数,就数据命令帧下发到下位机,下位机接收后由外部设备光开关操作后,返回当前通道号给上位机,上位机接收函数ReceivedData()接收后,将其参数存放对应位置,并更新标志位,等待界面软件读取。
优化
为了进一步优化读取通道号的逻辑,可进行Qt线程的链接函数。
- 在头文件的类里新增信号函数
void DataRead()
;调用类库时新增语connect(com,SIGNAL(DataRead()),SLOT(DisplayArgc()));
,即通过信号函数DataRead()来触发读取通道号的显示函数DisplayArgc(),该函数定义在mainwindow类里,后续可由界面软件函数替代。 - 那么信号函数DataRead()什么时候开始运行,可以由一个Qt线程来控制。在头文件里加一个受保护的线程函数,
protected: void run();
,并由调用类库时的语句com->start();
来控制该线程运行;接着在.cpp文件里编写该函数,只有当串口数据接收函数ReceivedData()内的标志位更新了才发射信号函数(这里有需求的可在run()里增加等待时间跳出) - 读取通道号的函数DisplayArgc()被触发后,就会根据标志位来显示当前通道号
通道号设置为3,点击Qt左下角的"运行",最后运行结果如下: