## 架构升级:Service Registry + Bridge 模式 - 新增 PluginSDK/IPluginServices.h:10 个纯虚服务接口(IDataProvider/ILinkProvider/...) - 新增 MavLinkServiceBridge:单 QObject 实现全部服务,隔离 MavLinkNode 依赖 - 升级 PluginManifest:支持 plugin.json 的 provides/consumes 声明式依赖 - 实现 ExtensionHost::autoWire():元对象自省自动连接信号槽 - 集成到 AppController:initModules() 中创建桥接器并注册到 ServiceRegistry - CockpitPlugin 演示服务发现:initialize() 中通过 PluginContext 查找服务 ## 代码清理 - Plugins/opmap:~280 行死代码(waypointsetting 100行注释块/tilematrix 54行/等27个文件) - Plugins/MavLinkNode:~200 行 GBK 乱码注释翻译为 UTF-8 + 12 行注释死代码 - Plugins/ToolsUI:~222 行死代码(ECU.cpp 82行/INS.cpp 113行/Parse/ToolsUI 等) - StatusUI/Setting/MissionUI:~65 行注释死代码 - Cockpit/leftladder.cpp:10 处 GBK 乱码翻译为中文 - 清理头文件注释掉的 #include(19 处)、空 if-else 分支、注释变量声明 ## 编译验证 - [100%] Built target GCS 零错误 - 运行时 timeout 3s 正常退出,无崩溃
18 KiB
C/C++ 编码规范
适用项目: GCS 地面控制站
语言标准: C++17
框架: Qt 5/6
构建工具: CMake 3.16+
格式化配置:.clang-format(根目录)
编码格式: UTF-8
1. 总则
1.1 核心原则
| 原则 | 说明 |
|---|---|
| 一致性优先 | 新增/修改的代码必须与所在文件或模块的现有风格保持一致 |
| 最小改动 | 仅修改目标代码,不随意改写无关代码、不随意添加无关注释 |
| 编译安全 | 修改后必须能通过编译;增删成员时检查所有引用 |
| 头文件自足 | 每个 .h 应包含其所有依赖的前向声明或 include |
1.2 编码原则
- 代码优先考虑可读性和可维护性
- 避免过度抽象,优先使用简单方案
- 所有面向用户的字符串必须使用
tr()包裹 - 禁止在新代码中使用裸
new/delete管理资源(除非 Qt 父子关系托管的 QObject 构造)
2. 文件组织
2.1 文件命名
| 类型 | 规范 | 示例 |
|---|---|---|
| 源文件 | PascalCase.cpp |
DataLink.cpp |
| 头文件 | PascalCase.h |
DataLink.h |
| UI 文件 | PascalCase.ui |
StatusUI.ui |
| 资源文件 | snake_case.qrc |
resources.qrc |
2.2 目录结构
当前项目实际结构:
gcs_nf/
├── App/ # 主程序入口 + MainWindow + AppController
├── PluginSDK/ # 插件开发包(静态库)
├── Plugins/ # 所有功能插件(每个插件一个目录)
│ ├── IPlugin.h # 插件接口 v2
│ ├── IPlugin_v3.h # 插件接口 v3(向后兼容 v2)
│ ├── Cockpit/ # 自绘飞行仪表盘
│ ├── MavLinkNode/ # MAVLink 协议解析
│ ├── dlink/ # 数据链路层
│ ├── opmap/ # 离线地图引擎
│ ├── Skin/ # 主题/皮肤
│ ├── Setting/ # 系统设置
│ └── ...
├── mavlink/ # MAVLink C 库(git submodule)
├── docs/ # 文档
├── bin/ # 构建产物输出目录
└── .clang-format # 代码格式化配置
文件组织约定:
- 每个插件目录下至少有一个与插件同名的
.h/.cpp主文件 - 使用
inc//src/分离头文件和源文件的模块(如 Cockpit、MavLinkNode),新增文件也遵循此布局 - UI 文件与使用它的类放在同一目录
- 插件目录下必须有
plugin.json清单文件
2.3 Include 顺序
遵循 Google C++ Style 的 include 顺序,各组之间空一行分隔:
1. 自身对应的头文件(仅 .cpp 中): #include "MyClass.h"
2. C 系统头文件: #include <cstdint>
3. C++ 标准库头文件: #include <memory>
4. 第三方库头文件 (Qt): #include <QObject>
5. 本项目头文件: #include "PluginSDK/PluginContext.h"
示范(.cpp 文件):
#include "PluginContext.h"
#include <cstdint>
#include <memory>
#include <QDebug>
#include <QObject>
#include "ServiceRegistry.h"
示范(.h 文件):
#ifndef PLUGINCONTEXT_H
#define PLUGINCONTEXT_H
#include <QObject>
#include "MavLinkService.h"
禁止事项:
- 禁止 用
""包含系统头文件或 Qt 头文件(如#include "QObject"),一律使用<> - 禁止 在头文件中包含不必要的头文件,优先使用前向声明
3. 命名规范
3.1 命名总表
| 元素 | 规范 | 示例 |
|---|---|---|
| 类名 / 结构体名 | PascalCase |
MavLinkNode, PluginContext |
| 函数 / 方法 | camelCase |
setPortName(), onDataReceived() |
| 成员变量 | m_camelCase |
m_portName, m_isConnected |
| 静态常量 / 枚举值 | kPascalCase 或 UPPER_SNAKE |
kDefaultBaud 或 DEFAULT_TIMEOUT |
| 局部变量 | camelCase |
portName, resultCount |
| 全局变量 | g_camelCase |
g_appConfig |
| 命名空间 | snake_case |
mapcontrol, plugin_utils |
| 宏 | UPPER_SNAKE |
MAVLINK_START_UART_PORT |
| 信号 (signal) | camelCase(过去式/完成态) | dataReceived, connectionStateChanged |
| 槽 (slot) | on + 过去式 |
onDataReceived, onStartClicked |
| 枚举类型 | PascalCase |
ConnectionState |
| 头文件守卫宏 | FILENAME_H |
MAVLINKSERVICE_H |
3.2 命名细则
类名:
class MavLinkNode : public QObject { }; // 正确 - PascalCase
class mavlink_node : public QObject { }; // 错误 - snake_case
成员变量:
private:
QString m_portName; // 正确 - m_camelCase
bool m_isConnected = false; // 正确
QString portName; // 错误 - 缺少 m_ 前缀
信号 / 槽:
signals:
void dataReceived(const QByteArray &data); // 正确 - 过去式
void connectionStateChanged(bool connected); // 正确 - 过去式
void signal_heartbeat(); // 弃用 - 旧 MavLinkNode 风格,新代码禁止
public slots:
void onDataReceived(const QByteArray &data); // 正确
void onStartClicked(); // 正确
void slot_data_received(); // 错误 - 弃用前缀
枚举:
enum class ConnectionState { // 正确 - enum class
Disconnected,
Connecting,
Connected,
Error
};
enum ErrorCode { // 旧风格(已有代码兼容)
ERR_NONE = 0,
ERR_TIMEOUT,
ERR_PARSE
};
4. 格式规范
4.1 格式化工具
项目根目录有 .clang-format,每次编辑后必须运行:
clang-format -style=file -i <修改的文件>
4.2 核心格式规则
从 .clang-format 提取:
| 规则 | 值 |
|---|---|
| 基础风格 | LLVM |
| 缩进宽度 | 4 空格 |
| Tab | 禁用(一律空格) |
| 行宽上限 | 120 字符 |
| 大括号风格 | Allman(大括号独立一行) |
| 指针/引用对齐 | 靠左 (int *p, QString &s) |
| 访问修饰符缩进 | -4(public: / private: 向外凸出) |
| include 排序 | 不自动排序(手动管理) |
| 最大连续空行 | 1 行 |
| 短函数单行 | 允许(如空构造/析构) |
4.3 Allman 大括号示范
void MyClass::doWork()
{
if (condition)
{
// ...
}
else
{
// ...
}
}
class MyClass : public QObject
{
Q_OBJECT
public:
explicit MyClass(QObject *parent = nullptr);
~MyClass() override = default;
signals:
void finished();
private:
int m_count = 0;
};
4.4 空行与空格
- 类内各组之间空一行:
public:/signals:/public slots:/private:之间 - 函数体之间空一行
- 逻辑相关代码块可以内部不空行,不同逻辑块之间空一行
if/for/while关键字后跟一个空格再跟(
5. 注释规范
5.1 注释语言
- 代码注释使用中文
- 面向用户的字符串使用
tr()包裹中文
5.2 注释格式
类/结构体注释 — 使用 /* */ 或 // 块注释(重要类使用多行):
// ============================================================
// AppController — 集中管理所有模块间的信号路由
// ============================================================
class AppController : public QObject
{
Q_OBJECT
// ...
};
函数注释 — 对公开接口和复杂逻辑的函数使用 Doxygen 风格:
/**
* @brief 将伺服器 PWM 值转换为角度
* @param pwm 输入的 PWM 值
* @param offset 修正偏移
* @return 转换后的角度值(度)
*/
double pwmToDeg(int pwm, double offset);
内联注释 — 对关键算法或非直观逻辑使用行尾注释:
painter.translate(width() * H_pos / 100, height() * V_pos / 100); /* 坐标变换为窗体中心 */
int side = qMin(width(), height()); /* 决定了模块只能是方形 */
5.3 注释禁止事项
- 禁止 保留被注释掉的"死代码"(
// old code ...)。提交前必须删除 - 禁止 中文内容出现乱码(确保文件保存为 UTF-8 编码)
- 禁止 注释写"废话"(如
// i 自增 1对i++的注释)
6. 头文件规范
6.1 头文件守卫
统一使用 #ifndef / #define / #endif,宏名使用 文件名大写_H 格式,结尾带宏名注释:
#ifndef PLUGINCONTEXT_H
#define PLUGINCONTEXT_H
// ... 头文件内容 ...
#endif // PLUGINCONTEXT_H
禁止使用 #pragma once(与项目现有代码保持一致)。
6.2 前向声明
在 .h 中,如果只需要类型的指针或引用,优先使用前向声明而非 include:
// MyPlugin.h
class QAction;
class QWidget;
namespace Ui { class MyPlugin; }
namespace mapcontrol { class OPMapWidget; }
class Config;
6.3 头文件内容顺序
#ifndef MYCLASS_H
#define MYCLASS_H
// 1. 必要的 Qt 头文件
#include <QObject>
// 2. 前向声明
class Config;
namespace Ui { class MyClass; }
// 3. 类定义
class MyClass : public QObject
{
Q_OBJECT
public:
// 构造/析构
// 公开方法
signals:
// 信号
public slots:
// 公开槽
protected:
// 保护成员
private:
// 私有成员
};
#endif // MYCLASS_H
7. 类设计规范
7.1 类成员可见性
从上到下的顺序:
public: 构造/析构 → 公开方法
signals: Qt 信号
public slots: Qt 公开槽
protected: 保护成员(含虚函数)
private: 私有成员变量和方法
7.2 构造与析构
使用 explicit 修饰单参数构造函数:
class PluginContext : public QObject
{
Q_OBJECT
public:
explicit PluginContext(const QString &pluginId, QObject *parent = nullptr);
~PluginContext() override = default;
};
- QObject 子类使用
QObject *parent作为最后一个参数,利用 Qt 父子内存管理 - 成员变量就地初始化(C++11 语法):
private:
QString m_pluginId; // 不需要就地初始化(QString 默认已空)
ServiceRegistry *m_serviceRegistry = nullptr;
MavLinkService *m_mavlinkService = nullptr;
bool m_isConnected = false;
int m_retryCount = 3;
- 析构函数中手动 delete 非 QObject 子类的裸指针,并设为 nullptr
- 使用
Q_UNUSED()标记未使用的参数:
void MainWindow::resizeEvent(QResizeEvent *event)
{
Q_UNUSED(event)
// ...
}
7.3 虚函数重写
重写虚函数必须加 override 关键字:
class MyPlugin : public IPlugin
{
public:
QString name() const override;
bool initialize(PluginContext *context) override;
void shutdown() override;
protected:
void paintEvent(QPaintEvent *event) override; // QWidget 虚函数
};
7.4 UI 文件使用
当类使用 .ui 文件时:
// StatusUI.h
namespace Ui { class StatusUI; }
class StatusUI : public QWidget
{
Q_OBJECT
public:
explicit StatusUI(QWidget *parent = nullptr);
~StatusUI() override;
private:
Ui::StatusUI *ui = nullptr; // 使用 m_ 前缀例外:此处用 ui
};
// StatusUI.cpp
#include "StatusUI.h"
#include "ui_StatusUI.h"
StatusUI::StatusUI(QWidget *parent)
: QWidget(parent)
, ui(new Ui::StatusUI)
{
ui->setupUi(this);
QFile file(":/qss/StatusUI.qss");
file.open(QFile::ReadOnly);
QTextStream filetext(&file);
QString stylesheet = filetext.readAll();
setStyleSheet(stylesheet);
file.close();
}
StatusUI::~StatusUI()
{
delete ui;
}
8. Qt 特定规范
8.1 Signal / Slot 连接方式
新代码优先使用函数指针语法:
// 推荐 — 编译期检查
connect(m_button, &QPushButton::clicked, this, &MyClass::onButtonClicked);
// 带 lambda
connect(m_timer, &QTimer::timeout, this, [this]() {
updateStatus();
});
// 信号到信号
connect(m_source, &Source::dataReady, m_target, &Target::onDataReady);
旧式 SIGNAL() / SLOT() 宏语法仅在维护旧代码时沿用,新代码禁止:
// 弃用 — 仅维护旧代码时保留
connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(onValueChanged(int)));
8.2 QObject 内存管理
- QObject 子类通过
parent参数加入 Qt 对象树,由 Qt 自动负责析构 - 非 QObject 的成员指针需要在析构函数中手动 delete
// QObject 子类 — 不需要手动 delete
m_button = new QPushButton(tr("开始"), this); // this 为 parent
// 非 QObject 裸指针 — 必须在析构函数中 delete
MyClass::~MyClass()
{
delete m_rawData;
m_rawData = nullptr;
}
8.3 QString 使用
- 所有界面字符串用
tr()包裹:
m_label->setText(tr("连接已建立"));
m_button->setToolTip(tr("点击以断开连接"));
- 数值格式化使用
QString::number():
QString value = QString::number(pwm, 'f', 2); // 保留 2 位小数
QString hex = QString::number(address, 16).toUpper(); // 十六进制大写
- 多段拼接使用
arg()或QStringLiteral:
QString msg = tr("端口: %1, 波特率: %2").arg(portName).arg(baudRate);
- 禁止 在界面代码中使用
std::string,除非是纯算法库内部
8.4 自绘组件(无 .ui 文件)
对于 Cockpit 等纯自绘组件,遵循以下模式:
// Cockpit.h
class Cockpit : public QWidget
{
Q_OBJECT
protected:
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
QColor m_groundColor = QColor(100, 100, 100);
qreal m_roll = 0.0;
};
// Cockpit.cpp
void Cockpit::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
// ... 绘制逻辑
}
8.5 qmake .pro / .pri 兼容
当前项目同时存在 CMake 和 qmake 构建系统。如果修改涉及 .pro 文件:
- 保持
.pri文件与CMakeLists.txt中的源文件列表同步 - 新增文件需同时添加到两个构建文件中
9. 错误处理与日志
9.1 日志等级与使用
qDebug() << "调试信息"; // 开发期间输出,发布前删除或注释
qInfo() << "关键操作日志"; // 应保留,用于运行时追踪
qWarning() << "非致命异常"; // 如文件打开失败、配置缺失等
qCritical() << "严重错误"; // 可能导致功能不可用的错误
9.2 错误处理模式
不抛出异常(项目不使用 try/catch/throw):
// 检查条件,提前返回
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
{
qWarning() << "无法打开文件:" << path << file.errorString();
return false;
}
// 检查空指针
if (!config)
{
qWarning() << "Config 未初始化";
return;
}
// JSON 解析错误处理
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError)
{
qWarning() << "JSON 解析失败:" << error.errorString();
return;
}
9.3 断言
- 对不应发生的逻辑错误使用
Q_ASSERT(Debug 模式生效,Release 模式移除) - 对 Release 也需要检查的条件使用普通 if 判断
Q_ASSERT(plugin != nullptr);
Q_ASSERT(!m_pluginId.isEmpty());
10. 插件开发规范
10.1 插件接口
当前项目同时支持两版插件接口:
- IPlugin v2 (IPlugin.h) — 所有现有插件使用
- IPlugin v3 (IPlugin_v3.h) — 基于订阅模式,新功能开发优先使用
10.2 新插件模板
// MyPlugin.h
#ifndef MYPLUGIN_H
#define MYPLUGIN_H
#include "IPlugin_v3.h"
class MyPlugin : public IPlugin_v3
{
Q_OBJECT
public:
explicit MyPlugin(QObject *parent = nullptr);
~MyPlugin() override = default;
// IPlugin 接口
QString name() const override;
QString version() const override;
bool initialize(PluginContext *context) override;
void shutdown() override;
private:
PluginContext *m_context = nullptr;
};
#endif // MYPLUGIN_H
10.3 plugin.json 清单
每个插件目录必须有 plugin.json:
{
"name": "MyPlugin",
"version": "1.0.0",
"description": "插件描述",
"activationEvents": ["onStartup"],
"contributes": {}
}
11. CMake 构建规范
11.1 CMake 文件位置
- 顶层
CMakeLists.txt— 全局设置 +add_subdirectory - 每个模块目录下的
CMakeLists.txt— 模块自身的构建规则
11.2 新增文件时的操作
- 将
.h/.cpp文件加入对应模块的CMakeLists.txt中的add_library或add_executable源文件列表 - 如模块有
.pri文件,同步更新 - 新增
Q_OBJECT类时,无需额外操作(AUTOMOC已开启)
12. AI 编辑指南
本章专门面向 AI 辅助编码场景,指导 AI 如何在此项目中安全地编辑代码。
12.1 编辑前的必读步骤
- 读取目标文件 — 使用 Read 工具读取待编辑文件的完整内容
- 理解上下文 — 检查该文件用到的所有类型、成员、信号槽
- 确认风格 — 观察现有代码的命名、缩进、注释风格,严格模仿
- 检查依赖 — 查看
#include列表,确保新增代码的依赖已包含
12.2 编辑操作规范
| 操作 | 规范 |
|---|---|
| 新增类 | 创建一个 .h + .cpp,遵循类设计规范(第 7 章) |
| 新增成员函数 | 在 .h 中声明,在 .cpp 中实现;检查是否需要 override |
| 新增成员变量 | 使用 m_camelCase 命名,放在 private: 区,作为最后手段才放 public: |
| 新增信号 | 放在 signals: 区,使用过去式命名 |
| 新增槽 | 放在 public slots: 或 private slots:,使用 onXxx 命名 |
| 修改函数体 | 仅修改目标函数,不随意重构无关代码 |
| 添加 include | 插入到 include 分组中的正确位置 |
| 删除代码 | 彻底删除包括相关注释和空行,不留"死代码" |
12.3 编辑后的必做检查
- 新增/修改的类是否添加了
Q_OBJECT宏 - 是否使用了
tr()包裹所有用户可见字符串 - 重写的虚函数是否加了
override - 单参数构造函数是否加了
explicit - 是否修复了
#include顺序 - 是否有匹配的
#ifndef/#define/#endif守卫(新文件) - 修改后是否运行了
clang-format -style=file -i <文件> - 是否同步更新了
CMakeLists.txt
12.4 常见编辑场景示范
场景 A:给现有类添加一个槽函数
步骤:
1. 读取 MyClass.h,在 public slots: 或 private slots: 区添加声明
2. 读取 MyClass.cpp,在文件末尾添加函数实现
3. 使用 clang-format 格式化
场景 B:创建一个新的简单对话框
步骤:
1. 在某插件目录下创建 MyDialog.h / MyDialog.cpp / MyDialog.ui
2. MyDialog.h:按照 7.4 节 UI 文件使用规范编写
3. MyDialog.cpp:实现构造/析构函数
4. 同步更新 CMakeLists.txt
5. 使用 clang-format 格式化所有新文件
场景 C:在现有文件中添加一个信号
步骤:
1. 读取头文件,在 signals: 区添加信号声明
2. 确认信号使用过去式名称(如 dataReady, finished)
3. 查找所有用到信号的地方(通过 grep),确认连接方式
12.5 代码样式速查卡片
命名: 类 PascalCase | 成员 m_camelCase | 信号过去式 | 槽 onXxx
格式: Allman 大括号 | 4 空格缩进 | 120 列宽 | 指针靠左 int *p
头文件: #ifndef_H 守卫 | #include <> 系统头 | #include "" 项目头
类结构: public → signals → public slots → protected → private
字符串: tr("中文字符串") | QString::number(x, 'f', 2)
调试: qDebug / qInfo / qWarning | 不用 try/catch
构造: explicit 单参构造 | QObject *parent = nullptr 最后 | 成员就地初始化
重写: 所有虚函数重写加 override
插件: 实现 IPlugin_v3 | 提供 plugin.json | 在 CMakeLists.txt 注册
编辑后: 运行 clang-format | 检查 tr() | 检查 CMakeLists.txt 同步
附录 A:与旧代码风格兼容说明
项目中有大量 2020-2022 年编写的旧代码,其风格与本文规范有所不同。AI 在修改旧文件时:
| 旧风格 | 新风格(本文规范) | 处理方式 |
|---|---|---|
signal_heartbeat() |
heartbeatReceived() |
仅新增信号用新风格;已有旧信号保持不变 |
SIGNAL(...) / SLOT(...) |
&Class::method |
新增 connect 用新语法;已有旧连接保持不变 |
无 override |
添加 override |
修改到旧函数时可顺手补上 |
无 explicit |
添加 explicit |
修改到旧类时可顺手补上 |
#include "QObject" |
#include <QObject> |
修改到 include 时可顺手修正 |
| public 成员变量 | private + m_ 前缀 | 仅新增成员用新风格;已有 public 成员保持不变 |
| 中文注释乱码 | UTF-8 中文注释 | 修改到乱码注释附近时修正 |