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。这个布局,就是整栋楼的骨架。
我刚开始看的时候,也觉得有点绕。后来我画了个图,一下就明白了。你看下面这个结构:
看到没?左边是工具栏,右边是内容区,底部是状态栏。内容区是一个 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的三大核心模块串起来,看一张完整的架构图:
你看,数据流是单向的:飞机通过MAVLink发数据上来,C++逻辑层解析、处理,然后通过信号通知QML界面更新。用户操作界面,QML调用C++方法,C++再通过MAVLink发指令给飞机。
这个模型,你搞清楚了,后面写插件就顺了。说白了,你的插件就是在这个模型里,插入你自己的逻辑和界面。
核心要点回顾:
- 主窗口: 左侧工具栏 + 右侧StackView内容区 + 底部状态栏。
- 混合编程: QML画界面,C++写逻辑,通过
setContextProperty和信号/槽通信。 - Vehicle: 飞机的数字孪生,所有状态和操作都通过它。
- MAVLink: 通信协议,QGC帮你封装好了,你只需要调用高级接口。
- Settings: 配置管理,用
SettingsGroup和SettingsFact,自动持久化。
嗯,这一章的内容就到这。架构这东西,光看文字可能有点干。我建议你打开QGC源码,对照着这一章的内容,找到我说的那几个关键文件,自己走一遍。相信我,看完源码再回来看这章,你会豁然开朗。