Qt 开发实战:从零打造一个跨平台串口调试助手
1. 引言
串口通信在嵌入式、物联网、工业自动化等领域应用广泛。虽然市面上已有成熟的串口调试工具(如 SSCOM、Putty 等),但自己动手用 Qt 开发一个,既能深入理解串口通信原理,又能按需定制功能,还能积累宝贵的项目经验。
本文带你从零开始,使用 Qt6 + C++ 开发一个具备串口扫描、参数配置、数据收发、十六进制支持、日志保存等功能的轻量级串口调试助手,并在虚拟串口环境下完成联调测试。
2. 开发环境
操作系统:Windows 10 / 11(也支持 Linux / macOS)
开发工具:Qt Creator 13.0.2
Qt 版本:Qt 6.7.2 / 6.8.0(支持 Qt 5.15+)
构建系统:CMake(也可用 qmake)
编译器:MinGW 13.1.0 / MSVC 2022
额外工具:LLCOM(串口调试助手,用于联调)、com0com(虚拟串口驱动,用于无硬件测试)
3. 项目结构
text
serial-assistant/ ├── CMakeLists.txt ├── main.cpp ├── mainwindow.h ├── mainwindow.cpp ├── mainwindow.ui ├── resources.qrc └── style/ └── style.qss
CMakeLists.txt:项目构建配置,需添加
SerialPort组件。main.cpp:程序入口,加载样式表。
mainwindow.h / .cpp:主窗口逻辑,串口操作核心。
mainwindow.ui:UI 布局,含按钮、下拉框、文本编辑区等。
resources.qrc:资源文件,打包样式表。
style/style.qss:QSS 样式表(可选,用于美化界面)。
4. 功能列表
✅ 自动扫描并显示可用串口
✅ 配置波特率(9600~115200)
✅ 打开/关闭串口(带状态提示)
✅ 异步接收数据(文本 / HEX 两种显示模式)
✅ 发送数据(文本 / HEX 两种模式)
✅ 清空接收区
✅ 保存接收数据到
.txt文件✅ 状态栏显示收发字节数
✅ QSS 美化界面(可选)
5. 核心实现步骤
5.1 CMake 配置(添加 SerialPort 模块)
cmake
# CMakeLists.txt 片段 find_package(Qt6 REQUIRED COMPONENTS Core Widgets SerialPort) target_link_libraries(serial-assistant PRIVATE Qt6::Core Qt6::Widgets Qt6::SerialPort )
5.2 扫描串口并填充下拉框
在MainWindow构造函数中:
cpp
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { ui->comboBoxPort->addItem(info.portName()); }5.3 打开/关闭串口
cpp
void MainWindow::on_pushButtonOpen_clicked() { if (serial->isOpen()) { serial->close(); ui->pushButtonOpen->setText("打开串口"); ui->comboBoxPort->setEnabled(true); ui->comboBoxBaud->setEnabled(true); statusBar()->showMessage("串口已关闭", 2000); return; } QString portName = ui->comboBoxPort->currentText(); qint32 baudRate = ui->comboBoxBaud->currentText().toInt(); serial->setPortName(portName); serial->setBaudRate(baudRate); serial->setDataBits(QSerialPort::Data8); serial->setParity(QSerialPort::NoParity); serial->setStopBits(QSerialPort::OneStop); serial->setFlowControl(QSerialPort::NoFlowControl); if (serial->open(QIODevice::ReadWrite)) { ui->pushButtonOpen->setText("关闭串口"); ui->comboBoxPort->setEnabled(false); ui->comboBoxBaud->setEnabled(false); connect(serial, &QSerialPort::readyRead, this, &MainWindow::handleReadyRead); statusBar()->showMessage(QString("已打开 %1 %2").arg(portName).arg(baudRate), 3000); } else { QMessageBox::critical(this, "错误", "打开串口失败:" + serial->errorString()); } }5.4 接收数据(文本 / HEX 切换)
cpp
void MainWindow::handleReadyRead() { QByteArray data = serial->readAll(); if (data.isEmpty()) return; if (ui->checkBoxHexDisplay->isChecked()) { QString hex = data.toHex(' ').toUpper(); ui->textEditReceive->appendPlainText(hex); } else { QString text = QString::fromUtf8(data); ui->textEditReceive->appendPlainText(text); } }5.5 发送数据(文本 / HEX 切换)
cpp
void MainWindow::on_pushButtonSend_clicked() { if (!serial->isOpen()) { QMessageBox::warning(this, "提示", "请先打开串口"); return; } QString input = ui->lineEditSend->text(); if (input.isEmpty()) return; QByteArray data; if (ui->checkBoxHexSend->isChecked()) { QString hex = input; hex.remove(' '); hex.remove(','); data = QByteArray::fromHex(hex.toUtf8()); if (data.isEmpty()) { QMessageBox::warning(this, "警告", "无效的十六进制格式"); return; } } else { data = input.toUtf8(); } qint64 written = serial->write(data); if (written == -1) { QMessageBox::critical(this, "错误", "发送失败:" + serial->errorString()); } else { statusBar()->showMessage(QString("发送 %1 字节").arg(written), 2000); } }5.6 清空接收区
cpp
void MainWindow::on_pushButtonClear_clicked() { ui->textEditReceive->clear(); }5.7 保存接收数据
cpp
void MainWindow::on_pushButtonSave_clicked() { QString content = ui->textEditReceive->toPlainText(); if (content.isEmpty()) { QMessageBox::information(this, "提示", "接收区为空"); return; } QString fileName = QFileDialog::getSaveFileName(this, "保存接收数据", QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".txt", "文本文件 (*.txt)"); if (fileName.isEmpty()) return; QFile file(fileName); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&file); out << content; file.close(); statusBar()->showMessage("数据已保存", 3000); } else { QMessageBox::critical(this, "错误", "无法写入文件:" + file.errorString()); } }5.8 QSS 全局样式(可选)
在resources.qrc中添加style/style.qss,然后在main.cpp中加载:
cpp
int main(int argc, char *argv[]) { QApplication a(argc, argv); QFile file(":/style/style.qss"); if (file.open(QFile::ReadOnly)) { QString style = QTextStream(&file).readAll(); a.setStyleSheet(style); file.close(); } MainWindow w; w.show(); return a.exec(); }样式表示例(style.qss):
css
QMainWindow { background-color: #f5f5f5; } QPushButton { background-color: #5c9eff; color: white; border: none; border-radius: 6px; padding: 8px 16px; font-weight: bold; } QPushButton:hover { background-color: #3a7bd5; } QPushButton:pressed { background-color: #2c5fa3; } QComboBox, QLineEdit, QPlainTextEdit { border: 1px solid #ccc; border-radius: 4px; padding: 4px 6px; background-color: white; } QPlainTextEdit, QLineEdit { font-family: Consolas, monospace; font-size: 11px; }6. 测试验证
6.1 虚拟串口环境搭建
使用com0com创建一对虚拟串口(如 COM5 ↔ COM6),然后用LLCOM(开源的 Lua 脚本串口工具)作为对端。
打开 com0com 的
setupc命令行,执行:text
install PortName=COM5 PortName=COM6
在设备管理器中确认 COM5、COM6 已出现(无黄色感叹号)。
6.2 联调测试步骤
启动 Qt 程序,选择COM5,波特率 115200,点击“打开”。
启动 LLCOM,选择COM6,相同波特率,点击“打开”。
在 Qt 发送区输入
Hello,点击发送 → LLCOM 接收区应显示Hello。在 LLCOM 发送区输入
World→ Qt 接收区应显示World。勾选 Qt 的“十六进制发送”,输入
01 02 03 FF发送 → LLCOM 勾选 HEX 显示,应看到相同内容。点击“清空接收区” → 内容清空。
点击“保存接收区” → 弹出保存对话框,保存为
.txt文件,打开查看内容正确。
7. 运行效果截图
8. 常见问题与解决方案
| 问题 | 解决方法 |
|---|---|
| 打开串口失败(权限错误) | Windows:以管理员身份运行;Linux:将用户加入dialout组 |
| 接收区显示乱码 | 检查波特率、数据位等参数是否匹配;尝试 HEX 显示模式 |
| 虚拟串口驱动安装失败 | 使用com0com 2.2.2.0版本,或禁用驱动签名(临时) |
| 打包发布后缺少 DLL | 使用windeployqt工具自动复制依赖库 |
9. 后续拓展方向
多线程接收:将串口读取移到子线程,防止界面卡顿(适合大数据量场景)。
波形显示:集成
Qt Charts,将传感器数值实时绘制曲线。定时自动发送:增加
QTimer,周期性发送预设指令。协议解析:支持 Modbus RTU、自定义帧头帧尾过滤。
配置文件保存:用
QSettings记住最近使用的串口号和波特率。
10. 项目总结
通过本项目,你不仅掌握 Qt 串口通信的完整开发流程,还熟悉了 CMake 管理项目、QSS 美化界面、资源文件使用、虚拟串口调试等实用技能。整个项目代码清晰、可扩展性强,可作为毕业设计、简历项目或日常开发工具。
**技术栈**:C++17 / Qt6 / QSerialPort / CMake / QSS
