實現QT與HTML頁面通訊
1. 前言
最近,C++和WEB本地混合應用開發模式逐漸流行起來,個人也認為標記語言描述的介面是介面開發的一個發展趨勢。WPF、Java FX,當然也少不了Html。基於Html的介面在開發效率,可移植性上都十分有優勢,所以也被很多程式採用
隨著HTML5技術風生水起,Qt開發團隊用近一年的時間開發了一個全新的基於Chromium的瀏覽器引擎Qt WebEngine,以支援面向未來的Hybrid應用開發,並完全支援桌面和嵌入式平臺。此外,Qt WebEngine不僅提供了易於使用的跨平臺API,還完全集成了Qt的圖形庫,允許網頁內容進行疊加,並與Qt使用者介面或OpenGL圖形效果混合。Qt 5.4仍然包含老舊的Qt WebKit模組,但在其後釋出的版本中將停止對於WebKit的支援,對此,Qt團隊建議開發者將專案統一從WebKit遷移至Qt WebEngine,而對於需要Web能力的新專案,最好直接採用Qt WebEngine開發。在現在下載的QT5.6 beta預編譯版本中已經沒有了Qt WebKit模組。
目前QT官方的文件中對如何從原來的WebKit遷移至QtWebEngine沒有提供足夠豐富的文件和指導。
本文記錄一些從WebKit遷移至QtWebEngine,實現C++與HTML和JS互動的一些經驗和例子。
2. 使用Qt WebEngine和WebChannel模組
在官方提供的Porting from QtWebKit to QtWebEngine---https://wiki.qt.io/Porting_from_QtWebKit_to_QtWebEngine文件中給出了從WebKit遷移至QtWebEngine的一些建議和指導,其中提到QWebFrame 已經被合併到QwebEnginePage中,Qt WebEngine不能和QnetworkAccessManager互動等。
原來在Webkit中使用的互動整合的類和方法也不能使用了,原來Webkit中常用的互動方式如下:
1) 在Qt程式碼中載入並顯示該頁面
QWebView view;
view.load(QUrl("test.html"));
view.show();
官方推薦的使用方式:
view->load(QUrl("http://qt-project.org/"));
view->show();
2) 在Qt物件中訪問web頁面元素
// myPlugin指向的物件可在HTML中用名字myPluginObject進行訪問
webView->page()->mainFrame()->addToJavaScriptWindowObject("myPluginObject",myPlugin);
// 當訊號signalEmitted被觸發時,呼叫JavaScript的functionToCall函式
webView->page()->mainFrame()->evaluateJavaScript("myPluginObject.signalEmitted.connect(functionToCall);")
官方推薦的使用方式:
方法一:(const QString & scriptSource, FunctorOrLambdaresultCallback)
方法二:使用QtWebChannel方式,這是官方的推薦方式,他可以很方便的實現C++和HTML/JS的雙向通訊,同時實現C++和HTML/JS的解耦,方便開發人員的分工及系統整合,參見後面的例子。
3) 在web頁面中訪問Qt物件
在web頁面中可以通過類似於下的JavaScript程式碼訪問Qt物件:
<ahref="javascript:document.getElementByIdx_x("myLabel").setText("通過JavaScript訪問Qt物件");"mce_href="javascript:document.getElementByIdx_x("myLabel").setText("通過JavaScript訪問Qt物件");">點選訪問Qt物件</a>
官方推薦的使用方式:
使用QtWebChannel方式,這是官方的推薦方式,他可以很方便的實現C++和HTML/JS的雙向通訊,同時實現C++和HTML/JS的解耦,方便開發人員的分工及系統整合,參見後面的例子。
在QT5.5和QT5.6中,利用Qt的Qt WebEngine和WebChannel模組,你完全可以進行本地桌面與web混合應用開發,你可以自由地混合JavaScript,樣式表,Web內容和Qt元件。基於Chromium的 Qt WebEngine是一個非常成熟的web瀏覽引擎。你可以在C++ 中執行JavaScript,或者在網頁中整合C++物件,並且通過JavaScript和這些物件進行互動。
一個現代的HTML渲染引擎只是混合開發的一半,另一半就是本地應用和渲染物件的互動。QT的Chromium的 Qt WebEngine 整合提供了這種解決方案,當然目前還不是很完善。
從QT5.4開始就新增了Qt WebChannel模組,該模組提供了在QML/C++ 和 HTML/Javascript之間的一個簡單、易用的橋接,從而使得開發能夠使用Qt和Web技術進行混合開發,目前QT官方也推薦是用QtWebChannel來橋接C++和HTML,參見Qt WebChannel – bridging the gap betweenC++/QML and the web------https://www.youtube.com/watch?v=KnvnTi6XafA,這是2014年Qt開發者大會上的一段視屏演講。
通過QtWebChannel能夠實現C++/QML和HTML/javascript客戶端之間進行無縫橋接,目前主要支援兩種方式的橋接。
- l 客戶機可以是任何支援WebSockets的JavaScript runtime機器和應用,客戶機可以是獨立的應用或瀏覽器。也就是說QtWebChannel是基於WebSockets協議之上。
- l 通過IPC的方式實現C++/QML和HTML/javascript客戶端之間進行通訊。在QT應用內嵌入HTML或JS頁面的情況下使用基於IPC的WebChannel通訊效率更高效。
在官方演進路線中提到將來的整合解決方案將支援以下特性:
- l 使用object標籤嵌入Qt Widgets元件。這可以讓使用C++程式碼的Qt元件包含在網頁中,作為網頁的部分外觀。
- l 在JavaScript中訪問C++物件。你可以在JavaScript環境中插入C++物件,讓網頁尾本直接訪問你的資料結構。
- l 在Qt中執行JavaScript。你可以在C++呼叫網頁環境中的JavaScript函式,觸發網頁事件。
- l 共享客戶端儲存。在JavaScript和C++中你都具有訪問資料庫的能力,這樣當下線時也能共享大量資料。
3. QtWebChannel實現C++和HTML/javascript頁面之間通訊例項
在例項中在一個介面裡實現整合QT C++元件和QWebEnginePage中嵌入的HTML頁面通訊,左邊的lineedit元件中輸入的內容通過一個document物件又QtWebChannel傳到web頁面,並又js解析在HTML頁面上顯示,右邊HTML頁面中的INPUT標籤中輸入的內容也由document物件通過QtWebChannel傳到QT C++介面。通過QtWebChannel使得JS能夠接收到QT C++中傳出的物件,及由C++發出的訊號,並與相對應的方法關聯,C++也能介紹到由JS傳來的物件,並呼叫相應的槽函式。
工程清單如下:
首先,定義一個document物件用於在C++和JS之間傳遞,該物件實現了訊號和槽:
Document.h內容:
#ifndefDOCUMENT_H
#defineDOCUMENT_H
#include<QObject>
#include<QString>
#include"ui_mainwidget.h"
namespaceUi{
classMainWidget;
}
classDocument:publicQObject
{
Q_OBJECT
Q_PROPERTY(QStringtextMEMBERs_textNOTIFYsendText)
public:
explicitDocument(QObject*parent=nullptr):QObject(parent){}
voidsetSendTextText(constQString&text);
voidsetUi(Ui::MainWidget*ui);
publicslots:
voidreceiveText(constQString&r_text);
signals:
voidsendText(constQString&text);
private:
voiddisplayMessage(constQString&message);
QStrings_text;
QStringrecieve_text;
Ui::MainWidget*mainUi;
};
#endif//DOCUMENT_H
Document.cpp內容:
#include"document.h"
voidDocument::setSendTextText(constQString&text)
{
s_text=text;
emitsendText(s_text);
}
voidDocument::displayMessage(constQString&message)
{
mainUi->editor->appendPlainText(message);
}
/*!
ThisslotisinvokedfromtheHTMLclientsideandthetextdisplayedontheserverside.
*/
voidDocument::receiveText(constQString&r_text)
{
displayMessage(QObject::tr("Receivedmessage:%1").arg(r_text));
}
voidDocument::setUi(Ui::MainWidget*ui)
{
mainUi=ui;
}
用QT Designer設計主介面,並實現主介面MainWidget類,內容如下:
mainwidget.h內容:
#ifndefMAINWIDGET_H
#defineMAINWIDGET_H
#include"document.h"
#include<QWidget>
#include<QString>
namespaceUi{
classMainWidget;
}
classMainWidget:publicQWidget
{
Q_OBJECT
public:
explicitMainWidget(QWidget*parent=0);
~MainWidget();
//publicQ_SLOTS:
//voidsetEnabled(bool);
privateslots:
voidon_pushButton_clicked();
private:
boolisModified()const;
Ui::MainWidget*ui;
Documentm_content;
};
#endif//MAINWIDGET_H
mainwidget.cpp內容:
#include"mainwidget.h"
#include"ui_mainwidget.h"
#include"previewpage.h"
#include"document.h"
#include<QFile>
#include<QWebChannel>
MainWidget::MainWidget(QWidget*parent):
QWidget(parent),
ui(newUi::MainWidget)
{
ui->setupUi(this);
PreviewPage*page=newPreviewPage(this);
ui->preview->setPage(page);
m_content.setUi(ui);
QWebChannel*channel=newQWebChannel(this);
channel->registerObject(QStringLiteral("content"),&m_content);
page->setWebChannel(channel);
ui->preview->setUrl(QUrl("qrc:/index.html"));
ui->editor->setPlainText("hello...\n");
}
MainWidget::~MainWidget()
{
deleteui;
}
boolMainWidget::isModified()const
{
returnui->editor->document()->isModified();
}
voidMainWidget::on_pushButton_clicked()
{
m_content.setSendTextText(ui->lineEdit->text());
}
再定義一個PreviewPage類用於載入HTML頁面,在主介面MainWidget類初始化的時候,將他主介面中的WebEngineView初始化為該例項物件,主要初始化程式碼如下:
PreviewPage*page=newPreviewPage(this); //建立例項物件
ui->preview->setPage(page); //將物件設定到主介面
ui->preview->setUrl(QUrl("qrc:/index.html")); //設定載入的HTML頁面
previewpage.h內容:
#ifndefPREVIEWPAGE_H
#definePREVIEWPAGE_H
#include<QWebEnginePage>
classPreviewPage:publicQWebEnginePage
{
Q_OBJECT
public:
explicitPreviewPage(QObject*parent=nullptr):QWebEnginePage(parent){}
protected:
boolacceptNavigationRequest(constQUrl&url,NavigationTypetype,boolisMainFrame);
};
#endif//PREVIEWPAGE_H
previewpage.cpp內容:
#include"previewpage.h"
#include<QDesktopServices>
boolPreviewPage::acceptNavigationRequest(constQUrl&url,
QWebEnginePage::NavigationType/*type*/,
bool/*isMainFrame*/)
{
//Onlyallowqrc:/index.html.
if(url.scheme()==QString("qrc"))
returntrue;
QDesktopServices::openUrl(url);
returnfalse;
}
使用的web頁面index.html內容如下:
<!DOCTYPEhtml>
<html>
<head>
<metahttp-equiv="Content-Type"content="text/html;charset=utf-8"/>
<scripttype="text/javascript"src="./qwebchannel.js"></script>
<scripttype="text/javascript">
//BEGINSETUP
functionoutput(message)
{
varoutput=document.getElementById("output");
output.innerHTML=output.innerHTML+message+"\n";
}
window.onload=function(){
output("settingupQWebChannel.");
newQWebChannel(qt.webChannelTransport,function(channel){
//makedialogobjectaccessibleglobally
varcontent=channel.objects.content;
document.getElementById("send").onclick=function(){
varinput=document.getElementById("input");
vartext=input.value;
if(!text){
return;
}
output("Sentmessage:"+text);
input.value="";
content.receiveText(text);
}
content.sendText.connect(function(message){
output("Receivedmessage:"+message);
});
content.receiveText("Clientconnected,readytosend/receivemessages!");
output("ConnectedtoWebChannel,readytosend/receivemessages!");
});
}
//ENDSETUP
</script>
<styletype="text/css">
html{
height:100%;
width:100%;
}
#input{
width:400px;
margin:010px00;
}
#send{
width:90px;
margin:0;
}
#output{
width:500px;
height:300px;
}
</style>
</head>
<body>
<textareaid="output"></textarea><br/>
<inputid="input"/><inputtype="submit"id="send"value="Send"onclick="javascript:click();"/>
</body>
</html>
主程式main.cpp的內容如下:
#include"document.h"
#include"mainwidget.h"
#include<QApplication>
intmain(intargc,char*argv[])
{
QApplicationa(argc,argv);
MainWidgetw;
w.show();
returna.exec();
}