QT的解耦方式

UI 上有一个按钮 + 一个文本框。
按钮被点时,将文本框内容交给逻辑类处理。
逻辑类处理完后,通过信号把结果或日志抛回给 UI。

核心思想:
UI 负责拿输入和展示输出,逻辑类只处理业务,不碰 UI。


场景:有个简单的“字符串处理器”

  • 输入框写文字
  • 点按钮
  • 逻辑类把文字变成大写
  • UI 显示日志
  • UI 显示处理结果

一、逻辑类(纯业务)

逻辑类不看 UI,不知道 QLineEdit,也不需要 MainWindow 指针。

// StringProcessor.h
#pragma once
#include <QObject>

class StringProcessor : public QObject
{
    Q_OBJECT
public:
    explicit StringProcessor(QObject *parent = nullptr);

public slots:
    void processInput(const QString &text);  // 只接收内容,不管 UI

signals:
    void resultReady(const QString &text);   // 处理结果
    void log(const QString &msg);            // 日志输出
};

实现:

// StringProcessor.cpp
#include "StringProcessor.h"

StringProcessor::StringProcessor(QObject *parent)
    : QObject(parent)
{
}

void StringProcessor::processInput(const QString &text)
{
    QString t = text.trimmed();
    QString upper = t.toUpper();

    emit log("开始处理输入...");
    emit log(QString("原始内容:%1").arg(t));
    emit log(QString("大写结果:%1").arg(upper));

    // 最终结果
    emit resultReady(upper);
}

特点:

  • 完全不关心 UI。
  • 不知道 MainWindow 和控件。
  • 只收字符串,输出字符串和日志。

二、UI 层(MainWindow)

UI 把控件事件“转换成”逻辑类的方法调用。

// MainWindow.cpp
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include "StringProcessor.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    processor_ = new StringProcessor(this);

    // 点击按钮:从 UI 拿内容,再给逻辑层
    connect(ui->pushButton, &QPushButton::clicked,
            this, [this] {
                QString text = ui->lineEdit->text();
                processor_->processInput(text);
            });

    // 日志输出
    connect(processor_, &StringProcessor::log,
            this, [this](const QString &msg) {
                ui->textEditLog->append(msg);
            });

    // 处理结果
    connect(processor_, &StringProcessor::resultReady,
            this, [this](const QString &result) {
                ui->labelResult->setText(result);
            });
}

在这里插入图片描述

这里 UI 做两件事:

  1. 从界面控件拿输入参数
  2. 接收逻辑类抛出的信号并显示结果

三、结果 —— 完全解耦

角色职责是否依赖 UI
StringProcessor业务逻辑(这里是字符串变大写)❌ 完全不依赖
MainWindow展示控件,拿输入,显示结果✔ 依赖 UI 本身
二者之间信号 + 槽✔ 解耦

逻辑类可以轻松复用,比如:

  • 以后换 QML UI
  • 换另一个窗口
  • 换成控制台程序
  • 写单元测试(不需要 UI)

都不用改 StringProcessor,一行不动。


四、用一张流程图总结

用户点按钮
→ MainWindow 读取 lineEdit
→ 调用 processor->processInput(text)
→ processor 处理
→ 发出 log(…) 信号
→ 发出 resultReady(…) 信号
→ MainWindow 接收信号并更新 UI

[UI按钮] --> [MainWindow拿输入] --> [StringProcessor处理逻辑]
                     ↑                      |
                     |                      v
                [展示结果] <-- signal ----- [结果]

五、这个模式能帮你什么?

  • 逻辑类不依赖 UI,不需要拿 ui->xxx 控件
  • UI 改名字、换控件、换界面不会影响逻辑类
  • 逻辑类更容易复用、测试、维护
  • 结构在项目变大时不会乱

下面给你一个非常清晰、完全解耦、实际能用的“通信 + 协议打包 + UI 触发”示例

它演示了三层:

  1. UI 层:负责拿控件内容、显示日志
  2. 逻辑层(协议/打包处理):负责组包、校验等
  3. 通信层:负责串口/TCP/CAN 真正发送

三层之间用信号 + 槽连接,不互相依赖 UI,也不依赖具体控件。

这就是最好的 Qt 解耦方式。


🎯 示例场景

用户点击按钮 → UI 获得“命令参数” → 协议层组包 → 通信层发送 → UI 显示日志。


一、协议层(不依赖 UI,不依赖通信)

只做一件事:打包协议报文。

// ProtocolBuilder.h
#pragma once
#include <QObject>

class ProtocolBuilder : public QObject
{
    Q_OBJECT
public:
    explicit ProtocolBuilder(QObject *parent = nullptr);

public slots:
    void buildCommand(const QString &param);

signals:
    void packetReady(const QByteArray &data);
    void log(const QString &msg);
};

实现:

// ProtocolBuilder.cpp
#include "ProtocolBuilder.h"

ProtocolBuilder::ProtocolBuilder(QObject *parent)
    : QObject(parent)
{
}

void ProtocolBuilder::buildCommand(const QString &param)
{
    emit log("开始组包...");

    QByteArray packet;
    packet.append(0x55);        // 起始字节
    packet.append(0xAA);        // 起始字节

    QByteArray p = param.toUtf8();
    packet.append(p);           // 载荷

    quint8 checksum = 0;
    for (char c : p)
        checksum ^= c;

    packet.append(checksum);    // 校验

    emit log("组包完成");
    emit packetReady(packet);   // 通知通信层去发送
}

✔ 没有 UI
✔ 没有通信对象
✔ 只有字符串 → 报文转换
这是纯“协议逻辑”。


二、通信层(不依赖 UI,不依赖协议具体内容)

只做一件事:把上层给的 QByteArray 发出去。

// CommunicationService.h
#pragma once
#include <QObject>
#include <QSerialPort>

class CommunicationService : public QObject
{
    Q_OBJECT
public:
    explicit CommunicationService(QObject *parent = nullptr);

public slots:
    void sendPacket(const QByteArray &data);

signals:
    void log(const QString &msg);

private:
    QSerialPort port_;
};

实现:

// CommunicationService.cpp
#include "CommunicationService.h"

CommunicationService::CommunicationService(QObject *parent)
    : QObject(parent)
{
    port_.setPortName("COM3");
    port_.setBaudRate(115200);
    port_.open(QIODevice::WriteOnly);
}

void CommunicationService::sendPacket(const QByteArray &data)
{
    port_.write(data);
    emit log(QString("发送字节数:%1").arg(data.size()));
}

✔ 不关心 UI
✔ 不关心协议内容(协议是什么无所谓)
✔ 只管把数据发出去


三、UI 层(MainWindow)

只负责按钮事件 + 显示日志,不做业务。

// MainWindow.cpp
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include "ProtocolBuilder.h"
#include "CommunicationService.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    builder_ = new ProtocolBuilder(this);
    communicator_ = new CommunicationService(this);

    // 点击按钮 -> 拿 UI 内容 -> 交给协议层
    connect(ui->btnSend, &QPushButton::clicked,
            this, [this]{
                QString text = ui->lineEditParam->text();
                builder_->buildCommand(text);
            });

    // 协议层组好包 -> 给通信层发
    connect(builder_, &ProtocolBuilder::packetReady,
            communicator_, &CommunicationService::sendPacket);

    // 日志合流:协议层日志
    connect(builder_, &ProtocolBuilder::log,
            this, [this](const QString &msg){
                ui->textEditLog->append("[协议] " + msg);
            });

    // 日志合流:通信层日志
    connect(communicator_, &CommunicationService::log,
            this, [this](const QString &msg){
                ui->textEditLog->append("[通信] " + msg);
            });
}

✔ UI 只管 UI
✔ 协议只管组包
✔ 通信只管发送
✔ 通过信号链接,完全解耦


🔥 整体数据流(可视化)

[按钮点击]
     ↓
 UI 从控件读取参数
     ↓
builder.buildCommand(param)
     ↓
组包完成 -> packetReady(packet)
     ↓
communicator.sendPacket(packet)
     ↓
串口/TCP 等设备发送成功

日志:

  • builder.emit(log)
  • communicator.emit(log)
  • UI 统一显示

🧩 这样设计的好处

不依赖颗粒度可复用性
ProtocolBuilderUI / 通信纯协议逻辑✔ 高
CommunicationServiceUI / 协议纯发送逻辑✔ 高
UI协议/通信内部细节只做显示与输入✔ 高

你随时可以:

  • 换协议(扩展 Builder,不动 UI & 通信)
  • 换通信方式(把 SerialPort 换成 TCP,不动 UI & 协议)
  • 换界面(换 QML 或移动端,只动 UI)

这就是正确的 Qt 业务架构。


好,咱就在前面“发送+组包”的基础上,加上:

  • 串口异步接收
  • 收到的原始字节丢给“协议解析器”
  • 解析出完整一帧后,发信号给上层使用

我会写一个最小可跑的框架级示例,你可以直接照这个结构改成自己的协议。


协议假设(先定个简单格式)

先随便定个协议格式,方便演示解析过程:

[0]  0x55
[1]  0xAA
[2]  len            // payload 长度
[3..3+len-1] payload
[last] checksum     // payload 所有字节 XOR

也就是:
头2字节 + 长度 + 负载 + 1字节校验。


1. 通信层:异步接收数据

CommunicationService 中,之前只有发送,现在加上接收:

// CommunicationService.h
#pragma once
#include <QObject>
#include <QSerialPort>

class CommunicationService : public QObject
{
    Q_OBJECT
public:
    explicit CommunicationService(QObject *parent = nullptr);

public slots:
    void sendPacket(const QByteArray &data);

signals:
    void log(const QString &msg);
    void rawDataReceived(const QByteArray &data);  // 新增:收到原始数据

private slots:
    void onReadyRead();                            // 串口有数据可读

private:
    QSerialPort port_;
};

实现:

// CommunicationService.cpp
#include "CommunicationService.h"

CommunicationService::CommunicationService(QObject *parent)
    : QObject(parent)
{
    port_.setPortName("COM3");
    port_.setBaudRate(115200);

    if (!port_.open(QIODevice::ReadWrite)) {
        emit log("串口打开失败");
        return;
    }

    // 关键点:readyRead 信号 = 异步接收
    connect(&port_, &QSerialPort::readyRead,
            this, &CommunicationService::onReadyRead);
}

void CommunicationService::sendPacket(const QByteArray &data)
{
    port_.write(data);
    emit log(QString("发送字节数:%1").arg(data.size()));
}

void CommunicationService::onReadyRead()
{
    QByteArray data = port_.readAll();  // 把当前缓冲区所有字节读出来
    emit log(QString("接收到原始字节数:%1").arg(data.size()));
    emit rawDataReceived(data);         // 丢给上层(协议解析器)
}

这里的“异步”就是靠 readyRead 信号:
串口一有数据,Qt 自动调 onReadyRead(),不需要你自己写循环。


2. 协议解析器:缓存 + 组帧 + 校验

新建一个类,只负责把零碎的 QByteArray 拼起来,按协议找完整帧,校验成功后发信号:

// ProtocolParser.h
#pragma once
#include <QObject>

class ProtocolParser : public QObject
{
    Q_OBJECT
public:
    explicit ProtocolParser(QObject *parent = nullptr);

public slots:
    void feedData(const QByteArray &data);   // 外部喂原始字节进来

signals:
    void frameParsed(const QByteArray &payload); // 成功解析出的 payload
    void log(const QString &msg);

private:
    void processBuffer();                    // 内部解析循环

private:
    QByteArray buffer_;                      // 解析缓存
};

实现:

// ProtocolParser.cpp
#include "ProtocolParser.h"

ProtocolParser::ProtocolParser(QObject *parent)
    : QObject(parent)
{
}

void ProtocolParser::feedData(const QByteArray &data)
{
    // 追加到缓存后处理
    buffer_.append(data);
    processBuffer();
}

void ProtocolParser::processBuffer()
{
    const int kHeaderSize = 3;  // 0x55 0xAA + len

    while (true) {
        // 1. 至少要有头 + 长度字节
        if (buffer_.size() < kHeaderSize + 1) {
            return;
        }

        // 2. 找到头 0x55 0xAA,如果前面有垃圾字节就丢弃
        int startIndex = buffer_.indexOf("\x55\xAA", 0);
        if (startIndex < 0) {
            // 没找到头,全部清空
            emit log("未找到帧头,清空缓存");
            buffer_.clear();
            return;
        }

        if (startIndex > 0) {
            emit log(QString("丢弃 %1 个无效字节").arg(startIndex));
            buffer_.remove(0, startIndex);
            if (buffer_.size() < kHeaderSize + 1)
                return;
        }

        // 3. 现在 buffer_[0]=0x55, buffer_[1]=0xAA, buffer_[2]=len
        quint8 len = static_cast<quint8>(buffer_[2]);

        int frameSize = 2 /*header*/ + 1 /*len*/ + len /*payload*/ + 1 /*checksum*/;

        if (buffer_.size() < frameSize) {
            // 数据还不完整,等下次再来
            return;
        }

        // 4. 取出一帧
        QByteArray frame = buffer_.mid(0, frameSize);
        buffer_.remove(0, frameSize);  // 从缓存中删除

        QByteArray payload = frame.mid(3, len);
        quint8 checksum = static_cast<quint8>(frame.last());

        // 5. 校验
        quint8 calc = 0;
        for (char c : payload)
            calc ^= static_cast<quint8>(c);

        if (calc != checksum) {
            emit log("校验失败,丢弃一帧");
            // 循环继续,看剩余缓存是否还能组出下一帧
            continue;
        }

        emit log("成功解析一帧");
        emit frameParsed(payload);
        // 循环继续,看缓存里还有没有更多帧
    }
}

核心逻辑几个点:

  1. buffer_ 把每次 readyRead 的数据拼起来,解决“半包/粘包”问题。
  2. 一遍一遍在缓存里找头、长度、是否够一帧,不够就等下次。
  3. 校验失败就丢弃这帧,继续找后面的。
  4. 每解析完一帧,就发 frameParsed(payload)

3. UI 层:把它们串起来

MainWindow 里把前面三个对象连成链路:

// MainWindow.h 里增加成员
class ProtocolBuilder;
class ProtocolParser;
class CommunicationService;

class MainWindow : public QMainWindow
{
    Q_OBJECT
    ...
private:
    ProtocolBuilder *builder_;
    ProtocolParser *parser_;
    CommunicationService *communicator_;
};

构造函数中初始化和 connect:

// MainWindow.cpp
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include "ProtocolBuilder.h"
#include "ProtocolParser.h"
#include "CommunicationService.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    builder_      = new ProtocolBuilder(this);
    parser_       = new ProtocolParser(this);
    communicator_ = new CommunicationService(this);

    // 发送:按钮 -> 协议组包 -> 通信发送
    connect(ui->btnSend, &QPushButton::clicked,
            this, [this]{
                QString text = ui->lineEditParam->text();
                builder_->buildCommand(text);
            });

    connect(builder_, &ProtocolBuilder::packetReady,
            communicator_, &CommunicationService::sendPacket);

    // 接收:通信收到原始数据 -> 解析器解析 -> UI 显示
    connect(communicator_, &CommunicationService::rawDataReceived,
            parser_, &ProtocolParser::feedData);

    connect(parser_, &ProtocolParser::frameParsed,
            this, [this](const QByteArray &payload){
                // 示例:把 payload 作为字符串显示
                QString s = QString::fromUtf8(payload);
                ui->textEditLog->append("[解析成功] payload = " + s);
            });

    // 日志:三个模块的 log 都汇总到 UI
    auto logToUi = [this](const QString &msg){
        ui->textEditLog->append(msg);
    };

    connect(builder_,      &ProtocolBuilder::log, logToUi);
    connect(parser_,       &ProtocolParser::log,  logToUi);
    connect(communicator_, &CommunicationService::log, logToUi);
}

4. 整体流程串一下(收 + 发)

发送:

  1. 用户点按钮
  2. MainWindow 拿到文本,调用 builder_->buildCommand(text)
  3. ProtocolBuilder 组包,发出 packetReady(packet)
  4. CommunicationService::sendPacket(packet) 写入串口

接收:

  1. 串口收到数据,Qt 触发 readyRead
  2. CommunicationService::onReadyRead() 读出字节,发 rawDataReceived(data)
  3. ProtocolParser::feedData(data) 存到缓存并解析
  4. 每解析出一帧,发 frameParsed(payload)
  5. MainWindow 收到 frameParsed,更新 UI / 业务逻辑

所有模块职责都很清楚:

  • Builder:只管“我要发什么”,负责 打包
  • Communicator:只管“怎么发、怎么收”,负责 IO
  • Parser:只管“收到的字节怎么拆”,负责 解析
  • MainWindow:只管 UI 显示和用户交互

如果你愿意,下一步可以再加一层:

  • 根据 payload 里的“命令字 / ID”,解析成结构体,例如 struct F005Frame { quint8 id; quint16 value; ... }
  • 用一个 CommandRouter 按命令号分发到不同的处理函数

你要是贴一下你真实协议的大致格式(哪几字节是什么),我可以直接按你的协议帮你写一版解析器骨架。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IOT-Power

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值