第2章:QGC源码结构解析——主框架、模块划分与核心类

做航线规划插件,说白了你得先搞清楚QGC这个大家伙是怎么搭起来的。我刚开始接触QGC源码时,第一反应是「这玩意儿也太大了」——几百个类、上千个文件,从哪下手?

后来我总结出一个经验:别试图一次看懂所有代码。你只需要抓住三个关键点——主框架怎么启动、模块怎么划分、核心类怎么协作。今天我们就围绕这三块来拆解。

2.1 QGC主框架:从main()到界面显示

QGC的入口在 src/main.cpp。嗯,这里其实没什么魔法,就是一个标准的Qt应用启动流程:

// 简化后的main函数逻辑
int main(int argc, char *argv[])
{
    QGCApplication app(argc, argv);  // 自定义QApplication子类
    app.initialize();                 // 初始化:日志、设置、插件系统
    MainWindow window;                // 主窗口
    window.show();
    return app.exec();
}

我个人习惯把QGC的启动分为三个阶段:

阶段 关键操作 涉及类
初始化 加载设置、初始化日志、注册QML类型 QGCApplication
插件加载 扫描插件目录、加载自定义插件 QGCCorePlugin、FactSystem
界面构建 创建主窗口、加载QML页面 MainWindow、QmlApplication
小技巧:调试时可以在 main.cpp 里加个 qDebug() 输出启动耗时。我曾经发现插件加载占了启动时间的40%,后来优化了插件初始化逻辑,启动快了将近一倍。

2.2 模块划分:QGC的「五脏六腑」

QGC的源码目录结构其实挺清晰的。我把它画成了一张图,你一看就明白:

QGC 核心模块架构 QGC 主框架 (MainWindow + QGCApplication) 通信层 (LinkManager) 业务逻辑层 (MissionController) UI层 (QML + Widget) MAVLink协议 串口/网络 航线管理 (MissionManager) 参数系统 (FactSystem) 地图/仪表盘 Vehicle (无人机对象) MissionItem (航点) GeoCoordinate (坐标) 信号与槽 (Signal & Slot) — 模块间通信总线 核心思路:模块间通过信号与槽解耦,不直接调用对方方法 Vehicle 状态变化 → 发射信号 → MissionController 接收 → 更新航线显示

你看,整个架构是分层解耦的。通信层只管收发MAVLink消息,业务层只管处理航线逻辑,UI层只管展示。各层之间通过信号与槽沟通——这也是Qt最强大的地方。

2.3 核心类:Vehicle 和 MissionController

做航线规划插件,你打交道最多的就是这两个类。我分别说说。

2.3.1 Vehicle:无人机的「数字孪生」

Vehicle 类在 src/Vehicle/Vehicle.h,它代表一架真实的无人机。说白了,QGC里所有关于飞机的信息——位置、姿态、电池、航线——都挂在这个类上。

关键成员:

class Vehicle : public QObject
{
    Q_OBJECT
public:
    // 无人机状态
    VehicleState* vehicleState();       // 位置、姿态、速度
    FactSystem* factSystem();           // 参数系统(PID、飞控参数)
    MissionManager* missionManager();   // 航线管理器

    // 通信
    void sendMavCommand(MAV_CMD cmd, ...);  // 发送MAVLink命令
    LinkInterface* activeLink();            // 当前通信链路

signals:
    void armedChanged(bool armed);          // 解锁状态变化
    void flightModeChanged(const QString& mode); // 飞行模式变化
    void missionChanged();                  // 航线变化
};
重点:Vehicle 是单例模式。整个QGC只有一个Vehicle实例(除非你连了多架飞机)。所有模块都通过 QGCApplication::vehicle() 获取它。

2.3.2 MissionController:航线的「大脑」

MissionController 在 src/MissionManager/MissionController.h。它负责管理整条航线——增删改查航点、上传下载、校验合法性。

我当初做自定义航线插件时,就是在这个类上扩展的。它的核心逻辑:

class MissionController : public QObject
{
    Q_OBJECT
public:
    // 航线操作
    void insertMissionItem(int index, MissionItem* item);
    void removeMissionItem(int index);
    void clearMission();

    // 上传/下载
    void startUpload();     // 上传航线到飞控
    void startDownload();   // 从飞控下载航线

    // 校验
    bool isMissionValid();  // 航线是否合法

signals:
    void missionItemsChanged();  // 航点列表变化
    void uploadProgress(int percent);
    void downloadComplete();

private:
    QList<MissionItem*> m_missionItems;  // 航点列表
    Vehicle* m_vehicle;                   // 关联的无人机
};
避坑指南:我曾经在自定义航点时,直接修改了 m_missionItems 列表,但没有发射 missionItemsChanged() 信号。结果UI死活不刷新,排查了半小时才发现。记住:修改数据后一定要发射对应的信号。

2.4 信号与槽机制:模块间的「神经纤维」

QGC里到处都是信号与槽。你想想看,通信层收到一条MAVLink消息,怎么通知UI更新?就是靠信号。

一个典型的流程:

  1. 通信层收到MAVLink消息 → 解析 → 发射 mavlinkMessageReceived()
  2. Vehicle 接收信号 → 更新内部状态 → 发射 armedChanged()
  3. MissionController 接收信号 → 更新航线数据 → 发射 missionItemsChanged()
  4. UI层 接收信号 → 刷新地图/列表显示

这种链式传递的好处是:每个模块只关心自己那一段。比如你要加一个新功能,只需要在合适的位置监听或发射信号,不用改别人的代码。

我的习惯:写插件时,我会先画一张「信号流图」——哪些信号我需要监听,哪些信号我需要发射。画清楚了再动手写代码,效率高很多。

2.5 本章小结

好了,我们快速过一下这章的核心:

  • 主框架:从 main() 到 MainWindow,分初始化、插件加载、界面构建三个阶段
  • 模块划分:通信层、业务逻辑层、UI层,各层通过信号与槽解耦
  • 核心类:Vehicle 是无人机数字孪生,MissionController 是航线大脑
  • 信号与槽:模块间通信的「总线」,修改数据后记得发射信号