Serial Studio Widget 扩展开发完全指南:包结构、清单参考与 QML 数据模型
2026/9/18 18:05:10 网站建设 项目流程

Serial Studio Widget 扩展开发完全指南:包结构、清单参考与 QML 数据模型

【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio

本篇指南以 Serial Studio 官方文档 Widget-Extension-Development.md 为核心,结合仓库内真实样例(examples/widget-extension)、内置扩展(app/rcc/extensions/widget)与源码实现,系统讲解如何为 Serial Studio 编写不重新编译应用即可运行的仪表盘组件扩展(widget extension)。读完本文,你将掌握 widget 扩展包的结构与info.json清单全部字段、四个必备 QML 属性与ExtensionDataModel数据模型、声明式配置表单的用法,以及安装、测试、故障排查与分发的完整流程。

什么是 Widget 扩展

Widget 扩展是一种类型为widget的扩展包:一份元数据(info.json)、一个 QML 文件,外加一组声明的设置项。安装后,它会出现在项目编辑器(Project Editor)中它声明所属实体类型(entity kind)的 Widget 列表中,并在仪表盘上像内置组件一样工作——拥有同样的画布、标题栏、工作区(workspaces)、冻结模式(freeze mode)与弹出窗口(pop-out windows)能力,不需要重新构建 Serial Studio。

Serial Studio 自带的两个组件CompassData Grid正是以这种方式构建的。它们随应用一起打包(bundled)而非由用户安装,但使用的就是本文描述的同一套包格式。其清单与 QML 源码分别位于 Compass.info.json、Compass.qml 与 app/rcc/extensions/widget/datagrid 目录下,是学习真实生产级扩展的绝佳范本。

需要特别说明的是,Widget 扩展不是 Pro 专属功能:它在 GPL 构建版与商业构建版中都能加载。同时,一个扩展包永远无法成为、替换或解锁某个 Pro 组件——内置组件的标识符是被保留的,任何声称占用内置标识符的包都会被拒绝(详见下文「保留标识符」)。

信任模型(Trust Model)

Widget 扩展以与 Serial Studio 本身相同的权限运行在应用内部。它的 QML 与应用程序共享同一个 QML 引擎,因此能够访问应用程序可访问的同一文件系统、同一网络与同一运行时。整个设计没有任何沙箱、能力边界或权限列表;任何声称扩展被隔离的文档都是错误的。这一点在 widget-manifest.json 模式文件 的文档注释中也被明确重申:「Extension widgets run with the same privileges as the application itself; nothing here isolates them.」

Serial Studio 采用的替代方案是询问。在你安装的包首次运行之前,会弹出一个对话框,说明该包是什么、由谁发布、它在磁盘上的位置,以及它将以应用程序的权限运行。拒绝运行会让包保持已安装但惰性(inert)状态;该决定按包版本记录,因此更新后需要再次询问。随应用捆绑的包则豁免此流程。

从源码可以印证这套「默认拒绝(default-deny)」的同意门控(consent gate)机制,实现在 WidgetExtensions.cpp 中:

  • consentRequired(id):随应用捆绑的包豁免,用户安装的包一律需要同意(package.isValid() && !package.bundled);
  • consentGranted(id):同意记录按「id + 版本」存储,因此更新包后旧同意自动失效、需要重新询问
  • canInstantiate(id):widget 允许执行的唯一判定——必须已注册,且要么随应用捆绑、要么已对当前版本显式同意,「default-deny 才是关键」(源码注释原文)。

安装 Widget 包的方式应与安装任何程序一致:从你信任的来源安装

包结构(Package Structure)

一个最小可用的 widget 扩展包只需两个文件:

com.example.level-bar/ info.json LevelBar.qml

源码树中 examples/widget-extension 目录提供了一份带注释、可直接复制的完整样例包(含info.jsonLevelBar.qmlREADME.md),是开始开发的最佳起点。

安装后的包位于**工作区文件夹(workspace folder)**中、应用包之外,因此升级或移动 Serial Studio 不会影响已安装的扩展:

~/Documents/Serial Studio/Extensions/widget/com.example.level-bar/

注意安装路径中的widget目录段来自包类型(本地包类型决定安装位置,而不是清单中的type字段单独决定),这也是 catalog.json 模式 中特别强调的规则。

info.json 清单参考

完整的清单示例(与源码树样例 info.json 一致):

{ "id": "com.example.level-bar", "type": "widget", "title": "Level Bar", "description": "A horizontal level bar for a single numeric dataset.", "author": "Your Name", "version": "1.0.0", "license": "MIT", "category": "Instruments", "files": ["info.json", "LevelBar.qml"], "widget": { "apiVersion": "1.0", "hostCompat": ">=1.0 <2.0", "scope": "dataset", "qml": "LevelBar.qml", "icon": "widgets/bar", "readsStringValues": false, "accepts": { "datasets": { "min": 1, "max": 1 }, "value": "numeric" }, "defaultSize": { "width": 360, "height": 120 }, "config": [], "dependencies": { "required": [], "optional": [] }, "experimental": false } }

widget块之外的键是 Extensions.md 中描述的标准扩展元数据;widget块是本类型专有的。各字段语义如下:

Key必填说明
apiVersion包编写时依据的清单格式版本。若包声明的主版本号比宿主更新,则该包不会被加载。模式要求形如^[0-9]+(\.[0-9]+)?$
hostCompat包支持的宿主 widget-API 范围,以空格分隔的比较符表示(">=1.0 <2.0")。省略或"*"表示任意版本。
scope"dataset""group",决定该包出现在哪个 Widget 列表中。
qml入口文件,相对于包文件夹的路径,不得指向包文件夹之外(模式^(?!.*\.\.)[^/\\][^\\]*$直接拒绝..相对路径)。
icon内置图标标识符(如"widgets/bar")或包内文件(如"icon.svg")。
acceptsdatasets.min/datasets.max约束数据集数量(模式约束为 0–4096 的整数),value取值"numeric""string""any"。不匹配的实体不会被提供该组件。
readsStringValues当组件渲染文本值而非数字时设为true。Serial Studio 只向主动请求字符串值的组件推送字符串值。
defaultSize弹出窗口的宽高(像素),模式约束为 48–8192。
config声明的设置项,详见「配置设置」一节。
dependencies本包依赖的其他扩展包。必需依赖缺失会使组件停止加载并上报;可选依赖缺失仅上报。每个依赖条目为{ "id": "...", "version": "<范围>" }version复用hostCompat的比较符语法。
experimental标记该包为进行中(work in progress)。

关于清单验证的边界条件,模式文件还给出了一些值得注意的细节:

  • id模式为^[A-Za-z0-9][A-Za-z0-9._-]*$(1–128 字符);由于id也是安装目录的一个路径段,该模式顺带排除了分隔符与父路径引用;
  • config数组最多 128 项,每项id须匹配^[A-Za-z_][A-Za-z0-9_]*$(1–64 字符);
  • dependenciesrequired/optional各自最多 32 项;
  • widget块设置了additionalProperties: false,未知键会被拒绝。

id是项目 group/dataset 的widget字段中存储的值,因此跨版本必须保持稳定。请使用反向域名标识符(reverse-domain identifier):内置组件的标识符(bargaugecompassdatagridplot3d等)是保留的,会被拒绝。完整的保留列表可在模式文件的reservedId定义中看到(包含mapgpsgyromultiplotaccelerometerimagepainterwebviewbarpanelterminalclockstopwatchnotification-logled-panelmeter等 20 个字符串)。唯一的例外是随应用捆绑的包通过replaces键声明替代某个内置标识符;宿主会拒绝任何从磁盘加载的包使用replaces

清单解析器的行为有专门的测试覆盖,见 tst_widget_manifest.cpp:包括版本范围语法(versionInRange_data中的各种 KAT 用例)、「无法解析的版本按失败关闭(fails closed)而不是当作任意版本」,以及保留标识符拒绝(错误码widget-id-reserved)与 API 主版本不匹配拒绝(错误码widget-api-version)。

Widget QML 文件

入口文件必须声明四个必备属性,Serial Studio 在创建组件时会将它们全部注入:

import QtQuick import QtQuick.Controls import SerialStudio Item { id: root required property color color required property string widgetId required property Item windowRoot required property ExtensionDataModel model Label { anchors.centerIn: parent color: root.color text: model.title + ": " + model.text } }
属性承载内容
model实时数据,见下一节。
color该组件在仪表盘上的强调色(accent colour)。
windowRoot组件所在的窗口,供对话框与弹出层使用。
widgetId组件的持久化键(persistence key)。

这四个属性的注入发生在 WidgetDelegate.qml 的buildWidget()中:对扩展组件调用dashboardWidget.createExtensionItem(loader, { model: …, windowRoot: …, color: …, widgetId: … }),对内置组件则用Qt.createComponent+createObject传入同一组参数——这正是扩展组件与内置组件在画布上「平起平坐」的机制。

导入面(Import surface)。一个包可以导入QtQuickQtQuick.ControlsSerialStudio。Serial Studio 自身的 QML 组件被编译进应用程序,无法从包文件夹导入;你需要的一切其他内容都必须随包携带。扩展没有组件库(component library)。

注意一个细节:虽然文档给出的最小示例只用了三个模块,但内置扩展(如 Compass)还会额外导入QtQuick.ShapesQtQuick.Effects等标准 Qt Quick 模块——只要属于 Qt 标准模块就可以自由使用,限制的只是应用内部编译的组件。Compass 中还大量使用了Cpp_ThemeManager.colors[...]Cpp_Misc_CommonFonts等 SerialStudio 命名空间下暴露给 QML 的 C++ 单例,这说明SerialStudio导入面下可用的宿主服务相当丰富(从源码看包括主题颜色、字体、图形后端能力开关Cpp_Misc_GraphicsBackend.effectsEnabled、仪表盘格式化Cpp_UI_Dashboard.formatValue等)。

ExtensionDataModel 数据模型参考

模型会重新发布仪表盘已经解析好的数据,并跟随仪表盘自身的更新节拍(update tick)。它没有逐帧开销,且包永远不会触碰任何应用对象来读取数据。

属性类型描述
titlestring数据集或组的显示标题,包含用户的重命名。
valuedouble数据集的数值(组取第一个数据集)。
textstring与仪表盘格式化方式相同的值文本,包含单位。
stringValuestring原始文本值。
unitsstring声明的单位。
isNumericbool当前值是否解析为数字。
minValue,maxValuedouble声明的显示范围。
decimalPoints,displayFormatint, string声明的格式化参数。
alarmsDefined,alarmTriggered,alarmSeveritybool, bool, int报警带(alarm-band)状态。
datasetCountint组件背后的数据集数量。
datasetsmodel组作用域(group-scope)组件的逐数据集行,角色见下表。
groupId,sourceId,uniqueIdint组件所渲染实体的身份标识。
groupScopebool是否为组作用域包。
extensionIdstring包自身的标识符。
configmap声明设置的当前值。
pausedbool可写;冻结模型发布的值。

datasets模型为每个数据集暴露一行,角色为:titletextvaluenumericValueisNumericunitsminValuemaxValuedecimalPointsdisplayFormatuniqueIdindexalarmsDefinedalarmSeverity,以及widgets(显示同一数据集的其他仪表盘组件,为{ windowId, icon, title }映射的列表)。

模型在值变化时发出updated()信号;QML 属性绑定会自动捕获该信号。从源码(ExtensionData.h)可以看到,除了pausedconfigalarmsDefined等少数属性外,绝大多数属性都以updated为 NOTIFY 信号;groupIdsourceIduniqueIdgroupScopeextensionIddatasets则是CONSTANT——实体身份在创建时即固定。

几个源码层面的实现要点,可以帮助你写出更高效的扩展 QML:

  • 逐行增量更新ExtensionRowsModel(ExtensionData.h 中定义)是一个QAbstractListModel子类,通过updateRow()就地更新行并触发dataChanged(),因此一个渲染五十个数据集的组组件不会每个节拍都重建委托。updateData()(ExtensionData.cpp)只在「行数变化」或「实际有值移动」时触发updated(),其余时候静默。
  • paused的语义:置true时挂起逐节拍刷新;恢复时立即拉取一次新鲜快照(setPausedif (!m_paused) updateData();)。
  • config合并逻辑reloadConfig()先从包描述符取出每个声明设置的默认值,再用项目文件为该组件存储的设置覆盖——这正是「声明默认值 + 项目级覆盖」两级配置的来源。

配置设置(Configuration Settings)

在清单中声明设置,Serial Studio 会自动渲染设置表单——无需编写任何 UI

"config": [ { "id": "barColor", "type": "choice", "label": "Bar colour", "default": "green", "options": ["green", "amber", "red"] }, { "id": "showValue", "type": "bool", "label": "Show numeric value", "default": true }, { "id": "smoothing", "type": "double", "label": "Smoothing", "default": 0.25, "min": 0, "max": 1 } ]

支持的类型为boolintdoublestringchoice。读取用model.config["barColor"],写入用model.setConfigValue("barColor", "amber")

从模式文件可得到每项的完整约束:

  • id(必填):^[A-Za-z_][A-Za-z0-9_]*$,1–64 字符;
  • type(必填):枚举["bool", "int", "double", "string", "choice"]
  • label:最长 128 字符;description:最长 512 字符;
  • default:任意值;
  • min/max:数值上下界;
  • optionschoice类型的候选值数组,最多 128 项。

从源码看,setConfigValue(ExtensionData.cpp)的实际行为是:通过projectModel.saveWidgetSetting(widgetId(), key, value)将值写入项目文件中该组件(按widgetId)对应的设置区,然后reloadConfig()立即重新合并。因此配置值按项目存储、随项目文件一起保存——换一台机器打开同一项目,设置依然在。

用户通过组件的标题栏菜单(caption menu)中的Widget Settings…进入表单;当包未声明任何设置时,该入口自动隐藏。在 WidgetDelegate.qml 中可以看到,设置表单通过extensionSettingsLoader.openDialog(root.extensionId, …)打开,而 Compass 还演示了一种更进阶的用法:把「当前页码」这类内部状态也通过model.setConfigValue("page", …)持久化到项目设置中,重启后恢复(见 Compass.qml 的Component.onCompletedConnections段)。

安装与测试

  1. 将包文件夹复制到~/Documents/Serial Studio/Extensions/widget/
  2. 重启 Serial Studio。目录在启动时读取,扩展管理器安装、更新或移除包时也会重新读取(对应源码中catalogChanged信号触发 WidgetDelegate.qml 的onCatalogChanged就地重建组件槽位)。
  3. 打开项目,选中一个数据集或组,从 Widget 列表中选择该组件。
  4. 在同意对话框出现时,允许该包运行。

迭代 QML 时,重启应用以拾取编辑。需要快速验证清单语法时,可以直接对照 widget-manifest.json 模式 检查;该模式同时被测试套件 tst_widget_manifest.cpp 引用,仓库 CI 会用它校验。

一个有用的迭代技巧:内置的 Compass 与 Data Grid 就是「随包捆绑的扩展」,其 QML 位于 app/rcc/extensions/widget,与用户安装的包使用完全相同的注入契约(model/color/windowRoot/widgetId)。遇到「扩展为什么拿不到数据」之类的疑惑时,对比阅读这些内置实现通常能最快定位问题。

包加载失败时的表现

无法渲染的包永远不会静默消失。组件槽位会显示一个指名原因的占位符,Problem Center 会列出对应条目:

上报内容原因
Manifest 不可用清单不是合法 JSON,或缺少idtitle"type": "widget"widget块。
保留标识符id是内置组件标识符。
与当前版本不兼容hostCompatapiVersion排除了当前构建。
没有可用的 QML 文件声明的入口文件缺失,或指向包外。
依赖缺失必需的依赖未安装,或其版本超出范围。
Widget 扩展加载失败QML 编译失败或在创建时报错,消息会携带 QML 错误信息。
未安装项目引用了本机未安装的包。
等待你的许可包已安装但尚未被允许运行。

占位符机制与「失败绝不静默」的保证可以在 WidgetDelegate.qml 的showPlaceholder(reason)中找到实现:它加载内置的ExtensionPlaceholder.qml组件,把原因、标题与扩展 id 传入并铺满槽位;QML 编译错误(Component.Error)时则直接展示component.errorString()

分发(Distribution)

Widget 包的托管与安装方式与其他扩展类型完全一致:在仓库的manifest.json中添加widget/<id>/info.json条目,然后让扩展管理器指向该仓库。仓库布局、托管方式与按平台分发的文件细节见 Extensions.md。

对仓库侧的文件格式,catalog.json 模式 给出了关键的完整性要求:每个可安装文件都必须携带 SHA-256 摘要与字节大小(schemaVersion: 2;不带摘要的 v1 目录会被安装器拒绝)。安装器先把文件下载到暂存目录,逐文件核对摘要,全部通过后才替换已安装版本;id同时是安装目录的一个路径段,因此模式严格排除了..等父路径引用。

需要明确的是:Serial Studio 在扩展下载时不验证签名,也不验证校验和(安装器自身的摘要校验针对的是仓库分发流程,而非对包作者的签名信任)。由于 Widget 包是在应用内部运行的代码,请从你的用户已经信任的地方发布,并明确告知他们该包的行为。

小结:从零到可分发包的三步路线

  1. 复制并改造样例:以 examples/widget-extension 为起点,改id(反向域名、避开保留标识符)与title,按需填写widget块。
  2. 实现 QML 契约:声明四个required propertycolorwidgetIdwindowRootmodel),只用QtQuick/QtQuick.Controls/SerialStudio导入面;需要用户可调项时在widget.config里声明并用model.config/model.setConfigValue读写。
  3. 安装验证后分发:复制到~/Documents/Serial Studio/Extensions/widget/<id>/,重启,选数据集/组并允许运行;确认占位符机制在你出错时能给出准确原因,然后把widget/<id>/info.json挂入仓库manifest.json分发。

整个过程中,记住信任模型的底线:你的 QML 以应用自身的权限运行,没有沙箱——写代码时把它当作应用本体的一部分来对待,只从可信来源分发。

【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio

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

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

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

立即咨询