ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

QGC二次开发:从自定义面板到MAVLink消息

QGC二次开发:从自定义面板到MAVLink消息

简介:这是一份面向初学者的QGC(QGroundControl)二次开发入门项目代码,适合具备少量C++与QT基础、希望掌握无人机地面站定制与功能扩展的开发者。压缩包内共3个文件,包含inscode项目配置、html说明页面及gitignore文件,整体仅5KB,虽体积小巧但聚焦核心源码与配置。教程内容覆盖软件名称与图标修改、官方中文版BUG修复与汉化、商标更换、信号与槽添加、QML与C++交互、MAVLINK解析与发送、地图更换及自定义MAVLINK消息等关键环节,同时涉及多机轨迹显示与地图画点功能。通过这份资料,读者可以了解QGC二次开发的整体流程,获得可直接运行的修改后QGC源码、开发环境配置虚拟机以及独家汉化版QGC的下载指引,特别适合用于快速验证开发思路或作为项目起步参考。目前已有432人浏览学习,也印证了其对入门者有切实的参考价值。

1. QGC二次开发到底在改什么,值不值得动这个手

手里那台自研载荷,通过串口把数据喂给了飞控,链路里数据明明是通的,可打开地面站,界面上只有电池、卫星数和飞行模式这些通用仪表,属于我们自己的传感器读数一个都看不到。QGC二次开发,解决的就是这一类需求:在开源的 QGC 地面站基础上,往界面里加自己的面板,往通信里加自己的协议,让地面站真正贴合你手上的设备,而不是套一个通用模板。它适合懂一点 MAVLink、想掌握全链路的开发者,也适合要交付定制地面站的团队。这篇文章会从工程结构、编译跑通、挂自定义面板、排查踩坑到扩展自定义消息,把一次完整二开的路子走通。真心建议先花半天跑通一个能编译的 QGC,后面所有的改动都建立在它之上,别在半成品上反复抠。

2. 读懂QGC工程布局:二开最该先认准的几块源码

2.1 Qt与QML混合架构:为什么这个结构对二次开发格外友好

QGC 是一个典型的 Qt 应用,界面层大量使用 QML,底层逻辑用 C++ 实现。这个混合架构对二次开发的价值在于:改界面的时候你不需要跟重型 C++ 编译流程死磕,QML 文件写错了通常只是界面显示异常,不会让整个程序直接崩溃;而消息收发、设备状态管理这些核心逻辑放在 C++ 侧,性能和可靠性有保障。两层之间通过注册机制和模型/视图方式连接,界面层拿到的是 C++ 暴露出来的属性,不会直接把底层数据铺到 QML 里。

一开始扎进这个工程,很多人会觉得自己在黑匣子里转圈,这很正常。你先记住一条主线:界面找 QML,逻辑找 C++,两者通过“注册 QML 类型”和“属性绑定”沟通。入门阶段,你的大部分改动只需要落在 QML 层,C++ 层能看懂信号槽和属性注册就够了。我见过不少开发者一上来就啃 Vehicle 的通信代码,结果界面还没跑通就先被消息流劝退,这个顺序是反的。

2.2 四个高频修改区:UI、Vehicle、MAVLink 与 Utilities

源码目录很多,但二开真正高频进出的是四个区域。第一块是 ui 目录以及资源文件里的 QML,地面站的工具栏、飞行仪表、参数设置界面全在这,你想加按钮、改布局、塞新面板,主要跟这里打交道。第二块是 src/Vehicle,它差不多是飞行器在软件里的投影,所有 MAVLink 消息进来之后的第一站就是这里,收发消息相关的行为改动都集中在它身上。

第三块是 MAVLink 协议库,它负责消息的编解码。如果你只是调用现有协议,可以不碰它,可一旦要自定义消息,就必须从这里下手,后面第 6 章会专门展开。第四块是 Utilities 目录,日志、坐标转换、格式化这类通用能力都在这。我通常会在二开第一天就把日志工具摸清楚,因为排查消息丢失、面板不显示这类问题时,日志是第一个要开的门。这四个区域的边界并不绝对,但按这个地图去找代码,比漫无目的地全库搜“voltage”省时间得多。

2.3 启动链路:从 main 函数到你的第一个 QML 窗口

弄明白程序怎么启动,能帮你判断自己的改动该放在哪一步。QGC 的启动流程去掉平台分支后可以简化为下面这个样子:

// QGC 启动流程精简示意(省略了平台初始化和单例清理) int main(int argc, char *argv[]) { QGCApplication* app = new QGCApplication(argc, argv); // toolBox 负责集中创建各类 Manager, // 自定义 QML 类型的注册也放在这个阶段完成 app->toolBox()->load(); // 主窗口本体是 QML,C++ 只负责窗口外壳和生命周期 app->showMainWindow(); return app->exec(); }

这段代码说明了两个关键点。一是 toolBox 是全局管理器的集中注册点,你要注册一个新的 C++ 服务或一个新的 QML 类型,通常要在它完成加载之前准备好;二是主窗口本质是一个 QML 界面,C++ 只提供窗口壳子。找到那个被资源文件引用的主窗口 QML,你就找到了几乎所有界面的挂载点。后续往里加自定义面板,本质上都是在这个 QML 组件树上找一个合适的位置插进去。

3. 从拉源码到首跑:把QGC编译环境的全过程走一遍

3.1 环境准备:两条主流路线的版本对齐建议

二开最忌讳的就是环境没对齐就开跑,结果编译报错全跟版本有关。Ubuntu 和 Windows 是两条主流路线。Ubuntu 下我习惯用 Qt 官方安装器装完整开发套件,再配 GCC 和必要的构建工具;Windows 下常见的组合是 MSVC 编译器加 Qt 对应位的开发库,也可以用 MinGW 组合,但迭代时注意别混用。无论哪条路线,位数必须和编译目标一致,64 位环境配 64 位 Qt,这个不齐,后面链接阶段全是怪错。

QGC 对 Qt 版本有明确要求,以官方仓库说明为准,我个人会选 Qt 5 的稳定分支并锁住版本,因为二开团队一般不希望地面站的界面框架频繁迁移。安装完成后,记得确认 qmake 命令指向的是你刚装的那套 Qt,而不是系统自带的旧版本,这一步用qmake -v验证就行。很多人的编译翻车,根子就出在 PATH 里混进了两个 Qt 版本。

3.2 拉取源码、同步子模块与编译命令

源码一定要带着子模块一起拉。QGC 依赖 MAVLink 库等子模块,只克隆主仓不拉子模块,后续生成代码时必报错。推荐的做法是第一次克隆就直接带子模块:

# 从官方仓库或团队内部分发渠道获取源码,务必携带子模块 git clone --recurse-submodules <你的QGC源码仓库地址> cd qgroundcontrol # 如果克隆时没带子模块,执行下面这条补拉 git submodule update --init --recursive # 使用传统 qmake 方式构建 mkdir -p build && cd build qmake ../qgroundcontrol.pro make -j4

参数说明:--recurse-submodules让 git 在克隆主仓时一并拉取所有子模块,省去后续手动同步;qmake ../qgroundcontrol.pro是让 Qt 的构建系统解析工程文件并生成 Makefile;make -j4表示用 4 个并行任务编译,机器内存不够时可以降到-j2。第一次编译会消耗比较久的时间,源码体积大、QML 资源多,半小时到一小时都是正常的,看到滚动刷屏不要慌。

3.3 首次编译的三个高频报错与对策

第一个高频报错是“缺少 Qt 头文件”,现象是 make 打印 fatal error 然后找不到某个 QtXXX 头文件。原因基本是装了精简版 Qt 或者没装对应开发模块,解决方法是回到安装器补全开发套件,而不是手动下载头文件硬塞。第二个是子模块相关报错,比如找不到 mavlink 头文件,这通常是克隆时漏了子模块,回到仓库根目录执行一次git submodule update --init --recursive就能解决。

第三个比较隐蔽,发生在 MAVLink 头文件生成环节,报错指向 Python 环境。QGC 构建过程会用 Python 脚本预先生成部分协议代码,系统缺 Python 或缺相关库就会失败。解决方法是补装 Python 3 并安装依赖库,然后在命令行里确认python3 --version可执行。每次遇到这类报错,先把完整报错信息读一遍再搜,很多新人栽在只看最后一行,实际根因在中间几行。编译这东西没有太多玄学,报错信息基本都指到了该改的位置。

4. 第一个实战:把一个自定义状态面板挂进QGC主界面

4.1 选挂载点:主界面布局与自定义组件的插入位置

主界面是一个由多个 QML 组件拼起来的布局树,侧边栏、工具栏、仪表区各有各的容器。二开最常见的两种做法:一种是在侧边栏区域预留一个容器,把自定义面板放进去,好处是常驻可见,坏处是改动原有布局结构,出错时影响面大;另一种是注册一个新的工具按钮,点击后以对话框或抽屉方式弹出面板,好处是不碰原有界面,出问题时只删掉自己加的东西就恢复原样。

对第一次做二开的人来说,我强烈建议走第二种。先做一个能独立弹出的小面板,验证“QML 能加载、数据能绑定”,再考虑常驻布局。这个顺序能帮你把“我的代码有问题”和“主布局被破坏了”这两个问题分开排查。挂载点确定之后,下一步就是真正把面板画出来,这不需要改动 C++ 侧,第一步可以先做纯静态版本。

4.2 用QML把状态面板画出来

下面这个面板示例用来展示自定义载荷的在线状态和电池电压,纯 QML 实现,不依赖复杂控件,放在哪个 QGC 版本上都能跑:

// PayloadStatusPanel.qml // 自定义载荷状态面板:显示在线状态与电池电压 import QtQuick 2.15 import QtQuick.Controls 2.15 Item { id: payloadPanel width: 220 height: 140 // 对外暴露属性,由父级组件或 C++ 侧赋值绑定 property real batteryVolt: 0.0 property bool payloadOk: false Rectangle { anchors.fill: parent radius: 6 color: payloadOk ? "#f4f7f4" : "#fdecea" border.color: "#bbbbbb" } Column { anchors { left: parent.left; top: parent.top; margins: 10 } spacing: 6 Text { text: "载荷状态" font.pixelSize: 16 font.bold: true } Text { text: "电压 " + batteryVolt.toFixed(1) + " V" } Text { text: "链路 " + (payloadOk ? "正常" : "异常") } } }

逻辑说明:面板本身是一个 Item,尺寸固定为 220 乘 140;背景 Rectangle 的颜色会根据payloadOk切换,在线是浅灰绿,离线是浅红,用于快速目检;Column 里放了三个 Text,分别显示标题、电压和链路状态。batteryVolt和payloadOk是暴露出来的属性,父级组件或 C++ 逻辑可以直接赋值,面板自身不关心数据来源。这个设计符合 QGC 界面层只做展示、数据由上层注入的习惯。

4.3 从C++把实时数据接进来

QML 面板本身不会产生数据,真正的数据来源是飞控或载荷设备。二开里常见的做法是写一个 C++ 管理类,持有实时状态,通过信号槽把数据推给 QML。下面这个 PayloadStateManager 是一个最小可用的状态容器:

// PayloadStateManager.h #pragma once #include <QObject> class PayloadStateManager : public QObject { Q_OBJECT public: explicit PayloadStateManager(QObject* parent = nullptr) : QObject(parent), _batteryVolt(0.0), _payloadOk(false) { } Q_INVOKABLE double batteryVolt() const { return _batteryVolt; } Q_INVOKABLE bool payloadOk() const { return _payloadOk; } public slots: void updateState(double volt, bool ok) { _batteryVolt = volt; _payloadOk = ok; emit stateChanged(); } signals: void stateChanged(); private: double _batteryVolt; bool _payloadOk; };

这个类暴露了两个只读接口给 QML,更新数据走updateState这个槽。要让 QML 能识别它,需要在启动阶段注册类型:

// main.cpp 或 toolBox 初始化阶段 qmlRegisterType<PayloadStateManager>("QGroundControl", 1, 0, "PayloadStateManager");

注册之后,在 QML 里就能用PayloadStateManager这个类型创建实例,把它的属性绑定到面板上。实践中的常见做法是:收到 MAVLink 消息后,在接收处理分支里调用updateState,QML 面板立即刷新。这里的关键在于 C++ 只管更新状态,UI 只做展示,中间靠信号stateChanged触发刷新,这是 Qt/QML 混合开发的经典套路。

5. 二次开发避坑指南:最容易翻车的五个位置

5.1 QML改了没反应:资源缓存和路径不一致

现象:修改了一个 QML 文件,重新编译启动,界面还是老样子,仿佛改了个寂寞。原因通常是两个,一是 QML 文件被打到了 Qt 的资源系统里,增量编译时资源可能没有重新打包;二是改错了文件,编辑器里打开的是工作副本,而工程资源加载的是另一个路径下的同名文件。解决方式:先搜索确认你改的文件确实在加载路径里,然后清空 build 目录重新 qmake 再编译。别第一时间怀疑缓存玄学,先排除路径问题,这一条我说过太多次,几乎占了界面改动问题的六成。

5.2 自定义面板不显示:先怀疑尺寸和挂载点

现象:代码没有报错,运行起来界面却找不到自己加的面板。原因排下来,最常见的是宽高为 0,尤其当面板被塞进 Column 或 Row 时,如果没有设置Layout.fillWidth或明确宽度,组件可能被压缩成不可见的一根线;其次是挂载点本身不在当前显示的页面中,比如放进了某个默认不打开的 Tab 页里。解决办法是先给面板一个固定宽高和一条高亮边框背景,让它强制可见,再逐步调整挂载位置。这个排错方法能帮你快速把“面板代码的问题”和“布局容器的问题”分开。

5.3 自定义MAVLink消息收不到:dialect没对上

现象:飞控端明明在发自定义消息,地面站里绑定的槽函数一直不触发。原因九成出在协议方言不一致上:QGC 编译时用的是某一组 MAVLink 方言生成的头文件,你的自定义消息 ID 不在其中,消息进入分发模块后没有对应的处理分支,直接被丢弃。解决步骤是先抓包确认消息确实到达了地面站端口,这一步我习惯看 UDP 14550 端口上的报文;再检查生成头文件时用的 XML 里是否包含了你的消息定义;最后在接收分发处补上处理 case。顺序一定不能反,先链路后代码,否则容易白改一通。

5.4 中文显示乱码:Windows下的编码问题

现象:界面或代码注释里的中文变成乱码和方块,Linux 上少见,Windows 上经常出现。原因是 MSVC 编译器对源文件编码的处理和 UTF-8 不一致,Qt 5 的字符串又默认按 UTF-8 解析,两边一冲突就乱。解决办法是给 MSVC 编译选项加上/utf-8,同时把 QML 文件统一保存为 UTF-8 格式,字符串内容习惯性套上tr()或QStringLiteral。这个坑不难绕,但第一次踩到会怀疑是自己代码写错,实际上就是编码约定问题,属于二开团队的默认必修课。

5.5 版本漂移:升级后老代码编译不过

现象:QGC 源码从旧版本切到新版本,之前的二开代码报出一堆类型错误和找不到成员。原因很直接,QGC 本身迭代很快,C++ 类名、QML 模块路径、接口签名都可能变化,二次开发代码还没来得及同步就是这副样子。解决方式分两层:团队层面,建立一个二开分支并锁定基线版本,不随便追官方 release;个人层面,每次改动记录清单,升级前先跑一遍全量编译,看哪些是纯 API 改名、哪些是结构性变化。这个坑我栽过不止一次,后来养成了“版本锁死 + 变更清单”的习惯,二开工程量越大,这两个习惯越值钱。

6. 进阶:让自定义MAVLink消息从定义到上机验证走通

走到这一步,你已经能在 QGC 里加自己的界面了。真正的二开项目通常还需要自定义消息,把载荷数据从飞控链路里捞出来。常见做法是新增一个自定义 dialect XML,在里面描述消息格式,再通过 MAVLink 工具链重新生成 C 头文件,让 QGC 的协议层认识这条消息。

<!-- 自定义消息示例,仅演示结构,ID 取自定义区间 --> <message id="42000" name="PAYLOAD_STATE"> <description>自定义载荷状态上报</description> <field type="uint8_t" name="state">0=离线 1=在线</field> <field type="float" name="battery_volt">载荷电池电压</field> </message>

生成头文件后,把新的协议文件放回 MAVLink 库目录,重新编译工程,然后在消息分发处为PAYLOAD_STATE增加一个处理分支,调用你自定义状态管理类的updateState接口。验证时先开模拟器或直接连飞控,用抓包工具确认报文抵达,再看程序日志里是否打印了解析结果。整个链路里最容易出错的是生成头文件的 XML 和你实际发送的字节结构不一致,代码没问题,数据也对不上。

二开做到这个深度,真正考验你的已经不是某个具体类怎么调,而是能不能把“链路、协议、界面、状态管理”这几层串起来排查问题。我自己的习惯是每改一步都保留一个能回滚的提交点,尤其涉及自定义消息时,前后端都留好版本对应关系,否则线上跑半个小时后客户端闪退,连回滚都找不到后悔药吃。希望这篇教程能帮你少走几段弯路,也把你从只会改界面的入门段位往上推一截。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进