QT中使用QWebEngineView和QWebChannel与HTML+JS进行互操作
使用WebEngineView与WebChannel,实现QT与html数据传输和事件响应。
1. 准备工作
1.1 项目配置
(1)使用QMake时,在pro文件中加入
QT += webchannel webengine
(2)使用CMake时,在CMakeList.txt中加入
find_package(Qt5 COMPONENTS Widgets WebEngineWidgets WebChannel REQUIRED)
target_link_libraries(webEngineTest PRIVATE Qt5::Widgets Qt5::WebEngineWidgets Qt5::WebChannel)
注意在QMake中不需要大小写区分,而CMake时就需要将大小写分开。
1.2 加入webEngineView和webChannel
在MainWindow.h中加入两个变量
QWebEngineView *webView = nullptr;
QWebChannel *webChannel = nullptr;
在MainWindow.cpp的相关函数中(可以是MainWindow的构造函数,也可以是菜单响应函数中)加入webEngineView
webView = new QWebEngineView(this); QString fpath = QCoreApplication::applicationDirPath(); webView->load( QUrl("file:///" + fpath + "/test.html")); webView->show();
加入webChannel
webChannel = new QWebChannel; webView->page()->setWebChannel(webChannel);
一定要注意:必须将webChannel设置为webEngineVIew的webChannel,才能通过webChannel与网页进行通信。
2. 准备交互的QT类
与html交互的主要工作需要一个QT类实现,这个类需要通过webChannel进行注册才能由js访问
下面是可以由js访问的WebClass类
#ifndef WEBCLASS_H #define WEBCLASS_H #include <QObject> #include <QMessageBox> class WebClass : public QObject { Q_OBJECT //Q_PROPERTY(QString content MEMBER m_content) Q_PROPERTY(QString content MEMBER m_content NOTIFY contentChanged) //该属性可由页面访问 public: WebClass(){}; QString getContent(){return m_content;} signals: void contentChanged(QString nc); public slots: void jscallme(const QString &text) //该函数是页面端调用的 { QMessageBox::information(NULL, "jscallme", text); } void jscallme() //该函数是页面端调用的 { QMessageBox::information(NULL, "jscallme", m_content); } private: QString m_content; }; #endif // WEBCLASS_H
它有几个约束:
(1)必须由QObject继承,并且添加了Q_OBJECT宏
(2)必须将JS可访问的函数设置为public slots
(3)如果JS需要访问其成员变量,除定义该变量(QString m_content;)外需要用Q_PROPERTY宏
Q_PROPERTY(QString content MEMBER m_content NOTIFY contentChanged) //该属性可由页面访问
其中content 为JS访问的变量名,m_content为本类的变量名;
NOTIFY contentChanged可以缺省,它表示当在C++变量发生变化时的发出的消息,该消息可由JS响应。
为了发送该消息,我们必须在WebClass类中声明一个信号函数,即
signals: void contentChanged(QString nc);
这个函数只能声明不能实现(它会由MOC编译实现),而响应函数由JS中指定:
webobj.contentChanged.connect(updateattribute);
这里webobj是WebClass注册后,在页面中的一个实例,contentChanged则是WebClass中的contentChanged信号函数,这一行将contentChanged信号与响应函数updateattribute进行关联,从而一旦C++对象中m_content成员的值由webobject->setProperty("content", v)函数进行修改(必须使用此函数,其他函数修改了不会引起信号)时,JS就会响应,它可以更新页面中相关元素的值。
3. 将交互类作为成员加入主类中
(1)加入类成员。我们这里的主类为MainWindow类
WebClass *webobj = nullptr;
(2)注册该成员
在主类的相应位置,注册该成员变量。我们在主类的构造函数中加入:
webChannel = new QWebChannel; webobj = new WebClass(); webChannel->registerObject("webobj", webobj); webView->page()->setWebChannel(webChannel);
注意,这里是在webEngineView设置webChannel之前完成了注册交互实体的工作。其中 webChannel->registerObject("webobj", webobj); 函数就是注册函数,"webobj"是JS访问该对象时的对象名, 后面的webobj则是C++对象实例的指针。通过WebChannel的注册工作,WebChannel就知道了该类的结构,并代理JS完成函数调用和成员访问。
4. 准备HTML页面
该页面中包含有一个特殊的JS文件,它是Qt目录"C:\Qt\Qt5.12.1\Examples\Qt-5.12.1\webchannel\shared"
下的qwebchannel.js
文件,要使用QWebChannel,必须引用该文件。因为QWebChannel
是由该文件定义,所以必须首先加载该文件。我们将该文件拷贝到html
目录下(当然也可以是需要的其他目录),然后编辑例子文件:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> <script src="qwebchannel.js"></script> <script type="text/javascript"> var webChannel = new QWebChannel(qt.webChannelTransport, //这里的webChannel是全局的变量,可以在其它位置访问 function(channel){ var webobj = channel.objects.webobj; window.foo = webobj; //将此webobj赋给了window.foo,则可以在其他函数中访问该对象(其中foo是任意合法名称,表示给window增加了一个成员) webobj.content = 'sdfef中文'; webobj.jscallme(); document.getElementById("ctext").innerHTML = webobj.content; webobj.contentChanged.connect(updateattribute); }); var updateattribute=function(text) { //document.write(text); //var webobj = webChannel.objects.webobj; //访问全局变量webChannel alert(window.foo.content); //这里可以访问全局的window.foo,它就是我们注册的webobj document.getElementById("mytext").innerHTML = text; //alert(webobj.content); } </script> <p id="mytext">This is my first html</p> <p id="ctext"></p> </body> </html>
(1)页面文件首先包含了qwebchannel.js,它是使用WebChannel的基础;
(2)页面创建了一个QWebChannel实例,作为全局变量,这样别的JS代码也可以访问它了;
(3)QWebChannel实例的构造函数有两个参数,第一个不清楚,且保持原样。第二个参数是一个回调函数(姑且这样称),该函数在创建时调用,函数的参数就是本次创建的QWebChannel实例;
(4)在var webobj = channel.objects.webobj;一行中,channel.objects.webobj是我们在QT主类中注册的webobj对象实例,我们将它赋值给了一个变量,以便于引用;
(5)下面一行window.foo = webobj,则表示为全局的window实例增加一个成员foo(这个名字可以自己定),它是webobj的别名,这样我们可以在别的地方访问webobj;
(6)下面三行是对注册实例webobj的访问
webobj.content = 'sdfef中文'; //访问在QT中定义为content的成员变量,其在QT中的成员是m_content; webobj.jscallme(); //调用该对象的public slots函数 document.getElementById("ctext").innerHTML = webobj.content; //使用该对象的成员为html元素赋值
(7)最后一行最为关键,它将QT的信号与JS的响应函数关联,从而响应QT发出的信号
webobj.contentChanged.connect(updateattribute);
(8)其中updateattribute是JS定义的响应函数名称,下面是它的定义:
var updateattribute=function(text) { //document.write(text); //var webobj = webChannel.objects.webobj; //访问全局变量webChannel alert(window.foo.content); //这里可以访问全局的window.foo,它就是我们注册的webobj document.getElementById("mytext").innerHTML = text; }
需要注意的是,这个响应函数应当与QT中C++的信号函数在参数上保持一致。
可以看出,这里可以访问全局成员window.foo
5. 程序全部代码
5.1 main.cpp
//main.cpp #include "mainwindow.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }
5.2 mainwindow类
//mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QtWebEngineWidgets/QWebEngineView> #include "webclass.h" #include <QtWebChannel/QWebChannel> QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void on_pushButton_clicked(); private: Ui::MainWindow *ui; QWebEngineView *webView = nullptr; QWebChannel *webChannel = nullptr; WebClass *webobj = nullptr; }; #endif // MAINWINDOW_H
//mainwindow.cpp #include "mainwindow.h" #include "./ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); webView = new QWebEngineView(this); QString fpath = QCoreApplication::applicationDirPath(); webView->load( QUrl("file:///" + fpath + "/test.html")); webView->show(); webView->move(0, 60); webView->resize(this->size().width(), this->size().height() - 60); //mainLayout->addWidget(webView); webChannel = new QWebChannel; webobj = new WebClass(); webChannel->registerObject("webobj", webobj); webView->page()->setWebChannel(webChannel); } MainWindow::~MainWindow() { delete ui; } int times = 1; void MainWindow::on_pushButton_clicked() { QString t = QString::number(times++); QString text = "1234567"; text.append(t); QVariant v(text); qDebug() << v.typeName() << endl; webobj->setProperty("content", v); qDebug() << webobj->getContent() << endl; }
本类中,on_pushButton_clicked是mainWindow中加入的一个pushbutton的响应函数,这个函数调用webobj->setProperty("content", v);来改变名为content的成员变量的值,并发送contentChanged信号,由页面响应。这里"content"是m_content在JS中的名称,本函数实际上是调用了JS来改变对象webobj的成员变量值,然后触发相关事件响应。
5.3 webclass类(交互注册类)
//webclass.h #ifndef WEBCLASS_H #define WEBCLASS_H #include <QObject> #include <QMessageBox> class WebClass : public QObject { Q_OBJECT //Q_PROPERTY(QString content MEMBER m_content) Q_PROPERTY(QString content MEMBER m_content NOTIFY contentChanged) //该属性可由页面访问 public: WebClass(); QString getContent(){return m_content;} signals: void contentChanged(QString nc); public slots: void jscallme(const QString &text) //该函数是页面端调用的 { QMessageBox::information(NULL, "jscallme", text); } void jscallme() //该函数是页面端调用的 { QMessageBox::information(NULL, "jscallme", m_content); } private: QString m_content; }; #endif // WEBCLASS_H
//webclass.cpp #include "webclass.h" WebClass::WebClass() { }
WebClass有两个slot可供JS调用,其中一个是带参的。这个参数前面的const是不能去掉的,否则会产生错误:Could not convert argument QJsonValue(string, “sd”) to target type . 可见,WebChannel的通信是通过JSON进行的。
5.4 HTML页面
见4.
5.5 CMakeList.txt
cmake_minimum_required(VERSION 3.5) project(ScenarioBuilder LANGUAGES CXX) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_PREFIX_PATH $ENV{QT5_14_0_vs2017_64}) # QtCreator supports the following variables for Android, which are identical to qmake Android variables. # Check http://doc.qt.io/qt-5/deployment-android.html for more information. # They need to be set before the find_package(Qt5 ...) call. #if(ANDROID) # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") # if (ANDROID_ABI STREQUAL "armeabi-v7a") # set(ANDROID_EXTRA_LIBS # ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so # ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so) # endif() #endif() find_package(Qt5 COMPONENTS Widgets WebEngineWidgets WebChannel REQUIRED) if(ANDROID) add_library(ScenarioBuilder SHARED main.cpp mainwindow.cpp mainwindow.h mainwindow.ui interactor.h interactor.cpp mywebview.cpp mywebview.h ) else() add_executable(ScenarioBuilder main.cpp mainwindow.cpp mainwindow.h mainwindow.ui interactor.h interactor.cpp mywebview.cpp mywebview.h ) endif() target_link_libraries(ScenarioBuilder PRIVATE Qt5::Widgets Qt5::WebEngineWidgets Qt5::WebChannel)