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 做两件事:
- 从界面控件拿输入参数
- 接收逻辑类抛出的信号并显示结果
三、结果 —— 完全解耦
| 角色 | 职责 | 是否依赖 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 触发”示例。
它演示了三层:
- UI 层:负责拿控件内容、显示日志
- 逻辑层(协议/打包处理):负责组包、校验等
- 通信层:负责串口/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 ¶m);
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 ¶m)
{
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 统一显示
🧩 这样设计的好处
| 层 | 不依赖 | 颗粒度 | 可复用性 |
|---|---|---|---|
| ProtocolBuilder | UI / 通信 | 纯协议逻辑 | ✔ 高 |
| CommunicationService | UI / 协议 | 纯发送逻辑 | ✔ 高 |
| 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);
// 循环继续,看缓存里还有没有更多帧
}
}
核心逻辑几个点:
- 用
buffer_把每次 readyRead 的数据拼起来,解决“半包/粘包”问题。 - 一遍一遍在缓存里找头、长度、是否够一帧,不够就等下次。
- 校验失败就丢弃这帧,继续找后面的。
- 每解析完一帧,就发
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. 整体流程串一下(收 + 发)
发送:
- 用户点按钮
- MainWindow 拿到文本,调用
builder_->buildCommand(text) ProtocolBuilder组包,发出packetReady(packet)CommunicationService::sendPacket(packet)写入串口
接收:
- 串口收到数据,Qt 触发
readyRead CommunicationService::onReadyRead()读出字节,发rawDataReceived(data)ProtocolParser::feedData(data)存到缓存并解析- 每解析出一帧,发
frameParsed(payload) - MainWindow 收到
frameParsed,更新 UI / 业务逻辑
所有模块职责都很清楚:
Builder:只管“我要发什么”,负责 打包Communicator:只管“怎么发、怎么收”,负责 IOParser:只管“收到的字节怎么拆”,负责 解析MainWindow:只管 UI 显示和用户交互
如果你愿意,下一步可以再加一层:
- 根据
payload里的“命令字 / ID”,解析成结构体,例如struct F005Frame { quint8 id; quint16 value; ... } - 用一个
CommandRouter按命令号分发到不同的处理函数
你要是贴一下你真实协议的大致格式(哪几字节是什么),我可以直接按你的协议帮你写一版解析器骨架。
261

被折叠的 条评论
为什么被折叠?



