☰
QGroundControl 用户界面设计模式解析:QML 页面与 C++ Controller 的 MVC 改良变体
2026/10/15 3:18:31 网站建设 项目流程
  • 无人机
  • 智能硬件

【免费下载链接】qgroundcontrol

Cross-platform ground control station for drones (Android, iOS, Mac OS, Linux, Windows)

项目地址:https://gitcode.com/gh_mirrors/qg/qgroundcontrol
点击查看免费下载

QGroundControl(QGC)是一个跨平台地面站应用(支持 Android、iOS、macOS、Linux、Windows),其用户界面(UI)设计遵循一套独特而统一的模式:UI 页面全部由 QML 编写,并与用 C++ 编写的自定义 "Controller" 进行多次通信。这套模式是 MVC(Model-View-Controller)设计模式的一种经过改良的变体(文档原话为 "somewhat hacked variant of the MVC design pattern")。本文以官方开发者指南《用户界面设计》为核心骨架,结合仓库源码(src/QmlControls、src/PlanView、src/FactSystem等)深入讲解 QML 与 C++ 的三种绑定机制、Controller 的编写方式、配套控件与调色板体系,以及该模式背后的多设备设计取舍。读完本文,你将理解 QGC 中任何一张页面的"视图层 + 控制层"是如何组织与通信的,并能据此上手编写或维护自己的 QGC QML 页面。

QGC UI 的核心模式:QML 视图 + C++ Controller

在 QGC 中,一个 UI 页面通常由两部分构成:

  • 视图层(View):用 QML 编写的页面,负责界面布局、交互与视觉呈现;
  • 控制层(Controller):用 C++ 编写、通过 Qt 元对象系统暴露给 QML 的类,负责业务逻辑、数据访问与状态维护。

这两部分通过 QML 引擎的上下文属性和类型注册机制相连:Controller 以 QML 元素的形式(如PlanMasterController、FactPanelController)直接实例化在 QML 页面中,页面通过属性绑定(Property Binding)读取其Q_PROPERTY,通过信号(signal)接收状态变化,通过Q_INVOKABLE方法触发动作。这与传统 MVC 中"View 观察 Model、Controller 更新 Model"的严格分层不同——QGC 没有独立的 Model 类,Controller 直接暴露数据属性,因此文档将其称为"改良变体"。

从源码看,这种模式的"改良"体现在:

  • Controller 直接作为 QML 元素存在:例如FactPanelController在 src/FactSystem/FactControls/FactPanelController.h 中声明为QML_ELEMENT,并暴露Vehicle *vehicle属性以及getParameterFact()、parameterExists()等可调用方法;
  • Controller 负责组装子对象并注入 QML:例如 src/PlanView/PlanView.qml 中,页面通过planMasterController.missionController、planMasterController.geoFenceController、planMasterController.rallyPointController访问由 C++ 侧创建好的子控制器,再通过_missionController.visualItems把任务项模型暴露给地图与列表视图;
  • 一个 QML 页面可以挂载多个 Controller:如 src/PlanView/PlanTreeView.qml 中同一列表视图依据当前编辑层(任务/围栏/集结点)绑定不同的 Controller。

按照官方文档,QML 代码正是通过以下三种机制绑定到与系统相关的信息:

  1. 自定义 Controller(C++ 对象,页面专属的业务逻辑与数据);
  2. 全局QGroundControl对象(提供对活动 Vehicle、链路管理、设置系统等全局服务的访问);
  3. FactSystem(事实系统)(提供对参数的访问,某些情况下提供自定义 Facts)。

下文逐一深入。

机制一:自定义 Controller——QML 与 C++ 的通信桥梁

Controller 是 QGC 中"页面级"逻辑的载体。以 Plan 视图为例,src/PlanView/PlanView.qml 中有这样一段:

PlanMasterController { id: planMasterController flyView: false Component.onCompleted: { _planMasterController.start() _missionController.setCurrentPlanViewSeqNum(0, true) } ... }

PlanMasterController是一个 C++ 类,通过 Qt QML 类型系统注册为 QML 元素。它负责创建并管理任务控制器(MissionController)、地理围栏控制器(GeoFenceController)与集结点控制器(RallyPointController),并通过Q_PROPERTY把这些子对象暴露给 QML:

property var _missionController: _planMasterController.missionController property var _geoFenceController: _planMasterController.geoFenceController property var _rallyPointController: _planMasterController.rallyPointController

QML 页面可以直接调用 C++ 方法并接收返回值,例如 PlanView 中保存/上传前的校验:

function checkReadyForSaveUpload(save) { if (readyForSaveState() == VisualMissionItem.NotReadyForSaveData) { waitingOnIncompleteDataMessage(save) return false } else if (readyForSaveState() == VisualMissionItem.NotReadyForSaveTerrain) { waitingOnTerrainDataMessage(save) return false } return true }

这里的readyForSaveState()就是由 C++ Controller 提供的Q_INVOKABLE方法;VisualMissionItem.NotReadyForSaveData等枚举值也是从 C++ 侧(Q_ENUM)注册进 QML 的。

FactPanelController:面向"事实"的通用控制器基类

另一类典型的 Controller 是FactPanelController(src/FactSystem/FactControls/FactPanelController.h)。它被声明为QML_ELEMENT,是许多参数/设置面板 C++ 控制器的基类,核心能力包括:

  • vehicle属性:暴露当前关联的Vehicle对象;
  • getParameterFact(int componentId, const QString &name):按组件 ID 与参数名获取对应的Fact对象;
  • parameterExists():判断参数是否存在;
  • getMissingParameters():查询首次下载时尚未就绪、但现在应该可用的参数。

它配合 QML 侧的信号missingParametersAvailable使用,让页面在参数真正可用后再刷新 UI,避免"参数未就绪"竞态。可以看到,Controller 承担了"把 C++ 世界的数据安全地搬运到 QML 世界"的职责。

通信机制小结:Q_PROPERTY / Q_INVOKABLE / 信号槽

QGC 中 QML 与 C++ 通信遵循标准 Qt 元对象系统约定:

机制作用示例
Q_PROPERTY暴露只读/可写属性,支持 QML 属性绑定与NOTIFY信号联动刷新Fact.value、FactPanelController.vehicle
Q_INVOKABLE暴露可从 QML 调用的方法,可带参数并返回结果PlanMasterController.readyForSaveState()、Fact.validate()
signal /NOTIFYC++ 侧状态变化时通知 QML 更新绑定Fact.valueChanged、FactPanelController.missingParametersAvailable
Q_ENUM/QML_NAMED_ELEMENT把枚举与类注册进 QML 命名空间VisualMissionItem.NotReadyForSaveData、QGroundControl全局对象

机制二:全局 QGroundControl 对象——系统级服务的门面

在 QML 中,QGroundControl是一个全局单例对象,提供对整个应用系统级服务的访问。其 C++ 实现为QGroundControlQmlGlobal(src/QmlControls/QGroundControlQmlGlobal.h):

class QGroundControlQmlGlobal : public QObject { Q_OBJECT QML_NAMED_ELEMENT(QGroundControl) QML_SINGLETON ... };

QML_NAMED_ELEMENT(QGroundControl)意味着 QML 里直接以QGroundControl作为元素名访问;QML_SINGLETON表示它是全局单例,无需实例化即可直接引用。

该对象暴露的常用属性(均为CONSTANT或带NOTIFY)包括:

  • linkManager:链路管理器(TCP/UDP/串口等链接);
  • multiVehicleManager:多飞行器管理器,通过它可以访问当前活动 Vehicle;
  • settingsManager:设置管理器,按模块暴露各类appSettings、planViewSettings等;
  • corePlugin:核心插件实例(可被自定义构建替换);
  • globalPalette:全局 QGC 调色板对象(返回始终可用的颜色组);
  • unitsConversion:单位换算工具;
  • qgcVersion/qgcAppDate/qgcDailyBuild:版本与构建信息;
  • zOrderTopMost/zOrderWidgets/zOrderMapItems等:地图分层中不同元素的 Z 序常量;
  • flightMapPosition/flightMapZoom:飞行地图的中心点与缩放级别(可读写、带信号)。

它还提供一批Q_INVOKABLE方法,典型的有:

  • saveGlobalSetting(key, value)/loadGlobalSetting(key, defaultValue):读写全局设置;
  • showMessageDialog(owner, title, text, buttons, acceptFunction, closeFunction):显示消息对话框,支持回调函数;
  • startPX4MockLink(...)、startAPMArduCopterMockLink(...)等:启动各类模拟链路(Mock Link),用于开发与测试;
  • copyToClipboard(text):复制文本到系统剪贴板。

在真实页面中它的用法非常普遍。例如 src/PlanView/PlanView.qml:

property var _appSettings: QGroundControl.settingsManager.appSettings property var _planViewSettings: QGroundControl.settingsManager.planViewSettings ... onVisibleChanged: { if(visible) { editorMap.zoomLevel = QGroundControl.flightMapZoom editorMap.center = QGroundControl.flightMapPosition } }

以及通过QGroundControl.showMessageDialog(_root, qsTr("Unable to %1")...)弹出模态提示(见 src/PlanView/PlanView.qml)。调色板的使用也通过全局对象:QGroundControl.globalPalette.buttonHighlight、QGroundControl.globalPalette.buttonText(见 src/QmlControls/AltFrameDialog.qml)。

从源码结构可以推断:QGroundControl相当于"门面(Facade)"——把LinkManager、MultiVehicleManager、SettingsManager、VideoManager、ADSBVehicleManager、NTRIPManager等一堆子系统统一收口到单个 QML 可见对象上,避免每个页面各自创建或到处传递单例。

应用层单例:QGCApplication 与 qgcApp()

与 QML 侧的QGroundControl相对应,C++ 侧还有一个全局应用对象QGCApplication。它在 src/QGCApplication.h 中被定义为:

#define qApp (static_cast<QGCApplication*>(QGuiApplication::instance())) #define qgcApp() qApp

QGCApplication继承自QGuiApplication,提供reportMissingParameter()、showAppMessage()、msecsSinceBoot()、qmlAppEngine()等应用级能力。C++ 代码中凡需访问应用级状态(例如 src/FactSystem/Fact.cc 中if (qgcApp())的判空用法),都会经由qgcApp()获取。

机制三:FactSystem——参数与自定义事实的数据层

文档明确指出 QML 绑定信息的第三个来源是FactSystem,它提供对参数的访问,在某些情况下提供自定义 Facts。这一体系的完整介绍见 docs/zh/qgc-dev-guide/fact_system.md,其核心概念如下。

Fact:系统中的单个值

Fact是"系统中单个值"的封装(src/FactSystem/Fact.h)。它被声明为QML_ELEMENT,意味着可以直接在 QML 中使用。每个 Fact 暴露大量Q_PROPERTY:

  • 值相关:value(可读写,带NOTIFY valueChanged)、rawValue、valueString、enumOrValueString、valueEqualsDefault;
  • 元数据相关:name、label、units、shortDescription、longDescription、category、group;
  • 取值范围:min、max、userMin、userMax、increment、decimalPlaces;
  • 枚举相关:enumStrings、enumValues、enumIndex;
  • 行为标记:readOnly、writeOnly、volatileValue、vehicleRebootRequired、qgcRebootRequired。

同时提供validate(cookedValue, convertOnly)、clamp(cookedValue)、rawToCooked(rawValue)等Q_INVOKABLE方法,用于在 UI 层做输入校验、限幅与单位换算。这正是"Fact 驱动自动 UI 生成"的基础——控件只需绑定 Fact,取值范围、单位、枚举选项、输入校验都由 Fact 与 FactMetaData 自动给出。

FactMetaData:驱动自动 UI 的元数据

与每个 Fact 关联的FactMetaData提供关于 Fact 的详细信息(类型、范围、单位、枚举、默认值、描述等),用于驱动用户界面自动生成和验证。元数据既可以由代码构造,也可以由 JSON 文件提供(如src/Settings、src/MissionManager目录下大量*.FactMetaData.json/*.SettingsGroup.json文件)。

FactGroup 与 Fact Controls

  • FactGroup:一组 Fact 的集合,用于组织事实并管理用户自定义事实。飞行器相关事实(含固件插件factGroups函数返回的组内事实)可通过getFact("factName")或getFact("factGroupName.factName")获取;
  • Fact Controls(事实控件):QML UI 控件,连接到 Fact 及其 FactMetaData,为用户提供修改/显示 Fact 值的界面,例如FactSlider、FactTextField、FactComboBox等(见 src/FactSystem/FactControls 目录)。

自定义构建支持

用户定义的事实可以通过覆盖FirmwarePlugin的factGroups函数添加到自定义固件插件类中(返回 FactGroup 名称映射),通过扩展FactGroup类并以其构造函数传入包含必要信息的 JSON 文件来定义事实元数据;还可以通过覆盖FirmwarePlugin的adata(原文如此,应为 metadata 相关函数)更改现有事实的元数据。这一机制让自定义构建(custom build)能够在不改动 QML 的前提下扩充 UI 可展示的参数。

为什么 QGC 的 QML 无法使用 Qt QML Designer

官方文档特别提醒:由于 QGC 使用的 QML 的复杂性,以及它依赖与 C++ 对象通信来驱动 UI,无法使用 Qt 提供的 QML Designer 来编辑 QML。这与前述三种绑定机制直接相关:

  • 大量属性来自 C++ 单例与 Controller(如QGroundControl.globalPalette.buttonHighlight、controller.visualItems),在设计器中这些对象要么不存在、要么处于未初始化状态;
  • 页面启动即依赖 C++ 侧完成的初始化(如PlanMasterController.start()),脱离运行环境的静态编辑无法正确呈现;
  • 动态重排(多设备自适应布局)依赖运行时屏幕尺寸与ScreenTools,设计器的固定画布无法模拟。

因此 QGC 开发者实际都是直接手写 QML 源码,配合 QGC 自身的单元测试与 QML UI 测试(见 test/QmlUITests 目录)来验证界面行为。

支撑这套模式的配套体系

围绕上述"QML + Controller"核心模式,QGC 还提供了一套统一的配套体系,让所有页面共享一致的外观与交互。

可复用控件集(QGroundControl.Controls)

QGC 提供一组基础控件用于构建界面(详见 docs/zh/qgc-dev-guide/user_interface_design/controls.md)。它们一般是 Qt 原生 QML 控件之上的薄封装,用 QGC 调色板绘制。使用时导入:

import QGroundControl.Controls

标准 Qt QML 控件的 QGC 变体(功能与对应 Qt 控件相同,仅绘制上使用 QGC 调色板):

  • QGCButton
  • QGCCheckBox
  • QGCColoredImage
  • QGCComboBox
  • QGCFlickable
  • QGCLabel
  • QGCMovableItem
  • QGCRadioButton
  • QGCSlider
  • QGCTextField

QGC 独有控件(用于创建标准 UI 元素):

  • DropButton——圆形按钮,点击后展开一组选项面板;典型例子是 Plan 视图中的同步(Sync)按钮;
  • ExclusiveGroupItem——支持 QMLExclusiveGroup概念的自定义控件的基项;
  • QGCView——系统中所有顶级视图的基本控件,提供对 FactPanel 的支持,并负责显示QGCViewDialog与QGCViewMessage;
  • QGCViewDialog——从 QGCView 右侧弹出的对话框,可指定接受/拒绝按钮及对话框内容;典型例子是点击某个参数后弹出的值编辑器对话框;
  • QGCViewMessage——QGCViewDialog的简化版本,允许指定按钮与简单文本消息;
  • QGCViewPanel——QGCView内部的主要视图内容;
  • RoundButton——以图片作为内部内容的圆形按钮控件;
  • SetupPage——所有"安装载具组件"页面(Setup 视图)的基本控件,提供标题、说明与组件页面内容区;
  • SelectableControl——允许用户从一组控件中选择要显示的内容;桌面端右键、移动端长按显示选择 UI;典型用法是让用户在多种仪表显示变体之间切换。

这些控件的实现集中在 src/QmlControls 目录(如QGCButton.qml、QGCCheckBox.qml、DropButton.qml、ExclusiveGroupItem.qml、SetupPage.qml、SelectableControl.qml等),它们共同构成"一次编写、处处复用"的 UI 积木。

字体与调色板(Font and Palette)

QGC 拥有一套标准字体与调色板,所有用户界面都应遵守(详见 docs/zh/qgc-dev-guide/user_interface_design/font_palette.md):

  • QGCPalette(QGC 调色板):提供浅色(light)与深色(dark)两种变体。浅色适合户外使用,深色适合室内。正常情况下绝不应直接为 UI 指定颜色,而应始终使用调色板中的颜色;否则你创建的界面将无法随浅色/深色样式切换。QML 中通过QGroundControl.globalPalette获取全局调色板(如 src/QmlControls/AltFrameDialog.qml 中的buttonHighlight、buttonText),或实例化QGCPalette { id: qgcPal; colorGroupEnabled: true }按控件局部使用(如 src/QmlControls/AppSettings.qml);
  • QGCMapPalette(QGC 地图调色板):用于绘制在地图之上的颜色。由于地图样式不同(尤其是卫星图与街道图),需要不同的颜色才能清晰绘制:卫星图需要更浅的颜色,街道图需要更深的颜色。QGCMapPalette为此提供一组颜色,并支持在地图上切换浅色/深色;
  • ScreenTools(屏幕工具):提供可用于指定字体大小的值,同时提供屏幕尺寸信息以及 QGC 是否运行在移动设备上的信息。典型用法如 src/PlanView/PlanView.qml 中ScreenTools.defaultFontPixelHeight、ScreenTools.defaultFontPixelWidth参与计算边距与面板宽度。

多设备设计模式:一个代码库适配所有屏幕

QGC 设计用于从桌面、笔记本、平板到小型手机屏幕等多种设备(详见 docs/zh/qgc-dev-guide/user_interface_design/multi_device_pattern.md)。其核心思路是用单一 QML 代码库 + 布局重排适配不同外形,而不是为每种屏幕尺寸单独编码。该模式建立在三个支柱之上:

  1. Qt 布局控件(Layout):QGC 没有针对不同屏幕尺寸的分离 UI 代码,通常依靠 QML 的 Layout 能力重排同一组 UI 代码;某些情况下在小屏幕上隐藏次要细节(简单的可见性切换模式);
  2. FactSystem:系统内部统一管理所有单个数据,这个数据模型连接到控件,控件自动获得数据驱动的 UI;
  3. 严重依赖可重用控件:QGC UI 由可复用控件与 UI 元素构建,任何添加到可复用控件的新功能立即可用于整个 UI;这些可复用控件连接 FactSystem 的 Facts,自动提供合适的 UI。

设计取舍的背景:QGC 以"约 1 人开发团队"的高效维护为前提,严格遵循该模式,否则会导致开发变慢、质量下降。这一决策使 QGC 能凭单一代码库发布到多个操作系统与多种外形设备。其优先级顺序是:平板 > 笔记本 > 桌面 > 手机(任何小屏幕)。手机大小的屏幕当前优先级最低,Fly 视图等主动飞行显示投入更多精力,而 Setup、Plan 等视图在小屏幕上"功能可用但体验可能较差"。

该模式也有明确代价(原文列出的缺点):

  • UI 最终是桌面/笔记本/平板/手机的混合风格,未必对其中任何一种做到"优化感";
  • 离优先级目标越远,混合方法的体验越差,小屏手机在可用性上受影响最大;
  • 某些场景下可复用控件集未必提供绝对最佳 UI,但为了不新增维护面仍坚持复用;
  • 所有操作系统使用同一套 UI 代码,QGC 不遵循各操作系统自身的 UI 设计准则,有自己的混合视觉风格;例如 Android 上的 QGC 不一定像安卓应用,iPhone 上的 QGC 也不会像大多数 iPhone 应用那样工作——但该风格对各类系统用户都是可理解的。

优点(原文所列):由于使用混合模型与控件集一次完成 UI 编码,设计新功能耗时更短(布局重排在 Qt QML 中非常强大);UI 功能代码跨外形一致,因此可仅在一个平台上做功能测试,布局流通过移动模拟器做视觉检查即可(常见做法:桌面构建调整窗口大小测试重排,通常也覆盖平板尺寸;手机小屏用模拟器直观验证)。

结语:一条贯穿 QGC 开发的主线

从 docs/zh/qgc-dev-guide/user_interface_design/index.md 出发可以看到,QGC 用户界面开发的主线非常清晰:QML 只负责"怎么呈现",C++ Controller 负责"数据与逻辑",二者通过属性绑定、Q_INVOKABLE方法与信号槽通信;QGroundControl全局单例收口所有系统级服务,FactSystem 提供统一的数据模型与自动 UI 能力;可复用控件、调色板与多设备布局规则保证所有页面风格一致、一套代码跑遍所有平台。无论你要为 QGC 新增一个设置页、任务项还是自定义构建的专属界面,遵循"QML 视图 + Controller + 全局对象 + FactSystem"这条模式主线,再配合控件集与调色板约定,就能写出与官方页面一致、可维护、可跨设备的 UI 代码。

  • 无人机
  • 智能硬件

【免费下载链接】qgroundcontrol

Cross-platform ground control station for drones (Android, iOS, Mac OS, Linux, Windows)

项目地址:https://gitcode.com/gh_mirrors/qg/qgroundcontrol
点击查看免费下载
上一篇:3分钟搞定!Deepin Boot Maker:零门槛U盘启动盘制作神器
下一篇:Milkdown 插件系列:@milkdown/plugin-diff 差异审阅插件深度解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询