C++ Qt 客户端架构设计
一份从零到一设计 Qt 桌面客户端架构的完整文档。以 RyBOS ToolBox 为实战案例,覆盖分层架构、插件式页面、线程模型、配置管理、日志系统、信号槽通信、登录认证、崩溃恢复等核心主题。重点讲"为什么这么设计"(费曼式 📌 讲解),配 RyBOS 项目真实代码。
图表在 /Qt客户端架构设计/diagrams/ 目录。图注名即原图文件名,看图注就能直接找到原图用 draw.io 编辑。
1 · 为什么要设计架构
1.1 没有架构的痛苦
新手 Qt 项目通常长这样:一个 MainWindow 类 3000 行,UI 布局、业务逻辑、网络请求、文件操作全塞在一起。刚开始能跑,但很快就会遇到:
- 改一个功能牵连其他功能——因为代码全耦合在一起
- UI 卡顿——耗时操作直接在主线程调用
- 配置散落各处——硬编码路径、端口、密码
- 出 bug 无法定位——没有日志,没有错误边界
- 无法复用——换一个项目,代码全推倒重来
1.2 好架构的标准
TIP
📌 打个比方:架构就像建筑图纸。盖一个狗窝不需要图纸,但盖一栋楼必须有。好架构的标准:
- 可维护:改一处不影响另一处(低耦合)
- 可扩展:加新功能不需要改旧代码(开闭原则)
- 可测试:每个模块能独立测试
- 可复用:模块能搬到另一个项目直接用
:::
1.3 RyBOS 的架构选择
RyBOS ToolBox 是一个多工具集合客户端(地图加载、坐标转换、点云处理、图像转换、视频播放、串口调试、加解密……),其架构设计核心决策:
| 决策 |
选择 |
理由 |
| 页面管理 |
插件式(ToolPage 接口) |
新工具只需实现接口,注册即可 |
| 线程模型 |
AsyncTaskExecutor + QThreadPool |
阻塞但不卡 UI,兼顾简单与响应 |
| 配置管理 |
ConfigManager 单例 + QSettings |
线程安全,变更通知,跨平台 |
| 日志系统 |
LogManager 单例 + TagLog |
按启动编号隔离,自动清理 |
| 通信方式 |
信号槽(松耦合) |
对象间不直接持有引用 |
| UI 导航 |
NavBar + QStackedWidget |
左侧导航栏 + 右侧页面栈 |
| 样式管理 |
QSS 资源文件 |
一套 QSS 统一全局风格 |
2 · 分层架构

2.1 四层架构
┌─────────────────────────────────────────────────────┐
│ Presentation Layer (表现层) │
│ MainWindow · NavBar · ToolPage · Dialogs │
├─────────────────────────────────────────────────────┤
│ Business Logic Layer (业务层) │
│ AsyncTaskExecutor · TaskScheduler · 业务处理类 │
├─────────────────────────────────────────────────────┤
│ Data Access Layer (数据层) │
│ ConfigManager · Database · FileSystem │
├─────────────────────────────────────────────────────┤
│ Infrastructure Layer (基础设施层) │
│ LogManager · DumpManager · Network · Crypto │
└─────────────────────────────────────────────────────┘
2.2 各层职责
表现层:只管"显示什么"和"接收什么操作"。不包含业务逻辑,不直接访问文件/网络。
业务层:处理"怎么做"。耗时任务通过 AsyncTaskExecutor 在后台执行,结果通过信号槽回调到表现层。
数据层:处理"数据从哪来"。ConfigManager 管配置,Database 管持久化,FileSystem 管文件读写。
基础设施层:全局服务。日志、崩溃捕获、加密、网络通信——被所有层共享,但不依赖任何上层。
:::tip
📌 依赖规则:上层可以依赖下层,下层不能依赖上层。表现层可以调业务层,但业务层不能直接操作 UI 控件。这样换 UI 框架时业务层不受影响。
2.3 RyBOS ToolBox 的目录结构对应
qt/ToolBox/src/
├── App/ ← 表现层(应用骨架)
│ ├── MainWindow.h/cpp ← 主窗口
│ ├── ToolPage.h ← 页面插件接口
│ └── main.cpp ← 入口
├── Widgets/ ← 表现层(通用控件)
│ ├── NavBar.h/cpp ← 导航栏
│ └── DirectoryDialog.h/cpp
├── Tools/ ← 表现层 + 业务层(各工具页面)
│ ├── CryptoTool/ ← 加解密工具
│ ├── GisCoordinate/ ← 坐标转换
│ ├── ImageMetadata/ ← 图片元数据
│ ├── MapLoader/ ← 地图加载
│ ├── OsgbConvert/ ← OSGB 转换
│ ├── PointCloudConvert/ ← 点云转换
│ ├── SerialDebug/ ← 串口调试
│ ├── TilesViewer/ ← 瓦片查看
│ └── VideoPlayer/ ← 视频播放
└── Map/ ← 业务层(地图相关逻辑)
├── CoordinateConverter.h
├── FlightPointLoader.h
├── KmlLoader.h
├── MapWidget.h
├── TileCache.h
└── TileDownloader.h
3 · 模块化设计

3.1 独立模块原则
RyBOS 的每个 Qt 模块都是独立的 mini-project:
qt/SomeModule/
├── CMakeLists.txt ← 独立构建
├── src/ ← 头文件 + 实现
├── example/ ← 独立示例
├── test/ ← 单元测试(可选)
├── 3rd/ ← 第三方依赖(可选)
└── README.md ← 模块文档
TIP
📌 为什么这样设计:每个模块可以独立编译、独立测试、独立复用。做坐标转换的项目只需引入 GeodeticConverter,不需要把整个 ToolBox 拖进来。这就像乐高积木——每块都是独立的,拼在一起就是一栋楼。
大部分模块采用 header-only 设计:
// AsyncTaskExecutor.h — 整个实现都在头文件中
#pragma once
#include <QObject>
#include <QtConcurrent>
namespace RyB
{
class AsyncTaskExecutor : public QObject
{
// 模板方法必须在头文件实现
template<typename ResultType>
static ResultType execute(std::function<ResultType()> task, ...);
};
} // namespace RyB
优点:无需 .cpp,引入一个 .h 即可使用,零依赖配置。
缺点:编译时间增长,模板代码必须 header-only。
3.3 模块依赖图
ToolBox (应用)
├── MainWindow → NavBar, ToolPage, QSS
├── MapLoaderPage → KmlLoader, TileDownloader, TileCache
├── GisCoordinatePage → CoordinateConverter
├── VideoPlayerPage → QtVideoThread, QtGLVideoWidget
├── SerialDebugPage → Serial, CommandProtocol
├── CryptoToolPage → Crypto
└── ...
每个 ToolPage 只依赖自己需要的模块,ToolPage 之间互不依赖。这保证了:
- 删除某个 ToolPage 不影响其他页面
- 新增 ToolPage 只需实现接口 + 注册
4 · 插件式页面系统

4.1 ToolPage 接口设计
ToolPage 是所有工具页面的抽象基类,定义了页面的生命周期接口:
// qt/ToolBox/src/App/ToolPage.h
class ToolPage : public QWidget
{
Q_OBJECT
public:
explicit ToolPage(QWidget* parent = nullptr) : QWidget(parent) {}
~ToolPage() override = default;
virtual QString name() const = 0; // 页面名称(显示在导航栏)
virtual QIcon icon() const = 0; // 页面图标
virtual void onActivate() {} // 页面被切换到时调用
virtual void onDeactivate() {} // 页面被切走时调用
};
4.2 为什么用 onActivate / onDeactivate
TIP
📌 打个比方:就像浏览器的标签页——切到某个标签页时才加载内容(懒加载),切走时可以暂停视频/停止定时器(释放资源)。如果所有页面同时活跃,会浪费大量 CPU 和内存。
典型用法:
class VideoPlayerPage : public ToolPage
{
void onActivate() override
{
// 页面切入:恢复播放
m_player->resume();
}
void onDeactivate() override
{
// 页面切出:暂停播放,释放 GPU 资源
m_player->pause();
}
};
4.3 MainWindow 的页面注册机制
// qt/ToolBox/src/App/MainWindow.cpp
void MainWindow::setupUi()
{
// 左侧导航栏
m_navBar = new NavBar(this);
m_navBar->setFixedWidth(256);
// 右侧页面栈
m_stackedWidget = new QStackedWidget(this);
// 导航栏点击 → 切换页面栈
connect(m_navBar, &NavBar::itemClicked,
this, &MainWindow::onNavItemClicked);
}
void MainWindow::addToolPage(ToolPage* page)
{
m_pages.append(page);
m_stackedWidget->addWidget(page); // 加入页面栈
m_navBar->addItem(page->icon(), page->name()); // 加入导航栏
if (m_pages.size() == 1)
{
onNavItemClicked(0); // 默认选中第一个页面
}
}
void MainWindow::onNavItemClicked(int index)
{
// 通知旧页面失活
if (m_currentIndex >= 0)
m_pages[m_currentIndex]->onDeactivate();
// 切换页面栈
m_currentIndex = index;
m_stackedWidget->setCurrentIndex(index);
// 通知新页面激活
m_pages[index]->onActivate();
m_navBar->setCurrentIndex(index);
}
4.4 main.cpp 中的注册流程
// qt/ToolBox/src/main.cpp
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
app.setApplicationName("RyBOS ToolBox");
app.setOrganizationName("RyB");
MainWindow window;
window.addToolPage(new MapLoaderPage(&window));
window.addToolPage(new GisCoordinatePage(&window));
window.addToolPage(new TilesViewerPage(&window));
window.addToolPage(new PointCloudConvertPage(PointCloudConvertPage::ConvertType::Las, &window));
window.addToolPage(new OsgbConvertPage(OsgbConvertPage::ConvertMode::Legacy, &window));
window.addToolPage(new ImageWebpConvertPage(&window));
window.addToolPage(new VideoPlayerPage(&window));
window.addToolPage(new SerialDebugPage(&window));
window.addToolPage(new CryptoToolPage(&window));
window.show();
return app.exec();
}
TIP
📌 扩展性:新增一个工具页面只需三步:①继承 ToolPage 实现页面 ②在 main.cpp 中 addToolPage ③完成。不需要改 MainWindow、不需要改 NavBar、不需要改其他页面。这就是开闭原则的体现——对扩展开放,对修改关闭。
5 · 线程模型

5.1 Qt 线程模型基础
Qt 的核心是事件循环(QEventLoop),运行在主线程中。所有 UI 操作必须在主线程完成。耗时操作如果直接在主线程调用,会阻塞事件循环,导致界面卡顿。
主线程(UI 线程)
└── QEventLoop
├── 处理鼠标/键盘事件
├── 处理定时器
├── 处理网络 IO 回调
└── 处理信号槽(默认排队连接)
5.2 三种异步方案对比
| 方案 |
适用场景 |
优点 |
缺点 |
| QThread + Worker |
长期运行的后台任务 |
灵活,可自定义事件循环 |
代码量大 |
| QtConcurrent + QFuture |
一次性耗时任务 |
简洁,自动线程池 |
不支持取消(需自己实现) |
| AsyncTaskExecutor |
需要阻塞等待结果但不卡 UI |
主线程等待,界面响应 |
局部事件循环有风险 |
5.3 AsyncTaskExecutor 详解
RyBOS 的 AsyncTaskExecutor 是一个独特的模式——在主线程中阻塞等待,但通过局部事件循环保持 UI 响应:
// 核心原理
template<typename ResultType>
static ResultType execute(std::function<ResultType()> task, ...)
{
// 1. 后台线程执行任务
QFuture<ResultType> future = QtConcurrent::run(task);
// 2. 局部事件循环等待(界面不卡!)
QEventLoop loop;
QFutureWatcher<ResultType> watcher;
QObject::connect(&watcher, &QFutureWatcher<ResultType>::finished,
&loop, &QEventLoop::quit);
watcher.setFuture(future);
loop.exec(); // ← 这里阻塞但处理 UI 事件
// 3. 返回结果
return future.result();
}
TIP
📌 为什么不用 std::future + wait():std::future::wait() 会完全阻塞线程,界面冻结。QEventLoop::exec() 虽然也阻塞调用栈,但内部会持续处理事件(鼠标、键盘、绘制),所以界面保持响应。这就像"你在等外卖时不会盯着门,而是边做其他事边等"。
5.4 带进度的异步任务
int result = AsyncTaskExecutor::executeWithProgress<int>(
[](std::function<void(int)> progress) -> int {
for (int i = 0; i <= 100; i += 10)
{
progress(i); // 在后台线程调用
QThread::msleep(200);
}
return 100;
},
[this](int progress) {
// 在主线程中执行,可以安全更新 UI
m_progressBar->setValue(progress);
}
);
关键设计:进度回调通过 QMetaObject::invokeMethod 投递到主线程,确保 UI 操作的线程安全。
5.5 QtThreadPool 线程池
对于不需要阻塞等待的后台任务,使用 QtThreadPool:
QtThreadPool pool(QThread::idealThreadCount()); // 根据 CPU 核数自动设置
pool.submit([]() {
// 在后台线程执行
processLargeFile("data.las");
});
5.6 线程安全规则
┌──────────────────────────────────────────────────┐
│ 规则1: UI 控件只能在主线程操作 │
│ 规则2: 后台线程不能直接调用 UI 方法 │
│ 规则3: 跨线程通信用信号槽(自动排队连接) │
│ 规则4: 共享数据用 QMutex 或 std::atomic 保护 │
│ 规则5: 后台线程用 QMetaObject::invokeMethod 投递 │
└──────────────────────────────────────────────────┘
6 · 配置管理

6.1 ConfigManager 设计
class ConfigManager : public QObject
{
Q_OBJECT
public:
// 单例模式
static ConfigManager& instance();
// 初始化
bool initialize(const QString& configFilePath = QString());
// 类型安全的 get/set
QString get(const QString& group, const QString& key,
const QString& defaultValue = QString()) const;
int getInt(const QString& group, const QString& key,
int defaultValue = 0) const;
double getDouble(const QString& group, const QString& key,
double defaultValue = 0.0) const;
bool getBool(const QString& group, const QString& key,
bool defaultValue = false) const;
// 设置值(线程安全,自动保存)
bool set(const QString& group, const QString& key, const QVariant& value);
// 配置变更通知
// emit valueChanged(group, key, oldValue, newValue)
private:
QSettings* m_settings; // Qt 配置读写
QMutex m_mutex; // 线程安全
bool m_autoSave; // 自动保存
};
6.2 设计要点
单例 + 实例双模式:
// 方式1:单例(全局配置)
ConfigManager& config = ConfigManager::instance();
config.initialize("config.ini");
QString host = config.get("network", "host", "localhost");
// 方式2:实例(模块独立配置)
ConfigManager moduleConfig("module.ini");
moduleConfig.set("app", "debug", true);
变更通知(信号槽):
// 配置变化时自动通知
connect(&ConfigManager::instance(), &ConfigManager::valueChanged,
this, [](const QString& group, const QString& key,
const QVariant& oldVal, const QVariant& newVal) {
if (group == "network" && key == "host")
{
reconnectToServer(newVal.toString());
}
});
TIP
📌 为什么不用全局变量:全局变量的问题是——没有持久化、没有类型安全、没有变更通知、没有线程安全。ConfigManager 把这些问题全解决了:QSettings 负责持久化,模板 get/set 负责类型安全,信号槽负责通知,QMutex 负责线程安全。
6.3 配置文件格式
QSettings 在不同平台使用不同后端:
| 平台 |
后端 |
格式 |
| Windows |
注册表 / INI |
config.ini |
| Linux |
INI 文件 |
~/.config/AppName/config.ini |
| macOS |
plist |
~/Library/Preferences/com.RyB.AppName.plist |
[network]
host=192.168.1.100
port=8888
timeout=5000
[app]
theme=dark
language=zh_CN
debug=true
[video]
low_latency=true
gpu_accel=true
7 · 日志系统

7.1 LogManager 设计
class LogManager
{
public:
static LogManager* getInstance();
// 初始化:创建本次启动的日志目录
bool initialize(const QString& baseLogDir = "logs",
int maxKeepCount = 10);
// 每次启动创建编号目录:logs/00001/, logs/00002/, ...
// 自动清理旧目录,只保留最新 10 个
QString getCurrentLogDirectory() const; // logs/00003/
QString getCurrentLogNumber() const; // "00003"
// 添加日志类别
void addLogCategory(const QString& category, const QString& filename);
// 例:addLogCategory("network", "network.log")
// addLogCategory("serial", "serial.log")
// 设置日志模式
void setLogMode(const QString& mode); // "debug" 或 "release"
};
7.2 按启动编号隔离
logs/
├── 00001/ ← 第一次启动
│ ├── app.log
│ ├── network.log
│ └── serial.log
├── 00002/ ← 第二次启动
│ ├── app.log
│ └── ...
├── 00003/ ← 第三次启动(当前)
│ ├── app.log
│ └── ...
└── ... ← 超过 10 个自动清理最旧的
TIP
📌 为什么按启动编号隔离:如果所有日志写到一个文件,文件会无限增长,而且不同启动的日志混在一起难以排查。按启动编号隔离后,每次启动的日志是独立的,排查问题时直接看对应编号的目录。保留 10 个是平衡磁盘空间和回溯能力。
7.3 TagLog 分类日志
// 不同模块用不同 TAG 输出日志
LOG_TAG("Network", "连接到服务器 %s:%d", host, port);
LOG_TAG("Serial", "发送数据: %s", hexData);
LOG_TAG("Crypto", "AES 加密完成, 密文长度: %d", cipherLen);
每个 TAG 对应一个独立的日志文件,方便按模块排查问题。
7.4 Debug / Release 模式
// debug 模式:所有日志都记录
// release 模式:DEBUG 级别日志不记录
LogManager::getInstance()->setLogMode("release");
8 · 信号槽架构

8.1 信号槽 vs 直接调用
直接调用(紧耦合):
PageA ──调用──→ PageB.method()
问题:PageA 必须知道 PageB 的存在和接口
信号槽(松耦合):
PageA ──emit signal──→ [Qt 元对象系统] ──slot──→ PageB
优点:PageA 不知道谁在监听,只负责"广播"
TIP
📌 打个比方:直接调用像"打电话"——你必须知道对方的号码。信号槽像"广播电台"——你只管播报,谁在听你不用管。换听众不需要改电台代码。
8.2 ToolBox 中的信号槽链路
NavBar::itemClicked(int) ← 用户点击导航栏
→ MainWindow::onNavItemClicked ← 主窗口处理切换
→ oldPage::onDeactivate() ← 通知旧页面
→ newPage::onActivate() ← 通知新页面
8.3 跨线程信号槽
// 后台线程 emit 信号 → 主线程 slot 执行(自动排队连接)
class Worker : public QObject
{
Q_OBJECT
signals:
void progressUpdated(int percent);
void taskFinished(QString result);
public slots:
void doWork()
{
for (int i = 0; i <= 100; i += 10)
{
emit progressUpdated(i); // 后台线程 emit
QThread::msleep(500);
}
emit taskFinished("完成");
}
};
// 主线程连接
QThread* thread = new QThread;
Worker* worker = new Worker;
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::progressUpdated, this, &MainWindow::onProgress);
connect(worker, &Worker::taskFinished, this, &MainWindow::onFinished);
thread->start();
8.4 连接方式选择
| 连接方式 |
行为 |
使用场景 |
Qt::AutoConnection(默认) |
同线程→直接,跨线程→排队 |
99% 场景 |
Qt::DirectConnection |
直接在 emit 线程执行 |
性能敏感的同线程通信 |
Qt::QueuedConnection |
排队到 receiver 线程执行 |
跨线程 UI 更新 |
Qt::BlockingQueuedConnection |
排队并阻塞 emit 线程 |
需要等待 UI 操作完成 |
9 · 登录与认证

9.1 LoginDialog 设计
class LoginDialog : public QDialog
{
Q_OBJECT
public:
explicit LoginDialog(QWidget* pParent = nullptr);
private slots:
void onLoginClicked();
private:
void initData(); // 创建控件、加载用户配置
void initLayout(); // 垂直布局
void initStyle(); // 窗口属性、密码模式
void initConnection(); // 信号槽连接
bool loadUsers(const QString& filePath); // 从 JSON 加载用户
bool validateUser(const QString& username,
const QString& password) const;
QLineEdit* m_pUsernameEdit;
QLineEdit* m_pPasswordEdit;
QPushButton* m_pLoginButton;
QLabel* m_pStatusLabel;
QJsonArray m_users;
};
9.2 四步初始化模式
LoginDialog::LoginDialog(QWidget* pParent)
: QDialog(pParent)
, m_pUsernameEdit(nullptr)
, m_pPasswordEdit(nullptr)
{
initData(); // 1. 创建控件、加载数据
initLayout(); // 2. 摆放控件
initStyle(); // 3. 设置外观
initConnection(); // 4. 连接信号槽
}
TIP
📌 为什么分四步:如果全部混在构造函数里,代码会变成一坨。分步后,每步职责清晰:数据→布局→样式→行为。这也是 RyBOS 编码约定中推荐的初始化模式。
9.3 登录流程
用户输入用户名密码
→ 点击登录按钮
→ 空值检查(前端验证)
→ validateUser()(后端验证)
→ 成功: QMessageBox 提示 + accept()
→ 失败: 红色提示 + 保持对话框打开
9.4 用户配置文件
{
"users": [
{ "username": "admin", "password": "admin123" },
{ "username": "operator", "password": "op456" }
]
}
WARNING
🚧 安全提示:明文 JSON 存密码仅适用于内网工具。生产环境应存储密码哈希(SHA256 + 盐),或使用 OAuth/Token 认证。参见通信安全学习笔记第 3-4 章。
9.5 启动流程集成
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// 登录
RyB::LoginDialog loginDialog;
if (loginDialog.exec() != QDialog::Accepted)
{
return 0; // 用户取消登录,退出程序
}
// 登录成功,显示主窗口
MainWindow window;
window.show();
return app.exec();
}
10 · 网络通信层

10.1 通信模块矩阵
| 模块 |
协议 |
场景 |
RyBOS 位置 |
| TcpSocket |
TCP |
可靠传输、命令控制 |
qt/TcpSocket |
| UdpSocket |
UDP |
广播、心跳、低延迟 |
qt/UdpSocket |
| HttpClient |
HTTP/HTTPS |
REST API、文件下载 |
qt/HttpClient |
| WebSocket |
WS/WSS |
实时双向通信 |
qt/WebSocket |
| Serial |
RS-232/485 |
串口设备通信 |
qt/Serial |
| LocalSocket |
UDS/Named Pipe |
本机 IPC |
qt/LocalSocket |
10.2 通信层设计原则
1. 统一的回调接口:
// 所有通信模块都通过信号通知结果
connect(&tcpClient, &TcpClient::dataReceived, this, &Page::onDataReceived);
connect(&tcpClient, &TcpClient::disconnected, this, &Page::onDisconnected);
2. 协议与传输分离:
应用层: CommandProtocol (帧格式、CRC、序列号)
传输层: TcpSocket / UdpSocket / Serial (字节流传输)
同一套命令帧协议可以跑在 TCP 上,也可以跑在串口上——只需换传输层。
3. 心跳保活:
// UDP 心跳
RyB::UdpHeartbeat heartbeat;
heartbeat.setTarget("192.168.1.100", 8888);
heartbeat.setInterval(1000); // 每秒一次
heartbeat.setHeartbeatData("PING");
heartbeat.start();
11 · 崩溃恢复与异常处理

11.1 DumpManager 崩溃捕获
// Windows: MiniDump
// Linux: core dump + signal handler
DumpManager::instance().initialize(
"crashdumps", // dump 文件目录
true // 自动重启
);
11.2 异常处理策略
┌─────────────────────────────────────────────────────┐
│ 层级 │ 异常处理策略 │
├───────────────┼─────────────────────────────────────┤
│ 表现层 │ catch 异常 → 显示错误对话框 │
│ 业务层 │ catch 异常 → 记录日志 → 重新抛出 │
│ 数据层 │ catch 异常 → 返回默认值/错误码 │
│ 基础设施层 │ 记录日志 + 触发崩溃捕获 │
└─────────────────────────────────────────────────────┘
11.3 AsyncTaskExecutor 的异常传播
try
{
QString result = AsyncTaskExecutor::execute<QString>([]() -> QString {
// 后台线程中的异常会被捕获并传播到主线程
if (someError)
throw std::runtime_error("处理失败");
return "成功";
});
}
catch (const std::exception& e)
{
QMessageBox::critical(this, "错误", e.what());
}
TIP
📌 为什么这很重要:如果后台线程的异常不被捕获,程序会调用 std::terminate 直接崩溃。AsyncTaskExecutor 通过 QFuture::result() 将异常重新抛出到主线程,让调用者能正常 catch。
12 · 跨平台策略
12.1 平台抽象层
// Qt 模块用 Q_OS_WIN / Q_OS_LINUX
#ifdef Q_OS_WIN
#include <windows.h>
// Windows 特定代码
#endif
// 纯 C++ 模块用 _WIN32
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#endif
12.2 跨平台注意事项
| 关注点 |
Windows |
Linux |
解决方案 |
| 路径分隔符 |
\ |
/ |
用 QDir::separator() 或 / |
| 配置位置 |
注册表/INI |
~/.config/ |
QSettings 自动处理 |
| 文件编码 |
GBK |
UTF-8 |
统一用 UTF-8,QString::fromUtf8 |
| 线程 |
Win32 API |
pthread |
用 QThread 封装 |
| Socket |
Winsock2 |
POSIX |
用 QTcpSocket 封装 |
| 串口 |
COM 口 |
/dev/ttyS* |
用 QSerialPort 封装 |
12.3 CMake 跨平台构建
# RyBOS 的 CMake 跨平台配置
cmake_minimum_required(VERSION 3.16)
project(RyBOS_ToolBox)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Qt 查找
find_package(Qt6 COMPONENTS Core Gui Widgets Network Concurrent REQUIRED)
# 平台特定
if(WIN32)
link_libraries(bcrypt) # Windows 加密库
endif()
# 统一输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
13 · 编码约定
13.1 RyBOS Qt/C++ 编码规范摘要
| 规范 |
约定 |
示例 |
| 命名空间 |
namespace RyB |
namespace RyB { ... } |
| Qt 类前缀 |
Qt 前缀 |
QtHttpServer, QtCrypto |
| 大括号 |
Allman 风格 |
{ 独占一行 |
| 成员变量 |
m_ 前缀 |
m_port, m_lastError |
| 静态成员 |
s_ 前缀 |
s_instance |
| 指针变量 |
p 前缀 |
pSocket, m_pStatusLabel |
| C++ 标准 |
C++17 |
CMAKE_CXX_STANDARD 17 |
| 头文件保护 |
传统 include guard |
#ifndef MODULE_H |
| 信号槽 |
函数指针 connect |
connect(ptr, &Class::signal, ...) |
| 注释 |
Doxygen 风格 + 中文 |
@brief, @param, @return |
13.2 文件头注释模板
/**
* @file MainWindow.h
* @brief 主窗口
* @author RyB
* @version 1.0.0
*
* 【模块概述】
* ToolBox 主窗口,管理导航栏和页面栈。
*
* 【使用示例】
* @code
* MainWindow window;
* window.addToolPage(new MapLoaderPage(&window));
* window.show();
* @endcode
*/
13.3 区块分隔注释
// ============================================================================
// 【MainWindow - 主窗口】
// ============================================================================
// ---------------------------------------------------------------------------
// 初始化
// ---------------------------------------------------------------------------
14 · 架构决策记录(ADR)
ADR-001: 选择插件式页面架构而非 MDI
- 日期:2024-01
- 状态:已采纳
- 背景:ToolBox 需要承载 10+ 种工具,工具间无关联
- 决策:用
ToolPage 接口 + QStackedWidget 实现插件式页面
- 替代方案:MDI(多文档界面)——但工具间不需要并排查看
- 后果:新增工具极简(3 步),但无法同时查看两个工具
ADR-002: 选择 AsyncTaskExecutor 而非纯异步
- 日期:2024-01
- 状态:已采纳
- 背景:某些操作需要"等待结果才能继续"(如文件转换),但纯阻塞会卡 UI
- 决策:用
QEventLoop + QFutureWatcher 实现阻塞但不卡 UI 的模式
- 替代方案:纯异步回调——但调用链太深(回调地狱)
- 后果:代码简洁(同步写法),但局部事件循环需注意重入风险
ADR-003: 选择 QSS 而非 QML
- 日期:2024-01
- 状态:已采纳
- 背景:需要统一深色主题,团队熟悉 C++ Widgets
- 决策:用 QSS(Qt Style Sheets)做样式,不用 QML/Qt Quick
- 替代方案:QML——但需要学习新语言,且 Widgets 生态更成熟
- 后果:样式灵活度有限(不如 QML 的动画),但学习成本低
- 日期:2024-01
- 状态:已采纳
- 背景:模块需要被多个项目复用,希望零依赖配置
- 决策:大部分模块采用 header-only,引入一个
.h 即可使用
- 替代方案:编译为静态库/动态库——但需要额外的构建配置
- 后果:使用极简,但编译时间增长(每个翻译单元都包含完整实现)
ADR-005: 选择 ConfigManager 单例而非全局变量
- 日期:2024-01
- 状态:已采纳
- 背景:配置需要全局访问、线程安全、变更通知、持久化
- 决策:
ConfigManager 单例 + QSettings 后端
- 替代方案:全局变量 / 依赖注入
- 后果:使用方便,但单例测试不友好(可通过
instance() 注入 mock 缓解)
附录 A · 架构速查
新增工具页面 Checklist
新增模块 Checklist
附录 B · RyBOS Qt 模块矩阵
| 模块 |
功能 |
架构角色 |
| AsyncTask |
阻塞式异步执行 |
业务层 - 线程模型 |
| ConfigManager |
配置管理 |
数据层 - 配置 |
| LogManager |
日志管理 |
基础设施层 |
| DumpManager |
崩溃捕获 |
基础设施层 |
| TaskScheduler |
定时任务调度 |
业务层 - 调度 |
| TcpSocket |
TCP 通信 |
基础设施层 - 网络 |
| UdpSocket |
UDP 通信 |
基础设施层 - 网络 |
| HttpClient |
HTTP 客户端 |
基础设施层 - 网络 |
| WebSocket |
WebSocket |
基础设施层 - 网络 |
| Serial |
串口通信 |
基础设施层 - 设备 |
| LocalSocket |
本地 IPC |
基础设施层 - IPC |
| LoginDialog |
登录对话框 |
表现层 - 认证 |
| DirectoryDialog |
目录选择 |
表现层 - 控件 |
| VideoPlayer |
视频播放 |
表现层 + 业务层 |
| Crypto |
加解密 |
基础设施层 - 安全 |
| Database |
数据库 |
数据层 - 持久化 |
| FileSystem |
文件操作 |
数据层 - 文件 |
| ImageMetadata |
图片元数据 |
业务层 - 图像 |
| JpgCompressor |
JPG 压缩 |
业务层 - 图像 |
| ScreenUtils |
屏幕工具 |
表现层 - 工具 |
| NetworkTools |
网络工具 |
基础设施层 - 工具 |
| ToolBox |
工具集合应用 |
应用层 - 主程序 |