3、QGC核心类:QGCApplication、MainWindow、QmlEngine、SettingsManager、LinkManager

QGC 的架构,说白了就是几个核心类撑起来的。你想想看,一个地面站软件要处理的事情太多了——启动、界面、设置、通信链路……如果全写在一个类里,那代码就彻底没法维护了。

我个人习惯,拿到一个开源项目,第一件事就是看它的核心类结构。QGC 在这方面做得挺清晰,五个类各司其职。今天我们就一个一个拆开来看。

3.1 QGCApplication:整个应用的“大脑”

QGCApplication 继承自 QApplication,是整个 QGC 的入口。它负责应用的初始化、生命周期管理,以及一些全局状态的控制。

我记得第一次看 QGC 的 main.cpp,发现它并没有直接创建 QApplication,而是创建了 QGCApplication。这个设计其实很巧妙——把启动逻辑封装在子类里,main 函数就变得非常干净。

核心职责:

  • 应用初始化(日志、翻译、命令行参数)
  • 全局单例管理(通过 _app 静态指针)
  • 应用退出流程控制
  • 跨平台适配(移动端 vs 桌面端)

来看一段典型的初始化代码:

// main.cpp 中的典型用法
QGCApplication* app = new QGCApplication(argc, argv);
if (!app->_initForNormalAppStart()) {
    return -1;
}
return app->exec();

这里有个坑,我曾经踩过。如果你在插件或自定义模块里需要访问 QGC 的全局状态,千万别直接 new QGCApplication,要用 qgcApp() 这个宏。它内部做了空指针检查,安全得多。

避坑指南:我曾经在自定义飞行日志模块里直接引用了 QGCApplication::_app,结果在单元测试环境下 _app 是 nullptr,直接崩溃。后来全部改用 qgcApp() 宏,问题解决。

3.2 MainWindow:界面的大管家

MainWindow 是 QGC 的主窗口,但它不是传统意义上的“窗口”。它更像一个容器,负责管理各种 QML 页面的切换和布局。

你想想看,QGC 的界面从“飞行视图”切换到“规划视图”,再到“设置视图”,这些切换逻辑全在 MainWindow 里。它内部维护了一个 QStackedWidget,每个页面就是一个独立的 QML 组件。

视图名称 QML 文件 MainWindow 中的索引
飞行视图 FlyView.qml 0
规划视图 PlanView.qml 1
设置视图 SettingsView.qml 2
分析视图 AnalyzeView.qml 3

我个人建议,如果你要做二次开发,比如加一个自定义页面,不要直接改 MainWindow 的 C++ 代码。更好的做法是在 QML 层通过 mainWindow.pushPage() 来动态添加。这样既保持了 C++ 层的稳定,又方便后续升级。

3.3 QmlEngine:C++ 和 QML 的桥梁

QmlEngine 是 QGC 中 QQmlApplicationEngine 的封装。它的核心任务就一个:把 C++ 对象暴露给 QML 使用。

说白了,QML 里那些 QGroundControl、controller、globals 对象,都是通过 QmlEngine 注册进去的。没有它,QML 就只是个画界面的壳,啥也干不了。

技巧:如果你想在 QML 里访问自己的 C++ 类,可以在 QGCApplication::_initForNormalAppStart() 里找到 qmlRegisterType<YourClass>() 的调用位置,照猫画虎加一行就行。

来看一个典型的注册代码:

// 在 QGCApplication 初始化中
qmlRegisterUncreatableType<Vehicle>("QGroundControl", 1, 0, "Vehicle", "Reference only");
qmlRegisterType<QGCQGeoCoordinate>("QGroundControl", 1, 0, "QGCQGeoCoordinate");

嗯,这里要注意,Vehicle 用了 UncreatableType,意思是 QML 里不能直接 new 一个 Vehicle 对象,只能通过 C++ 传引用。这种设计是为了防止 QML 层误操作创建出无效对象。

3.4 SettingsManager:配置的“保险箱”

SettingsManager 管理 QGC 的所有设置项。它把设置分成了几个大类:

  • AppSettings:应用级设置(语言、单位、UI 主题)
  • FlyViewSettings:飞行视图相关(地图类型、仪表盘布局)
  • PlanViewSettings:规划视图相关(默认航点高度、安全距离)
  • AutoConnectSettings:自动连接设置(串口、UDP、TCP)

每个设置项都是一个 QmlObjectListModel 或者 Fact 对象。我刚开始接触时觉得这层封装有点重,后来才发现它的好处——所有设置都支持 QML 双向绑定,改个值界面自动刷新。

二次开发常用操作:

  • 读取设置:QGCApplication::instance()->settingsManager()->appSettings()->language()->rawValue()
  • 写入设置:settingsManager->flyViewSettings()->defaultMissionAltitude()->setRawValue(100.0)
  • 监听变化:连接 Fact::valueChanged 信号

我曾经在做一个多语言切换功能时,直接修改了 AppSettings 里的 language 值,结果发现界面没有刷新。后来才知道,改完设置后需要调用 QGCApplication::instance()->loadLanguage() 才能生效。这个细节文档里没写,我是翻源码才找到的。

3.5 LinkManager:通信链路的“调度中心”

LinkManager 负责管理所有与无人机之间的通信链路。它支持多种链路类型:

链路类型 类名 典型场景
串口 SerialLink 数传电台、USB 直连
UDP UDPLink WiFi 连接、模拟器
TCP TCPLink 远程连接、桥接
蓝牙 BluetoothLink 移动端连接

LinkManager 的核心逻辑其实不复杂:维护一个 QList<SharedLinkInterfacePtr>,提供 createConnectedLink() 和 disconnectAll() 接口。但它的难点在于——链路状态变化时,要通知所有订阅者。

举个例子,当串口断开时,LinkManager 会发出 linkDisconnected() 信号,然后 VehicleManager 收到信号后会把对应的 Vehicle 对象标记为离线,接着 QML 界面上的连接状态图标就会变红。这一连串的事件传递,全靠 LinkManager 的信号机制驱动。

避坑指南:我曾经在自定义链路插件里直接调用了 LinkManager::addLink(),但没有设置正确的 LinkConfiguration,结果链路虽然添加成功了,但一直连不上。后来发现 LinkConfiguration 里的 setDynamic() 方法必须调用,否则链路管理器不会自动重连。

3.6 五个类的关系图谱

这五个类不是孤立的,它们之间有明确的依赖关系。我画了一张图,帮你理清它们之间的协作逻辑:

QGC 核心类协作关系图 QGCApplication MainWindow QmlEngine SettingsManager LinkManager 创建并管理 初始化 创建并管理 创建并管理 加载 QML 暴露给 QML 信号通知 💡 协作逻辑说明 QGCApplication 是顶层入口,负责创建其他四个核心类。 MainWindow 和 QmlEngine 配合完成界面渲染,SettingsManager 提供配置数据,LinkManager 提供通信链路状态。

从这张图可以看得很清楚:QGCApplication 是顶层入口,它创建并管理其他四个类。MainWindow 和 QmlEngine 配合完成界面渲染,SettingsManager 提供配置数据,LinkManager 提供通信链路状态。它们之间通过信号-槽机制解耦,这也是 QGC 架构设计的一个亮点。

3.7 二次开发中的常见操作模式

基于这五个核心类,二次开发通常有几种固定的操作模式。我总结了一下:

开发场景 涉及的核心类 典型操作
添加自定义设置项 SettingsManager 继承 SettingsGroup,注册 Fact
新增通信协议 LinkManager 实现 LinkInterface,注册到工厂
自定义主界面 MainWindow + QmlEngine 添加 QML 页面,注册 C++ 上下文
全局状态监控 QGCApplication 监听 applicationStateChanged 信号

嗯,这里要特别提醒一下。很多新手在做二次开发时,喜欢直接修改核心类的源码。我个人建议,除非万不得已,否则尽量通过继承和信号-槽机制来扩展功能。这样当 QGC 官方版本升级时,你的代码冲突会少很多。

举个例子,如果你想在链路断开时弹个提示框,不要改 LinkManager 的源码。正确的做法是:在你的插件里连接 LinkManager::linkDisconnected 信号,然后在槽函数里弹出提示。这样既实现了功能,又保持了核心类的纯净。

技巧:在 QGC 源码里搜索 qgcApp()-> 和 linkManager()->,你会发现大量现成的调用模式。这些就是最好的学习素材,比看文档管用多了。

好了,这五个核心类就讲到这里。它们就像 QGC 的“五脏六腑”,搞清楚了它们各自负责什么、怎么协作,后面的二次开发就会顺手很多。