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 · 分层架构

图 1 \xb7 分层架构总览

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 · 模块化设计

图 2 \xb7 模块化设计与依赖关系

3.1 独立模块原则

RyBOS 的每个 Qt 模块都是独立的 mini-project:

qt/SomeModule/ ├── CMakeLists.txt ← 独立构建 ├── src/ ← 头文件 + 实现 ├── example/ ← 独立示例 ├── test/ ← 单元测试(可选) ├── 3rd/ ← 第三方依赖(可选) └── README.md ← 模块文档
TIP

📌 为什么这样设计:每个模块可以独立编译、独立测试、独立复用。做坐标转换的项目只需引入 GeodeticConverter,不需要把整个 ToolBox 拖进来。这就像乐高积木——每块都是独立的,拼在一起就是一栋楼。

3.2 Header-Only 优先

大部分模块采用 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 · 插件式页面系统

图 3 \xb7 插件式页面系统

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 · 线程模型

图 4 \xb7 线程模型

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 · 配置管理

图 5 \xb7 配置管理架构

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 · 日志系统

图 6 \xb7 日志系统架构

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 · 信号槽架构

图 7 \xb7 信号槽通信模式

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 · 登录与认证

图 8 \xb7 登录认证流程

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 · 网络通信层

图 9 \xb7 网络通信层架构

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 · 崩溃恢复与异常处理

图 10 \xb7 崩溃恢复与异常处理

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 的动画),但学习成本低

ADR-004: 选择 Header-Only 优先

  • 日期:2024-01
  • 状态:已采纳
  • 背景:模块需要被多个项目复用,希望零依赖配置
  • 决策:大部分模块采用 header-only,引入一个 .h 即可使用
  • 替代方案:编译为静态库/动态库——但需要额外的构建配置
  • 后果:使用极简,但编译时间增长(每个翻译单元都包含完整实现)

ADR-005: 选择 ConfigManager 单例而非全局变量

  • 日期:2024-01
  • 状态:已采纳
  • 背景:配置需要全局访问、线程安全、变更通知、持久化
  • 决策:ConfigManager 单例 + QSettings 后端
  • 替代方案:全局变量 / 依赖注入
  • 后果:使用方便,但单例测试不友好(可通过 instance() 注入 mock 缓解)

附录 A · 架构速查

新增工具页面 Checklist

  • 继承 ToolPage,实现 name() 和 icon()
  • 实现 onActivate() / onDeactivate()(如需资源管理)
  • 耗时操作用 AsyncTaskExecutor 或 QtThreadPool
  • 配置读写通过 ConfigManager
  • 日志通过 LogManager + TagLog
  • 在 main.cpp 中 addToolPage 注册
  • 遵循 RyBOS 编码约定(Allman、m_ 前缀、namespace RyB)

新增模块 Checklist

  • 创建 qt/ModuleName/ 目录
  • CMakeLists.txt(find_package(Qt6) + add_library)
  • src/ModuleName.h(header-only 优先)
  • example/main.cpp(独立示例)
  • README.md(模块文档)
  • Qt 类加 Qt 前缀
  • Doxygen 文件头注释
  • 跨平台 #ifdef Q_OS_WIN / #ifdef _WIN32

附录 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 工具集合应用 应用层 - 主程序
本页目录