基于 Qt 5.8.0 的串口调试助手

核心依赖: 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

五、怎么运行

  1. 新建工程:Qt Creator → New Project → Qt Widgets Application
  2. 替换/填入 上面四个文件(.pro / main.cpp / mainwindow.h / mainwindow.cpp
  3. 构建 :Ctrl+B(确保 Qt 版本选的是 Qt 5.8 MinGW/MSVC
  4. 插上 USB-TTL / Arduino / ESP8266 → 选 COM → 点 打开串口 → 发送

六、常见问题排查

现象 原因
编译报错 QSerialPort: No such file .pro 忘了写 QT += serialport
打开失败 / 拒绝访问 串口被其它程序占用(另一串口助手/Arduino IDE 串口监视器)
中文乱码 STM32 端发的是 GBK ,把 fromUtf8 换成 QString::fromLocal8Bit(raw)
HEX 发送没反应 输入框格式应为 AA BB CCAABBCC,会自动清洗分隔符
相关推荐
sugar__salt1 小时前
Bun 新一代 JavaScript/TypeScript 运行时:从入门到实战
开发语言·javascript·typescript
geovindu1 小时前
go: Broadcast Pattern
开发语言·后端·设计模式·golang·广播模式
sycmancia1 小时前
Qt——Qt程序打包
开发语言·qt
郝学胜-神的一滴2 小时前
Qt 高级开发 026:QTabWidget御道,从筑基到化境
开发语言·c++·qt·程序人生·软件构建·用户界面
Jun6262 小时前
QT(14)-UBUNTU下QT使用串口
开发语言·qt·ubuntu
Jun6262 小时前
QT(16)-云端版本管理
开发语言·qt
ggaofeng2 小时前
试用zeroclaw
java·开发语言
~|Bernard|2 小时前
关于go语言中二维切片的append操作陷阱
开发语言·后端·golang
c++之路2 小时前
C/C++ 全链路编译工具汇总
c语言·开发语言·c++