2、QGC架构解析:主窗口框架、QML与C++混合编程模型、核心模块介绍

好,咱们进入第二章。上一章我们把QGC装起来了,也跑了个Hello World。这一章,咱们得把QGC的“骨架”摸清楚。

说白了,你要做插件开发,就得知道这栋楼的地基在哪、承重墙在哪、水电管线怎么走的。不然你随便敲一锤子,搞不好整栋楼就塌了。

我个人习惯,接手一个新项目,第一件事不是看代码细节,而是先看它的架构图。QGC的架构,其实非常清晰,就三层:UI层(QML)、逻辑层(C++)、通信层(MAVLink)。

核心观点: QGC的插件开发,本质就是在UI层(QML)写界面,在逻辑层(C++)写业务,然后通过信号与槽把它们粘起来。MAVLink是飞机和地面站对话的语言,你不需要精通,但得知道怎么发消息、收消息。

2.1 QGC主窗口框架:这栋楼是怎么盖的?

QGC的主窗口,不是我们传统WinForm或者MFC那种拖控件拖出来的。它完全基于Qt Quick,也就是QML。主窗口的入口文件是 qgroundcontrol.qml。

你打开这个文件,会发现它其实就是一个大的 Window 组件,里面套了一个 MainWindowLayout。这个布局,就是整栋楼的骨架。

我刚开始看的时候,也觉得有点绕。后来我画了个图,一下就明白了。你看下面这个结构:

QGC 主窗口框架结构图 MainWindow (qgroundcontrol.qml) 工具栏 Fly View Plan View Setup View Analyze View ...更多 内容区 (StackView) 根据左侧选择,动态加载不同的QML页面 例如:FlyView.qml, PlanView.qml 状态栏 (StatusBar) 点击切换

看到没?左边是工具栏,右边是内容区,底部是状态栏。内容区是一个 StackView,你点左边不同的按钮,它就加载不同的QML页面进去。这就是QGC的页面路由机制。

小技巧: 你的插件,本质上就是往这个 StackView 里塞一个新的QML页面。或者,你可以在工具栏里加一个新的按钮。这些,我们后面几章会手把手教。

2.2 QML与C++混合编程模型:怎么让界面和逻辑说话?

这是QGC最核心的设计模式。QML负责画界面,C++负责干重活。那它们怎么通信呢?

说白了,就三板斧:注册C++对象到QML上下文、信号与槽、属性绑定。

我举个例子。你界面上有个按钮,点一下要解锁飞机。QML里写:

// QML 代码
Button {
    text: "解锁"
    onClicked: {
        // 调用C++对象的方法
        myVehicle.arm()
    }
}

那这个 myVehicle 是哪来的?它是C++那边注册进来的。在C++的main函数或者某个初始化代码里,你会看到类似这样的代码:

// C++ 代码
// 将C++对象暴露给QML
QQmlContext *context = engine.rootContext();
context->setContextProperty("myVehicle", vehicleObject);

这样,QML里就能直接用 myVehicle 这个变量了。你调用它的方法,它内部就会发MAVLink消息给飞机。

反过来,C++要通知QML界面更新,比如飞机电池电量变了,怎么办?用信号。

// C++ 代码
// 在Vehicle类中定义信号
signals:
    void batteryChanged(float percent);

// 当电量更新时,发射信号
emit batteryChanged(85.5);
// QML 代码
Text {
    text: "电量: " + myVehicle.batteryPercent.toFixed(1) + "%"
}

// 或者用 Connections 监听信号
Connections {
    target: myVehicle
    onBatteryChanged: function(percent) {
        console.log("电量变了: " + percent)
    }
}

嗯,这里要注意。QML里的属性绑定是自动的。你只要把 myVehicle.batteryPercent 这个属性声明为 Q_PROPERTY,QML就会自动监听它的变化。这就是Qt的元对象系统在背后干活。

避坑指南: 我曾经犯过一个错,在C++里用多线程更新了一个属性,但没有通过信号通知主线程。结果QML界面死活不刷新。记住,所有与QML交互的C++对象,都必须在主线程操作。如果你要在后台线程处理数据,处理完了通过信号发回主线程。

2.3 核心模块介绍:Vehicle、MAVLink、Settings

这三个模块,是你做插件开发打交道最多的。咱们一个一个说。

2.3.1 Vehicle:飞机的数字孪生

Vehicle 类,是QGC里对一架飞机的抽象。它包含了飞机的所有状态:位置、姿态、电池、GPS、飞行模式、参数等等。

你可以把它理解成飞机的“数字孪生”。地面站里所有关于这架飞机的信息,都从这个类里拿。

它的主要职责:

  • 管理MAVLink通信: 接收飞控发来的消息,解析后更新内部状态。
  • 提供操作接口: 解锁、上锁、切换模式、起飞、降落、返航等。
  • 维护参数系统: 管理飞控的几百个参数(PID、传感器校准等)。

在QML里,你通过 QGC.qgcVehicle() 这个全局函数,就能拿到当前激活的Vehicle对象。比如:

// QML 代码
var vehicle = QGC.qgcVehicle()
if (vehicle) {
    console.log("当前飞行模式: " + vehicle.flightMode)
    console.log("电池电压: " + vehicle.battery.voltage)
}

2.3.2 MAVLink:飞机和地面站共同的语言

MAVLink,全称Micro Air Vehicle Link。它是一种轻量级的通信协议,专门用于无人机和地面站之间通信。

说白了,就是一套约定好的消息格式。比如,HEARTBEAT 消息告诉地面站飞机还活着;GLOBAL_POSITION_INT 消息告诉地面站飞机在哪;COMMAND_LONG 消息用来发送指令。

在QGC里,MAVLink的解析和封装,都在 MAVLinkProtocol 和 LinkManager 这两个类里。你不需要直接操作原始的MAVLink字节流。QGC已经帮你封装好了。

你需要知道的是:

  • 如何发送MAVLink命令: 通过 Vehicle 类的 sendCommand 方法。
  • 如何监听MAVLink消息: 通过 Vehicle 类的信号,比如 mavlinkMessageReceived。

举个例子,你想让飞机执行一个自定义的MAVLink命令:

// C++ 代码
void MyPlugin::sendCustomCommand(Vehicle* vehicle) {
    mavlink_command_long_t cmd = {};
    cmd.target_system = vehicle->id();
    cmd.target_component = MAV_COMP_ID_AUTOPILOT1;
    cmd.command = MAV_CMD_USER_1; // 自定义命令
    cmd.param1 = 1.0f;
    cmd.param2 = 2.0f;

    vehicle->sendCommandOnCurrentLink(cmd);
}

经验之谈: 我建议你花半小时,把 common.xml 这个MAVLink定义文件翻一遍。里面定义了所有标准消息和命令。你不需要背下来,但得知道有什么东西可以用。很多插件功能,其实就是组合几个MAVLink命令。

2.3.3 Settings:把配置管起来

QGC的配置系统,是基于 QSettings 封装的。但它做了一层更高级的抽象,叫 SettingsGroup 和 SettingsFact。

每个配置项,都是一个 SettingsFact。它包含了配置的名称、类型、默认值、取值范围、描述等元信息。一组相关的配置,组成一个 SettingsGroup。

比如,你插件的配置,可以这样定义:

// C++ 代码
class MyPluginSettings : public SettingsGroup {
    Q_OBJECT
    Q_PROPERTY(QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged)
    Q_PROPERTY(int updateInterval READ updateInterval WRITE setUpdateInterval NOTIFY updateIntervalChanged)

public:
    MyPluginSettings(QObject* parent = nullptr)
        : SettingsGroup(parent)
    {
        _serverUrl = new SettingsFact("serverUrl", "ws://localhost:8080", this);
        _updateInterval = new SettingsFact("updateInterval", 1000, this);
    }

    QString serverUrl() { return _serverUrl->rawValue().toString(); }
    int updateInterval() { return _updateInterval->rawValue().toInt(); }

signals:
    void serverUrlChanged();
    void updateIntervalChanged();

private:
    SettingsFact* _serverUrl;
    SettingsFact* _updateInterval;
};

然后,在QML里,你就可以像使用普通属性一样,读写这些配置:

// QML 代码
RowLayout {
    Label { text: "服务器地址:" }
    TextField {
        text: myPluginSettings.serverUrl
        onEditingFinished: myPluginSettings.serverUrl = text
    }
}

而且,这些配置是自动持久化的。你改了,下次启动QGC,它还在。不需要你手动读写文件。

小提示: 做插件开发,一定要用 SettingsGroup 来管理你的配置。不要自己搞个ini文件或者json文件。原因很简单:QGC的配置系统已经帮你处理好了跨平台、线程安全、信号通知等问题。你何必重复造轮子呢?

2.4 本章小结:一张图看懂QGC架构

好了,咱们把QGC的三大核心模块串起来,看一张完整的架构图:

QGC 核心架构与数据流 UI层 (QML) FlyView | PlanView | SetupView | 你的插件页面 通过 Q_PROPERTY 和信号绑定到 C++ 对象 逻辑层 (C++) Vehicle | MAVLinkProtocol | SettingsGroup 处理业务逻辑、管理状态、封装MAVLink通信 通信层 (MAVLink / 串口 / UDP / TCP) LinkManager | SerialLink | UDPLink | TCPLink 信号/槽 MAVLink 字节流

你看,数据流是单向的:飞机通过MAVLink发数据上来,C++逻辑层解析、处理,然后通过信号通知QML界面更新。用户操作界面,QML调用C++方法,C++再通过MAVLink发指令给飞机。

这个模型,你搞清楚了,后面写插件就顺了。说白了,你的插件就是在这个模型里,插入你自己的逻辑和界面。

核心要点回顾:

  • 主窗口: 左侧工具栏 + 右侧StackView内容区 + 底部状态栏。
  • 混合编程: QML画界面,C++写逻辑,通过 setContextProperty 和信号/槽通信。
  • Vehicle: 飞机的数字孪生,所有状态和操作都通过它。
  • MAVLink: 通信协议,QGC帮你封装好了,你只需要调用高级接口。
  • Settings: 配置管理,用 SettingsGroup 和 SettingsFact,自动持久化。

嗯,这一章的内容就到这。架构这东西,光看文字可能有点干。我建议你打开QGC源码,对照着这一章的内容,找到我说的那几个关键文件,自己走一遍。相信我,看完源码再回来看这章,你会豁然开朗。