首页 / 资讯中心 / 文章详情

Qt桌面端地图开发:QWebEngineView加载百度/谷歌地图与双向通信实战

Qt桌面端地图开发:QWebEngineView加载百度/谷歌地图与双向通信实战 ★ FEATURED ARTICLE
简介这份资源面向需要在Qt桌面应用中嵌入在线地图的C开发者重点解决百度地图与谷歌地图的加载与交互问题。包内共25个文件以cpp源码、h头文件、html页面、ui界面文件及pro工程文件为主另含编译生成的obj、exe、pdb等中间产物压缩包约773KB可直接参考或二次开发。内容围绕QWebView加载地图URL、通过JavaScript API初始化地图、设置API密钥以及借助javaScriptWindowObject实现C与JS双向通信等关键环节展开并附带百度地图与谷歌地图两个HTML示例页面便于对比两种地图服务的接入差异。已有2697人学习下载适合具备一定Qt基础、希望快速跑通地图嵌入流程的开发者可从中获取可运行的工程结构、地图初始化脚本与交互事件处理思路减少自行摸索API与WebKit集成的时间成本。1. 当你的桌面端项目需要一张能交互的地图做过桌面端上位机或者工具软件的开发者大概都遇到过这种需求设备上报的 GPS 坐标要在地图上实时打点或者物流调度系统里要画出车辆轨迹再或者只是想在界面里嵌一张能缩放、能拖拽的地图做区域选择。这时候如果项目用的是 Qt第一反应往往是——Qt 自带的 QtLocation 模块能不能直接用答案是能但配置起来相当折腾插件依赖、瓦片源、网络策略每一项都可能卡住半天。这份资源走的是另一条路绕开 QtLocation 的插件体系直接用 QWebEngineView 加载网页版地图通过 Qt 与 JavaScript 的双向通信来控制地图行为。百度地图和谷歌地图两套都做了适配切换地图源只需要改一个配置项。适合正在做 Qt 桌面项目、需要在界面里嵌入交互地图、又不想被 QtLocation 的插件配置折磨的开发者。下面从项目结构开始拆。2. 项目结构与技术选型为什么用 QWebEngineView 而不是 QtLocation2.1 两套方案的取舍Qt 官方提供的 QtLocation 模块理论上可以加载在线地图但它依赖 GeoServicePlugin不同平台下可用的插件不一样而且瓦片源的配置需要写 QML 或者 C 插件。更麻烦的是QtLocation 对国内地图服务商的支持几乎为零百度地图、高德地图都没有现成的插件可用。你当然可以自己写一个 GeoServicePlugin但那工作量已经超过项目本身了。QWebEngineView 的方案本质上是把地图当成一个网页来加载。百度地图和谷歌地图都有成熟的 JavaScript API网页端能做的事QWebEngineView 里都能做。Qt 侧只需要负责两件事把地图页面加载进来以及通过 QWebChannel 或者 runJavaScript 与页面里的 JS 通信。这个思路的好处是地图的所有交互逻辑缩放、拖拽、标注、路线规划都由地图 JS API 处理Qt 侧只关心业务数据。代价也有QWebEngineView 会引入 Chromium 内核编译出来的程序体积会增加大概 100MB 左右。如果你的项目对安装包体积极度敏感这个方案需要权衡。但对于大多数桌面工具软件来说这个体积是可以接受的。2.2 目录结构项目的目录组织大致如下QtMapDemo/ ├── QtMapDemo.pro # qmake 工程文件 ├── main.cpp # 程序入口 ├── mainwindow.h/cpp/ui # 主窗口 ├── mapview.h/cpp # 地图控件封装 ├── mapbridge.h/cpp # Qt 与 JS 通信桥接 ├── html/ │ ├── baidu_map.html # 百度地图页面 │ ├── google_map.html # 谷歌地图页面 │ └── qwebchannel.js # QWebChannel 前端库 └── config/ └── map_config.ini # 地图源与密钥配置核心文件是 mapview 和 mapbridge 两个类。mapview 继承自 QWebEngineView负责页面加载和生命周期管理mapbridge 是一个 QObject 子类注册到 QWebChannel 里暴露槽函数给 JS 调用同时提供信号让 Qt 侧接收 JS 的回调。2.3 工程配置.pro 文件里需要启用 webengine 模块QT core gui webenginewidgets webchannel network greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET QtMapDemo TEMPLATE app CONFIG c17 SOURCES main.cpp mainwindow.cpp mapview.cpp mapbridge.cpp HEADERS mainwindow.h mapview.h mapbridge.h FORMS mainwindow.ui RESOURCES resources.qrc这里有几个点需要注意。webenginewidgets 和 webchannel 是两个独立的模块都要加。network 模块虽然 QWebEngineView 内部会用到但如果你要在 Qt 侧发 HTTP 请求获取坐标数据也需要显式声明。c17 不是必须的但 QWebChannel 的一些新特性在 C17 下用起来更顺手。注意Qt 5.15 之后 webenginewidgets 从 QtWebEngine 拆成了独立模块如果你用的是 Qt 6模块名和头文件路径会有变化需要确认 Qt 版本。3. 地图加载核心实现从 QWebEngineView 初始化到页面渲染3.1 MapView 类的初始化MapView 继承 QWebEngineView构造函数里完成页面加载和通道建立// mapview.h class MapView : public QWebEngineView { Q_OBJECT public: explicit MapView(QWidget *parent nullptr); void loadMap(MapType type); // 切换地图源 void setCenter(double lng, double lat); void addMarker(double lng, double lat, const QString label); signals: void mapReady(); void markerClicked(int id); private: MapBridge *m_bridge; QWebChannel *m_channel; MapType m_currentType; };// mapview.cpp MapView::MapView(QWidget *parent) : QWebEngineView(parent) , m_currentType(MapType::Baidu) { // 创建通信桥接对象 m_bridge new MapBridge(this); m_channel new QWebChannel(this); m_channel-registerObject(mapBridge, m_bridge); // 将通道设置到页面 page()-setWebChannel(m_channel); // 允许本地文件访问远程资源 page()-settings()-setAttribute( QWebEngineSettings::LocalContentCanAccessRemoteUrls, true); // 连接桥接信号 connect(m_bridge, MapBridge::mapInitialized, this, MapView::mapReady); connect(m_bridge, MapBridge::markerClicked, this, MapView::markerClicked); }这段代码的关键在于 QWebChannel 的注册时机。必须在页面加载之前把 bridge 对象注册进去否则 HTML 里的 qwebchannel.js 初始化时找不到对象。另一个容易忽略的是 LocalContentCanAccessRemoteUrls 这个设置因为 HTML 文件是从本地资源加载的但地图瓦片和 API 脚本来自远程服务器不开这个选项页面会一片空白。3.2 百度地图页面的加载逻辑百度地图的 HTML 页面结构!DOCTYPE html html head meta charsetutf-8 script srcqwebchannel.js/script script srchttps://api.map.baidu.com/api?v3.0akYOUR_AK/script /head body stylemargin:0;padding:0; div idmap_container stylewidth:100%;height:100%;/div script var map, bridge; function initMap() { map new BMap.Map(map_container); var point new BMap.Point(116.404, 39.915); map.centerAndZoom(point, 12); map.enableScrollWheelZoom(true); // 建立 Qt 通信通道 new QWebChannel(qt.webChannelTransport, function(channel) { bridge channel.objects.mapBridge; bridge.mapInitialized(); }); } // 供 Qt 调用的全局函数 function setCenter(lng, lat) { if (!map) return; map.setCenter(new BMap.Point(lng, lat)); } function addMarker(lng, lat, label) { if (!map) return; var point new BMap.Point(lng, lat); var marker new BMap.Marker(point); map.addOverlay(marker); if (label) { var labelObj new BMap.Label(label, { offset: new BMap.Size(20, -10) }); marker.setLabel(labelObj); } marker.addEventListener(click, function() { bridge.markerClicked(marker.getId()); }); } window.onload initMap; /script /body /html这里有几个设计决策值得说明。initMap 放在 window.onload 里而不是立即执行是因为百度地图 API 脚本是异步加载的如果 DOM 还没准备好就调用 BMap.Map 会报错。QWebChannel 的初始化放在地图初始化之后确保 bridge 对象可用时地图已经就绪。addMarker 函数里给 marker 绑定了 click 事件通过 bridge.markerClicked 把点击事件传回 Qt 侧。这里传的是 marker 的 idQt 侧可以根据 id 找到对应的业务数据。注意 bridge.markerClicked 是直接调用 QWebChannel 暴露的方法不需要额外的信号连接。3.3 谷歌地图页面的差异处理谷歌地图的 JS API 与百度地图在接口设计上有明显差异script var map, bridge; function initMap() { map new google.maps.Map(document.getElementById(map_container), { center: {lat: 39.915, lng: 116.404}, zoom: 12, disableDefaultUI: false, scrollwheel: true }); new QWebChannel(qt.webChannelTransport, function(channel) { bridge channel.objects.mapBridge; bridge.mapInitialized(); }); } function setCenter(lng, lat) { if (!map) return; map.setCenter({lat: lat, lng: lng}); } function addMarker(lng, lat, label) { if (!map) return; var marker new google.maps.Marker({ position: {lat: lat, lng: lng}, map: map, title: label || }); marker.addListener(click, function() { bridge.markerClicked(marker.get(id) || 0); }); } /script script srchttps://maps.googleapis.com/maps/api/js?keyYOUR_KEYcallbackinitMap async defer/script谷歌地图的坐标参数是对象形式 {lat, lng}而百度地图是 BMap.Point(lng, lat)注意经纬度顺序也不一样。谷歌地图的 API 加载用 callback 参数指定初始化函数比百度地图的 window.onload 更规范一些。marker 的点击事件用 addListener 而不是 addEventListener这是谷歌地图自己的事件系统。3.4 Qt 侧调用 JS 函数MapView 提供的方法通过 runJavaScript 调用页面里的全局函数void MapView::setCenter(double lng, double lat) { QString js QString(setCenter(%1, %2)) .arg(lng, 0, f, 6) .arg(lat, 0, f, 6); page()-runJavaScript(js); } void MapView::addMarker(double lng, double lat, const QString label) { QString js QString(addMarker(%1, %2, %3)) .arg(lng, 0, f, 6) .arg(lat, 0, f, 6) .arg(label); page()-runJavaScript(js); }runJavaScript 是异步执行的不会阻塞 Qt 主线程。参数格式化时经纬度保留 6 位小数这是地图 API 的常见精度要求。label 参数直接拼接到 JS 字符串里如果 label 包含单引号会导致语法错误实际项目中需要做转义处理。提示runJavaScript 的第二个参数可以传一个回调函数用来接收 JS 的返回值。如果需要获取地图当前中心点坐标可以用这个方式。4. Qt 与 JavaScript 双向通信QWebChannel 桥接的完整链路4.1 MapBridge 类的设计MapBridge 是 Qt 侧和 JS 侧通信的核心它继承 QObject 并暴露给 QWebChannel// mapbridge.h class MapBridge : public QObject { Q_OBJECT public: explicit MapBridge(QObject *parent nullptr); signals: void mapInitialized(); void markerClicked(int id); void mapClicked(double lng, double lat); public slots: void onMapReady(); // JS 通知地图就绪 void onMarkerClicked(int id); // JS 通知标注被点击 void onMapClicked(double lng, double lat); // JS 通知地图被点击 QString getMapType(); // JS 查询当前地图类型 };// mapbridge.cpp MapBridge::MapBridge(QObject *parent) : QObject(parent) { } void MapBridge::onMapReady() { emit mapInitialized(); } void MapBridge::onMarkerClicked(int id) { emit markerClicked(id); } void MapBridge::onMapClicked(double lng, double lat) { emit mapClicked(lng, lat); } QString MapBridge::getMapType() { // 从配置读取当前地图类型 QSettings settings(config/map_config.ini, QSettings::IniFormat); return settings.value(map/type, baidu).toString(); }这里的设计模式是JS 侧调用 bridge 的 public slots 方法MapBridge 把这些调用转换成 Qt 信号发出去MapView 或者 MainWindow 连接这些信号做业务处理。getMapType 是一个同步方法JS 调用后会立即返回结果适合查询配置类数据。4.2 JS 侧调用 Qt 方法在 HTML 页面里通过 bridge 对象调用 Qt 侧的槽函数// JS 通知 Qt 地图已就绪 bridge.onMapReady(); // JS 通知 Qt 标注被点击 bridge.onMarkerClicked(42); // JS 通知 Qt 地图被点击带经纬度 map.addEventListener(click, function(e) { bridge.onMapClicked(e.point.lng, e.point.lat); }); // JS 查询 Qt 侧配置 bridge.getMapType(function(result) { console.log(当前地图类型:, result); });注意 getMapType 的调用方式和其他方法不同。有返回值的方法在 QWebChannel 里是异步的需要通过回调函数接收结果。这个设计一开始容易踩坑以为能直接 var type bridge.getMapType()实际上拿到的是 undefined。4.3 地图源切换的完整流程切换地图源需要重新加载页面并重新建立通道void MapView::loadMap(MapType type) { m_currentType type; QString htmlFile; switch (type) { case MapType::Baidu: htmlFile qrc:/html/baidu_map.html; break; case MapType::Google: htmlFile qrc:/html/google_map.html; break; } // 重新注册通道页面重载后旧通道失效 m_channel new QWebChannel(this); m_channel-registerObject(mapBridge, m_bridge); page()-setWebChannel(m_channel); // 加载新页面 QUrl url(htmlFile); page()-load(url); }每次切换地图源都重新创建 QWebChannel 并注册 bridge这是因为页面重载后旧的通道对象已经失效。如果不重新注册新页面里的 qwebchannel.js 会连接失败表现为 bridge 为 null所有通信中断。4.4 通信链路的数据流整个通信链路可以这样理解Qt 侧调用 page()-runJavaScript() 执行 JS 全局函数这是 Qt 到 JS 的方向JS 侧通过 bridge 对象调用 Qt 槽函数这是 JS 到 Qt 的方向。两个方向都是异步的Qt 侧不需要等待 JS 执行完成JS 侧调用 Qt 方法也不会阻塞页面渲染。这种异步特性在大多数场景下没问题但如果你需要确保某个操作执行完成后再进行下一步就需要用回调或者信号来同步。比如加载地图后立即添加标注如果地图还没初始化完成就调用 addMarkerJS 侧会因为 map 变量为 null 而静默失败。正确的做法是等 mapInitialized 信号触发后再执行后续操作。5. 避坑与常见问题排查5.1 页面白屏或地图不显示现象QWebEngineView 加载后一片空白控制台没有明显报错。原因通常有三个一是 LocalContentCanAccessRemoteUrls 没有开启本地 HTML 无法加载远程的地图 API 脚本二是 API 密钥无效或过期百度地图和谷歌地图都会返回鉴权失败三是网络请求被拦截某些企业网络环境会屏蔽地图服务域名。解决方法是先在 QWebEngineView 里打开开发者工具通过 page()-setDevToolsPage() 或者环境变量 QTWEBENGINE_REMOTE_DEBUGGING看 Network 面板里 API 脚本是否加载成功。如果返回 403 就是密钥问题如果请求根本没发出去就是网络策略问题。5.2 QWebChannel 连接失败现象JS 侧 bridge 对象为 null调用 bridge 的任何方法都报错。原因QWebChannel 的注册时机不对或者页面重载后没有重新注册。另一个常见原因是 qwebchannel.js 文件没有正确加载这个文件需要从 Qt 安装目录复制到项目资源里不能从 CDN 加载。解决确认 page()-setWebChannel() 在 page()-load() 之前调用。如果是页面重载场景每次 load 之前都要重新 setWebChannel。qwebchannel.js 建议直接嵌入 HTML 或者通过 qrc 资源加载不要依赖网络。5.3 经纬度坐标偏移现象标注打点位置和实际位置有几百米偏差。原因国内地图服务商使用的坐标系不同。百度地图用 BD-09谷歌地图用 WGS-84高德地图用 GCJ-02。如果数据源是 GPS 原始坐标WGS-84直接放到百度地图上就会有偏移。解决在 Qt 侧做坐标转换或者在 JS 侧调用地图 API 提供的转换接口。百度地图有 BMap.Convertor 可以做坐标转换谷歌地图需要自己实现 WGS-84 到 GCJ-02 的转换算法。建议在数据进入地图之前统一转成目标地图的坐标系。5.4 程序打包后地图加载失败现象开发环境正常打包发布后地图不显示。原因qrc 资源里的 HTML 文件路径在打包后可能变化或者 qwebchannel.js 没有被正确打包。另一个可能是 QtWebEngineProcess 可执行文件没有被部署到正确位置。解决用 windeployqt 工具自动部署依赖确认 QtWebEngineProcess.exe 和相关 DLL 在发布目录里。qrc 资源路径用 :/ 前缀而不是 qrc:/虽然两者在开发环境都能用但打包后 :/ 更可靠。5.5 runJavaScript 执行无效果现象调用 runJavaScript 后页面没有任何变化也没有报错。原因JS 函数名拼写错误、参数类型不匹配、或者函数还没定义就调用了。runJavaScript 是异步的执行失败不会在 Qt 侧抛出异常。解决在 runJavaScript 的回调里打印返回值确认 JS 是否执行。或者在 JS 函数里加 console.log通过开发者工具查看。确保调用的全局函数在 window 对象上而不是在某个闭包里。6. 二次开发改造从 Demo 到可维护的地图模块拿到这个项目之后最直接的改造方向是把它封装成一个可复用的地图控件。我一般会做这几件事。第一把地图配置抽成独立的配置文件。API 密钥、默认中心点、默认缩放级别、地图类型这些都放到 ini 或者 json 里不要硬编码在代码里。这样换一个项目只需要改配置不用动代码。// 配置读取示例 QSettings settings(config/map_config.ini, QSettings::IniFormat); QString ak settings.value(baidu/ak).toString(); double defaultLng settings.value(map/default_lng, 116.404).toDouble(); double defaultLat settings.value(map/default_lat, 39.915).toDouble(); int defaultZoom settings.value(map/default_zoom, 12).toInt();第二把地图操作封装成业务友好的接口。不要在上层代码里直接拼 JS 字符串而是提供 addMarker(MarkerData)、drawPolyline(QList ) 这样的方法。MarkerData 里包含经纬度、标题、图标、自定义数据等字段MapView 内部负责转换成 JS 调用。第三处理地图加载的异步时序。提供一个 whenReady() 方法返回 QFuture 或者用信号通知确保上层代码在地图初始化完成后再执行操作。我见过太多项目在地图还没加载完就调 addMarker然后排查半天为什么标注不显示。第四考虑离线场景。如果项目部署环境没有外网需要提前下载地图瓦片或者使用离线地图方案。百度地图有离线 SDK但配置比较复杂。一个折中方案是在内网部署一个瓦片代理服务QWebEngineView 加载的 HTML 里把瓦片地址指向内网服务器。第五日志和调试。QWebEngineView 的 console.log 默认不会输出到 Qt 的日志系统需要重写 QWebEnginePage::javaScriptConsoleMessage 把 JS 日志转发到 Qt 侧。这个在排查问题时非常有用。class MapPage : public QWebEnginePage { protected: void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString message, int lineNumber, const QString sourceID) override { qDebug() [JS] sourceID : lineNumber message; } };从那以后我每次做地图相关的项目都会先把 console 转发和配置外置这两件事做了后面省下来的排查时间远超前期投入。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站