# micro_flow_EXE **Repository Path**: numda/micro_flow_EXE ## Basic Information - **Project Name**: micro_flow_EXE - **Description**: QT(C++)开发的串口上位机,包含配置文件、串口参数读写、报警、数值显示、动态曲线显示、动态表格显示、日志模块、菜单栏等。仅供个人交流学习使用。 - **Primary Language**: C++ - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 19 - **Created**: 2024-10-09 - **Last Updated**: 2024-10-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README
# 修订历史 | 日期 | 作者 | 版本 | 备注 | | :-------: | :-------: | :--: | :--------------------: | | 2021-4-26 | Jiaqin Bi | 1.0 | 初稿 | | 2021-4-30 | Jiaqin Bi | 1.1 | 新增函数列表、优化代码 | | 2021-5-8 | Jiaqin Bi | 1.2 | 优化代码、修改函数列表 |
# 一、QT Creator使用指南 软件下载地址:链接:https://pan.baidu.com/s/1sEofnZX4B88qOgLY1SwWGQ 提取码:1234 软件安装方式:默认安装。 ## 1、新建项目 1. 选择模板 ![image-20210422135227267](pic/new prj1.png) 2. 创建名称,选择路径 ![image-20210422135352852](pic/new prj2.png) 3. windows下编译直接选择qmake 4. 填写类名,并选择基类,自行选择是否需要ui界面设计文件 ![image-20210422135611650](pic/new prj3.png) 基类中QMainWindow为带有菜单栏的类;QWidget为单个窗口的类;QDialog为对话框的类。 当勾选了Generate form时上位机界面可以通过组件拖拽的形式布局,修改起来比较灵活;若未勾选则需要自行通过代码创建组件并设计布局。 5. 接下来一路默认即可。项目文件以及运行界面如下所示: image-20210422140206978 项目列表中.pro文件为工程文件,QT Creator打开工程时需要找到此文件; Headers文件夹下的为头文件; Sources下的为cpp源文件; Forms下的为ui设计文件; ## 2、项目运行 QT Creator左下角一般在开发过程中选择Debug方式编译运行,在发布时选择Release版本。点击运行按钮为编译运行,点击Debug按钮为Debug方式运行,点击锤子构建按钮为编译项目。如下所示: ![image-20210422140616447](pic/run prj1.png) 快捷键:Ctrl+B 编译不运行 ; Ctrl+R 编译运行。 ## 3、新建类文件 1. 在项目下文件夹上右键选择Add new… 2. 若需要添加源文件和头文件则选择C++ Class,选择下面两个则只创建相应的单个文件。 image-20210422141324383 3. 填入类名并选择基类,点击下一步。如下所示: image-20210422141603798 4. 直接下一步直到完成即可。最后会创建两个文件,如下所示: image-20210422141732500 ## 4、UI界面设计指南 双击.ui文件会进入布局页面,如下所示: image-20210422142232724 设计完成之后,点击左边工具栏编辑、可以看到.ui文件的源码(只读),可以看到源码都是标签和属性。如下所示: image-20210422142536985 ## 5、简单示例 设计一个点击按钮控制文字的显示隐藏的简易工程。 1. 双击.ui文件进入设计界面,调整界面大小(光标在界面右下角拖住调整即可) 2. 拖入一个按钮组件(Push Button)到界面中,双击拖入的按钮修改名称为“隐藏” 3. 选中按钮,到属性区找到objectName修改按钮组件名为“btn_showText” 4. 继续拖入一个Label组件,双击可以修改默认显示文字,然后按照3的步骤修改组件名为lb_text 5. 进入主类文件mainwindow.cpp(此处以新建项目widget_DEMO为例) 6. 在mainwindow.h文件的第18行添加如下代码 ```c++ public slots: void btn_showText_click(); //添加槽函数声明 ``` 7. 在mainwindow.cpp文件的默认构造函数中添加信号与槽的连接代码 ```c++ connect(ui->btn_showText, &QPushButton::clicked, this, &MainWindow::btn_showText_click); ``` 8. 实例化槽函数,代码如下: ```c++ void MainWindow::btn_showText_click() { static bool show_flag = true; show_flag = !show_flag; ui->lb_text->setVisible(show_flag); } ``` 9. 效果如下: ![image-20210422144956278](pic/new prj example.png) 点击按钮隐藏文字,再次点击又显示文字。 ## 6、信号与槽 关于5中示例里面使用到的**信号与槽**是QT中传递信号时最常用的方法。具体介绍此处不做说明。 ## 7、开发语言与开发方式 QT开发比较灵活,有原生的QT(C++)的开发方式,也有pyqt的方式(qt做界面,python写后台)。本次项目中使用的开发语言为C++开发。 # 二、界面设计(690*440) ## 1、首页 image-20210422163732847 ## 2、第二页 ![image-20210422163938716](pic/ui designer2.png) ## 3、第三页 ![image-20210422164124687](pic/ui designer3.png) ## 4、菜单弹出框(455*195) ![image-20210422164554161](pic/ui designer4.png) # 三、组件使用指南(部分) ## 1、Tab Widget 类似于标签页的形式,两个`tab`页面的控件互不影响,但是可以直接访问。 ### 1.1、使用方法 ![image-20210418213136408](pic/image-20210418213136408.png) 如上图所示,将`Tab Widget`组件拖拽到`ui`页面中,同时调整大小和标题。显示时两个tab页面或者多个tab页面互不影响。 选中`tabWdiget`组件。点击需要修改名称的`tab`页,在属性中找到`currentTabText`即可修改显示名称,如下所示: ![image-20210419143045785](pic/image-20210419143045785.png) ### 1.2、访问方法 直接通过`ui`指针访问即可。 # 四、串口通信设计 ## 1、简介 串口通信主要使用到的类为`QT`提供的`QSerialPort`,可以实现配置、连接以及数据的收发。 ## 2、QSerialPort类使用指南 1. 在.pro工程文件中添加`QT += serialport`,重新编译一次; 2. 添加头文件 ````c++ #include #include //获取系统串口信息 ```` 3. 串口配置代码 ```c++ QSerialPort global_port; //串口变量 global_port.setPortName("COM7"); //设置端口号 global_port.setBaudRate(QSerialPort::Baud115200); //波特率 global_port.setParity(QSerialPort::NoParity); //校验位 global_port.setDataBits(QSerialPort::Data8); //数据位 global_port.setStopBits(QSerialPort::OneStop); //停止位 global_port.open(QSerialPort::ReadWrite); //打开 bool类型 ``` ## 3、获取PC机连接的串口 获取电脑连接的所有串口信息需要通过`QSerialPortInfo`类来实现。代码如下: ```c++ for (auto portInfo: QSerialPortInfo::availablePorts()){ //获取本机所有串口名称 //ui->cmbSerialPort->addItem(portInfo.portName()); //将串口名添加到组合框中 qDebug()<\xC0" cmd_recv_length: 9 t_recv: 31.41 cmd_send: "01 03 00 00 00 02 C4 0B" cmd_recv: "\x01\x03\x04\x00\x00\fE>\xC0" cmd_recv_length: 9 t_recv: 31.41 ``` 可以看到接收的数据帧为空,并且经过数据解析之后会显示` 9.80909e-45 `,长度为0,因此需要做数据处理,且中间接收到的异常数据帧也需要做处理,数据处理使用到的知识点为`**异常处理**`。 ## 2、处理 **C++异常处理:** ```c++ try { // 保护代码 }catch( ExceptionName e1 ) { // catch 块 }catch( ExceptionName e2 ) { // catch 块 }catch( ExceptionName eN ) { // catch 块 } ``` ```c++ double division(int a, int b) { if( b == 0 ) { throw "Division by zero condition!"; } return (a/b); } ``` 关键字`try`:块中的代码标识将被激活为特定异常,其后通常跟着一个或多个catch块; 关键字`catch`:在需要处理问题的地方做异常捕获; 关键字`throw`:问题出现时,程序会抛出一个异常,此时通过throw抛出。 添加 如下代码: ```c++ try { if(array.length() == 0){ //接收到的数据帧为0是抛出异常 throw("recv cmd is null!"); } } catch (const char* msg) { //接收到抛出的异常时直接退出本次接收的数据帧的处理 qDebug()< 3. 将`Widget`组件设置好高度和宽度后,组件上右键选择`提升为`,如下图所示 ![image-20210418170839853](pic/image-20210418170839853.png) 如上图中出现`qchartview.h`,直接选中即可,否则在下面`提升的类名称`中填入`QChartView`,然后选中即可。 4. 代码如下 ```c++ /** * Serial.cpp */ ... /*---------------------------曲线相关设计1(Begin)-------------------------------------*/ timer = new QTimer(this); //创建定时器 connect(timer,SIGNAL(timeout()),this,SLOT(DrawLine())); //连接定时器与定时溢出处理槽函数 /*---------------------------曲线相关设计1(End)-------------------------------------*/ ... /*---------------------------曲线相关设计2(Begin)-------------------------------------*/ void Serial::char_init() { if(ui->tabWidget->currentIndex() == 1){ QPen penY(Qt::darkBlue,3,Qt::SolidLine,Qt::RoundCap,Qt::RoundJoin); chart = new QChart(); series = new QSplineSeries; axisX = new QDateTimeAxis(); axisY = new QValueAxis(); scatterSeries->setPointLabelsFormat("@yPoint"); scatterSeries->setPointLabelsVisible(); scatterSeries->setMarkerSize(12); chart->legend()->hide(); //隐藏图例 chart->addSeries(series); //把线添加到chart axisX->setTickCount(10); //设置坐标轴格数 axisY->setTickCount(5); axisX->setFormat("hh:mm:ss"); //设置时间显示格式 axisY->setMin(0); //设置Y轴范围 axisY->setMax(30); axisX->setTitleText("实时时间"); //设置X轴名称 axisY->setLinePenColor(QColor(Qt::darkBlue)); //设置坐标轴颜色样式 axisY->setGridLineColor(QColor(Qt::darkBlue)); axisY->setGridLineVisible(true); //设置Y轴网格显示 axisY->setLinePen(penY); axisX->setLinePen(penY); axisY->setTitleText("数值"); chart->addAxis(axisX,Qt::AlignBottom); //设置坐标轴位于chart中的位置 chart->addAxis(axisY,Qt::AlignLeft); // chart->setBackgroundBrush(QBrush(QColor(64, 64, 64))); // 设置统计图背景 series->attachAxis(axisX); //把数据添加到坐标轴上 series->attachAxis(axisY); series->setName("温度"); series->setPen(QPen(QColor(60, 187, 251), 3)); // 设置曲线的颜色和粗细 //把chart显示到窗口上 ui->widget->setChart(chart); ui->widget->setRenderHint(QPainter::Antialiasing); //设置抗锯齿 timer->start(); timer->setInterval(1000); //设置定时周期 } } //画数据、动态更新数据 void Serial::DrawLine() { QDateTime currentTime = QDateTime::currentDateTime(); //设置坐标轴显示范围 chart->axisX()->setMin(QDateTime::currentDateTime().addSecs(-60 * 1)); //系统当前时间的前一秒 chart->axisX()->setMax(QDateTime::currentDateTime().addSecs(0)); //系统当前时间 //增加新的点到曲线末端 series->append(currentTime.toMSecsSinceEpoch(), obj_param.temper_f); //obj_param.temper_f为动态数据 } /*---------------------------曲线相关设计2(End)-------------------------------------*/ ``` ```c++ /** * Serial.h */ /*-------------曲线类-----------------*/ #include #include #include #include #include "QDateTime" /*-----------------------------------*/ private slots: /*-----------曲线相关设计4(Begin)----------------*/ void DrawLine(); //画线(这里可以考虑使用函数数组) /*-------------曲线相关设计4(End)----------------*/ private: /*-----------曲线相关设计1(Begin)----------------*/ QTimer *timer; //计时器 QChart *chart; //画布 QSplineSeries *series; //线 QScatterSeries *scatterSeries; //离散点 QDateTimeAxis *axisX; //轴 QValueAxis *axisY; void char_init(); /*-----------曲线相关设计1(End)----------------*/ ``` 4、演示如下: ![chart view](pic/chart view.gif) ## 3、散点标记 ```c++ QChart *chart = new QChart(); //创建容器 QScatterSeries *scatterSeries = new QScatterSeries(); chart->addSeries(QScatterSeries); //散点图加入到容器中 scatterSeries->setVisible(true); //设置可见 scatterSeries->setMarkerSize(6); //设置点的大小 scatterSeries->attachAxis(axisX); //将数据点绑定X轴 scatterSeries->attachAxis(axisY); //将数据点绑定Y轴 scatterSeries->setName("散点"); //散点图标题 scatterSeries->setPointLabelsFormat("@yPoint"); //设置标记点样式 @xPoint , @ypoint scatterSeries->append(currentTime.toMSecsSinceEpoch(), obj_param.temper_f); //添加数据 x , y ``` ## 4、曲线上显示数值和标记点 若想在曲线的转折处显示拐点标记,可不必使用散点图,直接使用下面的方式即可。 ```c++ QChart *chart = new QChart(); //创建容器 QSplineSeries *series = new QSplineSeries; ... series->setPointLabelsFormat("@yPoint"); //拐点处显示数值的格式 series->setPointLabelsVisible(true); //拐点处显示数值 series->setPointsVisible(true); //拐点处显示拐点 series->append(currentTime.toMSecsSinceEpoch(), obj_param.temper_f); //添加数据 x , y ... ``` ## 5、隐藏部分图例 ```c++ //隐藏部分图例的方法,但是曲线和散点图的图例只能同时显示、同时隐藏 foreach (QLegendMarker* marker, chart->legend()->markers()){ if(marker->type() == QLegendMarker::LegendMarkerTypeXY) { marker->setVisible(false); } } ``` 提供的隐藏样式类别如下: | Constant | Value | Description | | ------------------------------------------ | ----- | ------------------------------------------------------ | | QLegendMarker::LegendMarkerTypeArea | 0 | A legend marker for an area series. | | QLegendMarker::LegendMarkerTypeBar | 1 | A legend marker for a bar set. | | QLegendMarker::LegendMarkerTypePie | 2 | A legend marker for a pie slice. | | QLegendMarker::LegendMarkerTypeXY | 3 | A legend marker for a line, spline, or scatter series. | | QLegendMarker::LegendMarkerTypeBoxPlot | 4 | A legend marker for a box plot series. | | QLegendMarker::LegendMarkerTypeCandlestick | 5 | A legend marker for a candlestick series. | ## 6、复选框隐藏曲线 1. 复选框选中的判断 ```c++ //以cb_temper复选框为例 ui->cb_temper->isChecked(); ``` 2. 曲线的隐藏 ```c++ //以series1曲线为例 QSplineSeries *series1 = new QSplineSeries; series1->setVisible(false); ``` 3. 控制逻辑 复选框选中设置显示曲线,复选框未选中取消曲线显示。代码如下: ```c++ if(ui->cb_temper->isChecked()){ //复选框按下 series1->setVisible(true); }else{ series1->setVisible(false); } ``` ## 7、主题切换 主题切换方法为setTheme() ```c++ QChart *chart = new QChart(); chart->setTheme(QChart::ChartThemeBrownSand); //浅色主题设置 ``` 主题列表如下: | 类型 | 值 | 说明 | | :----------------------------: | :--: | :--: | | QChart::ChartThemeLight | 0 | | | QChart::ChartThemeBlueCerulean | 1 | | | QChart::ChartThemeDark | 2 | | | QChart::ChartThemeBrownSand | 3 | | | QChart::ChartThemeBlueNcs | 4 | | | QChart::ChartThemeHighContrast | 5 | | | QChart::ChartThemeBlueIcy | 6 | | | QChart::ChartThemeQt | 7 | | # 九、菜单设计 ## 1、介绍 前期有介绍到需要菜单功能时使用的基类为`QMainWindow`。同时菜单上添加图标时需要使用到Qt资源文件,后面会进行介绍。 ## 2、新建步骤 1. 双击UI文件进入设计界面 2. 在界面左上角点击”在这里输入“即可进行添加,如下所示: ![4-1添加文件菜单.png](pic/new title1.jpeg) 3. 子菜单也是同样添加,如下所示: ![image-20210423143110652](pic/new title2.png) 4. 图标添加在第三小节中介绍 ## 3、添加图标 双击下方的动作,弹出对话框,如下所示: ![image-20210423143505687](pic/new title3.png) 点击图标,可以选择图标添加方式,如下所示: ![image-20210423143751494](pic/new title4.png) 由于打包发布时发现直接`选择文件`作为图标时,会出现图标不显示的情况,所以本次通过`选择资源`进行图标的添加,通过资源文件添加时需要使用到第四小节介绍到的资源文件。 点击`选择资源`后就可以选择图标,如下所示: ![image-20210423150428464](pic/new title5.png) ## 4、添加资源文件 1. 首先将图标文件的文件夹拷贝到项目目录下; 2. 新建资源文件如下: ![4-6选择资源文件模板.png](pic/add res.jpeg) 3. 添加资源文件夹名称,点击下一步 4. 前缀修改成拷贝过来的保存在文件夹目录名称,添加文件 5. 保存 # 十、动态表格设计 ## 1、简介 使用`TableWidget`或者`TableView`组件可以实现表格动态插入数据。通过代码可以实现自动翻页、上一页、下一页、首页、尾页、清除数据等功能。效果如下: ![tablewidget](pic/tablewidget.gif) ## 2、QTableWidget组件的使用 ![image-20210420132632810](pic/image-20210420132632810.png) 添加好之后可以在`UI`设计界面设置好行列数,也可以后续通过代码来设计,本次选择后续代码加入。 ## 3、表格初始化 初始化表格包括表行头、列头以及行数和列数等。 代码如下所示: ```c++ ui->tableWidget->setRowCount(12); //设置行数 ui->tableWidget->setColumnCount(6); //设置列数 //设置表头 ui->tableWidget->setHorizontalHeaderLabels(QStringList()<<"索引"<<"时间"<<"温度"<<"状态"<<"流量"<<"状态"); ui->tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignHCenter);//表头字体居中 ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);//单元格不可编辑 ui->tableWidget->horizontalHeader()->setStyleSheet("QHeaderView::section{background:rgb(2,20,130);color: white;}");//设置表头背景和字体颜色 ui->tableWidget->verticalHeader()->setVisible(false); //隐藏列表头 ui->tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); //自适应列宽 ui->tableWidget->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); //第一列索引列宽最小,其他列自适应列宽 //ui->tableWidget->verticalHeader()->setSectionResizeMode(QHeaderView::Stretch); //自适应行高 //插入数据如下,通过行列 //ui->tableWidget->setItem(row,col, new QTableWidgetItem("data")); ``` ## 4、数据插入 数据插入通过行列来定位要插入的单元格,行和列的下标都是从0开始,插入的内容需要格式化成字符串的形式才能插入。示例如下所示: ```c++ //在第一行第一列插入数据 //ui->tableWidget->setItem(0,0, new QTableWidgetItem("单元格1")); ``` ## 5、翻页设计(上一页、下一页、页码) 本次翻页通过滚动条的位置来进行设计,所以需要动态插入页以及动态插入行数据。 **设计思路:**每页12行6列(去除原始的列头),插入第13行数据时需要先新建一个12行的页面,依次类推(25、37…) 1. 首先在初始化表格的时候默认插入一页12行6列的空白表格,此时总页码(page_int)和当前页(page_current)均为1; 2. 插入数据时设置一个变量(table_row_count)来记录此时插入的条数,当插入需要插入第13行数据时先判断(table_row_count-1)%12是否为零,为零则增加新页,否则当前页数据未插满; 3. 插入第13行数据时考虑加入新页(依旧是12行6列的空白页),此时总页码+1,若自动翻页则当前页码+1; 4. 判断后续插入行是否为25、37等行数的时候都需要通过(table_row_count-1)%12==0来判断,是则插入新页,更新页码; 5. 当需要自动翻页的时候,每次插入新页的时候都需要将滚动条滚动到最下面来显示最新的页面,同时当前页码和总页码相同; 6. 若不需要自动翻页时,则需要通过一个标志位来判断是否需要翻页,上一页和下一页也是如此,本次设计手动翻页的时候会将自动翻页功能关闭,以免影响翻页停留观看数据时会跳转到最新页面; 7. 翻页时上一页为滚动条当前位置-12,下一页为滚动条当前位置+12,同时需要判断首页和尾页位置以及更新当前页码; 8. 插入数据为定时器自动插入(需判断此时是否能接收到有效数据)。 代码如下所示: ```c++ /** * @brief 表格定时插入数据 */ void Serial::insert_item() { int row_count = ui->tableWidget->rowCount(); //查找当前已经设置的行数 if((table_row_count-1)%12 == 0 && (table_row_count-1)!=0){ //第13行时需要插入一个新的页面 ui->tableWidget->setRowCount((row_count)+12); //行数不够,直接添加一个新页面 page_int = ((table_row_count-1)%12==0)?(page_int+1):(page_int); //设置页数 page_current = page_int; //当前页面 QString page_str = QString::number(page_int, 10); ui->lb_page->setText("/"+page_str); //显示总页数 if(t_scroll_bottom_flag){ //滚动条显示在最底部的标志 ui->tableWidget->scrollToBottom(); //换页时将滚动条设置到最底部达到换页效果 ui->le_current_page->setText(page_str); //只有滚动在最底部时才将当前页显示为最新页面 } } //qDebug()<<"当前表最大行数:"<tableWidget->rowCount()<<"\t 当前插入行count="<tableWidget->verticalScrollBar()->value(); if(page_int == 1){ //第一页 ui->tableWidget->setItem(table_row_count-1,0, new QTableWidgetItem(QString::number(table_row_count, 10))); ui->tableWidget->setItem(table_row_count-1,1, new QTableWidgetItem(currentTime.toString("hh:mm:ss"))); ui->tableWidget->setItem(table_row_count-1,2, new QTableWidgetItem(QString("%1 ℃").arg(obj_param.temper_f))); ui->tableWidget->setItem(table_row_count-1,3, new QTableWidgetItem("安全")); ui->tableWidget->setItem(table_row_count-1,4, new QTableWidgetItem(QString("%1 ml/min").arg(obj_param.flow_f))); ui->tableWidget->setItem(table_row_count-1,5, new QTableWidgetItem("危险")); for(int i=0;i<=5;i++){ ui->tableWidget->item(table_row_count-1,i)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter); } }else{ //多页 ui->tableWidget->setItem(table_row_count-1,0, new QTableWidgetItem(QString::number(table_row_count, 10))); ui->tableWidget->setItem(table_row_count-1,1, new QTableWidgetItem(currentTime.toString("hh:mm:ss"))); ui->tableWidget->setItem(table_row_count-1,2, new QTableWidgetItem(QString("%1 ℃").arg(obj_param.temper_f))); ui->tableWidget->setItem(table_row_count-1,3, new QTableWidgetItem("安全")); ui->tableWidget->setItem(table_row_count-1,4, new QTableWidgetItem(QString("%1 ml/min").arg(obj_param.flow_f))); ui->tableWidget->setItem(table_row_count-1,5, new QTableWidgetItem("危险")); for(int i=0;i<=5;i++){ ui->tableWidget->item(table_row_count-1,i)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter); } } table_row_count ++; } /** * @brief 表格自动翻页复选框 */ void Serial::cb_tscroll_auto() { if(ui->cb_tscroll_auto->isChecked()){ //自动翻页复选框按下 t_scroll_bottom_flag = true; ui->tableWidget->scrollToBottom(); QString page_str = QString::number(page_int, 10); ui->le_current_page->setText(page_str); }else{ t_scroll_bottom_flag = false; } } /** * @brief 表格上一页按钮 */ void Serial::btn_page_up_click() { if(ui->le_current_page->text().toInt()-1 > 0){ page_current = page_current-1; //当前页面 QString page_str = QString::number(page_current, 10); t_scroll_bottom_flag = false; //false ui->tableWidget->verticalScrollBar()->value(); ui->tableWidget->verticalScrollBar()->setSliderPosition(ui->tableWidget->verticalScrollBar()->value()-12); ui->le_current_page->setText(QString::number((ui->le_current_page->text().toInt()-1),10)); ui->cb_tscroll_auto->setCheckState(Qt::Unchecked); } } /** * @brief 表格下一页按钮 */ void Serial::btn_page_down_click() { if(ui->le_current_page->text().toInt()+1 <= page_int){ t_scroll_bottom_flag = false; //false ui->tableWidget->verticalScrollBar()->value(); ui->tableWidget->verticalScrollBar()->setSliderPosition(ui->tableWidget->verticalScrollBar()->value()+12); ui->le_current_page->setText(QString::number((ui->le_current_page->text().toInt()+1),10)); ui->cb_tscroll_auto->setCheckState(Qt::Unchecked); } } /** * @brief 表格首页按钮 */ void Serial::btn_page_home_click() { t_scroll_bottom_flag = false; //false ui->tableWidget->scrollToTop(); ui->le_current_page->setText("1"); ui->cb_tscroll_auto->setCheckState(Qt::Unchecked); } /** * @brief 表格尾页按钮 */ void Serial::btn_page_last_click() { t_scroll_bottom_flag = true; //true ui->tableWidget->scrollToBottom(); QString page_str = QString::number(page_int, 10); ui->le_current_page->setText(page_str); } ``` ## 6、表格居中对齐 居中对齐若在前面对整页的单元格进行对齐且部分单元格内无数据时会直接异常退出,有可能是因为页面未填满数据,所以本次居中对齐是每插入一行就居中一行。代码如下: ```c++ //每次插入一行时将此行所有的单元格居中对齐,以此可以视觉上看到表格中的数据居中对齐,但是其实没有插入数据的表格并不是居中对齐的状态 for(int i=0;i<=5;i++){ //6列 ui->tableWidget->item(table_row_count-1,i)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter); } ``` ## 7、清除表格数据 清除表格数据的方法为`clear()`,但是清除之前需要尽心二次确认。代码如下: ```c++ /** * @brief 清空表格 */ void Serial::btn_page_clear_click() { // qDebug()<<"主窗口x:"<frameGeometry().x() <<"\t主窗口y:"<< this->frameGeometry().y(); // qDebug()<<"主窗口宽:"<frameGeometry().width() <<"\t主窗口高:"<< this->frameGeometry().height(); QMessageBox msgbox; msgbox.setWindowTitle("提示"); msgbox.setInformativeText("确定要清空表格吗?"); msgbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); msgbox.setDefaultButton(QMessageBox::Ok); // qDebug()<<"message窗口宽:"<frameGeometry().x()+this->frameGeometry().width()/2-178/2, this->frameGeometry().y()+this->frameGeometry().height()/2-116/2, msgbox.width(), msgbox.height()); int ret = msgbox.exec(); if(ret == QMessageBox::Ok){ //清空表格所作的操作 ui->tableWidget->clear(); ui->le_current_page->setText("1"); ui->lb_page->setText("/1"); t_scroll_bottom_flag = true; table_row_count = 1; page_int = 1; page_current = 1; table_init(); } } ``` ## 8、总结 总体来说表格设计需要考虑以下自己的设计思路是否灵活,代码冗余度。同时需要查看帮助文档中此组件的一些方法和属性等。 # 十一、打开文件(夹)的方式 ## 1、打开文件夹 方法一如下: ```c++ bool ok = QDesktopServices::openUrl(QUrl::fromLocalFile(QDir::currentPath()+"/log/")); if(ok){ print_log("日志文件夹已打开", 0); show_tips("日志文件夹已打开"); }else{ } ``` 方法二如下: ```c++ //此方法只能打开指定目录的同级目录 QProcess process; QString fileName; fileName = QDir::currentPath()+"/temp/"; fileName.replace("/", "\\"); //windows下必须 process.start(QString("explorer /select, %1").arg(QDir::toNativeSeparators(fileName))); process.waitForFinished(); process.startDetached("explorer /select," + fileName); ``` ## 2、打开文件 ```c++ QDir *path = new QDir(QDir::currentPath()); //读取当前路径 if(!path->exists("log")) //log为存放文件的文件夹 { //不存在此目录 return ; } QFile tempfile1(QDir::currentPath()+"/log/log.txt"); if(!tempfile1.exists()){ //不存在文件 return; } QStringList cmd; cmd << QDir::toNativeSeparators(tempfile1.fileName()); QProcess::startDetached("notepad", cmd); ``` # 十二、 新建弹出窗口 ## 1、新建步骤 1、新建文件如下所示 image-20210420202638534 2、然后模板根据自己的需求选择即可,如下所示 image-20210420202731344 3、然后自定义类名,选择下一步,直接点完成即可 image-20210420202821523 image-20210420202851971 ## 2、添加页面代码 代码如下: ```c++ //1 添加头文件 #include "dialogabout.h" //关于弹窗 ... ... ... //2 添加指针变量 private: DialogAbout *dialog_about; ... ... ... //3 添加槽函数 public slots: void menu_about_click(); //关于 ... ... ... //4 连接槽函数与信号 connect(ui->m1_about, SIGNAL(triggered()), this, SLOT(menu_about_click())); //5 完善槽函数 void Serial::menu_about_click() { dialog_about = new DialogAbout(this); dialog_about->setModal(false); dialog_about->setWindowTitle("关于"); dialog_about->show(); } ``` ## 3、效果 效果如下所示: ![image-20210420203923295](pic/image-20210420203923295.png) # 十三、多线程编程 ## 1、 相关知识 - 进程支持多线程,但是必须有一个主进程 - 多个线程之间是相互独立的 - 主线程具有特殊性 - 其它线程都是通过主线程 直接或间接启动的 - 主线程结束,其它所有线程都要结束 - 子线程退出不影响主线程 - 多线程不是必须的,大多数情况下,一个主线程就可以完成工作 - 线程越多,对系统造成的负担越大 - 跨线程操作,则使用**信号与槽** ## 2、 实现的两种方法 - `QThread`(较为传统) - `QtConcurrent`(高级API,较为方便) # 十四、问题与解决方法 ## 1、调用默认浏览器打开网页 ```c++ QString URL = "https://wwwbaidu.com/"; QDesktopServices::openUrl(QUrl(URL.toLatin1())); ``` ## 2、 上位机图标的修改 1. 在[Iconfont-阿里巴巴矢量图标库](https://www.iconfont.cn/)网站下载相应图标,32x32 48x48均可; 2. 在[PNG转ICO - 在线转换图标文件](https://www.aconvert.com/cn/icon/png-to-ico/)网站转换成ico; 3. 复制ico文件到QT工程源代码目录; 4. 在.pro文件中修改RC_ICONS = 新的ico图标 ## 3、标题的修改 ```c++ this->setWindowTitle("标题"); ``` ## 4、一个CPP使用另一个CPP的成员 参考连接:https://blog.csdn.net/qq_38836568/article/details/114502260 以serial.cpp(主文件)和chartfigure.cpp为例,chartfigure.cpp使用serial.cpp中的成员方法 1. 在serial.h类中的public下添加静态指针,如下所示 ```c++ static Serial *serialUI; //静态指针变量 ``` 2. 在serial.cpp全局变量中初始化静态指针,如下所示 ```c++ Serial *Serial::serialUI = nullptr; //初始化 ``` 3. 在serial.cpp类中静态指针给静态指针赋值,如下所示 ```c++ serialUI = this; //赋值 ``` 4. 在chartfigure.cpp使用serial.cpp中的show_tips方法 ```c++ //show_tips("图表已使用深色主题"); Serial::serialUI->show_tips("图表已使用深色主题"); ``` ## 5、一个UI使用另一个UI的控件 方法:指针传参 以serial.cpp(主文件)和charttable.cpp为例,charttable.cpp控制serial.cpp中的控件 1. 在charttable.h中添加头文件,如下所示 ```c++ #include "ui_serial.h" ``` 2. 在charttable.cpp中public中添加成员方法,如下所示 ```c++ void insert_item(float temper, float flow, Ui::Serial *ui); //表格插入数据 ``` 3. 使用如下所示 ```c++ uiS->le_Upper_temper->text() ``` 4. 在serial.cpp中调用charttable.cpp的insert_item成员方法时直接传入ui即可。 ## 6、配置文件中文乱码 - 首先读取时设置文件的编码格式(`GB2312`),如下所示 ```c++ QString iniFilePath = QCoreApplication::applicationDirPath()+"/config.ini"; //配置文件路径 QSettings settings(iniFilePath, QSettings::IniFormat); //设定QSettings配置文件的名称 settings.setIniCodec(QTextCodec::codecForName("GB2312")); //设置配置文件的编码格式,防止中文乱码 ``` - 使用`nodepad++`重新修改配置文件的编码格式并保存,如下所示: ![image-20210425152846781](pic/gb2312.png) # 十五、工程文件使用介绍 ## 1、工程目录 使用QT打开工程文件之后,目录结构如下所示: ![image-20210426195006429](pic/prj tree.png) 介绍如下: ```python micro_flow_EXE/ #工程名 | -- micro_flow_EXE.pro #工程文件 | -- Headers/ #头文件文件夹 | | |-- chartfigure.h #曲线图表页头文件 | | |-- charttable.h #动态表格页头文件 | | |-- dialogabout.h #菜单栏关于弹窗头文件 | | |-- dialogcompany.h #菜单栏公司简介弹窗头文件 | | |-- modbuscrc.h #modbus crc计算头文件 | | |-- serial.h #主页面文件头文件 | | |-- typeconver.h #类型转换头文件 | -- Sources/ #源文件文件夹 | | |-- chartfigure.cpp #曲线图表页源文件 | | |-- charttable.cpp #动态表格页源文件 | | |-- dialogabout.cpp #菜单栏关于弹窗源文件 | | |-- dialogcompany.cpp #菜单栏公司简介弹窗源文件 | | |-- modbuscrc.cpp #modbus crc计算源文件 | | |-- serial.cpp #主页面文件源文件 主页面 | | |-- typeconver.cpp #类型转换源文件 | -- Forms/ #UI表单页 | | |-- chartfigure.ui #曲线图表页ui表单文件 | | |-- charttable.ui #动态表格页ui表单文件 | | |-- dialogabout.ui #菜单栏关于弹窗ui表单文件 | | |-- dialogcompany.ui #菜单栏公司简介弹窗ui表单文件 | | |-- serial.h #主页面文件ui表单文件 | -- Resources/ #资源文件夹 ``` ## 2、函数列表(部分) ### 1、初始化部分 ```c++ ////文件--Serial.cpp Serial.h /** * @brief 系统初始化函数 按钮、图表等默认状态 */ void Serial::system_init(){} /** * @brief 初始化发送命令 */ void Serial::cmd_init_ready(){} /** * @brief 临时文件(夹)初始化 */ void Serial::file_fold_init(){} ``` ### 2、串口通信 ```c++ ////文件--Serial.cpp Serial.h /** * @brief 按照用户选择或者默认ini配置打开串口 */ void Serial::btn_OpenSerial_clicked(){} /** * @brief 关闭串口 */ void Serial::btn_CloseSerial_clicked(){} /** * @brief 系统定时发送发送数据帧请求接收数据 * 连续10次(10s)接收空数据帧判定从机掉线,进行软件弹框提示 * 同时判断串口是否连接正常 */ void Serial::auto_send_recv_data(){} /** * @brief 插拔串口之后手动扫描串口设备 */ void Serial::btn_ScanSerial_clicked(){} /** * @brief 主动获取计算机所有串口信息,并读取ini文件设置默认显示的串口号 */ void Serial::getSerialPort(){} ``` ### 3、CRC计算 ```c++ ////文件--modbuscrc.cpp modbuscrc.h /** * @brief 计算CRC校验值,返回CRC校验值 * @param 需要计算校验位的前六位十六进制字符串 * @return crc校验位十六进制字符串 */ QString crcCalculation(QString message){} ``` ### 4、修改模块地址等参数 ```c++ ////文件--Serial.cpp Serial.h /** * @brief 设置从机地址,弹框修改 * 功能码:06 写地址 * 数据帧格式:原地址 06 00 02 新地址(1-255,占四位) 校验码高位 校验码低位 */ void Serial::btn_set_slave_addr(){} /** * @brief 设置modbus串口连接参数 * 参数全部取自下拉框 * 1.模块地址: * 2.功 能 码:06 * 3.寄存器地址: * 4.数 据: * --------------------------------------------------------------- * 15-12位波特率 11-8位--数据位 7-4位--停止位 0-3位--奇偶校验位 * 0--4800 0--8 0--1 0--None * 1--9600 1--9 1--1.5 1--Odd * 2--19200 2--无 2--2 2--Even * 3--38400 3--无 3--无 3--无 * 4--43000 4--无 4--无 4--无 * 5--56000 5--无 5--无 5--无 * 6--57600 6--无 6--无 6--无 * 7--115200 7--无 7--无 7--无 * --------------------------------------------------------------- * 5.CRC校验位: * 例子:修改01从机通信参数为波特率9600,8位数据位,1位停止位,无奇偶校验位的数据帧为01 06 00 03 10 00 74 0A(16 进制) */ void Serial::btn_set_serial_param(){} /** * @brief 还原modbus串口连接参数 * 波特率--9600 * 数据位--8 * 停止位--1 * 奇偶校验位--None */ void Serial::btn_back_serial_param(){} ``` ### 5、数码管数据显示页 数码管显示在auto_send_recv_data()函数中已完成,下面是数值的填充代码以及数据保存等操作函数 ```c++ ////文件--Serial.cpp Serial.h ui->lcd_TemperValue->setDigitCount(5); //数码管数字显示位数 ui->lcd_TemperValue->setStyleSheet("background-color: yellow"); //读取到数字之后的背景色 ui->lcd_TemperValue->display(obj_param.temper_f); //将读取到的温度数值显示到数码管上 ui->lcd_FlowValue->setDigitCount(5); ui->lcd_FlowValue->setStyleSheet("background-color: yellow"); ui->lcd_FlowValue->display(obj_param.flow_f); /** * @brief 保存流量数据到指定文件 */ void Serial::save_flowDataTo_file(){} /** * @brief 保存温度数据到指定文件 */ void Serial::save_temperDataTo_file(){} /** * @brief 调用本机记事本程序打开历史温度数据 */ void Serial::query_temperData_history(){} /** * @brief 调用本机记事本程序打开历史流量数据 */ void Serial::query_flowData_history(){} /** * @brief 分割温度和流量 * @param str */ void Serial::ArraySplit(QByteArray str){} ``` ### 6、曲线显示页 ```c++ ////文件--chartfigure.cpp chartfigure.h /** * @brief 图表初始化 */ void ChartFigure::chart_init(){} /** * @brief 追加点、平滑曲线、动态更新数据 */ void ChartFigure::DrawLine(qreal x, qreal y1, qreal y2){} /** * @brief 深色主题按钮 */ void ChartFigure::btn_chart_dark_click(){} /** * @brief 浅色主题按钮 */ void ChartFigure::btn_chart_light_click(){} /** * @brief 检查图表曲线显示状态 */ void ChartFigure::check_chart_state(){} /** * @brief 显示温度曲线 */ void ChartFigure::cb_temper_show(){} /** * @brief 显示流量曲线 */ void ChartFigure::cb_flow_show(){} /** * @brief 显示全部曲线 */ void ChartFigure::cb_show_all(){} /** * @brief 隐藏全部曲线 */ void ChartFigure::cb_hide_all(){} ``` ### 7、表格显示页 ```c++ ////文件--charttable.cpp charttable.h /** * @brief 表格初始化 */ void ChartTable::table_init(){} /** * @brief 表格定时插入数据 */ void ChartTable::insert_item(float temper, float flow, Ui::Serial *uiS){} /** * @brief 表格自动翻页复选框 */ void ChartTable::cb_tscroll_auto(){} /** * @brief 表格上一页按钮 */ void ChartTable::btn_page_up_click(){} /** * @brief 表格下一页按钮 */ void ChartTable::btn_page_down_click(){} /** * @brief 表格首页按钮 */ void ChartTable::btn_page_home_click(){} /** * @brief 表格尾页按钮 */ void ChartTable::btn_page_last_click(){} /** * @brief 清空表格 */ void ChartTable::btn_page_clear_click(){} ``` ### 8、自动报警 ```c++ ////文件--Serial.cpp Serial.h /** * @brief 设置温度安全上限 */ void Serial::set_temper_upper(){} /** * @brief 设置温度安全下限 */ void Serial::set_temper_lower(){} /** * @brief 设置流量安全上限 */ void Serial::set_flow_upper(){} /** * @brief 设置流量安全下限 大于0 */ void Serial::set_flow_lower(){} /** * @brief 设置LED报警灯的样式 * @param size * @param color */ void Serial::set_warning_LED(int size, int color){} /** * @brief 温度报警复位 */ void Serial::btn_reset_temper_alarm(){} /** * @brief 流量报警复位 */ void Serial::btn_reset_flow_alarm(){} /** * @brief 复位所有报警 */ void Serial::btn_reset_allParam_alarm(){} /** * @brief 还原实时监控数据的安全上下限参数 * 温度上限:50℃ * 温度下限:-10℃ * 流量上限:30 待修改 * 流量下限:0 */ void Serial::btn_back_safe_limit_param(){} ``` ### 9、日志 ```c++ ////文件--Serial.cpp Serial.h /** * @brief 打印操作日志 * @param str, type type==1时为紧急通知,显示红色 type==0时正常显示 */ void Serial::print_log(QString str, int type){} /** * @brief 清除日志 */ void Serial::btn_clear_log(){} /** * @brief 保存当前日志 */ void Serial::btn_save_log(){} /** * @brief QT窗口左下角显示提示信息 * @param tip,传入的提示语 */ void Serial::show_tips(QString tip){} ``` ### 10、菜单栏 ```c++ ////文件--Serial.cpp Serial.h dialogcompany.h dialogabout.h /** * @brief 菜单栏退出 */ void Serial::menu_exit_click(){} /** * @brief 菜单栏打开日志文件 */ void Serial::menu_open_log_file(){} /** * @brief 菜单栏打开日志文件夹 */ void Serial::menu_open_log_fold(){} /** * @brief 打开临时文件夹 */ void Serial::menu_open_temp_fold(){} /** * @brief 菜单栏打开公司官网(调用PC机默认浏览器) */ void Serial::menu_company_web(){} /** * @brief 菜单栏打开公司简介 */ void Serial::menu_company_about(){} /** * @brief 菜单栏打开关于 */ void Serial::menu_about_click(){} ``` ### 11、更新时间 ```c++ ////文件--Serial.cpp Serial.h /** * @brief 更新系统时间 */ void Serial::lb_getDateTime(){} ``` ### 12、类型转换 ```c++ ////文件--typeconver.cpp typeconver.h /** * @brief 判断字符串是否为整数(仅判断格式,不考虑范围) * @param char* * @return */ bool isInt(const char* str){} /** * @brief 判断字符串是否为浮点数(仅判断格式,不考虑范围) * @param char * * @return */ bool isFloat(const char * str){} /** * @brief 将字符型进制转化为16进制 * @param str * @return QByteArray */ QByteArray QString2Hex(QString str){} /** * @brief 将单个字符串转换为hex * @param ch * @return char */ char ConvertHexChar(char ch){} ``` # 十六、项目打包EXE 1. 项目编译时选择`Release`版本,点击左下角构建编译 2. 点击左边工具栏的项目选项,打开构件目录下的release,复制exe文件到自行指定的英文文件夹下。 3. 打开`QT`命令行工具,我的是`Qt5.9.9(MinGW 5.3.0 32-bit)`,命令行中进入步骤2中的目录 4. 使用`windeployqt`进行链接库的构建,命令为`windeployqt 软件名.exe`,每个人的exe名字不同使用自己的就行,成功之后会看到当前目录下多了很多库文件。 ![image-20210708203137286](pic/image-20210708203137286.png) 5. 使用`Enigma Virtual Box`进行库文件封装,即将所有文件合并成一个`exe`文件运行,软件界面如下所示: ![image-20210423151954730](pic/release2.png) 6. 点击`Enter Input File Name`中选择之前单独复制出来的exe文件,如下所示: ![image-20210708203214182](pic/image-20210708203214182.png) 7. 点击左下角`Add->Add Floder Recursive`,选择库文件所在的文件夹(`exe`所在的文件夹),弹框点击`OK` ![image-20210423152116833](pic/release4.png) 8. 出现如下界面后执行第9步 ![image-20210423152218362](pic/release5.png) 9. 点击`Process->RUN`,等待完成`close`即可。如下所示: ![image-20210708203234168](pic/image-20210708203234168.png) 10. 成功之后会生成一个带`_boxed.exe`的可执行文件,将其与给出的`config.ini`配置文件拷贝至同一目录即可。