核心依赖: Qt 5.1+ 自带
Qt Serial Port模块,5.8 完全支持。唯一前提:工程文件里写
QT += serialport,然后#include <QSerialPort>/#include <QSerialPortInfo>。
一、工程文件
SerialAssistant.pro
qmake
QT += core gui serialport
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = SerialAssistant
TEMPLATE = app
SOURCES += main.cpp \
mainwindow.cpp
HEADERS += mainwindow.h
关键就是第一行那个
serialport,少了它编译直接报找不到 QSerialPort。
二、mainwindow.h --- 头文件
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QPushButton>
#include <QComboBox>
#include <QTextEdit>
#include <QLineEdit>
#include <QLabel>
#include <QCheckBox>
#include <QRadioButton>
#include <QGroupBox>
#include <QTimer>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QSpacerItem>
#include <QTime>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
/* 串口操作 */
void onScanPorts();
void onTogglePort();
void onReadyRead();
/* 发送 */
void onSend();
void onAutoSendTimeout();
/* UI 操作 */
void onClearRecv();
void onClearSend();
void onRecvHexToggled(bool checked);
void onSendHexToggled(bool checked);
private:
void buildUI(); // 手写布局
void applyPortSettings(); // 把下拉框的值应用到 m_port
void logRecv(const QByteArray &raw);
QByteArray hexStrToBytes(const QString &hexStr);
QString bytesToHexStr(const QByteArray &data);
/* ---- 串口 ---- */
QSerialPort *m_port = nullptr;
bool m_isOpen = false;
/* ---- 控件 ---- */
// 配置区
QComboBox *cmbPort, *cmbBaud, *cmbDataBits, *cmbStopBits, *cmbParity, *cmbFlow;
QPushButton *btnOpenClose, *btnScan;
// 接收区
QTextEdit *txtRecv;
QRadioButton *rbRecvText, *rbRecvHex;
QCheckBox *ckTimestamp;
QPushButton *btnClearRecv;
QLabel *lblRecvCnt;
// 发送区
QTextEdit *txtSend;
QRadioButton *rbSendText, *rbSendHex;
QPushButton *btnSend, *btnClearSend;
QCheckBox *ckAutoSend;
QLineEdit *leInterval;
QLabel *lblSendCnt;
// 状态指示
QLabel *lblStatus, *lblLed;
/* ---- 统计 ---- */
qint64 m_recvBytes = 0;
qint64 m_sendBytes = 0;
QTimer *m_autoTimer = nullptr;
};
#endif // MAINWINDOW_H
三、mainwindow.cpp --- 核心实现
cpp
#include "mainwindow.h"
#include <QMessageBox>
#include <QScrollBar>
/* ==================== 构造函数 ==================== */
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), m_port(new QSerialPort(this))
{
buildUI();
onScanPorts(); // 启动时扫描一次串口
/* 串口数据到达信号 */
connect(m_port, &QSerialPort::readyRead,
this, &MainWindow::onReadyRead);
/* 错误处理(Qt 5.8 推荐 errorOccurred,旧版用 SIGNAL(error()) )*/
#if QT_VERSION >= QT_VERSION_CHECK(5,8,0)
connect(m_port, &QSerialPort::errorOccurred,
this, QSerialPort::SerialPortError e{
if(e != QSerialPort::NoError){
lblStatus->setText("串口错误: " + m_port->errorString());
if(m_isOpen) onTogglePort(); // 自动关闭
}
});
#else
connect(m_port, SIGNAL(error(QSerialPort::SerialPortError)),
this, SLOT(handleError(QSerialPort::SerialPortError)));
#endif
resize(800, 560);
setWindowTitle("串口调试助手 --- Qt 5.8");
}
MainWindow::~MainWindow() {}
/* ==================== UI 布局(纯手写,不用 Designer 也能跑) ==================== */
void MainWindow::buildUI()
{
QWidget *central = new QWidget(this);
setCentralWidget(central);
/* ---------- 1. 串口配置行 ---------- */
cmbPort = new QComboBox; cmbPort->setMinimumWidth(90);
btnScan = new QPushButton("刷新");
btnOpenClose = new QPushButton("打开串口");
btnOpenClose->setCheckable(true);
cmbBaud = new QComboBox;
cmbBaud->addItems({"1200","2400","4800","9600","19200",
"38400","57600","115200","230400","460800"});
cmbBaud->setCurrentText("115200");
cmbDataBits = new QComboBox;
cmbDataBits->addItems({"5","6","7","8"}); cmbDataBits->setCurrentIndex(3);
cmbStopBits = new QComboBox;
cmbStopBits->addItems({"1","1.5","2"});
cmbParity = new QComboBox;
cmbParity->addItems({"None","Even","Odd","Space","Mark"});
cmbFlow = new QComboBox;
cmbFlow->addItems({"None","Hardware(RTS/CTS)","Software(XON/XOFF)"});
auto *hlCfg = new QHBoxLayout;
hlCfg->addWidget(new QLabel("端口:")); hlCfg->addWidget(cmbPort);
hlCfg->addWidget(btnScan);
hlCfg->addSpacing(12);
hlCfg->addWidget(new QLabel("波特率:")); hlCfg->addWidget(cmbBaud);
hlCfg->addWidget(new QLabel("数据位:")); hlCfg->addWidget(cmbDataBits);
hlCfg->addWidget(new QLabel("停止位:")); hlCfg->addWidget(cmbStopBits);
hlCfg->addWidget(new QLabel("校验:")); hlCfg->addWidget(cmbParity);
hlCfg->addWidget(new QLabel("流控:")); hlCfg->addWidget(cmbFlow);
hlCfg->addStretch();
hlCfg->addWidget(btnOpenClose);
/* ---------- 2. 接收区 ---------- */
txtRecv = new QTextEdit; txtRecv->setReadOnly(true);
rbRecvText = new QRadioButton("文本"); rbRecvText->setChecked(true);
rbRecvHex = new QRadioButton("HEX");
ckTimestamp = new QCheckBox("显示时间戳");
btnClearRecv = new QPushButton("清空接收");
lblRecvCnt = new QLabel("Rx: 0 B");
auto *hlRecvOpt = new QHBoxLayout;
hlRecvOpt->addWidget(new QLabel("接收:"));
hlRecvOpt->addWidget(rbRecvText); hlRecvOpt->addWidget(rbRecvHex);
hlRecvOpt->addWidget(ckTimestamp);
hlRecvOpt->addStretch();
hlRecvOpt->addWidget(lblRecvCnt);
hlRecvOpt->addWidget(btnClearRecv);
/* ---------- 3. 发送区 ---------- */
txtSend = new QTextEdit; txtSend->setMaximumHeight(110);
rbSendText = new QRadioButton("文本"); rbSendText->setChecked(true);
rbSendHex = new QRadioButton("HEX");
btnSend = new QPushButton("发送"); btnSend->setEnabled(false);
btnClearSend = new QPushButton("清空发送");
ckAutoSend = new QCheckBox("定时发送");
leInterval = new QLineEdit("1000"); leInterval->setMaximumWidth(65);
lblSendCnt = new QLabel("Tx: 0 B");
auto *hlSendOpt = new QHBoxLayout;
hlSendOpt->addWidget(new QLabel("发送:"));
hlSendOpt->addWidget(rbSendText); hlSendOpt->addWidget(rbSendHex);
hlSendOpt->addWidget(ckAutoSend);
hlSendOpt->addWidget(new QLabel("间隔(ms):"));
hlSendOpt->addWidget(leInterval);
hlSendOpt->addStretch();
hlSendOpt->addWidget(lblSendCnt);
hlSendOpt->addWidget(btnClearSend);
hlSendOpt->addWidget(btnSend);
/* ---------- 4. 状态条 ---------- */
lblLed = new QLabel("●"); lblLed->setStyleSheet("color:#888;font-size:18px;");
lblStatus = new QLabel("未连接");
auto *hlStatus = new QHBoxLayout;
hlStatus->addWidget(lblLed); hlStatus->addWidget(lblStatus);
hlStatus->addStretch();
/* ---------- 总布局 ---------- */
auto *vl = new QVBoxLayout(central);
vl->setContentsMargins(8,8,8,8);
vl->addLayout(hlCfg);
vl->addSpacing(4);
vl->addLayout(hlRecvOpt);
vl->addWidget(txtRecv, 1);
vl->addSpacing(4);
vl->addLayout(hlSendOpt);
vl->addWidget(txtSend);
vl->addSpacing(4);
vl->addLayout(hlStatus);
/* ---------- 定时器(定时发送) ---------- */
m_autoTimer = new QTimer(this);
connect(m_autoTimer, &QTimer::timeout, this, &MainWindow::onAutoSendTimeout);
/* ---------- 信号槽 ---------- */
connect(btnScan, &QPushButton::clicked, this, &MainWindow::onScanPorts);
connect(btnOpenClose, &QPushButton::clicked, this, &MainWindow::onTogglePort);
connect(btnSend, &QPushButton::clicked, this, &MainWindow::onSend);
connect(btnClearRecv, &QPushButton::clicked, this, &MainWindow::onClearRecv);
connect(btnClearSend, &QPushButton::clicked, this, &MainWindow::onClearSend);
connect(rbRecvHex, &QRadioButton::toggled,this,&MainWindow::onRecvHexToggled);
}
/* ==================== 扫描串口 ==================== */
void MainWindow::onScanPorts()
{
QString cur = cmbPort->currentText();
cmbPort->clear();
const auto ports = QSerialPortInfo::availablePorts();
for (const auto &info : ports) {
QString label = info.portName();
if (!info.description().isEmpty())
label += " (" + info.description() + ")";
cmbPort->addItem(label, info.portName()); // userdata 存裸 COMx
}
// 恢复上次选中
int idx = cmbPort->findText(cur);
if(idx != -1) cmbPort->setCurrentIndex(idx);
}
/* ==================== 打开 / 关闭串口 ==================== */
void MainWindow::onTogglePort()
{
if (!m_isOpen) {
/* ---- 打开 ---- */
QString portName;
int ud = cmbPort->currentIndex();
if (ud >= 0)
portName = cmbPort->itemData(ud).toString();
else
portName = cmbPort->currentText().section(' ',0,0); // fallback
if (portName.isEmpty()) {
QMessageBox::warning(this,"提示","请先选择串口");
btnOpenClose->setChecked(false);
return;
}
m_port->setPortName(portName);
applyPortSettings();
if (!m_port->open(QIODevice::ReadWrite)) {
QMessageBox::critical(this,"错误",
QString("无法打开串口 %1\n%2")
.arg(portName, m_port->errorString()));
btnOpenClose->setChecked(false);
return;
}
m_isOpen = true;
btnOpenClose->setText("关闭串口");
lblLed->setStyleSheet("color:#0c0;font-size:18px;");
lblStatus->setText("已连接: " + portName);
btnSend->setEnabled(true);
cmbPort->setEnabled(false); cmbBaud->setEnabled(false);
cmbDataBits->setEnabled(false); cmbStopBits->setEnabled(false);
cmbParity->setEnabled(false); cmbFlow->setEnabled(false);
}
else {
/* ---- 关闭 ---- */
if (m_autoTimer->isActive()) m_autoTimer->stop();
m_port->close();
m_isOpen = false;
btnOpenClose->setText("打开串口");
lblLed->setStyleSheet("color:#888;font-size:18px;");
lblStatus->setText("未连接");
btnSend->setEnabled(false);
cmbPort->setEnabled(true); cmbBaud->setEnabled(true);
cmbDataBits->setEnabled(true); cmbStopBits->setEnabled(true);
cmbParity->setEnabled(true); cmbFlow->setEnabled(true);
}
}
/* ==================== 应用下拉框参数到 QSerialPort ==================== */
void MainWindow::applyPortSettings()
{
// 波特率(自定义非标准波特率也支持)
qint32 baud = cmbBaud->currentText().toInt();
m_port->setBaudRate(baud);
// 数据位
int db = cmbDataBits->currentText().toInt();
if(db==5) m_port->setDataBits(QSerialPort::Data5);
else if(db==6) m_port->setDataBits(QSerialPort::Data6);
else if(db==7) m_port->setDataBits(QSerialPort::Data7);
else m_port->setDataBits(QSerialPort::Data8);
// 停止位
QString sb = cmbStopBits->currentText();
if(sb=="1") m_port->setStopBits(QSerialPort::OneStop);
else if(sb=="2") m_port->setStopBits(QSerialPort::TwoStop);
else m_port->setStopBits(QSerialPort::OneAndHalfStop);
// 校验位
QString par = cmbParity->currentText();
if(par=="None") m_port->setParity(QSerialPort::NoParity);
else if(par=="Even") m_port->setParity(QSerialPort::EvenParity);
else if(par=="Odd") m_port->setParity(QSerialPort::OddParity);
else if(par=="Space")m_port->setParity(QSerialPort::SpaceParity);
else m_port->setParity(QSerialPort::MarkParity);
// 流控
QString fl = cmbFlow->currentText();
if(fl.startsWith("None"))
m_port->setFlowControl(QSerialPort::NoFlowControl);
else if(fl.startsWith("Hardware"))
m_port->setFlowControl(QSerialPort::HardwareControl);
else
m_port->setFlowControl(QSerialPort::SoftwareControl);
}
/* ==================== 接收数据(readyRead 信号触发) ==================== */
void MainWindow::onReadyRead()
{
QByteArray raw = m_port->readAll();
if (raw.isEmpty()) return;
m_recvBytes += raw.size();
lblRecvCnt->setText(QString("Rx: %1 B").arg(m_recvBytes));
logRecv(raw);
}
void MainWindow::logRecv(const QByteArray &raw)
{
if (rbRecvHex->isChecked()) {
txtRecv->moveCursor(QTextCursor::End);
txtRecv->insertPlainText(bytesToHexStr(raw) + " ");
} else {
txtRecv->moveCursor(QTextCursor::End);
QString decoded = QString::fromUtf8(raw);
// 如果是 GBK/GB2312 的中文环境,换成 fromLocal8Bit:
// decoded = QString::fromLocal8Bit(raw);
if (ckTimestamp->isChecked()) {
txtRecv->insertPlainText(
QString("[%1] ").arg(QTime::currentTime().toString("HH:mm:ss.zzz"))
);
}
txtRecv->insertPlainText(decoded);
txtRecv->verticalScrollBar()->setValue(
txtRecv->verticalScrollBar()->maximum());
}
}
/* ==================== 发送 ==================== */
void MainWindow::onSend()
{
if (!m_isOpen || !m_port->isOpen()) return;
QByteArray out;
QString input = txtSend->toPlainText().trimmed();
if (rbSendHex->isChecked()) {
out = hexStrToBytes(input);
if (out.isEmpty() && !input.isEmpty()) {
lblStatus->setText("HEX格式错误!请用空格分隔,如: AA BB CC");
return;
}
} else {
out = input.toUtf8(); // 中文 → toLocal8Bit() 看你的设备编码
}
qint64 n = m_port->write(out);
if (n > 0) {
m_sendBytes += n;
lblSendCnt->setText(QString("Tx: %1 B").arg(m_sendBytes));
}
/* 定时发送启停 */
if (ckAutoSend->isChecked() && !m_autoTimer->isActive()) {
int ms = leInterval->text().toInt();
if(ms<20) ms=20;
m_autoTimer->start(ms);
}
if (!ckAutoSend->isChecked() && m_autoTimer->isActive()) {
m_autoTimer->stop();
}
}
void MainWindow::onAutoSendTimeout()
{
if (!m_isOpen) { m_autoTimer->stop(); return; }
onSend(); // 复用发送逻辑
}
/* ==================== HEX 互转工具 ==================== */
QByteArray MainWindow::hexStrToBytes(const QString &hexStr)
{
QByteArray result;
QString s = hexStr;
s.remove(' '); s.remove(':'); s.remove(',');
if ((s.length() % 2) != 0) s.prepend('0');
bool ok = false;
for (int i = 0; i < s.length(); i += 2) {
quint8 b = static_cast<quint8>(s.mid(i,2).toUInt(&ok,16));
if (!ok) return QByteArray(); // 非法字符
result.append(static_cast<char>(b));
}
return result;
}
QString MainWindow::bytesToHexStr(const QByteArray &data)
{
QStringList parts;
for (unsigned char c : data)
parts.append(QString("%1").arg(c,2,16,QLatin1Char('0')).toUpper());
return parts.join(' ');
}
/* ==================== 清空 ==================== */
void MainWindow::onClearRecv()
{
txtRecv->clear();
m_recvBytes = 0;
lblRecvCnt->setText("Rx: 0 B");
}
void MainWindow::onClearSend()
{
txtSend->clear();
}
void MainWindow::onRecvHexToggled(bool) { /* 切换显示模式 */ }
void MainWindow::onSendHexToggled(bool) { /* 切换发送模式 */ }
四、main.cpp
cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setStyle("Fusion"); // 看起来更干净
MainWindow w;
w.show();
return a.exec();
}
参考代码 基于Qt 5.8.0的串口调试助手 www.youwenfan.com/contentcsv/103521.html
五、怎么运行
- 新建工程:Qt Creator → New Project → Qt Widgets Application
- 替换/填入 上面四个文件(
.pro/main.cpp/mainwindow.h/mainwindow.cpp) - 构建 :Ctrl+B(确保 Qt 版本选的是 Qt 5.8 MinGW/MSVC)
- 插上 USB-TTL / Arduino / ESP8266 → 选 COM → 点 打开串口 → 发送
六、常见问题排查
| 现象 | 原因 |
|---|---|
编译报错 QSerialPort: No such file |
.pro 忘了写 QT += serialport |
| 打开失败 / 拒绝访问 | 串口被其它程序占用(另一串口助手/Arduino IDE 串口监视器) |
| 中文乱码 | STM32 端发的是 GBK ,把 fromUtf8 换成 QString::fromLocal8Bit(raw) |
| HEX 发送没反应 | 输入框格式应为 AA BB CC 或 AABBCC,会自动清洗分隔符 |