在项目开发中,常常会有在原生应用程序中嵌入 HTML 页面或者 Web 项目,并且需要应用程序与所加载的 HTML 页面的相互通信的需求。

本篇文章基于 Qt 框架,讲解如何使用 Qt WebChannel 实现 C++/QML 和 HTML 页面之间交互,包括:

  • 在 HTML 页面调用 C++/QML 对象中的函数(异步)
  • 向 HTML 中发送 QML/C++ 信号
  • 在 HTML 中调用 QML/C++ 对象的属性
  • 在 HTML 中调用 QML/C++ 对象的枚举类型和枚举值

2011 年 10 月,诺姆·罗森塔尔(Noam Rosenthal)提出了这个方法,他称之为 Qt WebChannel。他的想法既简单又强大:通过利用 Qt 的自省(introspection,也叫内省)系统,他可以在 JavaScript 客户端创建模拟对象,该对象可以反射服务端 QML/QObject 对象的 API。对于 QML 主机和 HTML/JavaScript 客户端之间的通信方式,他选择了 WebSockets,但是 WebChannel 提供的 API 是完全异步的

Qt WebChannel 支持服务端(QML/C++ 应用程序)和客户端(HTML/JavaScript 或 QML 应用程序)之间的点对点(peer-to-peer)通信。它提供了一个 JavaScript 库,用于将 C++ 和 QML 应用程序与 HTML/JavaScript 和 QML 客户端无缝集成。客户端必须使用 JavaScript 库来访问主机端应用程序发布的序列化的 QObjects 对象。

注:本文中提到的客户端和服务端其实是在同一个应用程序内,因为 WebChannel 是基于 WebSocket 实现的,所以会有这种客户端和服务端的叫法。

QML/HTML 混合应用程序

本节演示 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)
        //}
    }
}

HTML / JavaScript 客户端

在客户端,首先,通过 Qt 资源 URL 包含客户端 qwebchannel.js 库(该文件可以在 %QtDir%Srcqtwebchannelexampleswebchannelsharedqwebchannel.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 实例。

 参考:https://www.pressc.cn/1085.html

---------------------------------------------------------------------------------------------------------------------------------------------------