Qt6重写基于qcustomplot的GUI串口收发绘图软件
有人反馈之前Qt5版本的串口控件教程链接: QT5系列教程二—基于qcustomplot的QT5 GUI串口收发绘图软件实现现在因为版本升级的原因,无法成功复刻了。加之自己电脑重做系统顺手安装了最新版Qt6。查阅了Qcustomplot官方论坛,很多友人反馈虽然最后更新时间为2022年,但是2.1.1版本在Qt6.9上使用很丝滑,想着测试下。特此记录。
效果展示

软件准备
版本升级参考这个贴子吧 大差不差,基本思路
使用Qt自带的Maintenance Tool将Qt6.9升级为QT6.10
运行调试旧代码
运行
选择“Qt 6.10.1 llvm-mingw 64-bit”编译器,之前那个版本写的比较早,所以用最新下载的QcustomPlot2.1.1中新文件替换了旧的文件。对Qt5老程序进行编译(项目迁移最快的方法就是先运行下,看报啥错,报啥解决啥),很幸运,只有几个警告,编译成功。
调试
下位机发送串口信息的Arduino代码
这次手头没有找到wemos D1mini,只有一块Uno。无所谓,Arduino代码最帅气的地方就是通用性。只要更换下载的板子类型就能丝滑运行。
// 下位机Arduino代码
int i=1; //初始化全局变量用来计数
String str;
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);//串口波特率9600
randomSeed(analogRead(5)); // randomize using noise from analog pin 5
}
void loop() {
// put your main code here, to run repeatedly:
randNumber = random(300); //300以内随机数
str="sample: "+String(i)+" "+String(randNumber);//字头+序号+随机数
Serial.println(str);//串口输出
i++;
delay(100);
}
上位机运行状态
- 电脑接入下位机后,可以顺利找到COM口,点击连接后,文本框里可以正常显示数据,

- 绘图部分不工作,等很久,会跳动一小下。
- 原本程序中用作调试的,在成功解析到“sample”字头后就在调试窗口输出的语句一直没有执行。
qDebug() << "Got a sample " << parts.at(1).toDouble() << parts.at(2).toDouble();//在调试栏输出收到的数值
//对接收的数据进行处理并进行绘制
if (str.startsWith("sample:",Qt::CaseInsensitive)) {
QStringList parts = str.split(" ");//wemosD1MINI上传的数据格式是三个部分,每个部分用空格分开。eg:sample:+空格+ID+空格+采集值
if (parts.size() == 3) {//判断采集数值正确
qDebug() << "Got a sample " << parts.at(1).toDouble() << parts.at(2).toDouble();//在调试栏输出收到的数值
double num = parts.at(1).toDouble();
double mag = parts.at(2).toDouble();
mData->add(QCPGraphData(num, mag));//将收到的数据放入mData中
ui->plot->rescaleAxes();//重设数周比例
ui->plot->replot();//重绘
}
}
调试思路
能够在文本框里显示数据说明串口的接受功能没有问题。而原本用于调试的语句却没有反应,问题应该出现在if (str.startsWith("sample:",Qt::CaseInsensitive))没有执行。所以增加qDebug() <<str这句调试语句到if判断之外。此处的str为上几句代码中sting数据转换之后的串口原始数据流。通过这种方式来看看此时输出的数据是什么。为什么明明数据有接收到,却判断不出来。
运行后串口显示支离破碎的信息。没有完整的“sample:+序号+数值”的形式。大概长这样:

这下就很明显了,数据收到了,但是不是预想的一条条的完整状态。所以if语句根本就没有执行过。所以自然也不会画图了。
考虑是串口数据接受判断的问题。找到了Qt自带的qtserialport-everywhere-src-6.10.0(下载地址:https://mirrors.tuna.tsinghua.edu.cn/qt/official_releases/qt/6.10/6.10.0/submodules/))1中的例程。运行查看了下,发现这个串口接受用的是阻塞的方式,早就忘光了串口数据阻塞和非阻塞的细节。所以狗屁通了一下。

看来问题就在这里了。所以把现在的现象总结归纳了下,生成提示词给到狗屁通寻求帮助。具体提示词如下。主要结构为三个部分:介绍下位机数据格式,介绍目前接收到的数据现状,给出串口收发源代码。

狗屁通很靠谱,直接给出诊断结果。


狗屁通甚至人性化的为我提供了代码整合服务,于是我将mainwindow.h和mainwindow.cpp两个文件上传。

结果反馈:


通过以上修改。程序完美运行。感谢狗屁通。
源代码
链接: https://gitcode.com/happyjoey217/Qt6SerailDraw
mainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QTimer>
#include "qcustomplot.h"
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_connect_pushButton_clicked();
void on_send_Button_clicked();
void on_saveGraphButton_clicked();
void on_clear_Button_clicked();
void serialReadyRead();
private:
Ui::MainWindow *ui;
QByteArray mRecvBuffer; // 串口接收缓存
QSerialPort *mSerial;
QList<QSerialPortInfo> mSerialPorts;
QTimer *mSerialScanTimer;
QSharedPointer<QCPGraphDataContainer> mData;
void updateSerialPorts();
};
#endif // MAINWINDOW_H
mainWindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("Serial");
mSerial = new QSerialPort(this);
updateSerialPorts();
mSerialScanTimer = new QTimer(this);
mSerialScanTimer->setInterval(5000);
mSerialScanTimer->start();
connect(mSerialScanTimer, &QTimer::timeout,
this, &MainWindow::updateSerialPorts);
connect(ui->lineEdit, &QLineEdit::returnPressed,
this, &MainWindow::on_send_Button_clicked);
connect(mSerial, &QSerialPort::readyRead,
this, &MainWindow::serialReadyRead);
//绘图部分
mData = QSharedPointer<QCPGraphDataContainer>(new QCPGraphDataContainer);
/* 绘图初始化 */
ui->plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
ui->plot->legend->setVisible(true);
QFont legendFont = font();
legendFont.setPointSize(10);
ui->plot->legend->setFont(legendFont);
ui->plot->legend->setSelectedFont(legendFont);
ui->plot->legend->setSelectableParts(QCPLegend::spItems);
ui->plot->yAxis->setLabel("Magnitude");
ui->plot->xAxis->setLabel("Sample");
ui->plot->clearGraphs();
ui->plot->addGraph();
ui->plot->graph()->setPen(QPen(Qt::black));
ui->plot->graph()->setData(mData);
ui->plot->graph()->setName("STM32 ADC");
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::updateSerialPorts()
{
mSerialPorts = QSerialPortInfo::availablePorts();
ui->serialComboBox->clear();
for (QSerialPortInfo port : mSerialPorts) {
ui->serialComboBox->addItem(port.portName(), port.systemLocation());
}
}
void MainWindow::on_connect_pushButton_clicked()
{
ui->connect_pushButton->setEnabled(false);
QString serialLoc = ui->serialComboBox->currentData().toString();
if (mSerial->isOpen()) {
qDebug("Serial already connected, disconnecting!");// << "Serial already connected, disconnecting!";
mSerial->close();
}
mSerial->setPortName(serialLoc);
mSerial->setBaudRate(QSerialPort::Baud9600);
mSerial->setDataBits(QSerialPort::Data8);
mSerial->setParity(QSerialPort::NoParity);
mSerial->setStopBits(QSerialPort::OneStop);
mSerial->setFlowControl(QSerialPort::NoFlowControl);
if(mSerial->open(QIODevice::ReadWrite)) {
qDebug("SERIAL: OK!") ;
} else {
qDebug("SERIAL: ERROR!") ;
}
ui->connect_pushButton->setEnabled(true);
}
void MainWindow::on_send_Button_clicked()
{
if (mSerial->isOpen()) {
QString str= ui->lineEdit->text();
ui->lineEdit->clear();
str.append("\r\n");
mSerial->write(str.toLocal8Bit());
} else {
qDebug( "Serial port not connected!");
}
}
void MainWindow::serialReadyRead()
{
// 1. 先把所有收到的数据追加到缓存
mRecvBuffer.append(mSerial->readAll());
// 2. 按照 "\r\n" 切分消息(ESP32 默认以 \r\n 结尾)
int index;
while ((index = mRecvBuffer.indexOf("\r\n")) != -1)
{
// 取出一条完整消息
QByteArray line = mRecvBuffer.left(index);
mRecvBuffer.remove(0, index + 2); // 删除包括 \r\n
// 转 QString
QString str = QString::fromUtf8(line);
// 显示到文本框
ui->outputTextBrowser->insertPlainText(str + "\n");
QScrollBar *sb = ui->outputTextBrowser->verticalScrollBar();
sb->setValue(sb->maximum());
// 解析 sample: x y 结构
if (str.startsWith("sample:", Qt::CaseInsensitive))
{
QStringList parts = str.split(" ");
if (parts.size() == 3)
{
double num = parts.at(1).toDouble();
double mag = parts.at(2).toDouble();
qDebug() << "Got sample" << num << mag;
// 加入绘图
mData->add(QCPGraphData(num, mag));
ui->plot->rescaleAxes();
ui->plot->replot();
}
}
}
}
void MainWindow::on_saveGraphButton_clicked()
{
//step1:生成txt文件
/* QString filename = QFileDialog::getSaveFileName(this,
tr("Save pdf"), "",
tr("Pdf files (*.txt)"));
if (!filename.isEmpty()) {
//ui->plot->savePdf(filename);
QFile file(filename);//文件命名
if (!file.open(QFile::WriteOnly | QFile::Text)) //检测文件是否打开
{
QMessageBox::information(this, "Error Message", "Please Select a Text File!");
return;
}
QTextStream out(&file); //分行写入文件
out << ui->outputTextBrowser->toPlainText();
}*/
//step2:将描述和文件名存入数据库
QSqlDatabase ldb = QSqlDatabase::addDatabase("QODBC");
ldb.setDatabaseName("DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};FIL={MS Access};DBQ=E:/serialPortTest/my.accdb");
QSqlQuery mquery;
bool ok = ldb.open();
qDebug( "!");
if(ok)
{
qDebug( "DB connected!");
//存数据到数据库
mquery.next();
bool isok=mquery.exec("Insert INTO [test] ([content], [filename]) VALUES ('tomato', '202103241043.txt')");
//检测存入正确
isok=mquery.exec("select * from test;");
if (!isok)
{
qDebug( "DB not connected!");
ldb.close();
}
mquery.next();
mquery.seek(1);
QString sHostName=mquery.value(1).toString();
mquery.clear();
ldb.close();
qDebug()<<sHostName;
}
else{
// 打开本地数据库失败,
qDebug()<<ldb.lastError();
}
}
void MainWindow::on_clear_Button_clicked()
{
mData->clear();
ui->plot->rescaleAxes();
ui->plot->replot();
}
qt中Desktop Qt 6.10.1 llvm-mingw 64-bit和Desktop Qt 6.10.1 MinGw 64-bit区别
图片来自狗屁通截图。


更换编译器
左侧菜单->项目->选择编译器

参考文献
博客中给出了清华大学开源软件镜像网站中的串口控件的使用方法。这个开源镜像中软件好多。nice。链接: https://blog.youkuaiyun.com/qq_36393978/article/details/127861862 ↩︎
Qt6串口绘图软件重构
1万+

被折叠的 条评论
为什么被折叠?



