Files
gcs-nf/.agents/rule/编码规范.md
T
hm e7cf44504c feat: Service Registry + Bridge 解耦架构 + 全工程代码清理
## 架构升级: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 正常退出,无崩溃
2026-06-01 09:46:36 +08:00

18 KiB
Raw Blame History

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
静态常量 / 枚举值 kPascalCaseUPPER_SNAKE kDefaultBaudDEFAULT_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)
访问修饰符缩进 -4public: / 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 自增 1i++ 的注释)

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_ASSERTDebug 模式生效,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 新增文件时的操作

  1. .h / .cpp 文件加入对应模块的 CMakeLists.txt 中的 add_libraryadd_executable 源文件列表
  2. 如模块有 .pri 文件,同步更新
  3. 新增 Q_OBJECT 类时,无需额外操作(AUTOMOC 已开启)

12. AI 编辑指南

本章专门面向 AI 辅助编码场景,指导 AI 如何在此项目中安全地编辑代码。

12.1 编辑前的必读步骤

  1. 读取目标文件 — 使用 Read 工具读取待编辑文件的完整内容
  2. 理解上下文 — 检查该文件用到的所有类型、成员、信号槽
  3. 确认风格 — 观察现有代码的命名、缩进、注释风格,严格模仿
  4. 检查依赖 — 查看 #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 中文注释 修改到乱码注释附近时修正