使用 Qt WebChannel 實現 C++/QML 和 HTML 頁面之間交互
在項目開發中,常常會有在原生應用程序中嵌入 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%\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 實例。
參考:https://www.pressc.cn/1085.html
---------------------------------------------------------------------------------------------------------------------------------------------------
寫在前面
本文適合有一定Qt及HTML經驗的人閱讀。
Qt(C++)和QML間交互
想要了解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);//發送信號 }
將類注冊到QML中
QT使用qmlRegisterType方法將類注冊到QML中,例:
qmlRegisterType<MyClass>("RegisterMyType",1,0,"MyClassType");
其中,第一個參數是作為QML中引用的url;第二個參數是主版本號;第三個參數是次版本號;第四個參數是QML中使用的元素名稱。本例中,QML模塊可以使用下面方法引用該類,例:
import RegisterMyType 1.0
QML中對象創建
經過上面的步驟之后,我們就可以直接在QML中創建MyClassType對象了。例:
MyClassType {
id: myobj
}
QML中連接信號
對象創建成功后,我們可以為QML綁定感興趣的信號了。
Connections {
target: myobj;
onMystringChanged: {
// 這里的value是signal信號函數里面的參數
console.log("value: " + value)
}
}
QML直接使用對象
除了上面的方法,我們還可以通過直接使用對象的方式,來進行信號的綁定。在上面的例子中,我們可以下面的方式,我們首先在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間交互
Qt和HTML間的交互式通過WebChannel來實現的。
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]
}
WebEngineView
WebChannel聲明好之后,下面就是如何使用它。我們定義WebEngineView元素,用來加載HTML文件,並指定關聯的WebChannel,使用方式如下:
WebEngineView {
id: webView
url: "./map.html"
webChannel: myChannel
}
至此,QML端的工作就已經完成了。下面講一下如何在HTML端使用WebChannel。
引入qwebchannel.js庫
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++ 對象的槽或信號……所以,這是最簡單好用的一種途徑。
-
class ColorMaker : public QObject
-
{
-
Q_OBJECT
-
-
public:
-
ColorMaker(QObject *parent = 0);
-
~ColorMaker();
-
-
signals:
-
void colorChanged(const QColor & color);
-
void currentTime(const QString &strTime);
-
-
public slots:
-
void start();
-
void stop();
-
-
};
我們定義了 start() / stop() 兩個槽, colorChanged() / currentTime() 兩個信號,都可以在 QML 中使用。
Q_INVOKABLE 宏
在定義一個類的成員函數時使用 Q_INVOKABLE 宏來修飾,就可以讓該方法被元對象系統調用。這個宏必須放在返回類型前面。
-
-
class ColorMaker : public QObject
-
{
-
Q_OBJECT
-
-
public:
-
ColorMaker(QObject *parent = 0);
-
~ColorMaker();
-
-
Q_INVOKABLE GenerateAlgorithm algorithm() const;
-
Q_INVOKABLE void setAlgorithm(GenerateAlgorithm algorithm);
-
-
signals:
-
void colorChanged(const QColor & color);
-
void currentTime(const QString &strTime);
-
-
public slots:
-
void start();
-
void stop();
-
一旦你使用 Q_INVOKABLE 將某個方法注冊到元對象系統中,在 QML 中就可以用 ${Object}.${method} 來訪問,colorMaker 的 main.qml 中有使用 algorithm() 和 setAlgorithm() 的 QML 代碼 :
-
Component.onCompleted: {
-
colorMaker.color = Qt.rgba( 0, 180, 120, 255);
-
colorMaker.setAlgorithm(ColorMaker.LinearIncrease);
-
changeAlgorithm(colorAlgorithm, colorMaker.algorithm());
-
}
Q_ENUMS
如果你要導出的類定義了想在 QML 中使用枚舉類型,可以使用 Q_ENUMS 宏將該枚舉注冊到元對象系統中。
ColorMaker 類定義了 GenerateAlgorithm 枚舉類型,支持 RandomRGB / RandomRed 等顏色生成算法。現在 ColorMaker 類的聲明變成了這個樣子:
-
class ColorMaker : public QObject
-
{
-
Q_OBJECT
-
Q_ENUMS (GenerateAlgorithm)
-
-
public:
-
ColorMaker (QObject *parent = 0);
-
~ColorMaker();
-
-
enum GenerateAlgorithm{
-
RandomRGB,
-
RandomRed,
-
RandomGreen,
-
RandomBlue,
-
LinearIncrease
-
};
-
-
Q_INVOKABLE GenerateAlgorithm algorithm() const;
-
Q_INVOKABLE void setAlgorithm(GenerateAlgorithm algorithm);
-
-
signals:
-
void colorChanged(const QColor & color);
-
void currentTime(const QString &strTime);
-
-
public slots:
-
void start();
-
void stop();
-
};
一旦你使用 Q_ENUMS 宏注冊了你的枚舉類型,在 QML 中就可以用 ${CLASS_NAME}.${ENUM_VALUE} 的形式來訪問,比如 ColorMaker.LinearIncrease ,上節展示的 QML 代碼片段已經使用了導出的枚舉類型。
Q_PROPERTY
Q_PROPERTY 宏用來定義可通過元對象系統訪問的屬性,通過它定義的屬性,可以在 QML 中訪問、修改,也可以在屬性變化時發射特定的信號。要想使用 Q_PROPERTY 宏,你的類必須是 QObject 的后裔,必須在類首使用 Q_OBJECT 宏。
下面是 Q_PROPERTY 宏的原型:
-
Q_PROPERTY(type name
-
(READ getFunction [WRITE setFunction] |
-
MEMBER memberName [(READ getFunction | WRITE setFunction)])
-
[RESET resetFunction]
-
[NOTIFY notifySignal]
-
[REVISION int]
-
[DESIGNABLE bool]
-
[SCRIPTABLE bool]
-
[STORED bool]
-
[USER bool]
-
[CONSTANT]
-
[FINAL])
是不是很復雜?你可以為一個屬性命名,可以設定的選項數超過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 的使用:
-
class QQuickText : public QQuickImplicitSizeItem
-
{
-
Q_OBJECT
-
Q_ENUMS(HAlignment)
-
-
-
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
-
Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged)
-
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
-
...
-
-
public:
-
enum HAlignment { AlignLeft = Qt::AlignLeft,
-
AlignRight = Qt::AlignRight,
-
AlignHCenter = Qt::AlignHCenter,
-
AlignJustify = Qt::AlignJustify };
-
...
-
QString text() const;
-
void setText( const QString &);
-
-
QFont font() const;
-
void setFont( const QFont &font);
-
-
QColor color() const;
-
void setColor( const QColor &c);
-
...
-
};
現在給我們的 ColorMaker 類添加一些屬性,以便 QML 可以獲取、設置顏色值。新的 ColorMaker 類如下:
-
class ColorMaker : public QObject
-
{
-
Q_OBJECT
-
Q_ENUMS (GenerateAlgorithm)
-
Q_PROPERTY (QColor color READ color WRITE setColor NOTIFY colorChanged)
-
Q_PROPERTY (QColor timeColor READ timeColor)
-
-
public:
-
ColorMaker (QObject *parent = 0);
-
~ColorMaker();
-
-
enum GenerateAlgorithm{
-
RandomRGB,
-
RandomRed,
-
RandomGreen,
-
RandomBlue,
-
LinearIncrease
-
};
-
-
QColor color() const;
-
void setColor(const QColor & color);
-
QColor timeColor() const;
-
-
Q_INVOKABLE GenerateAlgorithm algorithm() const;
-
Q_INVOKABLE void setAlgorithm(GenerateAlgorithm algorithm);
-
-
signals:
-
void colorChanged(const QColor & color);
-
void currentTime(const QString &strTime);
-
-
public slots:
-
void start();
-
void stop();
-
-
protected:
-
void timerEvent(QTimerEvent *e);
-
-
private:
-
GenerateAlgorithm m_algorithm;
-
QColor m_currentColor;
-
int m_nColorTimer;
-
};
現在我們的 ColorMaker 已經是一個完整的類了,有信號、有槽、有使用 Q_INVOKABLE 注冊的方法,還導出了枚舉類型,小小麻雀五臟俱全。
是時候看看它的實現了。翠花,上代碼:
-
-
-
-
-
ColorMaker::ColorMaker(QObject *parent)
-
: QObject(parent)
-
, m_algorithm(RandomRGB)
-
, m_currentColor(Qt::black)
-
, m_nColorTimer( 0)
-
{
-
qsrand(QDateTime::currentDateTime().toTime_t());
-
}
-
-
ColorMaker::~ColorMaker()
-
{
-
}
-
-
QColor ColorMaker::color() const
-
{
-
return m_currentColor;
-
}
-
-
void ColorMaker::setColor( const QColor &color)
-
{
-
m_currentColor = color;
-
emit colorChanged(m_currentColor);
-
}
-
-
QColor ColorMaker::timeColor() const
-
{
-
QTime time = QTime::currentTime();
-
int r = time.hour();
-
int g = time.minute()* 2;
-
int b = time.second()* 4;
-
return QColor::fromRgb(r, g, b);
-
}
-
-
ColorMaker::GenerateAlgorithm ColorMaker::algorithm() const
-
{
-
return m_algorithm;
-
}
-
-
void ColorMaker::setAlgorithm(GenerateAlgorithm algorithm)
-
{
-
m_algorithm = algorithm;
-
}
-
-
void ColorMaker::start()
-
{
-
if(m_nColorTimer == 0)
-
{
-
m_nColorTimer = startTimer( 1000);
-
}
-
}
-
-
void ColorMaker::stop()
-
{
-
if(m_nColorTimer > 0)
-
{
-
killTimer(m_nColorTimer);
-
m_nColorTimer = 0;
-
}
-
}
-
-
void ColorMaker::timerEvent(QTimerEvent *e)
-
{
-
if(e->timerId() == m_nColorTimer)
-
{
-
switch(m_algorithm)
-
{
-
case RandomRGB:
-
m_currentColor.setRgb(qrand() % 255, qrand() % 255, qrand() % 255);
-
break;
-
case RandomRed:
-
m_currentColor.setRed(qrand() % 255);
-
break;
-
case RandomGreen:
-
m_currentColor.setGreen(qrand() % 255);
-
break;
-
case RandomBlue:
-
m_currentColor.setBlue(qrand() % 255);
-
break;
-
case LinearIncrease:
-
{
-
int r = m_currentColor.red() + 10;
-
int g = m_currentColor.green() + 10;
-
int b = m_currentColor.blue() + 10;
-
m_currentColor.setRgb(r % 255, g % 255, b % 255);
-
}
-
break;
-
}
-
emit colorChanged(m_currentColor);
-
emit currentTime(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
-
}
-
else
-
{
-
QObject::timerEvent(e);
-
}
-
}
我使用一個周期為 1000 的定時器來產生顏色,定時器觸發時根據算法來構造新的顏色值,發射 colorChanged 信號,同時也發送一個 currentTime 信號。
注冊一個 QML 中可用的類型
看過了怎樣實現一個可供 QML 訪問的類,這節我們看看怎樣將一個 C++ 類型注冊為 QML 類型以及怎樣在 QML 中使用這個類型。
要達到這種目的,大概可以分四步:
1、實現 C++ 類
2、注冊 QML 類型
3、在 QML 中導入類型
4、在 QML 創建由 C++ 導出的類型的實例並使用
ColorMaker 已經就緒了,現在看看怎樣將其注冊為 QML 可以使用的類型。
注冊 QML 類型
要注冊一個 QML 類型,有多種方法可用,如 qmlRegisterSingletonType() 用來注冊一個單例類型, qmlRegisterType() 注冊一個非單例的類型, qmlRegisterTypeNotAvailable() 注冊一個類型用來占位, qmlRegisterUncreatableType() 通常用來注冊一個具有附加屬性的附加類型,……好吧,我這里只說常規的類型注冊,其它的,請您參考 Qt SDK 吧。
qmlRegisterType() 是個模板函數,有兩個原型:
-
template< typename T>
-
int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName);
-
-
template< typename T, int metaObjectRevision>
-
int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName);
前一個原型一般用來注冊一個新類型,而后一個可以為特定的版本注冊類型。后面這個牽涉到 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 文件:
-
-
-
-
-
-
-
int main(int argc, char *argv[])
-
{
-
QGuiApplication app(argc, argv);
-
qmlRegisterType<ColorMaker>( "an.qt.ColorMaker", 1, 0, "ColorMaker");
-
-
QtQuick2ApplicationViewer viewer;
-
viewer.setMainQmlFile(QStringLiteral( "qml/colorMaker/main.qml"));
-
viewer.showExpanded();
-
-
return app.exec();
-
}
上面的代碼將 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 ,就像上面的代碼中那樣。
完整的 colorMaker 實例
如何定義一個可以導出到 QML 中的 C++ 類、如何注冊 QML 類型、如何在 QML 中使用 C++ 導出的類型,都介紹完了,現在來看看完整的 colorMaker 。
-
import QtQuick 2.0
-
import QtQuick.Controls 1.1
-
import an.qt.ColorMaker 1.0
-
-
Rectangle {
-
width: 360;
-
height: 360;
-
Text {
-
id: timeLabel;
-
anchors.left: parent.left;
-
anchors.leftMargin: 4;
-
anchors.top: parent.top;
-
anchors.topMargin: 4;
-
font.pixelSize: 26;
-
}
-
ColorMaker {
-
id: colorMaker;
-
color: Qt.green;
-
}
-
-
Rectangle {
-
id: colorRect;
-
anchors.centerIn: parent;
-
width: 200;
-
height: 200;
-
color: "blue";
-
}
-
-
Button {
-
id: start;
-
text: "start";
-
anchors.left: parent.left;
-
anchors.leftMargin: 4;
-
anchors.bottom: parent.bottom;
-
anchors.bottomMargin: 4;
-
onClicked: {
-
colorMaker.start();
-
}
-
}
-
Button {
-
id: stop;
-
text: "stop";
-
anchors.left: start.right;
-
anchors.leftMargin: 4;
-
anchors.bottom: start.bottom;
-
onClicked: {
-
colorMaker.stop();
-
}
-
}
-
-
function changeAlgorithm(button, algorithm){
-
switch(algorithm)
-
{
-
case 0:
-
button.text = "RandomRGB";
-
break;
-
case 1:
-
button.text = "RandomRed";
-
break;
-
case 2:
-
button.text = "RandomGreen";
-
break;
-
case 3:
-
button.text = "RandomBlue";
-
break;
-
case 4:
-
button.text = "LinearIncrease";
-
break;
-
}
-
}
-
-
Button {
-
id: colorAlgorithm;
-
text: "RandomRGB";
-
anchors.left: stop.right;
-
anchors.leftMargin: 4;
-
anchors.bottom: start.bottom;
-
onClicked: {
-
var algorithm = (colorMaker.algorithm() + 1) % 5;
-
changeAlgorithm(colorAlgorithm, algorithm);
-
colorMaker.setAlgorithm(algorithm);
-
}
-
}
-
-
Button {
-
id: quit;
-
text: "quit";
-
anchors.left: colorAlgorithm.right;
-
anchors.leftMargin: 4;
-
anchors.bottom: start.bottom;
-
onClicked: {
-
Qt.quit();
-
}
-
}
-
-
Component.onCompleted: {
-
colorMaker.color = Qt.rgba( 0, 180, 120, 255);
-
colorMaker.setAlgorithm(ColorMaker.LinearIncrease);
-
changeAlgorithm(colorAlgorithm, colorMaker.algorithm());
-
}
-
-
Connections {
-
target: colorMaker;
-
onCurrentTime:{
-
timeLabel.text = strTime;
-
timeLabel.color = colorMaker.timeColor;
-
}
-
}
-
-
Connections {
-
target: colorMaker;
-
onColorChanged:{
-
colorRect.color = color;
-
}
-
}
-
}
main.qml 的界面分成了三部分,參看圖 1 。頂部是一個 Text ,用來顯示由 ColorMaker 提供的時間,我使用 Connections 對象,指定 target 為 colorMaker ,在 onCurrentTime 信號處理器中改變 timeLabel 的文本和顏色。這里使用 ColorMaker 的 timeColor 屬性,該屬性的讀取函數是 timeColor ,回看一下 colorMaker.cpp 中的實現:
-
QColor ColorMaker::timeColor() const
-
{
-
QTime time = QTime::currentTime();
-
int r = time.hour();
-
int g = time.minute()* 2;
-
int b = time.second()* 4;
-
return QColor::fromRgb(r, g, b);
-
}
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 修改后如下:
-
-
-
-
-
-
-
int main(int argc, char *argv[])
-
{
-
QGuiApplication app(argc, argv);
-
-
QtQuick2ApplicationViewer viewer;
-
-
viewer.rootContext()->setContextProperty( "colorMaker", new ColorMaker);
-
-
viewer.setMainQmlFile(QStringLiteral( "qml/colorMaker/main.qml"));
-
viewer.showExpanded();
-
-
return app.exec();
-
}
正式這行代碼從堆上分配了一個 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 中使用我們導出的屬性
在 QML 中使用關聯到 C++ 對象的屬性
一旦調用 setContextProperty() 導出了屬性,就可以在 QML 中使用了,不需要 import 語句哦。下面是 main.qml 修改后的代碼:
-
import QtQuick 2.0
-
import QtQuick.Controls 1.1
-
//[1]
-
//import an.qt.ColorMaker 1.0
-
-
Rectangle {
-
width: 360;
-
height: 360;
-
Text {
-
id: timeLabel;
-
anchors.left: parent.left;
-
anchors.leftMargin: 4;
-
anchors.top: parent.top;
-
anchors.topMargin: 4;
-
font.pixelSize: 26;
-
}
-
/* [2]
-
ColorMaker {
-
id: colorMaker;
-
color: Qt.green;
-
}
-
*/
-
-
Rectangle {
-
id: colorRect;
-
anchors.centerIn: parent;
-
width: 200;
-
height: 200;
-
color: "blue";
-
}
-
-
Button {
-
id: start;
-
text: "start";
-
anchors.left: parent.left;
-
anchors.leftMargin: 4;
-
anchors.bottom: parent.bottom;
-
anchors.bottomMargin: 4;
-
onClicked: {
-
colorMaker.start();
-
}
-
}
-
Button {
-
id: stop;
-
text: "stop";
-
anchors.left: start.right;
-
anchors.leftMargin: 4;
-
anchors.bottom: start.bottom;
-
onClicked: {
-
colorMaker.stop();
-
}
-
}
-
-
function changeAlgorithm(button, algorithm){
-
switch(algorithm)
-
{
-
case 0:
-
button.text = "RandomRGB";
-
break;
-
case 1:
-
button.text = "RandomRed";
-
break;
-
case 2:
-
button.text = "RandomGreen";
-
break;
-
case 3:
-
button.text = "RandomBlue";
-
break;
-
case 4:
-
button.text = "LinearIncrease";
-
break;
-
}
-
}
-
-
Button {
-
id: colorAlgorithm;
-
text: "RandomRGB";
-
anchors.left: stop.right;
-
anchors.leftMargin: 4;
-
anchors.bottom: start.bottom;
-
onClicked: {
-
var algorithm = (colorMaker.algorithm() + 1) % 5;
-
changeAlgorithm(colorAlgorithm, algorithm);
-
colorMaker.setAlgorithm(algorithm);
-
}
-
}
-
-
Button {
-
id: quit;
-
text: "quit";
-
anchors.left: colorAlgorithm.right;
-
anchors.leftMargin: 4;
-
anchors.bottom: start.bottom;
-
onClicked: {
-
Qt.quit();
-
}
-
}
-
-
Component.onCompleted: {
-
colorMaker.color = Qt.rgba( 0, 180, 120, 255);
-
//[3]
-
//colorMaker.setAlgorithm(ColorMaker.LinearIncrease);
-
colorMaker.setAlgorithm( 2);
-
changeAlgorithm(colorAlgorithm, colorMaker.algorithm());
-
}
-
-
Connections {
-
target: colorMaker;
-
onCurrentTime:{
-
timeLabel.text = strTime;
-
timeLabel.color = colorMaker.timeColor;
-
}
-
}
-
-
Connections {
-
target: colorMaker;
-
onColorChanged:{
-
colorRect.color = color;
-
}
-
}
-
}
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() 。它們的函數原型如下:
-
T QObject::findChild( const QString & name = QString(),\
-
Qt::FindChildOptions options = \
-
Qt::FindChildrenRecursively) const;
-
QList<T> QObject::findChildren( const QString & name = \
-
QString(), Qt::FindChildOptions options = \
-
Qt::FindChildrenRecursively) const;
-
QList<T> QObject::findChildren( const QRegExp & regExp, \
-
Qt::FindChildOptions options = \
-
Qt::FindChildrenRecursively) const;
-
QList<T> QObject::findChildren( const QRegularExpression & re,\
-
Qt::FindChildOptions options = \
-
Qt::FindChildrenRecursively) const;
都是模板方法,從命名上也可以看出,一個返回單個對象,一個返回對象列表。閑話少說,現在讓我們看看如何查詢一個或多個對象,我們先以 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 對象,那么你可以這么調用(同步方式):
-
QString retVal;
-
QMetaObject::invokeMethod(obj, "compute", Qt::DirectConnection,
-
Q_RETURN_ARG(QString, retVal),
-
Q_ARG(QString, "sqrt"),
-
Q_ARG( int, 42),
-
Q_ARG( double, 9.7));
如果你要讓一個線程對象退出,可以這么調用(隊列連接方式):
-
QMetaObject::invokeMethod(thread, "quit",
-
Qt::QueuedConnection);
callQml 示例
現在讓我們創建一個新的項目,名字是 callQml ,添加 changeColor.h 、 changeColor.cpp 兩個文件。 main.qml 內容如下:
-
import QtQuick 2.0
-
import QtQuick.Controls 1.1
-
-
Rectangle {
-
objectName: "rootRect";
-
width: 360;
-
height: 360;
-
Text {
-
objectName: "textLabel";
-
text: "Hello World";
-
anchors.centerIn: parent;
-
font.pixelSize: 26;
-
}
-
-
Button {
-
anchors. right: parent. right;
-
anchors.rightMargin: 4;
-
anchors.bottom: parent.bottom;
-
anchors.bottomMargin: 4;
-
text: "quit";
-
objectName: "quitButton";
-
}
-
}
我們給根元素起了個名字 "rootRect" ,給退出按鈕起了個名字 "quitButton" ,給文本起了名字 "textLabel" ,我們會在 C++ 代碼中通過這些個名字來查找對應的對象並改變它們。
現在來看看 main.cpp :
-
-
-
-
-
-
-
-
-
-
int main(int argc, char *argv[])
-
{
-
QGuiApplication app(argc, argv);
-
-
QtQuick2ApplicationViewer viewer;
-
viewer.setMainQmlFile(QStringLiteral( "qml/callQml/main.qml"));
-
viewer.showExpanded();
-
-
QQuickItem * rootItem = viewer.rootObject();
-
new ChangeQmlColor(rootItem);
-
QObject * quitButton = rootItem->findChild<QObject*>( "quitButton");
-
if(quitButton)
-
{
-
QObject::connect(quitButton, SIGNAL(clicked()), &app, SLOT(quit()));
-
}
-
-
QObject *textLabel = rootItem->findChild<QObject*>( "textLabel");
-
if(textLabel)
-
{
-
//1. failed call
-
bool bRet = QMetaObject::invokeMethod(textLabel, "setText", Q_ARG(QString, "world hello"));
-
qDebug() << "call setText return - " << bRet;
-
textLabel->setProperty( "color", QColor::fromRgb( 255, 0, 0));
-
bRet = QMetaObject::invokeMethod(textLabel, "doLayout");
-
qDebug() << "call doLayout return - " << bRet;
-
}
-
-
return app.exec();
-
}
在一開始我通過 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 類定義如下:
-
-
-
-
-
-
class ChangeQmlColor : public QObject
-
{
-
Q_OBJECT
-
public:
-
ChangeQmlColor(QObject *target, QObject *parent = 0);
-
~ChangeQmlColor();
-
-
protected slots:
-
void onTimeout();
-
-
private:
-
QTimer m_timer;
-
QObject *m_target;
-
};
-
-
實現文件 changeColor.cpp :
-
-
-
-
-
-
ChangeQmlColor::ChangeQmlColor(QObject *target, QObject *parent)
-
: QObject(parent)
-
, m_timer( this)
-
, m_target(target)
-
{
-
qsrand(QDateTime::currentDateTime().toTime_t());
-
connect(&m_timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
-
m_timer.start( 1000);
-
}
-
-
ChangeQmlColor::~ChangeQmlColor()
-
{}
-
-
void ChangeQmlColor::onTimeout()
-
{
-
QColor color = QColor::fromRgb(qrand()% 256, qrand()% 256, qrand()% 256);
-
m_target->setProperty( "color", color);
-
}
參考: https://blog.csdn.net/u012611644/article/details/89425204
---------------------------------------------------------------------------------------------------------------------------------------------------
QWebEngine打開chrome devtool調試工具
在代碼中加入下面這段
//QWebEngine DEBUG --remote-debugging-port=9223
qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "9223");
加入load qml前添加
在瀏覽器中訪問http://localhost:9223/ 即可進行調試
---------------------------------------------------------------------------------------------------------------------------------------------------
(譯)通過WebChannel/WebSockets與QML中的HTML交互
一、前言
Qt允許使用所謂的混合GUI創建應用程序——在這種GUI中,可以將本機部件與基於html的內容混合在一起。通過WebChannel和WebSockets公開QObject,這種混合甚至支持這些本地部分和html端之間的交互。
二、如何顯示HTML內容
- 使用webEngineView;
- 使用webView;
- 使用獨立的Web瀏覽器(不會集成到應用程序中);
這三種方法以不同的方式進行,但都支持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建立一些傳輸。
三、與QML中的HTML交互
好的,我們可以顯示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上獲得的幫助 - 我根本無法進行一切工作。





