基于 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,会自动清洗分隔符
相关推荐
用户8055336980316 小时前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner17 小时前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner9 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner10 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00612 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术12 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript