项目背景
项目中,遇到在嵌入式下展示html内容,并与之交互,Qt允许使用所谓的混合GUI创建应用程序——在这种GUI中,可以将本机部件与基于html的内容混合在一起。通过WebChannel和WebSockets提供交互,这种混合甚至支持这些本地部分和html端之间的交互。有三种方式:
- QWebkit
- QWebEngine
- QWebView
QWebkit 在Qt5.6之后已经不再支持,本文只针对QWebEngine和 QWebView。
如何显示HTML内容
使用webEngineView;
使用webView;
使用独立的Web浏览器(不会集成到应用程序中);
这三种方法以不同的方式进行,但都支持QML和HTML之间的通信。
确切的说,WebEngineView以一种方式完成,而WebView(就像网络浏览器一样)以另一种方式完成。WebEngineView和WebView是两码事。
webEngineView
WebEngineView是由Qt自己基于Chromium (Qt WebEngine)的web浏览器引擎提供的web视图。它是一个功能齐全的web浏览器,与Qt捆绑并集成在一起,这很好,但同时这意味着您需要将它与您的应用程序一起拖动,这是一个相当大的东西。
webView
WebView是一个web视图,但不同之处在于它使用平台的本地web浏览器(如果可用的话),因此它不需要将完整的web浏览器堆栈作为应用程序的一部分(WebEngineView就是这种情况),因此您的应用程序更轻量级。另一点是,有些平台根本不允许任何非系统的web浏览器,因此WebView是唯一可用的选项。
webEngineView 和 webView的区别
根据本文,WebEngineView和WebView的关键区别在于Qt如何与这些视图中的html内容通信。由于Chromium IPC功能,WebEngineView提供了最简单的方式-直接通过WebChannel,。而WebView(以及外部web浏览器)要求您首先为WebChannel建立一些传输。
QML/HTML 混合应用程序
本应用用到了 WebChannel 和 WebEngine 两个主要模块,所以要将下面这行添加到 qmake .pro 文件中:
QT += webchannel webengine
QML 服务端
在 QML 服务端,首先导入 Qt WebChannel 模块,以及 Qt WebEngine 模块:
import QtWebChannel 1.0
import QtWebEngine 1.5
然后创建一个想要发布到 HTML/JavaScript 客户端的对象:
QtObject {
id: myObject
// 注册方法 1
// 使用注册方法 2 时不需要此行代码
WebChannel.id: "foo" //这个 id 可以在 html 中使用
// 以下为 JavaScript 代码可以访问的信号、方法和属性
signal someSignal(string message);
function someMethod(message) {
console.log(message);
someSignal(message);
return "foobar";
}
property string hello: "world"
}
最后将该对象在 WebView 控件中发布到 HTML 客户端中:
WebEngineView {
anchors.fill: parent
url: "file:///C:/test.html"
webChannel: WebChannel {
id: webChannel
// 注册方法 1
registeredObjects: [myObject]
// 注册方法 2
//Component.onCompleted: {
// // "foo" 是该对象在 JavaScript 端的调用标识
// webChannel.registerObject("foo", myObject)
//}
}
}
main.qml文件如下
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 1.4
import QtWebView 1.1
import QtWebChannel 1.14
import QtWebEngine 1.10
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
QtObject {
id: myObject
// 注册方法 1
// 使用注册方法 2 时不需要此行代码
WebChannel.id: "foo" //这个 id 可以在 html 中使用
// 以下为 JavaScript 代码可以访问的信号、方法和属性
signal someSignal(string message);
function someMethod(message) {
console.log(message);
someSignal(message);
return "foobar";
}
property string hello: "world"
}
WebEngineView {
anchors.fill: parent
url: "qrc:/1.html"
webChannel: WebChannel {
id: webChannel
// 注册方法 1
registeredObjects: [myObject]
// 注册方法 2
//Component.onCompleted: {
// // "foo" 是该对象在 JavaScript 端的调用标识
// webChannel.registerObject("foo", myObject)
//}
}
}
}
html文件如下
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
</head>
<script type="text/javascript" src="qrc:/qwebchannel.js"></script>
<script type="text/javascript">
function test(){
new QWebChannel(qt.webChannelTransport, function(channel) {
// 所有通过 WebChannel 发布的对象都可以在 channel.objects 中访问的到
window.foo = channel.objects.foo;
// 访问一个属性
alert(foo.hello);
// Writing a property will instantly update the client side cache.
// The remote end will be notified about the change asynchronously
foo.hello = "Hello World!";
// 连接信号
foo.someSignal.connect(function(message) {
alert("Got signal: " + message);
});
// 调用方法,并*异步*接收返回值
foo.someMethod("bar", function(ret) {
alert("Got return value: " + ret);
});
// One can also access enums that are marked with Q_ENUM:
//console.log(foo.MyEnum.MyEnumerator);
});
}
</script>
<body>
<div><button type="button" onClick="test()>Click Me!</button></div>
</body>
</html>
HTML / JavaScript 客户端
在客户端,首先,通过 Qt 资源 URL 包含客户端 qwebchannel.js 库(该文件可以在 %QtDir%\Src\qtwebchannel\examples\webchannel\shared\qwebchannel.js 中找到,经过测试在 Qt 5.12 以后不将该文件加入资源也可),并在 HTML 文件中插入一段 JavaScript:
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
然后,在 JavaScript 代码中实例化 QWebChannel 对象并设置回调函数。当 web 通道的初始化成功时,将调用回调函数。此外,您可以传递 qt.webChannelTransport 对象到 channel 中,详见下文:
new QWebChannel(qt.webChannelTransport, function(channel) {
// 所有通过 WebChannel 发布的对象都可以在 channel.objects 中访问的到
window.foo = channel.objects.foo;
// 访问一个属性
alert(foo.hello);
// Writing a property will instantly update the client side cache.
// The remote end will be notified about the change asynchronously
foo.hello = "Hello World!";
// 连接信号
foo.someSignal.connect(function(message) {
alert("Got signal: " + message);
});
// 调用方法,并*异步*接收返回值
foo.someMethod("bar", function(ret) {
alert("Got return value: " + ret);
});
// One can also access enums that are marked with Q_ENUM:
//console.log(foo.MyEnum.MyEnumerator);
});
C++/QML/HTML 混合应用程序
利用在 QML 应用程序中可以引用 C++ 类的这个特点,我们也可以实现在 HTML 页面中调用 C++ 类的需求。
首先定义 C++ 对象并将它注册到 QML 中,我们在 main.cpp 中添加下面的一行,注意, TestObejct 类必须直接继承自 Object:
qmlRegisterType<TestObejct>("TestObejct", 1, 0, "TestObejct");
然后在 QML 文件中将这个类对象注册到 WebChannel,你可以使用上节中提到的方法直接调用 C++ 类中已存在的信号、方法、属性和枚举类型,也可以在 QML 中继续扩展其他方法:
import TestObejct 1.0
…
TestObejct {
id: myObject
WebChannel.id: “foo”
// 在 QML中可以继续扩展信号、方法和属性
signal someSignal2(string message);
function someMethod2(message) {
console.log(message);
someSignal2(message);
return "foobar";
}
property string hello2: "world"
}
Qt WebChannel 不只可以在 QML 应用程序中使用,在纯 Qt/C++ 应用程序中也可以创建一个 QWebChannel 并发布 QObject 实例
项目需求(html页面定位)
项目用到了html页面定位功能,即点击一个导航按钮,使页面直接跳转到该位置(滚动条直接滚到到位置)
有两种方式实现:
1 借用a标签的href定位,瞄点
<div id="content">
<div class="btn-container">
<a class="btn" href="#anchor1">锚点1</a>
<a class="btn" href="#anchor2">锚点2</a>
<a class="btn" href="#anchor3">锚点3</a>
</div>
<div id="anchor1" class="anchor-con">anchor1</div>
<div id="anchor2" class="anchor-con">anchor2</div>
<div id="anchor3" class="anchor-con">anchor3</div>
</div>
2 简单的window.scrollTo方法使用
该方法需要在html文件中加载jquery.min.js:
<script type=\"text/javascript\" src=\"qrc:/jquery.min.js\"></script>
html 中js方法
function textClicked(){
$(window).scrollTop($(\"#pos0\").offset().top);
}
html 标签
<div id=\"pos1\"><span class=\"Title\">考点解析</span></div>
单击导肮按钮 “文字解析”即可实现定位
<button id=\"textPos\" class=\"topnavi\" onClick=\"textClicked()\">文字解析</button>
遇到的问题:
1 qwebchannel.js 版本不一样 ,项目中刚开始始终在html文件中调用QtObject方法不可行,后来发现qwebchannel.js文件不对
2 html中显示中文问题,webengine找不到中文字库,因为在嵌入式下,需要自己手动编译
3 Uncaught ReferenceError: $ is not defined报错;$未定义是为什么呢?
原因一:你未引用jquery库jquery.min.js文件,或者说路径错误;
扩展 :Chrome打开PDF报错:Not allowed to navigate top frame to data URL
Chrome打开PDF报错:Not allowed to navigate top frame to data URL
var pdf = "data:application/pdf;base64," + data;
window.open(pdf);
在Chrome使用window.open()打开pdf报错:
Not allowed to navigate top frame to data URL: data:application/pdf;base64,JVBERi0xLjMKMyAwIG9iago8PC9UeXBlIC9QYWdlCi9QYXJlbnQgMSAwIFIKL1…
原因分析
Chrome 60 开始禁止页面使用data:url的方式跳转导航,禁止data:url导航的使用包括:
<a href="data:xxxxx"> 点击跳转
window.open(“data:xxx”)
window.location="data:xxx"
对于使用data:url直接来加载数据的场景不会禁止,如
直接加载图片
解决方案
Chrome并没有禁止直接使用data:url的方式加载数据,如iframe,所以可以把数据放到iframe的属性src里。
var win = window.open();
var pdf = "data:application/pdf;base64," + data;
win.document.write('<iframe src="' + pdf + '" frameborder="0" style="border:0; top:0px; left:0px; bottom:0px; right:0px; width:100%; height:100%;" allowfullscreen></iframe>')
扩展:JS控制div跳转到指定的位置的解决方案总结
1.给链接a加个#的方式来实现跳转
html页面:
<div id="container">
<a href="#div1">div1</a>
<a href="#div2">div2</a>
<a href="#div3">div3</a>
</div>
<div id="div1">div1</div>
<div id="div2">div2</div>
<div id="div3">div3</div>
css样式:
div {
height: 800px;
width: 400px;
border: 2px solid black;
}
#container{
position: fixed;
margin:50px 500px;
}
该锚点法,不需要任何的js代码,即可实现跳转的方法。缺点:点击链接url发生变化,刷新的话会有问题。此方法貌似只能在.html后缀的页面才能起作用,对于.cshtml页面不起作用。
2.用animate属性,当点击锚点后,页面滚动到相应的DIV。
接着上面的代码,具体添加如下代码:
html页面:
<div id="container">
<p id="p1">div1</p>
<p id="p2">div2</p>
<p id="p3">div3</p>
</div>
<div id="div1">div1</div>
<div id="div2">div2</div>
<div id="div3">div3</div>
css样式页面同上,看下js代码如下:
<script type="text/javascript" src="http://code.jquery.com/jquery-1.8.0.min.js"></script>
$(document).ready(function() {
$("#p1").click(function() {
$("html, body").animate({
scrollTop: $("#div1").offset().top }, {duration: 500,easing: "swing"});
return false;
});
$("#p2").click(function() {
$("html, body").animate({
scrollTop: $("#div2").offset().top }, {duration: 500,easing: "swing"});
return false;
});
$("#p3").click(function() {
$("html, body").animate({
scrollTop: $("#div3").offset().top }, {duration: 500,easing: "swing"});
return false;
});
});
3.简单的window.scrollTo方法使用
即滚动到坐标为(100,500)的地方。比较单一,且没有缓慢的效果
function scrollWindow(){
window.scrollTo(100,500);
}
4.用js的srollIntoView方法进行使用。这里贴下代码:
html页面:
<ul>
<li><a href="javascript:;" data-tab="eat">吃饭</a></li>
<li><a href="javascript:;" data-tab="sleep">睡觉</a></li>
<li><a href="javascript:;" data-tab="walk">逛街</a></li>
</ul>
<div >
<div data-tab="eat" style="background:cyan; height:500px;">
吃饭
</div>
<div data-tab="sleep" style="background:lightgreen;height:2000px;">
睡觉
</div>
<div data-tab="walk" style="background:LightSalmon;height:1000px;">
逛街
</div>
</div>
js代码如下:
[].slice.call(document.querySelectorAll('a')).forEach(function(el){
el.addEventListener('click', function(){
var target = document.querySelector('div[data-tab=' + this.getAttribute('data-tab')+ ']' )
target.scrollIntoView(true);
})
})
注意一个问题,object.scrollIntoView(bool),前面是对象,通常原生的获取对象都是下面的写法:
document.getElementById('#xxx' ).scrollIntoView(true);
那么如果用jquery来调用该方法的话,需要写成下面这样的:
$('#xxx')[0].scrollIntoView(true);