关于Qt应用嵌入地图这一块 我是好久之前就开始做了,前段时间测试的时候貌似是没问题了,可刚拉出去溜溜时就出BUG了。。。在查找BUG时发现几个坑,不小心的话还是很容易掉进去的,所以想着记录下来和大家分享一下 。关于创建这一块我也没写过,那就从工程建立开始——
0 准备工作
开发环境 Qt5.7 MSVC2015
(我刚开始接触Qt时使用的MinGW版本 后来就是要嵌入网页时发现MinGW好像没有QWebEngine
模块)
需要文件
- 地图文件
BDMap.html
- 连接工具
QwebChannel.js
其中 地图文件是要自己创建,QwebChannel.js
是Qt
自带的,不需要任何修改,拷贝到地图文件同一层目录即可。文章最后会附上项目链接,项目中可以找到。
基本思路
- 在界面中要创建网页容器
QWebEngineView
类,这是Qt
提供的继承于QWidget
类的一个窗口,使用时在.Pro
文件中要添加QT += webenginewidgets
; - 创建
Qt
与Html
的连接通道QWebChannel
,通过该通道可以实现主程序与网页之间的通信。当然,通道的建立是双方面的,即主程序和网页中都要进行相关操作; - 通道建立完成即可实现主程序与网页之间的函数相互调用,并由此实现参数传递。
一、工程建立
-
建立一个主窗口项目,名称为
BDMap
,项目文件中添加QT += webenginewidgets
,UI界面如下图所示:
其中MapWidget
由QWidget
提升而成:
-
右键项目,添加自定义
bridge
类,继承于QObject
,在主窗口中包含头文件并创建对象JSBridge
; -
在项目文件夹下新建文件夹
Baidu_JS
,并将BDMap.html
和qwebchannel.js
拷贝到该文件夹下;在右击项目添加资源文件map
,并将BDMap.html
和qwebchannel.js
导入其中。
(网页文件可以不作为资源使用,不过要指定文件路径,当程序发布到别的电脑使用时,网页文件要跟着拷贝过去)
至此,项目创建完成,文件结构为:
二、程序编写
-
bridge
类作为主程序和网页之间的"桥梁",起到从网页接收数据的作用。不妨定义一个公有槽函数:void bridge::RcvPoint(const QString &lng, const QString &lat) { qDebug()<<lng<<","<<lat; }
即接收地图传来的坐标并打印。
-
主窗口构造函数:
MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); bridge *JSBridge = new bridge(this); QWebChannel *channel = new QWebChannel(this); channel->registerObject("window",(QObject*)JSBridge); ui->MapWidget->page()->setWebChannel(channel); ui->MapWidget->page()->load(QUrl("qrc:/Baidu_JS/BDMap.html")); }
首先分别创建
bridge
对象JSBridge
和QWebChannel
对象channel
然后将
JSBridge
注册到channel
中,注册的名称为window
接着将通道
channel
添加到网页中最后加载网页,完成。
3.主窗口通道建立完成之后就轮到html
中了。在html
中首先加载qwebchannel.js
工具。
<script src="qwebchannel.js"></script>
(再说一遍,.js
要和.html
放在同一层目录)
接着,可以在地图初始化完成后创建通道:
new QWebChannel(qt.webChannelTransport, function(channel) {
mw = channel.objects.window;
});
这样,就可以在后面调用我们在bridge
中定义的槽函数RcvPoint(lng,lat)
了:
mp.addEventListener("click",function(e){
mw.RcvPoint(e.point.lng,e.point.lat);
});
BDMap.html:
<!DOCTYPE html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<title>CanvasLayer</title>
<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=你的密钥"></script>
<style type="text/css">
body, html,#container {width: 100%;height: 100%;overflow: hidden;margin:0;font-family:"微软雅黑";}
</style>
</head>
<body>
<div id="container"></div>
</body>
</html>
<script src="qwebchannel.js"></script>
<script type="text/javascript">
var mp = new BMap.Map("container");
mp.centerAndZoom(new BMap.Point(116.3964,39.9093), 10);
mp.enableScrollWheelZoom();
new QWebChannel(qt.webChannelTransport, function(c) {
mw = c.objects.window;
});
mp.addEventListener("click",function(e){
mw.RcvPoint(e.point.lng,e.point.lat);
});
</script>
有话要说
1).
new QWebChannel(qt.webChannelTransport, function(channel) {
mw = channel.objects.window;
});
对于这样一个函数,里面哪些是可变的?
首先,这个函数名是qwebchannel.js
内置的,不可变,第一个参数为channel
的类型(或者说作用)是Qt端到Web端的一个转换通道;第二个参数是一个自定义的function()
,他的参数channel
是可变的,甚至可以用 c
来代替,但要和下一行的保持一致;
mw
相当于一个全局变量,但也是我自定义的,甚至可以用b
来代替;
再看等号右边,channel
说过了,要和上边一致;右边的objects
是他的一个元素,不可变;再右边的window
也是自定义的,但要和主窗口中JSBridge
的注册名一致。
2).这样说可能还是有点不明白,但再举一个例子就清楚多了:
比如说有两个变量 int a =5;int b = 10;
,现在要把这两个数交换一下应该怎么做?——最简单的做法是再定义一个int c;
,然后
c = a;
a = b;
b = c;
在这里边,c
就是一个中间变量,只起到一个传递作用,就相当于上边的channel
,我们在主程序中创建一个bridge
对象,然后要注册到channel
中,说白了就是在channel
中保存为window
(注册名),然后到html
中将保存的对象取出来赋给mw
,所以这个mw
就是主程序中的bridge
!
三、功能完善
- 把收到的坐标在主界面的显示框中显示
(这部分涉及的就是不同类之间的通信问题了,我之前在多线程的创建中有详细讲过)
一句话们就是通过信号和槽传参——bridge
在接收到经纬度之后,发出一个以经纬度为参数的信号,主窗口响应信号,并在槽函数中进行显示。 - 在主界面的输入框中输入坐标,点击发送,地图定位到该点
首先在html
中定义一个以经纬度为参数的定位函数
在主程序中使用function SetPoint(lng,lat){ mp.setCenter(new BMap.Point(lng,lat)); }
runJavaScript(cmd)
调用html
中的函数:
至此,项目功能基本实现。地图实现更深层功能需参照百度地图API示例与百度地图JSAPI3.0参考类这两个网站。void MainWindow::on_pushButton_clicked() { QString context = ui->lineEdit_SendMsg->text(); if(!context.contains(',')) { qDebug()<<"输入格式错误"; //输入格式 经度+纬度,中间以英文逗号‘,’隔开 return; } QString lng = context.split(',').at(0); QString lat = context.split(',').at(1); ui->MapWidget->page()->runJavaScript(QString("SetPoint(%1,%2)").arg(lng).arg(lat)); }
四、排坑工作
好吧,终于到这一步了,我是真没想到前面写这么多的,过了这么久才步入正题。。。
先说说遇到的第一个问题
如果在主窗口的构造函数中直接加载地图,当电脑没有网络时,控制台会持续报错:
当然这些错误仅仅在控制台中显示,而且除了地图模块外其他功能不会受影响,但我有强迫症——在地图初始化之前先检测一下有没有网络连接,如果没联网就不初始化地图,这下果然没有报错了;但是有一天不小心点到了地图页面某个按钮,程序都没报错,直接没了!
后来通过Debug发现出错位置在ui->MapWidget->page()->runJavaScript()
那里,才恍然大悟,**地图功能都没初始化,我却在按钮槽函数调用了html中的函数。。。**赶紧改,面前有两个选择,一是去掉网络检测;二是设置一个标志位,在每次调用runJavaScript()之前检测一下标志位,。。。。我当然选择了后者
这个问题解决了,但是新的问题又出现了——如果刚运行程序时电脑没联网,地图没初始化,中间电脑网络恢复了 我又想用地图这怎么办?重启软件——这肯定不是一个好方法,稍微好一点的方法是设一个按钮重新调用一下地图初始化函数,OK,这个问题貌似也解决了,但是——这个初始化函数只能运行一次,实测的结果是当多次运行地图加载函数ui->MapWidget->page()->load(..)
时,程序会出错,控制台会持续报错
[08:53:20:219] Uncaught TypeError: Cannot read property 'x' of undefined qrc:/Baidu_JS/my_sample.html _line1
而且地图无法正常使用,卡顿,缩放时坐标跟着偏移,无法绘制轨迹等等,具体是什么什么原因我暂时也没搞明白。
还有一个问题 当主程序有多个坐标需要上传到地图进行标注或者绘制时,一般采用for循环的方式,感觉上没什么问题,但实际操作时会发现如果坐标不止一个,在地图上绘制的顺序是错乱的,而且有可能发过去十个坐标只标注出来五六个,一开始以为是绘制的问题,直到将发送的顺序与接收的顺序对比一下才发现接收的顺序就是错乱的,而且有丢失。这主要是因为 主程中发送相邻两个坐标的时间间隔都是在1ms之内,也就几百us,但是在地图上标注,绘制所花的时间远远大于1ms,大概是几十ms的样子,所以说如果地图端采用“接收一个、绘制一个”这样的方式的话,就会出现 第一个坐标还没处理完,接下来又接收到了好几个坐标,那这几个点来不及处理肯定就丢失了。这个问题最终采用的是信号与槽的方式解决,即主程序发送一个坐标后,地图端开始处理,等到处理完给主程序返回一个信号,主程序收到该信号后再发送下一个,这样以此类推直到最后一个
END
PS:在Qt中调试JS程序时,不像直接在Chrome浏览器调试一样,想看的信息可以直接console.log()一下,采用alert()吧又是阻塞的,有时候想看一下某个数据很难,这时候可以在bridge类中专门定义一个函数用于打印数据:
void bridge::printMessage(QString msg1, QString msg2)
{
qDebug()<<"JS_PRINT:"<<msg1<<" "<<msg2<<;
}
当在html中想看某个变量的值时,直接调用
mw.printMessage(data1,data2);
就可以在Qt的控制台看到了,而且显示几个参数完全可以自定义,调试时很方便。