尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

QML地图插件开发:在Plugin中设置加载地图类型与默认底图

QML地图插件开发:在Plugin中设置加载地图类型与默认底图 刚把地图插件框架跑通紧接着就有人问底图的类型能不能不写在QML里而是让插件来决定加载什么这个需求其实挺实在的。项目越大越不想看到每个页面里都有一句activeMapType写死某种底图换服务商的时候全项目搜代码太痛苦了。这篇是系列第三篇专门聊Map在Plugin中设置加载地图类型这件事。核心思路一句话地图类型不是QML的功能而是插件的服务能力。插件不仅提供瓦片和网络请求还应该决定自己支持哪些地图类型、默认激活哪一种。文章里会把插件侧的地图类型注册机制、参数传递链路、源码示例、常见坑一次讲透。适合正在做QML地图开发、或者想彻底搞懂自定义地理服务插件的人参考。1. 为什么要把地图类型设置下沉到Plugin1.1 QML地图框架里的Plugin与Map到底什么关系很多刚接触QML地图的人会有一个误解以为Map元素什么都能干只是拿来用就行了。实际上QML里的Map只是一个壳真正干活的是它背后挂载的Plugin。地图类型、瓦片加载、坐标投影、网络请求全部由插件层完成。看一下最基础的用法Plugin { id: myPlugin name: osm } Map { anchors.fill: parent plugin: myPlugin }name: osm就是告诉Qt去加载名为geoservices_osm的插件库。Map组件拿到插件之后会从插件获取supportedMapTypes也就是这个插件支持哪些地图类型。然后你才能在QML里通过activeMapType切换。换句话讲QML侧能看到的supportedMapTypes是插件给的不是Map自己生成的。服务商能提供什么底图、提供几种底图完全取决于插件。既然如此地图类型的管理逻辑放在插件里就再自然不过了。1.2 常规做法的痛点activeMapType散落各处很多团队实际开发中是怎么写地图类型切换的在页面里加一个下拉框遍历map.supportedMapTypes然后用户一选你就赋值onCurrentIndexChanged: { map.activeMapType map.supportedMapTypes[CBox.currentIndex] }这在简单项目里没什么问题但项目稍微复杂一点痛点就出来了。第一个坑是类型列表不可控。supportedMapTypes返回什么QML侧就只能用什么。假如底层某个瓦片服务不稳定你想临时禁用某种类型那就得改插件重新编译或者在前端代码里做一堆校验去过滤列表非常别扭。第二个坑是默认加载类型不统一。有的页面打开后默认显示街景地图有的页面又默认显示卫星图。三五个人写下来的代码风格完全不一样最后大家靠着复制粘贴保持一致性一旦高管要改成统一默认底图改到怀疑人生。第三个坑是底图类型跟业务配置耦合太深。比如某些业务场景只允许卫星底图加业务标注另一些场景又只能用街景底图。这种配置本身是运维和业务侧的诉求放在QML代码里不合适的——改一次配置就要发一次包发完包还会被测试追着问为什么底图变了。说到底地图类型不是一个界面交互问题它是一个服务配置问题。服务配置就应该下沉到插件层由插件来管。1.3 插件级控制到底解决了什么把地图类型设置移植到插件里之后最明显的好处是QML层退化为纯展示。页面上不再出现任何硬编码的activeMapTypeMap加载后是什么底图插件说了算。具体收益集中在三个点白名单逻辑闭合在插件内部。插件可以读取配置参数决定supportedMapTypes里放几个类型。QML侧拿到的列表天然就是过滤后的不需要前端做二次判断。默认加载类型由插件统一指定。你甚至可以做到根据不同的环境变量或配置文件加载不同的默认底图开发环境用测试瓦片、生产环境用正式瓦片QML文件一概不动。换服务商时只换插件不改页面。今天用服务商A明天换服务商B只要B插件也注册了同样名字的地图类型前端代码一行都不用动。项目后期这块收益非常明显。你想想看如果你的工程里有几十个页面都引用了地图有一天要整体切换底图服务你是愿意全局搜索几十处activeMapType还是只改一个插件里的几行注册代码2. 地图类型从插件走到QML的完整链路2.1 QGeoMapType一张底图的“身份档案”在Qt Location这套体系里地图类型不是一个简单的字符串。C侧对应的是QGeoMapType类你可以把它理解成一张底图的“身份档案”。一份完整的QGeoMapType大概包含这些信息内部的MapStyle枚举比如街景、卫星、混合、地形等逻辑名称比如street、satellite这是业务代码里真正用到的标识描述文本、是否支持移动端、是否夜间样式等信息一个整数mapId有些版本还会带布局标识和瓦片元数据为了直观可以这样类比地图类型就像餐厅的菜单套餐。每种套餐有几个固定菜品点菜的客户QML不需要关心后厨怎么做只需要知道菜单上写着哪些套餐。而插件就是后厨或者更进一步说是那个负责定义菜单的人。supportedMapTypes就是插件递给QML的一份菜单。正常情况下菜单上有什么客户才能点什么。如果插件只是在内部实现了某种底图但没把它塞进supportedMapTypes外部再怎么操作也选不到这个类型。2.2 插件工厂与引擎的职责划分Qt地理服务插件这个体系核心是两层的工厂层和引擎层。工厂类是QGeoServiceProviderFactory它的职责只有一个——接收参数然后创建出对应的引擎。QML里配置的PluginParameter最终会以QVariantMap的形式传递到工厂的createMappingManagerEngine方法。引擎类是QGeoMappingManagerEngine地图相关的具体工作都在这层。我们关心的supportedMapTypes就是引擎在构造函数里通过setSupportedMapTypes注册进去的。引擎还负责createMap返回一个能真正画地图的QGeoTiledMap实例。这个分层有个很微妙的地方setSupportedMapTypes只能调用一次必须在引擎初始化的时候完成。想后续动态增删地图类型基本没有官方标准办法。这就更加说明地图类型本质上是一个“服务启动时就应该定好”的配置项。2.3 PluginParameterQML到底是怎么给插件传参的既然地图类型在服务启动时就定死那外部怎么控制呢答案就是PluginParameter。看这个示例Plugin { id: mapPlugin name: my_custom PluginParameter { name: mapping.mapTypes; value: street,satellite } PluginParameter { name: mapping.defaultMapType; value: satellite } }这两个PluginParameter最终会进入C侧的QVariantMap parameters。插件工厂在创建引擎的时候可以从parameters里读出来。这就是“在Plugin中设置加载地图类型”的核心通路QML侧通过参数告诉插件我只要街景和卫星两种类型默认加载卫星底图插件根据参数去注册地图类型、设置默认类型QML侧的Map自动加载插件提供的默认类型注意一下mapping.前缀是Qt Location里一个约定俗成的命名方式不是强制规范。但建议保留这个前缀方便排查问题的时候一眼看出哪些参数是给地图模块的。3. 源码实现在Plugin中设置加载地图类型3.1 插件工程骨架与元数据配置自己实现一个地理服务插件工程结构并不复杂。关键是要遵守Qt的插件识别规则否则你编译出来的库Qt根本不认。先看工程文件TEMPLATE lib TARGET geoservices_myprovider QT location-private positioning-private CONFIG plugin CONFIG c17 SOURCES \ mygeoserviceproviderfactory.cpp \ mymapengine.cpp HEADERS \ mygeoserviceproviderfactory.h \ mymapengine.h OTHER_FILES \ geoservices_myprovider.json PLUGIN_TYPE geo这里有两个细节值得注意。TARGET必须以geoservices_开头这是Qt Location识别地图插件的前缀规则。PLUGIN_TYPE geo决定了插件会被安装到Qt的geo插件目录下搜索路径才正确。元数据文件geoservices_myprovider.json内容如下{ Keys: [myprovider] }这个Keys字段就是QML侧Plugin的name属性匹配依据。QML里写name: myproviderQt就会去加载这个插件库。3.2 C侧注册地图类型并指定默认加载类型核心代码在引擎构造函数里。下面这个例子我通过读取插件参数来决定注册哪些地图类型并在创建地图时直接指定默认类型MyMapEngine::MyMapEngine(const QVariantMap parameters, QGeoServiceProvider::Error *error, QString *errorString) : QGeoMappingManagerEngine(parameters) { // 先定义好这个插件“能力范围内”的全部地图类型 QListQGeoMapType availableTypes; availableTypes QGeoMapType(QGeoMapType::StreetMap, street, Standard Street Map, false, false, 101, QByteArray(my_street), QGeoCameraCapabilities()); availableTypes QGeoMapType(QGeoMapType::SatelliteMapDay, satellite, Satellite Map, false, false, 102, QByteArray(my_satellite), QGeoCameraCapabilities()); availableTypes QGeoMapType(QGeoMapType::HybridMapDay, hybrid, Hybrid Map, false, false, 103, QByteArray(my_hybrid), QGeoCameraCapabilities()); // 从QML侧参数里取需要启用的类型列表 QString enabled parameters.value(mapping.mapTypes, street,satellite).toString(); QListQGeoMapType supported; for (const QGeoMapType type : availableTypes) { if (enabled.split(,).contains(type.name(), Qt::CaseInsensitive)) { supported type; } } // 白名单为空时给一个兜底类型 if (supported.isEmpty()) supported availableTypes.first(); setSupportedMapTypes(supported); // 记录默认类型 QString defaultName parameters.value(mapping.defaultMapType, street).toString(); for (const QGeoMapType type : supported) { if (type.name() defaultName) { m_defaultMapType type; break; } } } QGeoMap *MyMapEngine::createMap() { QGeoTiledMap *map new QGeoTiledMap(this, nullptr); // 重要在地图创建阶段直接指定默认加载的地图类型 map-setActiveMapType(m_defaultMapType); return map; }这段代码的重点在于mapping.mapTypes参数决定了最终supportedMapTypes里装了什么而mapping.defaultMapType则决定地图创建时激活的是哪一个。QML侧拿到的是一个已经被过滤好的菜单Map组件加载时使用的又是插件指定的默认类型两条链路都闭合在插件内部了。有个细节要提醒不同Qt版本的QGeoMapType构造函数签名会有差异。比如有些版本没有QByteArray参数有些版本则要求传插件名作为第一个参数。编译时不通过的话优先去查当前Qt版本的qgeomaptype.h按头文件里的构造函数重载来调整。3.3 QML侧把“加载哪个类型”交给插件参数C侧处理完之后QML侧就非常清爽了。整个main.qml大概长这样import QtQuick 2.15 import QtQuick.Window 2.15 import QtLocation 5.15 import QtPositioning 5.15 Window { visible: true width: 800 height: 600 Plugin { id: mapPlugin name: myprovider PluginParameter { name: mapping.mapTypes; value: street,satellite } PluginParameter { name: mapping.defaultMapType; value: satellite } } Map { id: map anchors.fill: parent plugin: mapPlugin center: QtPositioning.coordinate(39.9, 116.4) zoomLevel: 10 ComboBox { id: typeBox anchors.top: parent.top anchors.right: parent.right width: 140 height: 36 model: map.supportedMapTypes textRole: name onCurrentIndexChanged: { if (currentIndex 0) map.activeMapType map.supportedMapTypes[currentIndex] } } } }看到这里你可能想问ComboBox里还是给用户暴露了切换能力啊没问题插件已经通过参数限制了supportedMapTypes只有街景和卫星两种下拉框里不会出现混合地图。如果你连切换都不想让用户看到把ComboBox删掉就行。Map加载后默认就是卫星底图一切由插件控制。文本角色textRole: name里用的name就是QGeoMapType的name属性不是C字段名是QML侧暴露的模型角色。3.4 一个容易忽略的配对细节有个问题需要特别说明mapping.mapTypes参数里写的字符串必须和C侧注册QGeoMapType时的name参数完全一致。大小写也要一致。否则过滤的时候匹配不上supported列表就会为空进而触发兜底逻辑加载默认的第一个类型你可能会以为“参数没生效”。我自己排查这种问题的时候踩过不少坑。最稳妥的做法是C侧定义一个常量表配置字符串和注册名称引用同一份数据源不要两处手敲。这类问题本质上就是两个地方维护同一份字符串早晚会不一致。4. 实操中的典型问题排查记录4.1 地图空白插件没被加载地图区域一片空白控制台也没有明显的QML报错这种情况大概率是插件没加载成功。排查顺序我建议这样先用QLibraryInfo::location(QLibraryInfo::PluginsPath)找到Qt插件目录确认插件库已经拷贝到geo子目录下。Windows上你可能需要手动把geoservices_myprovider.dll复制过去。再看插件库名是否以geoservices_开头很多人在这一步翻车。名字不对Qt的插件加载器扫不到Map就以为这个供应商不存在。最后检查.json文件里的Keys字段和QML里的name是否一致。比如你QML里写name: myprovider那么Keys数组里必须有一个myprovider。至于Keys和库名前缀的关系Qt对这部分比较宽松但是约定优先别折腾。4.2 插件加载了但supportedMapTypes是空的如果你在QML里打印map.supportedMapTypes.length得到0问题基本出在引擎构造函数里。逐个排查这几个点工厂的createMappingManagerEngine是否被正确调到可以在工厂方法里加qDebug()看输出如果工厂方法直接被拦截了检查IID字符串必须是org.qt-project.Qt.QGeoServiceProviderFactorysetSupportedMapTypes是否在引擎构造时执行了createMap是否正常返回了一个合法的QGeoMap实例还有一个隐藏原因如果引擎构造时发生了异常底层会认为插件初始化失败Map拿不到任何类型。这种情况在日志里往往会有一条Factory failed to create engine之类的输出留意一下。4.3 activeMapType设置了但没有反应有人会这样写Map { plugin: mapPlugin activeMapType: someType }看起来没问题但就是不生效。这种通常是两个原因第一someType不是map.supportedMapTypes里的成员。QML侧给activeMapType赋值时底层会校验这个类型是否被当前插件支持不支持就静默忽略。你赋值的时候觉得没问题实际上类型列表已经被参数过滤过了你赋值给它的那个类型并不在列表里。第二Map的plugin属性还没生效你就赋值了。插件加载是异步的Map组件拿到插件、生成地图实例、再暴露supportedMapTypes需要一定时间。在onCompleted里直接赋值很可能拿到的还是空列表。稳妥的做法是监听plugin.supportedMapTypes发生变化再设置默认的activeMapType。不过按照本文插件侧的做法默认类型已经在createMap里指定了QML里根本不需要手动赋值默认类型。这个坑是给那些仍然想在QML侧管默认类型的人提个醒。4.4 修改了插件参数地图类型却不刷新开发阶段常遇到。改了PluginParameter的value重新编译运行发现底图还是老样子感觉参数没生效。麻烦的地方在于Qt导航和地图插件有缓存。有些平台会把地理服务的配置缓存到本地特别是QML引擎还可能有已加载插件的缓存。改完参数后可以试试这几步清掉构建目录里的qmlcache删掉应用运行时写的配置缓存目录重启Qt Creator有时进程没完全退出插件旧实例还在内存里更麻烦的一种情况是你同时装了两个版本的插件库一个在Qt安装目录一个在应用运行目录。Qt的插件搜索规则有时会优先加载某个目录导致你改了半天程序里跑的却是另一份库文件。调试这种问题直接无脑建议路径里搜一下geoservices_myprovider把所有旧库文件全找出来统一清理再测试。4.5 多个插件共存时的类型路由冲突如果系统里装了多个地理服务插件比如官方osm和你自己的myprovider两者都去注册地图类型在某些场景下可能会出现类型互相覆盖的错觉。其实QML侧不会搞混因为Map.plugin已经明确指定了用哪个插件。但如果你在一个插件里创建了多个引擎或者工厂方法里对不同providerName做了分支就要注意createMappingManagerEngine的第一个参数providerName一定要跟你JSON里的Keys对应上。否则可能出现你创建了引擎但Qt认为这个供应商标识不属于当前插件后续逻辑全部走不通。跨插件的mapId建议也不要重复。QGeoMapType的mapId虽然不是一个绝对全局唯一的标识但多个插件使用相同ID会把排查问题的成本拉高。给自己插件的地图类型分配一个独立的ID区间比如从1000开始起能省很多麻烦。5. 更进一步把地图类型做成配置驱动5.1 用JSON文件定义地图类型清单代码里写死地图类型已经比QML层写死先进了一步但还不够灵活。如果你的插件要服务多个项目每个项目的地图类型可能都不一样这时候再为了类型列表去改代码、重编译就很蠢了。可以考虑把地图类型清单抽到一个JSON配置文件里插件启动时自动读取。一个典型的配置长这样{ mapTypes: [ { name: street, style: StreetMap, description: Standard Street Map, mapId: 101 }, { name: satellite, style: SatelliteMapDay, description: Satellite Map, mapId: 102 } ], defaultMapType: satellite }然后用Qt的JSON解析接口读取逐个构造QGeoMapTypeQJsonArray types doc.object().value(mapTypes).toArray(); for (const QJsonValue v : types) { QJsonObject obj v.toObject(); QGeoMapType::MapStyle style styleFromString(obj.value(style).toString()); QGeoMapType type(style, obj.value(name).toString(), obj.value(description).toString(), false, false, obj.value(mapId).toInt(), QByteArray(my_street), QGeoCameraCapabilities()); supported type; } setSupportedMapTypes(supported);这样插件的行为就完全由配置文件驱动了。QA要验证某种底图直接改JSON再重启应用不用重新编译插件迭代速度完全不一样。5.2 参数优先级与缓存刷新当配置有了多个来源就一定要理清优先级。我的建议是插件参数QML侧最高其次是指定的配置文件最后才是代码内置的默认值。实现逻辑其实不复杂。先从parameters里读mapping.configPath如果有配置路径就读取JSON如果参数里没有configPath就尝试从标准资源目录找默认配置文件都找不到就用代码里内置的类型表。每一层都复用同一个bool applyTypesFromConfig(...)的辅助函数代码干净每条路径都短。不过刷新问题要提前想清楚。插件引擎在启动时就调了setSupportedMapTypes之后你改了配置文件不会自动生效必须重启应用或者重新创建引擎。这是Qt Location框架的机制限制不是你自己能轻易绕过的。所以配置驱动更适合“启动时决定”的场景不适合运行时热切换。5.3 今天只做了一小步后续还能扩展很多到这里地图类型的管理已经从QML页面彻底下沉到了插件层并且通过配置实现了灵活定制。这个架构基础打下来之后你可以顺手扩展出很多有价值的能力。比如配合权限系统不同登录用户拿到不同的supportedMapTypes。管理后台账号能看街景和卫星普通操作工只能看业务底图不需要改QML插件根据会话参数过滤即可。再比如灰度发布某几个类型只对特定环境的实例开放判断逻辑放在插件里零侵入。我个人觉得这套设计最大的价值不是省了几行代码而是让“地图类型”这个东西从“前端代码里的一个变量”变成了“服务端下发的一份配置”。变量终有一天会被注释、被覆盖、被散落的页面遗忘但配置可以被审计、被管理、被统一回收。做嵌入式客户端的人尤其要早点建立这个意识。回到实际测试中C侧读参数、过滤类型、设置默认类型的流程跑通后QML文件几乎不用再维护。以后领导说“默认底图换成街景”你改一行配置或者参数值就够了连重启方式都可以自动化。这种快乐只有被几十处activeMapType支配过的人才懂。
返回列表