C++程式中使用QML繫結機制
QML被定為一種可容易使用C++擴充套件,並可擴充套件C++的語言.使用Qt Declarative模組中的類可在C++中載入和操作QML中的元件,通過Qt的元物件系統,QML和C++物件可輕易的使用訊號和槽機制進行通訊.此外,QML外掛可以建立釋出可重用QML元件.
你可能有很多種理由要將QML和C++混合使用.如:
- 使用C++原始碼中的函式/功能 (如使用基於Qt的C++資料模型,或呼叫三方C++庫中的函式)
- 建立QML元件(用於自己的專案或釋出給其他人使用)
要使用Qt Declarative模組,必須包含和連結相應的模組,請見module index page.Qt Declarative UI Runtime
核心模組類
Qt Declarative模組提供了一組C++ API用於在C++中擴充套件QML應用程式,並可將QML嵌入到C++應用程式中.Qt Declarative模組中有幾個核心類為實現這個目標提供了必要的支援:
QDeclarativeEngine engine;
QDeclarativeComponent component(&engine, QUrl::fromLocalFile("MyRectangle.qml"));
QObject *rectangleInstance = component.create();
// ...
delete rectangleInstance;
QML與C++結合的方式
使用C++擴充套件QML應用程式有很多種方式.例如::
- 在C++中載入QML元件並進行操作(可操作其子元素)
- 直接將C++物件及其屬性嵌入到QML元件中(例如,在QML中呼叫指定的C++物件,或使用資料集來模擬一個列表模型)
- 定義新的QML元素(QObject繼承)並可在QML程式碼中直接建立
這些方式在下面做展示.當然這些方式相互間不衝突,在應用程式中可根據需要組合使用.
在C++中載入QML元件
例如,有如下所示的一個MyItem.qml檔案:
import QtQuick 1.0
Item {
width: 100; height: 100
}
|
|
object->setProperty("width", 500);
QDeclarativeProperty(object, "width").write(500);
當然,也可將物件轉換為其實際型別,以便於在編譯時期安全的呼叫方法.本例中基於MyItem.qml的物件是一個Item,由QDeclarativeItem 類來定義:
QDeclarativeItem *item = qobject_cast<QDeclarativeItem*>(object);
item->setWidth(500);
定位子物件
QML元件本質上是一個具有兄弟和子節點的物件樹.可使用QObject::findChild()傳遞一個物件名稱獲取QML元件的子物件.例如MyItem.qml中的根物件具有一個Rectangle子元素:
import QtQuick 1.0
Item {
width: 100; height: 100
Rectangle {
anchors.fill: parent
objectName: "rect"
}
}
可這樣獲取子物件:
QObject *rect = object->findChild<QObject*>("rect");
if (rect)
rect->setProperty("color", "red");
如果objectName被用於ListView,Repeater代理,或其他生成多個例項的代理上,將會有多個子物件具有相同的名稱(objectName).這時,使用QObject::findChildren()獲取所有叫做objectName的子元素.
警告: 由於這種方法可以在C++中獲取並操作物件樹中內部的QML元素,除了測試和建立原型外我們不建議採用這種方式.QML和C++整合在一起的一個優勢就是將QML的使用者介面與C++邏輯和資料集相隔離,如果在C++中直接獲取並操作QML物件中的內部元件會打破這個策略. 這將使開發變得困難,如更改了QML檢視,新的元件中不含objectName子元素,會發生錯誤.最好的情況是C++實現對QML使用者介面實現和內部組成QML物件樹不做任何假設.
在QML元件中嵌入C++物件
當在C++應用程式中載入QML場景時,將C++資料嵌入到QML物件中是很有幫助的.QDeclarativeContext 可以向QML元件暴漏資料,將資料從C++注入到QML中.
例如,下面的QML項中有一個currentDateTime值,但並沒有在這個上下文中宣告:
// MyItem.qml
import QtQuick 1.0
Text { text: currentDateTime }
QDeclarativeView view;
view.rootContext()->setContextProperty("currentDateTime", QDateTime::currentDateTime());
view.setSource(QUrl::fromLocalFile("MyItem.qml"));
view.show();
上下文屬性可以儲存為QVariant或者QObject*型別.這意味著自定義的C++物件也可以使用這種方式注入,而且可以直接在QML中讀取或修改這些物件.我們將上例中的QDateTime值修改為一個嵌入的QObject例項,讓QML程式碼呼叫物件例項的方法:
|
// MyItem.qml import QtQuick 1.0 Text { text: applicationData.getCurrentDateTime() }(注意C++向QML返回的date/time值可使用Qt.formatDateTime() 及相關函式進行格式化.)
如果QML需要接收上下文的訊號,可使用Connections元素進行連線.例如,如果ApplicationData有一個叫做dataChanged()的訊號,這個訊號可以使用Connections物件連線到一個訊號處理器上:
Text { text: applicationData.getCurrentDateTime() Connections { target: applicationData onDataChanged: console.log("The application data changed!") } }
定義新的QML元素
QML中可以定義新的QML元素,同樣也可在C++中定義;事實上很多QML元素都是通過C++類實現的.當使用這些元素建立一個QML物件時,只是簡單的建立了這個基於QObject的C++類的例項,並設定了屬性.
例如,下面是一個帶有image屬性的ImageViewer類:
#include <QtCore>
#include <QtDeclarative>
class ImageViewer : public QDeclarativeItem
{
Q_OBJECT
Q_PROPERTY(QUrl image READ image WRITE setImage NOTIFY imageChanged)
public:
void setImage(const QUrl &url);
QUrl image() const;
signals:
void imageChanged();
};
qmlRegisterType<ImageViewer>("MyLibrary", 1, 0, "ImageViewer");
載入到C++應用程式或外掛中的QML程式碼就可以操作ImageViewer物件:
import MyLibrary 1.0 ImageViewer { image: "smile.png" }
這裡建議不要使用QDeclarativeItem文件指定屬性之外的功能.這是因為GraphicsView後臺依賴QML的實現細節,因此QtQuick項可再向底層移動,在QML角度上可以應用但不能修改.要最小化自定義可視項的可移植要求,就應儘量堅持使用QDeclarativeItem文件標記的屬性.從QDeclarativeItem中繼承但沒有文件化的屬性都是與實現細節相關的;他們不受官方支援可能在相關的釋出版本中被去掉.
注意自定義的C++類不必從QDeclarativeItem繼承;只有在需要顯示時才是必須的.如果項不可見,可從QObject繼承.
在QML和C++之間交換資料
QML和C++物件之間可通過訊號槽,屬性修改等機制進行通訊.對於一個C++物件,任何暴露在Qt的元物件系統中的資料--屬性,訊號,槽和使用Q_INVOKABLE標記的方法都可在QML中訪問.在QML端,所有QML物件的資料都可在Qt元物件系統和C++中訪問.
呼叫函式
QML函式可在C++中呼叫,反之亦然.
所有的QML函式都被暴漏在了元資料系統中,並可通過QMetaObject::invokeMethod()呼叫.C++應用程式呼叫QML函式:
// MyItem.qml import QtQuick 1.0 Item { function myQmlFunction(msg) { console.log("Got message:", msg) return "some return value" } }
|
在QML中呼叫C++函式,函式必須是Qt的槽或標記了Q_INVOKABLE巨集的函式,才能在QML中訪問.下面範例中,QML程式碼呼叫了(使用QDeclarativeContext::setContextProperty()設定到QML中的)myObject物件的方法:
// MyItem.qml import QtQuick 1.0 Item { width: 100; height: 100 MouseArea { anchors.fill: parent onClicked: { myObject.cppMethod("Hello from QML") myObject.cppSlot(12345) } } }
|
QML支援呼叫C++的過載函式.如果C++中有多個同名不同參的函式,將根據引數數量和型別呼叫正確的函式.
接收訊號
所有QML訊號都可在C++中訪問,像任何標準的Qt C++訊號一樣可使用QObject::connect()進行連線.相反,任何C++訊號都可被QML物件的訊號處理函式接收.
下面的QML元件具有一個叫做qmlSignal的訊號.這個訊號使用QObject::connect()連線到了一個C++物件的槽上,當qmlSignal觸發時會呼叫cppSlot()函式:
// MyItem.qml import QtQuick 1.0 Item { id: item width: 100; height: 100 signal qmlSignal(string msg) MouseArea { anchors.fill: parent onClicked: item.qmlSignal("Hello from QML") } }
|
要在QML中連線Qt C++的訊號,使用on<SignalName>語法訪問訊號控制代碼.如果C++物件可直接在QML中建立(見上面的Defining new QML elements),訊號處理函式可在物件定義時指定.在下面例子中,QML程式碼建立了一個ImageViewer物件,C++物件的imageChanged和loadingError訊號連線到QML中的onImageChanged和onLoadingError訊號處理函式:
ImageViewer { onImageChanged: console.log("Image changed!") onLoadingError: console.log("Image failed to load:", errorMsg) }
|
(注意如果訊號被宣告為屬性的NOTIFY訊號,QML就允許使用on<Property>Changed控制代碼訪問這個訊號,即使訊號的名稱不是<Property>Changed.上例中,如果將imageChanged訊號改為imageModified,onImageChanged訊號處理函式還是會被呼叫的.)
// MyItem.qml import QtQuick 1.0 Item { Connections { target: imageViewer onImageChanged: console.log("Image has changed!") } }
|
C++訊號可以使用列舉值作為引數,列舉定義在類中隨訊號觸發而傳遞,這個列舉必須使用Q_ENUMS巨集註冊.見Using enumerations of a custom type.
修改屬性
C ++中可以訪問QML物件的所有屬性.對如下QML物件:
// MyItem.qml import QtQuick 1.0 Item { property int someNumber: 100 }
QDeclarativeEngine engine;
QDeclarativeComponent component(&engine, "MyItem.qml");
QObject *object = component.create();
qDebug() << "Property value:" << QDeclarativeProperty::read(object, "someNumber").toInt();
QDeclarativeProperty::write(object, "someNumber", 5000);
qDebug() << "Property value:" << object->property("someNumber").toInt();
object->setProperty("someNumber", 100);
你應該總使用QObject::setProperty(),QDeclarativeProperty 或QMetaProperty::write()修改QML屬性值,使QML引擎知道屬性已經被修改.例如,假設有一個自定義的元素PushButton,帶有一個buttonText屬性,反映內部的m_buttonText成員變數值.直接修改成員變數值是不明智的:
// BAD!
QDeclarativeComponent component(engine, "MyButton.qml");
PushButton *button = qobject_cast<PushButton*>(component.create());
button->m_buttonText = "Click me";
由於直接修改了成員變數的值,越過了Qt的元物件系統,QML引擎就無法知道值被修改過.這樣繫結到buttonText的屬性就不會更新,任何onButtonTextChanged處理函式都不會被呼叫.
任何使用Q_PROPERTY巨集宣告的Qt屬性都可在QML中訪問.下面修改本文件前面例子,ApplicationData類具有一個backgroundColor屬性.這個屬性可在QML中進行讀寫:
// MyItem.qml import QtQuick 1.0 Rectangle { width: 100; height: 100 color: applicationData.backgroundColor MouseArea { anchors.fill: parent onClicked: applicationData.backgroundColor = "red" } }
|
注意backgroundColorChanged被標記為backgroundColor屬性的NOTIFY訊號.如果Qt屬性沒有相關的NOTIFY訊號,屬性就不能用於QML的屬性繫結,因為當屬性值被修改時QML引擎不會得到通知.如果在QML中使用自定義型別,確保屬性具有NOTIFY訊號,以便於用於屬性繫結中.
支援的資料型別
用於QML中的任何C++資料--自定義屬性,或訊號和函式的引數,QML都必須支援其型別.
預設QML支援如下資料型別:
JavaScript陣列和物件
QML內建支援在QVariantList和JavaScript陣列之間,QVariantMap和JavaScript物件間的轉換.
例如,如下定義在QML中的函式需要兩個引數,一個數組一個物件,使用標準的JavaScript語法訪問陣列和物件輸出其中的內容.C++程式碼呼叫了這個函式,傳遞QVariantList 和QVariantMap引數,將自動轉換為JavaScript的陣列和物件:
// MyItem.qml Item { function readValues(anArray, anObject) { for (var i=0; i<anArray.length; i++) console.log("Array item:", anArray[i]) for (var prop in anObject) { console.log("Object item:", prop, "=", anObject[prop]) } } }
|
This produces output like:
Array item: 10
Array item: #00ff00
Array item: bottles
Object item: language = QML
Object item: released = Tue Sep 21 2010 00:00:00 GMT+1000 (EST)
使用自定義列舉型別
要在自定義C++元件中使用列舉,列舉型別必須使用Q_ENUMS巨集註冊到Qt的元物件系統.例如,如下C++型別具有一個Status列舉型別:
class ImageViewer : public QDeclarativeItem
{
Q_OBJECT
Q_ENUMS(Status)
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
public:
enum Status {
Ready,
Loading,
Error
};
Status status() const;
signals:
void statusChanged();
};
假設ImageViewer類已經使用qmlRegisterType()進行註冊,現在其Status列舉可用在QML中:
ImageViewer { onStatusChanged: { if (status == ImageViewer.Ready) console.log("Image viewer is ready!") } }
要使用內建的列舉,C++類必須註冊到QML中.如果C++型別不可例項化,可使用qmlRegisterUncreatableType()註冊.在QML中列舉值其首字母必須大寫.
列舉值作為訊號引數
C++訊號可以向QML中傳遞一個列舉型別引數,假設列舉和訊號定義在同一個類中,或列舉值定義在Qt名稱空間(Qt Namespace)中.
此外,如果C++訊號帶有一個列舉引數,應該使用connect()函式與QML中的函式相關聯,列舉型別必須使用qRegisterMetaType()註冊.
對於QML訊號,作為訊號引數的列舉值使用int型別替代:
ImageViewer { signal someOtherSignal(int statusValue) Component.onCompleted: { someOtherSignal(ImageViewer.Loading) } }
從字串做自動型別轉換
為了方便,在QML中一些基本型別的值可使用格式化字串指定,便於在QML中向C++傳遞簡單的值.
Type | String format | Example |
---|---|---|
顏色名稱, "#RRGGBB", "#RRGGBBAA" | "red", "#ff0000", "#ff000000" | |
QDate | "YYYY-MM-DD" | "2010-05-31" |
"x,y" | "10,20" | |
QRect | "x,y,寬x高" | "50,50,100x100" |
QSize | "寬x高" | "100x200" |
QTime | "hh:mm:ss" | "14:22:55" |
QUrl | URL字串 | "http://www.example.com" |
"x,y,z" | "0,1,0" | |
列舉值 | 列舉值名稱 | "AlignRight" |
這些字串格式用於設定QML屬性值和向C++函式傳遞引數.本文件中有很多範例進行演示;在上面的範例中,ApplicationData類有一個QColor型別的backgroundColor屬性,在QML中使用字串"red"而不是一個QColor物件進行賦值.
如果喜歡使用顯式型別賦值,Qt物件提供了便利的全域性函式來建立物件的值.例如Qt.rgba()建立一個基於RGBA的QColor值.這個函式返回的QColor型別的值可用於設定QColor型別的屬性,或呼叫需要QColor型別引數的C++函式.
建立QML外掛
Qt Declarative模組包含一個QDeclarativeExtensionPlugin類,這個抽象類用於建立QML外掛.可在QML應用程式中動態載入QML擴充套件型別.
使用Qt資源系統管理資原始檔
Qt resource system 可將資原始檔儲存在二進位制可執行檔案中.這對建立QML/C++聯合的應用程式很有幫助,可通過資源系統的URI(像其他圖片和聲音資原始檔一樣)排程訪問QML檔案,而不是使用相對或絕對檔案系統路徑.注意如果使用資源系統,當QML資原始檔被修改後必須重新編譯可執行應用程式,以便於更新包中的資源.
要在QML/C++應用程式中使用資源系統:
- 建立一個.qrc資源集合檔案,以XML格式例舉資原始檔
- 在C++中,使用:/prefix或qrc排程URL載入主QML檔案資源
這樣做後,QML中所有已相對路徑指定的檔案都從資原始檔中載入.使用資源系統完全對QML層透明;即QML程式碼可以用相對路徑來訪問資原始檔,而不帶有qrc排程.這個排程(qrc)只用於在C++中引用資原始檔.
這是使用Qt資源系統的應用程式包.目錄結構如下:
project
|- example.qrc
|- main.qml
|- images
|- background.png
|- main.cpp
|- project.pro
main.qml 和 background.png 檔案作為資原始檔打包.這在example.qrc資原始檔中指定:
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/">
<file>main.qml</file>
<file>images/background.png</file>
</qresource>
</RCC>
由於background.png 是一個資原始檔,main.qml可在example.qrc中使用的相當路徑引用它:
// main.qml import QtQuick 1.0 Image { source: "images/background.png" }
要使QML檔案正確的定位資原始檔,main.cpp載入主QML檔案--main.qml,訪問資原始檔需要使用qrc排程(scheme):
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QDeclarativeView view;
view.setSource(QUrl("qrc:/main.qml"));
view.show();
return app.exec();
}
最後在project.pro中將RESOURCES 變數設定為用來構建應用程式資源的example.qrc 檔案:
QT += declarative
SOURCES += main.cpp
RESOURCES += example.qrc