在項目開發中,經常會有在原生應用程序中嵌入 HTML 頁面或者 Web 項目,而且須要應用程序與所加載的 HTML 頁面的相互通訊的需求。javascript
本篇文章基於 Qt 框架,講解如何使用 Qt WebChannel 實現 C++/QML 和 HTML 頁面之間交互,包括:php
2011 年 10 月,諾姆·羅森塔爾(Noam Rosenthal)提出了這個方法,他稱之爲 Qt WebChannel。他的想法既簡單又強大:經過利用 Qt 的自省(introspection,也叫內省)系統,他能夠在 JavaScript 客戶端建立模擬對象,該對象能夠反射服務端 QML/QObject 對象的 API。對於 QML 主機和 HTML/JavaScript 客戶端之間的通訊方式,他選擇了 WebSockets,可是 WebChannel 提供的 API 是徹底異步的。html
Qt WebChannel 支持服務端(QML/C++ 應用程序)和客戶端(HTML/JavaScript 或 QML 應用程序)之間的點對點(peer-to-peer)通訊。它提供了一個 JavaScript 庫,用於將 C++ 和 QML 應用程序與 HTML/JavaScript 和 QML 客戶端無縫集成。客戶端必須使用 JavaScript 庫來訪問主機端應用程序發佈的序列化的 QObjects 對象。java
注:本文中提到的客戶端和服務端實際上是在同一個應用程序內,由於 WebChannel 是基於 WebSocket 實現的,因此會有這種客戶端和服務端的叫法。c++
本節演示 QML 應用程序和 HTML 之間如何進行交互。git
本應用用到了 WebChannel 和 WebEngine 兩個主要模塊,因此要將下面這行添加到 qmake .pro 文件中:github
QT += webchannel webengine
在 QML 服務端,首先導入 Qt WebChannel 模塊,以及 Qt WebEngine 模塊:web
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 客戶端中:chrome
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)
//}
}
}
在客戶端,首先,經過 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);
});
利用在 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
---------------------------------------------------------------------------------------------------------------------------------------------------
本文適合有必定Qt及HTML經驗的人閱讀。
想要了解Qt(C++)和QML間的信息交互,就不得不提到Qt的信號與槽機制。
信號與槽是qt的特有信息傳輸機制。它本質上是一種觀察者模式。當某個事件觸發時,它就會發出一個相似廣播的信號。若是有對象對這個信號感興趣,它就使用鏈接函數,將想要處理的信號和本身的一個函數(qt中成爲槽)綁定來進行處理。當信號發出時,槽函數就會自動被執行。
咱們經過一個例子來進行說明。
首先,咱們定義一個c++的類,該類須要繼承QObject類,這樣纔有信號槽的能力。同時,須要在該類中添加Q_OBJECT宏。例:
#include <QObject>classMyClass : public QObject
{
Q_OBJECT
};
使用Q_PROPERTY定義屬性,該屬性可被qml使用。它還具備一些附加特性:READ用於讀屬性的值;WRITE用於設置屬性的值;NOTIFY則定義一個信號,該信號用來表示屬性發生改變。信號會攜帶一個參數,表示屬性的新值。l例如:
Q_PROPERTY(QString mystring READ getString WRITE setString NOTIFY mystringChanged)
QT使用connect函數來綁定信號和槽,例:
connect(this, SIGNAL(mystringChanged(QString)), this, SLOT(onMystringChanged(QString)));
上面代碼中,當有mystringChanged信號發出時,onMystringChanged函數就是被執行。信號與槽機制不只能夠用來進行c++類之間的通訊,也能夠用來實現c++和qml之間的通訊,咱們繼續使用上面的例子來講明。
當mystring有變化時,會觸發setString回調。咱們能夠在setString函數中觸發mystringChanged信號。
void MyClass::setString(QString string){
emit mystringChanged(string);//發送信號
}
QT使用qmlRegisterType方法將類註冊到QML中,例:
qmlRegisterType<MyClass>("RegisterMyType",1,0,"MyClassType");
其中,第一個參數是做爲QML中引用的url;第二個參數是主版本號;第三個參數是次版本號;第四個參數是QML中使用的元素名稱。本例中,QML模塊可使用下面方法引用該類,例:
import RegisterMyType 1.0
通過上面的步驟以後,咱們就能夠直接在QML中建立MyClassType對象了。例:
MyClassType {
id: myobj
}
對象建立成功後,咱們能夠爲QML綁定感興趣的信號了。
Connections {
target: myobj;
onMystringChanged: {
// 這裏的value是signal信號函數裏面的參數
console.log("value: " + value)
}
}
除了上面的方法,咱們還能夠經過直接使用對象的方式,來進行信號的綁定。在上面的例子中,咱們能夠下面的方式,咱們首先在C++代碼中作以下聲明:
QQmlApplicationEngine *qmlEngine = new QQmlApplicationEngine;
QQmlComponent component(qmlEngine, QUrl("qrc:/MyClass.qml"));
MyClass *myClass = qobject_cast<MyClass *>(component.create());
qmlEngine->rootContext()->setContextProperty("myQmlClass", myClass);
其中,QQmlComponent用來封裝QML組件。須要注意的是,MyClass.qml中,須要使用上面講到的MyClassType做爲頂層元素。 setContextProperty函數定義暴露給QML的對象。第一個參數是QML中使用的對象名稱,至關於重命名,可在QML中直接使用;第二個參數暴露給QML的對象。而信號的綁定,只須要將上面講到的Connections中的target修改成myQmlClass便可。即:
Connections {
target: myQmlClass;
onMystringChanged: {
// 這裏的value是signal信號函數裏面的參數
console.log("value: " + value)
}
}
Qt和HTML間的交互式經過WebChannel來實現的。
WebChannel提供一種機制使得QObject或者QML訪問HTML。全部的屬性,信號和公共的槽函數均可以被HTML使用。
WebChannel由一些屬性和方法組成。
屬性包括:
registeredObjects
A list of objects which should be accessible to remote clients.
The objects must have the attached id property set to an identifier, under which the object is then known on the HTML side.
Once registered, all signals and property changes are automatically propagated to the clients. Public invokable methods, including slots, are also accessible to the clients.
If one needs to register objects which are not available when the component is created, use the imperative registerObjects method.
簡單翻譯一下:
這是一個提供給HTML訪問的object列表。
object須要有id標識,這樣才能被HTML識別。
object一旦被註冊,全部的信號和屬性的改變都會被自動傳遞到客戶端。還包括公共的方法和槽。
若是組件建立時object還不可用,可使用registerObject方法。
transports
A list of transport objects, which implementQWebChannelAbstractTransport. The transports are used to talk to the remote clients.
一個傳輸列表,實現了QWebChannelAbstractTransport類。用來跟客戶端交互。
其它方法具體再也不贅述,能夠參考後面的參考文獻。這裏咱們主要講一下registeredObjects的用法。
registeredObjects提供給HTML訪問的object列表。object須聲明id屬性,這樣HTML才能知道它;同時,object要聲明WebChannel.id,HTML使用該id訪問對象。
QtObject定義以下:
QtObject {
id: myObject
WebChannel.id: "myWebObject"
property string name: "QtObjectName"
signal onMystringChanged(var myStr)
}
WebChannel定義以下:
WebChannel {
id: myChannel
registeredObjects: [myObject]
}
WebChannel聲明好以後,下面就是如何使用它。咱們定義WebEngineView元素,用來加載HTML文件,並指定關聯的WebChannel,使用方式以下:
WebEngineView {
id: webView
url: "./map.html"
webChannel: myChannel
}
至此,QML端的工做就已經完成了。下面講一下如何在HTML端使用WebChannel。
HTML想要使用QWebChannel,須要先引用qwebchannel庫,這是一個JavaScript類庫,使用方式以下:
<script type="text/javascript" src="qwebchannel.js"></script>
而後在增長以下代碼:
new QWebChannel(qt.webChannelTransport, function(channel) {
var myClass = channel.objects.myClass;
var myObject = channel.objects.myWebObject;
myObject.onMystringChanged.connect(function(myStr) {
console.log(myStr);
});
});
信號與槽機制是Qt的核心思想,咱們須要加深理解,在實際應用中靈活使用。
這裏只講了C++和QML,QML和HTML間的交互。C++和HTML間也能夠直接交互,之後有時間再來跟你們一塊兒分享。
能力有限,若是錯誤之處,請不吝賜教,謝謝。
參考:https://zhuanlan.zhihu.com/p/62987738
---------------------------------------------------------------------------------------------------------------------------------------------------
qmlRegisterType 是一個能夠將C++實現的類在QML中調用的,鏈接C++和QML的一個工具,很是重要的函數!!!
首先來看QtHelp關於qmlRegisterType 的介紹
int qmlRegisterType(const char * uri, int versionMajor, int versionMinor, const char * qmlName)
This template function registers the C++ type in the QML system with the name qmlName, in the library imported from uri having the version number composed from versionMajor and versionMinor.
Returns the QML type id.
能夠看到qmlRegisterType裏總共4個參數,第一個參數* uri指的是QML中import後的內容,至關於頭文件名,第二個第三個參數分別是主次版本號,第四個指的是QML中類的名字。
下面舉個例子
在main.cpp文件中
#include <QtQml>
qmlRegisterType<MySliderItem>("com.mycompany.qmlcomponents", 1, 0, "Slider");
在main.qml文件中:
import com.mycompany.qmlcomponents 1.0
Slider {
}
注意:第四個QML的類名首字母必定要大寫,要否則會報錯。。並且是那種你找不到的。。
有兩種使用方法:
int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)
int qmlRegisterType(const QUrl &url, const char *uri, int versionMajor, int versionMinor, const char *qmlName)
後面一種只是多了一個連接參數,能夠鏈接非本地的各種庫函數
QUrl url("http://www.example.com/List of holidays.xml");
// url.toEncoded() == "http://www.example.com/List%20of%20holidays.xml"
接下來咱們的學習將會伴隨 colorMaker 項目進行,等咱們講完,一個完整的 colorMaker 項目也會完成。須要新建兩個文件, colorMaker.h 和 colorMaker.cpp 。
colorMaker 只是一個示例項目,我在 C++ 中實現一個 ColorMaker 類,它能夠被註冊爲一個 QML 類型供 QML 像內建類型同樣使用,它的實例也能夠導出爲 QML 上下文屬性在 QML 中訪問。咱們的示例只是在界面頂部顯示當前時間(時間文字的顏色隨時間變化而變化),在界面中間顯示一個變色矩形,在界面底部放置幾個按鈕來控制顏色如何變化。
圖 1 是效果圖:
圖 1 colorMaker 效果圖
在 QML 中使用 C++ 類和對象
咱們知道, QML 實際上是對 JavaScript 的擴展,融合了 Qt Object 系統,它是一種新的解釋型的語言, QML 引擎雖然由 Qt C++ 實現,但 QML 對象的運行環境,說到底和 C++ 對象的上下文環境是不一樣的,是平行的兩個世界。若是你想在 QML 中訪問 C++ 對象,那麼必然要找到一種途徑來在兩個運行環境之間創建溝通橋樑。
Qt 提供了兩種在 QML 環境中使用 C++ 對象的方式:
在 C++ 中實現一個類,註冊到 QML 環境中, QML 環境中使用該類型建立對象
在 C++ 中構造一個對象,將這個對象設置爲 QML 的上下文屬性,在 QML 環境中直接使用改屬性
無論哪一種方式,對要導出的 C++ 類都有要求,不是一個類的全部方法、變量均可以被 QML 使用,所以咱們先來看看怎樣讓一個方法或屬性能夠被 QML 使用。
實現能夠導出的 C++ 類
前提條件
要想將一個類或對象導出到 QML 中,下列前提條件必須知足:
從 QObject 或 QObject 的派生類繼承
使用 Q_OBJECT 宏
看起來好像和使用信號與槽的前提條件同樣……沒錯,的確是同樣的。這兩個條件是爲了讓一個類可以進入 Qt 強大的元對象系統(meta-object system)中,只有使用元對象系統,一個類的某些方法或屬性纔可能經過字符串形式的名字來調用,才具備了在 QML 中訪問的基礎條件。
一旦你導出了一個類,在 QML 中就必然要訪問該類的實例的屬性或方法來達到某種目的,不然我真想不來你要幹什麼……而具備什麼特徵的屬性或方法才能夠被 QML 訪問呢?
信號,槽
只要是信號或者槽,均可以在 QML 中訪問,你能夠把 C++ 對象的信號鏈接到 QML 中定義的方法上,也能夠把 QML 對象的信號鏈接到 C++ 對象的槽上,還能夠直接調用 C++ 對象的槽或信號……因此,這是最簡單好用的一種途徑。
咱們定義了 start() / stop() 兩個槽, colorChanged() / currentTime() 兩個信號,均可以在 QML 中使用。
Q_INVOKABLE 宏
在定義一個類的成員函數時使用 Q_INVOKABLE 宏來修飾,就可讓該方法被元對象系統調用。這個宏必須放在返回類型前面。
一旦你使用 Q_INVOKABLE 將某個方法註冊到元對象系統中,在 QML 中就能夠用 ${Object}.${method} 來訪問,colorMaker 的 main.qml 中有使用 algorithm() 和 setAlgorithm() 的 QML 代碼 :
Q_ENUMS
若是你要導出的類定義了想在 QML 中使用枚舉類型,可使用 Q_ENUMS 宏將該枚舉註冊到元對象系統中。
ColorMaker 類定義了 GenerateAlgorithm 枚舉類型,支持 RandomRGB / RandomRed 等顏色生成算法。如今 ColorMaker 類的聲明變成了這個樣子:
一旦你使用 Q_ENUMS 宏註冊了你的枚舉類型,在 QML 中就能夠用 ${CLASS_NAME}.${ENUM_VALUE} 的形式來訪問,好比 ColorMaker.LinearIncrease ,上節展現的 QML 代碼片斷已經使用了導出的枚舉類型。
Q_PROPERTY
Q_PROPERTY 宏用來定義可經過元對象系統訪問的屬性,經過它定義的屬性,能夠在 QML 中訪問、修改,也能夠在屬性變化時發射特定的信號。要想使用 Q_PROPERTY 宏,你的類必須是 QObject 的後裔,必須在類首使用 Q_OBJECT 宏。
下面是 Q_PROPERTY 宏的原型:
是否是很複雜?你能夠爲一個屬性命名,能夠設定的選項數超過10個……我是以爲有點兒頭疼。不過,不是全部的選項都必須設定,看一個最簡短的屬性聲明:
Q_PROPERTY(int x READ x)
上面的聲明定義了一個類型爲 int 名爲 x 的屬性,經過方法 x() 來訪問。
type name 這兩個字段想必不用細說了吧? type 是屬性的類型,能夠是 int / float / QString / QObject / QColor / QFont 等等, name 就是屬性的名字。
其實咱們在實際使用中,不多可以用全 Q_PROPERTY 的全部選項,就往 QML 導出類這種場景來講,比較經常使用的是 READ / WRITE / NOTIFY 三個選項。咱們來看看都是什麼含義。
READ 標記,若是你沒有爲屬性指定 MEMBER 標記,則 READ 標記必不可少;聲明一個讀取屬性的函數,該函數通常沒有參數,返回定義的屬性。
WRITE 標記,可選配置。聲明一個設定屬性的函數。它指定的函數,只能有一個與屬性類型匹配的參數,必須返回 void 。
NOTIFY 標記,可選配置。給屬性關聯一個信號(該信號必須是已經在類中聲明過的),當屬性的值發生變化時就會觸發該信號。信號的參數,通常就是你定義的屬性。
其它標記的含義,請參考 Qt SDK 。
QML 中的 Text 類型對應 C++ 中的 QQuickText 類,下面是我摘取的部分代碼,能夠看到 Q_ENUMS 和 Q_PROPERTY 的使用:
如今給咱們的 ColorMaker 類添加一些屬性,以便 QML 能夠獲取、設置顏色值。新的 ColorMaker 類以下:
如今咱們的 ColorMaker 已是一個完整的類了,有信號、有槽、有使用 Q_INVOKABLE 註冊的方法,還導出了枚舉類型,小小麻雀五臟俱全。
是時候看看它的實現了。翠花,上代碼:
我使用一個週期爲 1000 的定時器來產生顏色,定時器觸發時根據算法來構造新的顏色值,發射 colorChanged 信號,同時也發送一個 currentTime 信號。
註冊一個 QML 中可用的類型
看過了怎樣實現一個可供 QML 訪問的類,這節咱們看看怎樣將一個 C++ 類型註冊爲 QML 類型以及怎樣在 QML 中使用這個類型。
要達到這種目的,大概能夠分四步:
一、實現 C++ 類
二、註冊 QML 類型
三、在 QML 中導入類型
四、在 QML 建立由 C++ 導出的類型的實例並使用
ColorMaker 已經就緒了,如今看看怎樣將其註冊爲 QML 可使用的類型。
要註冊一個 QML 類型,有多種方法可用,如 qmlRegisterSingletonType() 用來註冊一個單例類型, qmlRegisterType() 註冊一個非單例的類型, qmlRegisterTypeNotAvailable() 註冊一個類型用來佔位, qmlRegisterUncreatableType() 一般用來註冊一個具備附加屬性的附加類型,……好吧,我這裏只說常規的類型註冊,其它的,請您參考 Qt SDK 吧。
qmlRegisterType() 是個模板函數,有兩個原型:
前一個原型通常用來註冊一個新類型,然後一個能夠爲特定的版本註冊類型。後面這個牽涉到 Qt Quick 的類型和版本機制,三言兩語不能盡述,我們單說前一個原型的使用。要使用 qmlRegisterType 須要包含 QtQml 頭文件。
先說模板參數 typename ,它就是你實現的 C++ 類的類名。
qmlRegisterType() 的第一個參數 uri ,讓你指定一個惟一的包名,相似 Java 中的那種,一是用來避免名字衝突,而是能夠把多個相關類聚合到一個包中方便引用。好比咱們常寫這個語句 "import QtQuick.Controls 1.1" ,其中的 "QtQuick.Controls" 就是包名 uri ,而 1.1 則是版本,是 versionMajor 和 versionMinor 的組合。 qmlName 則是 QML 中可使用的類名。
下面是 colorMaker 示例的 main.cpp 文件:
上面的代碼將 ColorMaker 類註冊爲 QML 類 ColorMaker ,主版本爲 1 ,次版本爲 0 ,而我起的包名則是 an.qt.ColorMaker 。註冊動做必定要放在 QML 上下文建立以前,不然的話,木有用滴。
在 QML 中導入 C++ 註冊的類型
一旦你在 C++ 中註冊好了 QML 類型,就能夠在 QML 文檔中引入你註冊的包,而後使用註冊的類型。要引入包,使用 import 語句。好比要使用咱們註冊的 ColorMaker 類,能夠在 QML 文檔中加入下面的 import 語句:
import an.qt.ColorMaker 1.0
在 QML 中建立 C++ 導入類型的實例
引入包後,你就能夠在 QML 中建立 C++ 導入類型的對象了,與 QML 內建類型的使用徹底同樣。以下是建立一個 ColorMaker 實例的代碼:
Rectangle {
width: 360;
height: 360;
ColorMaker {
id: colorMaker;
color: Qt.green;
}
}
如你所見,ColorMaker 的使用與 Retangle 沒什麼區別。若是你想在別處引用 ColorMaker 的實例,能夠給實例指定一個惟一的 id ,就像上面的代碼中那樣。
如何定義一個能夠導出到 QML 中的 C++ 類、如何註冊 QML 類型、如何在 QML 中使用 C++ 導出的類型,都介紹完了,如今來看看完整的 colorMaker 。
main.qml 的界面分紅了三部分,參看圖 1 。頂部是一個 Text ,用來顯示由 ColorMaker 提供的時間,我使用 Connections 對象,指定 target 爲 colorMaker ,在 onCurrentTime 信號處理器中改變 timeLabel 的文本和顏色。這裏使用 ColorMaker 的 timeColor 屬性,該屬性的讀取函數是 timeColor ,回看一下 colorMaker.cpp 中的實現:
timeColor() 函數獲取當前時間,取時、分、秒轉換爲 R 、 G 、 B 值,構造一個 QColor 對象。
我構造了ColorMaker 類的一個實例, id 爲 colorMaker ,初始化顏色值爲 green 。
colorMaker 實例界面的中間是一個 Rectangle 對象,id 是 colorRect 。我使用 Connections 對象,指定 target 爲 colorMaker ,在 onColorChanged 信號處理器中改變 colorRect 的顏色。
界面的底部就是幾個按鈕,使用錨佈局把它們排成一行。 start 按鈕的 onClicked 信號處理器調用 colorMaker 的 start() 槽,啓動顏色生成器。 stop 按鈕的 onClicked 信號處理器調用 colorMaker 的 stop() 槽,中止顏色生成器。而 colorAlgorithm 按鈕則每點擊一次就切換一個顏色生成算法,同時調用 changeAlgorithm() 函數,根據算法改變按鈕上的文字。 quit 按鈕一點就退出應用。
main.qml 還引入了一個新內容:定義函數。這個能夠參考 JavaScript 的教程。咱們定義的 changeAlgorithm 函數,接受兩個參數, button 和 algorithm 。若是你是 C++ 程序猿,可能有點兒不適應:怎麼參數就木有類型呢哈…… JavaScript 就是醬紫滴,擁有動態類型,一個變量在賦值時決定其類型。
這就是 colorMaker 的所有了。
導出一個 C++ 對象爲 QML 的屬性
上面看了怎樣導出一個 QML 類型在 QML 文檔中使用,你還能夠把 C++ 中建立的對象做爲屬性傳遞到 QML 環境中,而後在 QML 環境中訪問。咱們仍是以 colorMaker 爲例,對其代碼作適當修改來適應本節的內容。
註冊屬性
要將一個對象註冊爲屬性很簡單,colorMaker 的 main.cpp 修改後以下:
正式這行代碼從堆上分配了一個 ColorMaker 對象,而後註冊爲 QML 上下文的屬性,起了個名字就叫 colorMaker 。
viewer.rootContext() 返回的是 QQmlContext 對象。 QQmlContext 類表明一個 QML 上下文,它的 setContextProperty() 方法能夠爲該上下文設置一個全局可見的屬性。要注意的是,你 new 出來的對象, QQmlContext 只是使用,不會幫你刪除,你須要本身找一個合適的時機來刪除它。
還有一點要說明,由於咱們去掉了 qmlRegisterType() 調用,因此在 main.qml 中不能再訪問 ColorMaker 類了,好比你不能經過類名來引用它定義的 GenerateAlgorithm 枚舉類型, colorMaker.setAlgorithm(ColorMaker.LinearIncrease) 語句會致使下面的報錯:
ReferenceError: ColorMaker is not defined
如今來看如何在 QML 中使用咱們導出的屬性
一旦調用 setContextProperty() 導出了屬性,就能夠在 QML 中使用了,不須要 import 語句哦。下面是 main.qml 修改後的代碼:
main.qml 代碼主要修改了三處,我已經使用方括號標註出來了。由於我將導出的屬性命名爲 colorMaker ,和導出 ColorMaker 類時構造的實例的 id 同樣,因此改動少了些。
你看到了,導出的屬性能夠直接使用,與屬性關聯的對象,它的信號、槽、可調用方法(使用 Q_INVOKABLE 宏修飾的方法)、屬性均可以使用,只是不能經過類名來引用枚舉值了。
在 C++ 中使用 QML 對象
看過了如何在 QML 中使用 C++ 類型或對象,如今來看如何在 C++ 中使用 QML 對象。
咱們可使用 QML 對象的信號、槽,訪問它們的屬性,都沒有問題,由於不少 QML 對象對應的類型,本來就是 C++ 類型,好比 Image 對應 QQuickImage , Text 對應 QQuickText……可是,這些與 QML 類型對應的 C++ 類型都是私有的,你寫的 C++ 代碼也不能直接訪問。腫麼辦?
Qt 最核心的一個基礎特性,就是元對象系統,經過元對象系統,你能夠查詢 QObject 的某個派生類的類名、有哪些信號、槽、屬性、可調用方法等等信息,而後也可使用 QMetaObject::invokeMethod() 調用 QObject 的某個註冊到元對象系統中的方法。而對於使用 Q_PROPERTY 定義的屬性,可使用 QObject 的 property() 方法訪問屬性,若是該屬性定義了 WRITE 方法,還可使用 setProperty() 修改屬性。因此只要咱們找到 QML 環境中的某個對象,就能夠經過元對象系統來訪問它的屬性、信號、槽等。
查找一個對象的孩子
QObject 類的構造函數有一個 parent 參數,能夠指定一個對象的父親, QML 中的對象其實藉助這個組成了以根 item 爲父的一棵對象樹。
而 QObject 定義了一個屬性 objectName ,這個對象名字屬性,就能夠用於查找對象。如今該說到查找對象的方法了: findChild() 和 findChildren() 。它們的函數原型以下:
都是模板方法,從命名上也能夠看出,一個返回單個對象,一個返回對象列表。閒話少說,如今讓咱們看看如何查詢一個或多個對象,咱們先以 Qt Widgets 爲例來講明用法哈。
示例 1 :
QPushButton *button = parentWidget->findChild<QPushButton *>("button1");
查找 parentWidget 的名爲 "button1" 的類型爲 QPushButton 的孩子。
示例 2 :
QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname");
返回 parentWidget 全部名爲 "widgetname" 的 QWidget 類型的孩子列表。
使用元對象調用一個對象的方法
QMetaObject 的 invokeMethod() 方法用來調用一個對象的信號、槽、可調用方法。它是個靜態方法,其函數原型以下:
bool QMetaObject::invokeMethod(QObject * obj, const char * member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0 = QGenericArgument( 0 ), QGenericArgument val1 = QGenericArgument(), QGenericArgument val2 = QGenericArgument(), QGenericArgument val3 = QGenericArgument(), QGenericArgument val4 = QGenericArgument(), QGenericArgument val5 = QGenericArgument(), QGenericArgument val6 = QGenericArgument(), QGenericArgument val7 = QGenericArgument(), QGenericArgument val8 = QGenericArgument(), QGenericArgument val9 = QGenericArgument()) [static]
其實 QMetaObject 還有三個 invokeMethod() 函數,不過都是上面這個原型的重載,因此咱們只要介紹上面這個就 OK 了。
先說返回值吧,返回 true 說明調用成功。返回 false ,要麼是由於沒有你說的那個方法,要麼是參數類型不匹配。
第一個參數是被調用對象的指針。
第二個參數是方法名字。
第三個參數是鏈接類型,看到這裏你就知道, invokeMethod 爲信號與槽而生,你能夠指定鏈接類型,若是你要調用的對象和發起調用的線程是同一個線程,那麼可使用 Qt::DirectConnection 或 Qt::AutoConnection 或 Qt::QueuedConnection ,若是被調用對象在另外一個線程,那麼建議你使用 Qt::QueuedConnection 。
第四個參數用來接收返回指。
而後就是多達 10 個能夠傳遞給被調用方法的參數。嗯,看來信號與槽的參數個數是有限制的,不能超過 10 個。
對於要傳遞給被調用方法的參數,使用 QGenericArgument 來表示,你可使用 Q_ARG 宏來構造一個參數,它的定義是:
QGenericArgument Q_ARG( Type, const Type & value)
返回類型是相似的,使用 QGenericReturnArgument 表示,你可使用 Q_RETURN_ARG 宏來構造一個接收返回指的參數,它的定義是:
QGenericReturnArgument Q_RETURN_ARG( Type, Type & value)
好啦,總算把這個天殺的函數介紹完了,下面咱們看看怎麼用。
假設一個對象有這麼一個槽 compute(QString, int, double) ,返回一個 QString 對象,那麼你能夠這麼調用(同步方式):
若是你要讓一個線程對象退出,能夠這麼調用(隊列鏈接方式):
如今讓咱們建立一個新的項目,名字是 callQml ,添加 changeColor.h 、 changeColor.cpp 兩個文件。 main.qml 內容以下:
咱們給根元素起了個名字 "rootRect" ,給退出按鈕起了個名字 "quitButton" ,給文本起了名字 "textLabel" ,咱們會在 C++ 代碼中經過這些個名字來查找對應的對象並改變它們。
如今來看看 main.cpp :
在一開始我經過 viewer.rootObject() ,獲取到了做爲根元素的 Rectangle ,而後把它交給一個 ChangeQmlColor 對象,該對象會內部經過一個定時器,一秒改變一次傳入對象的顏色。
緊接着,我使用 QObject 的 findChild() 找到了 quitButton 按鈕,把它的 clicked() 信號鏈接到 QGuiApplication 的 quit() 槽上。因此你點擊這個按鈕,應用就退出了。
後來,我又經過名字 "textLabel" 找到了 textLabel 對象。首先我企圖使用 invodeMethod() 調用 setText() 方法來改變 textLabel 的文本,這個註定是會失敗的,由於 QML 中的Text 對象對應 C++ QQuickText 類,而 QQuickText 沒有名爲 setText 的槽或者可調用方法。我查看了頭文件 qquicktext_p.h ,發現它有一個使用 Q_INVOKABLE 宏修飾的 doLayout() 的方法,因此後來我又調用 doLayout() ,此次成功了。
圖 2 callQml 運行效果圖
Hello World 這行字變成了紅色,是由於我在 main() 函數中使用 setProperty 修改了 textLabel 的 color 屬性。
下面是 Qt Creator 應用程序輸出窗口的信息,能夠驗證對 Text 方法的調用是否成功:
Starting D:\projects\...\release\callQml.exe...
QMetaObject::invokeMethod: No such method QQuickText::setText(QString)
call setText return - false
call doLayout return - true
好啦,最後看看界面背景爲麼變成了淺綠色。這正是下面這行代碼的功勞:
new ChangeQmlColor(rootItem);
它以 rootItem 爲參數建立了一個 ChangeQmlColor 對象,而 ChangeQmlColor 類會改變傳給它的對象的顏色。
ChangeQmlColor 類定義以下:
實現文件 changeColor.cpp :
---------------------------------------------------------------------------------------------------------------------------------------------------
QWebEngine打開chrome devtool調試工具
在代碼中加入下面這段
//QWebEngine DEBUG --remote-debugging-port=9223
qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "9223");
加入load qml前添加
在瀏覽器中訪問http://localhost:9223/ 便可進行調試
---------------------------------------------------------------------------------------------------------------------------------------------------
(譯)經過WebChannel/WebSockets與QML中的HTML交互 1、前言Qt容許使用所謂的混合GUI建立應用程序——在這種GUI中,能夠將本機部件與基於html的內容混合在一塊兒。經過WebChannel和WebSockets公開QObject,這種混合甚至支持這些本地部分和html端之間的交互。
這三種方法以不一樣的方式進行,但都支持QML和HTML之間的通訊。
確切的說,WebEngineView以一種方式完成,而WebView(就像網絡瀏覽器同樣)以另外一種方式完成。WebEngineView和WebView是兩碼事。
(1)webEngineView
WebEngineView是由Qt本身基於Chromium (Qt WebEngine)的web瀏覽器引擎提供的web視圖。它是一個功能齊全的web瀏覽器,與Qt捆綁並集成在一塊兒,這很好,但同時這意味着您須要將它與您的應用程序一塊兒拖動,這是一個至關大的東西。
(2)webView
WebView是一個web視圖,但不一樣之處在於它使用平臺的本地web瀏覽器(若是可用的話),所以它不須要將完整的web瀏覽器堆棧做爲應用程序的一部分(WebEngineView就是這種狀況),所以您的應用程序更輕量級。另外一點是,有些平臺根本不容許任何非系統的web瀏覽器,所以WebView是惟一可用的選項。
(3)webEngineView 和 webView的區別
根據本文,WebEngineView和WebView的關鍵區別在於Qt如何與這些視圖中的html內容通訊。因爲Chromium IPC功能,WebEngineView提供了最簡單的方式-直接經過WebChannel,。而WebView(以及外部web瀏覽器)要求您首先爲WebChannel創建一些傳輸。
好的,咱們能夠顯示HTML,可是如何從QML與之交互呢?一切都經過WebChannel。在HTML端,它是經過特殊的JavaScript庫- Qt WebChannel JavaScript API完成的。
(1)WebEngineView - 直接使用WebChannel
WebEngineView能夠直接使用WebChannel,以這個存儲庫爲基礎進行講解。
// 一個具備屬性、信號和方法的對象——就像任何普通的Qt對象同樣
QtObject {
id: someObject
// ID,在這個ID下,這個對象在WebEngineView端是已知的
WebChannel.id: "backend"
property string someProperty: "Break on through to the other side"
signal someSignal(string message);
function changeText(newText) {
txt.text = newText;
return "New text length: " + newText.length;
}
}
Text {
id: txt
text: "Some text"
onTextChanged: {
// 此信號將在WebEngineView端觸發一個函數(若是鏈接的話)
someObject.someSignal(text)
}
}
WebEngineView {
url: "qrc:/index.html"
webChannel: channel
}
WebChannel {
id: channel
registeredObjects: [someObject]
}複製代碼
這裏咱們建立WebChannel並將其ID分配給WebEngineView,並在通道上註冊QtObject的ID。固然,您能夠從c++端「注入」一個c++ /Qt對象,而不是在QML端定義的QtObject。
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript">
// 這是QML端的QtObject
var backend;
window.onload = function()
{
new QWebChannel(qt.webChannelTransport, function(channel) {
// 在channel.object下,全部發布的對象在通道中都是可用的
// 在附加的WebChannel.id屬性中設置的標識符。
backend = channel.objects.backend;
//鏈接信號
backend.someSignal.connect(function(someText) {
alert("Got signal: " + someText);
document.getElementById("lbl").innerHTML = someText;
});
});
}
// 演示異步交互
var result = "ololo";
function changeLabel()
{
var textInputValue = document.getElementById("input").value.trim();
if (textInputValue.length === 0)
{
alert("You haven't entered anything!");
return;
}
// 調用方法並接收返回值
backend.changeText(textInputValue, function(callback) {
result = callback;
// 因爲它是異步的,所以稍後將出現此警報並顯示實際結果
alert(result);
// 將變量重置爲默認值
result = "ololo";
});
// 此警告將首先出現,並顯示默認的「ololo」
alert(result);
}
// 您還能夠從QML端讀取/寫入QtObject的屬性
function getPropertyValue()
{
var originalValue = backend.someProperty;
alert(backend.someProperty);
backend.someProperty = "some another value";
alert(backend.someProperty);
backend.someProperty = originalValue;
}
</script>複製代碼
在這裏,您須要在windows.onload事件上建立一個QWebChannel並獲取後端對象。以後,您能夠調用它的方法,鏈接到它的信號並訪問它的屬性。
下面是一個簡單的例子,演示了QML(藍色矩形外的全部內容)和HTML(藍色矩形內的部分)之間的通訊:
這是它的模式:
注意,交互是異步完成的——查看changeLabel()函數並注意警報的順序。
(2)WebView - WebSockets上的WebChannel
WebView(和外部Web瀏覽器)沒法直接使用WebChannel。您須要首先建立一個WebSockets傳輸,而後在其上使用WebChannel。
這僅使用QML是沒法實現的,所以您還必須編寫一些C ++代碼。這有點使人沮喪,但更使人沮喪的是文檔沒有明確提到它。
因此,當我發現這一點時,我決定重寫一個C ++示例。當我差很少完成時,我也獲得了Stack Overflow的答案,幾乎展現瞭如何在QML中作的全部事情,我最終獲得了兩個解決方案,以下。
(a)主要是c++完成
這個函數的大部分工做都是用c++完成的,QML沒用什麼。
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
// 不要忘記這個
QtWebView::initialize();
QWebSocketServer server(
QStringLiteral("WebSockets example"),
QWebSocketServer::NonSecureMode
);
if (!server.listen(QHostAddress::LocalHost, 55222)) { return 1; }
// 在QWebChannelAbstractTransport對象中包裝WebSocket客戶端
WebSocketClientWrapper clientWrapper(&server);
// 設置通道
QWebChannel channel;
QObject::connect(&clientWrapper, &WebSocketClientWrapper::clientConnected,
&channel, &QWebChannel::connectTo);
// 設置核心並將其發佈到QWebChannel
Backend *backend = new Backend();
channel.registerObject(QStringLiteral("backend"), backend);
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("someObject", backend);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty()) { return -1; }
return app.exec();
}複製代碼
這裏最重要的是WebSocketClientWrapper(WebSocketTransport在下面使用)。這是必須本身實現的,而文檔的幫助不大。
使用WebSocketClientWrapper,您最終能夠鏈接QWebChannel並註冊您的對象(在個人例子中是Backend,儘管我保留了相同的ID - someObject),所以它將在HTML端可用。
注意,此次我須要註冊一個已經建立的c++對象(不是類型),因此我使用setContextProperty。
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript">
// 這是QML端的QtObject
var backend;
window.onload = function()
{
var socket = new WebSocket("ws://127.0.0.1:55222");
socket.onopen = function()
{
new QWebChannel(socket, function(channel) {
backend = channel.objects.backend;
// 鏈接信號
backend.someSignal.connect(function(someText) {
alert("Got signal: " + someText);
document.getElementById("lbl").innerHTML = someText;
});
});
};
}
</script>複製代碼
與WebEngineView示例中的index.html不一樣,這裏首先須要創建WebSocket鏈接,做爲QWebChannel的傳輸。其他的都是同樣的。
Text {
id: txt
text: "Some text"
onTextChanged: {
someObject.someSignal(text)
}
Component.onCompleted: {
someObject.textNeedsToBeChanged.connect(changeText)
}
function changeText(newText) {
txt.text = newText;
}
}
WebView {
id: webView
url: "qrc:/index.html"
}複製代碼
QML代碼也有一點不一樣。首先,someObject這是一個上下文屬性,所以不須要導入和聲明它。其次,c++對象和QML組件之間的交互須要再添加一個信號(textNeedsToBeChanged)。
所以,交互模式也變得有點奇怪:
幸運的是,有一個更好的解決方案。下面就是。
(b)主要是QML
我更喜歡這個例子,由於它主要在QML中完成,C ++上只有一點點。我是在Stack Overflow上獲得的這個答案。
首先,咱們須要實現WebChannel的傳輸。
class WebSocketTransport : public QWebChannelAbstractTransport
{
Q_OBJECT
public:
Q_INVOKABLE void sendMessage(const QJsonObject &message) override
{
QJsonDocument doc(message);
emit messageChanged(QString::fromUtf8(doc.toJson(QJsonDocument::Compact)));
}
Q_INVOKABLE void textMessageReceive(const QString &messageData)
{
QJsonParseError error;
QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error);
if (error.error)
{
qWarning() << "Failed to parse text message as JSON object:" << messageData
<< "Error is:" << error.errorString();
return;
} else if (!message.isObject())
{
qWarning() << "Received JSON message that is not an object: " << messageData;
return;
}
emit messageReceived(message.object(), this);
}
signals:
void messageChanged(const QString & message);
};複製代碼
而後將其註冊到QML
#include "websockettransport.h"
int main(int argc, char *argv[])
{
// ...
qmlRegisterType("io.decovar.WebSocketTransport", 1, 0, "WebSocketTransport");
// ...
}複製代碼
剩下的都在QML
import io.decovar.WebSocketTransport 1.0
// ...
// 一個具備屬性、信號和方法的對象——就像任何普通的Qt對象同樣
QtObject {
id: someObject
// ID,在這個ID下,這個對象在WebEngineView端是已知的
WebChannel.id: "backend"
property string someProperty: "Break on through to the other side"
signal someSignal(string message);
function changeText(newText) {
txt.text = newText;
return "New text length: " + newText.length;
}
}
WebSocketTransport {
id: transport
}
WebSocketServer {
id: server
listen: true
port: 55222
onClientConnected: {
if(webSocket.status === WebSocket.Open) {
channel.connectTo(transport)
webSocket.onTextMessageReceived.connect(transport.textMessageReceive)
transport.onMessageChanged.connect(webSocket.sendTextMessage)
}
}
}
Text {
id: txt
text: "Some text"
onTextChanged: {
//此信號將在WebView端觸發一個函數(若是鏈接)
someObject.someSignal(text)
}
}
WebView {
url: "qrc:/index.html"
}
WebChannel {
id: channel
registeredObjects: [someObject]
}複製代碼
index.html與前面的例子相同,建立一個WebSocket並將其用做QWebChannel的傳輸。
順便說一下,正如我在前面提到的,WebView和獨立/外部瀏覽器是同樣的,因此您能夠在web瀏覽器中打開index.html,它將以相同的方式工做-只是不要忘記從代碼中刪除qrc:/並複製qwebchannel.js到相同的文件夾。
在這個存儲庫中能夠找到這三個示例的完整源代碼。
儘管WebChannel和WebSockets都有超過5個例子,但很難理解它是如何工做的?爲何沒有一個讓它與QML一塊兒工做的例子?
如今,關於qwebchannel.js。看一下文檔頁面的第一段:
要與QWebChannel或WebChannel通訊,客戶機必須使用並設置QWebChannel .js提供的JavaScript API。對於運行在Qt WebEngine中的客戶機,能夠經過qrc:///qtwebchannel/qwebchannel.js加載文件。對於外部客戶端,須要將文件複製到web服務器。
所以,對於集成的web視圖,咱們可使用一個特殊的資源qrc:///qtwebchannel/qwebchannel。可是咱們在哪裏能夠爲外部客戶端找到這個文件呢?是的,這個文件在這個或其餘任何頁面上都找不到。幸運的是,你能夠從如下例子中找到答案:
QWebChannelAbstractTransport的文檔頁面也不是一個詳細的頁面,由於它沒有一行代碼,更不用說示例了。它對於WebChannel的必要性是這樣不經意間被簡單說起的:
請注意,只要將QWebChannel鏈接到QWebChannelAbstractTransport,它就能夠徹底運行。
基本上,若是不是我找到的存儲庫以及在Stack Overflow上得到的幫助 - 我根本沒法進行一切工做。