1. 程式人生 > >C++程式中使用QML繫結機制

C++程式中使用QML繫結機制

原文地址:http://doc.qt.digia.com/4.7-snapshot/qtbinding.html
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

文件展示如何使用這個模組建立基於C++的應用程式.

核心模組類

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
 }

// Using QDeclarativeComponent
 QDeclarativeEngine engine;
 QDeclarativeComponent component(&engine,
         QUrl::fromLocalFile("MyItem.qml"));
 QObject *object = component.create();
 ...
 delete object; 
 // Using QDeclarativeView
 QDeclarativeView view;
 view.setSource(QUrl::fromLocalFile("MyItem.qml"));
 view.show();
 QObject *object = view.rootObject();

 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程式碼呼叫物件例項的方法:

class ApplicationData : public QObject
 {
     Q_OBJECT
 public:
     Q_INVOKABLE QDateTime getCurrentDateTime() const {
         return QDateTime::currentDateTime();
     }
 };

 int main(int argc, char *argv[]) {
     QApplication app(argc, argv);

     QDeclarativeView view;

     ApplicationData data;
     view.rootContext()->setContextProperty("applicationData", &data);

     view.setSource(QUrl::fromLocalFile("MyItem.qml"));
     view.show();

     return app.exec();
 } 
// 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"
     }
 }

 // main.cpp
 QDeclarativeEngine engine;
 QDeclarativeComponent component(&engine, "MyItem.qml");
 QObject *object = component.create();

 QVariant returnedValue;
 QVariant msg = "Hello from C++";
 QMetaObject::invokeMethod(object, "myQmlFunction",
         Q_RETURN_ARG(QVariant, returnedValue),
         Q_ARG(QVariant, msg));

 qDebug() << "QML function returned:" << returnedValue.toString();
 delete object;

在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)
         }
     }
 }

 class MyClass : public QObject
 {
     Q_OBJECT
 public:
     Q_INVOKABLE void cppMethod(const QString &msg) {
         qDebug() << "Called the C++ method with" << msg;
     }

 public slots:
     void cppSlot(int number) {
         qDebug() << "Called the C++ slot with" << number;
     }
 };

 int main(int argc, char *argv[]) {
     QApplication app(argc, argv);

     QDeclarativeView view;
     MyClass myClass;
     view.rootContext()->setContextProperty("myObject", &myClass);

     view.setSource(QUrl::fromLocalFile("MyItem.qml"));
     view.show();

     return app.exec();
 }

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")
     }
 }

 class MyClass : public QObject
 {
     Q_OBJECT
 public slots:
     void cppSlot(const QString &msg) {
         qDebug() << "Called the C++ slot with message:" << msg;
     }
 };

 int main(int argc, char *argv[]) {
     QApplication app(argc, argv);

     QDeclarativeView view(QUrl::fromLocalFile("MyItem.qml"));
     QObject *item = view.rootObject();

     MyClass myClass;
     QObject::connect(item, SIGNAL(qmlSignal(QString)),
                      &myClass, SLOT(cppSlot(QString)));

     view.show();
     return app.exec();
 }

要在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)
 }

 class ImageViewer : public QDeclarativeItem
 {
     Q_OBJECT
     Q_PROPERTY(QUrl image READ image WRITE setImage NOTIFY imageChanged)
 public:
     ...
 signals:
     void imageChanged();
     void loadingError(const QString &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!")
     }
 }

 ImageViewer viewer;

 QDeclarativeView view;
 view.rootContext()->setContextProperty("imageViewer", &viewer);

 view.setSource(QUrl::fromLocalFile("MyItem.qml"));
 view.show();

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(),QDeclarativePropertyQMetaProperty::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"
     }
 }

 class ApplicationData : public QObject
 {
     Q_OBJECT
     Q_PROPERTY(QColor backgroundColor
             READ backgroundColor
             WRITE setBackgroundColor
             NOTIFY backgroundColorChanged)

 public:
     void setBackgroundColor(const QColor &c) {
         if (c != m_color) {
             m_color = c;
             emit backgroundColorChanged();
         }
     }

     QColor backgroundColor() const {
         return m_color;
     }

 signals:
     void backgroundColorChanged();

 private:
     QColor m_color;
 };

注意backgroundColorChanged被標記為backgroundColor屬性的NOTIFY訊號.如果Qt屬性沒有相關的NOTIFY訊號,屬性就不能用於QML的屬性繫結,因為當屬性值被修改時QML引擎不會得到通知.如果在QML中使用自定義型別,確保屬性具有NOTIFY訊號,以便於用於屬性繫結中.

支援的資料型別

用於QML中的任何C++資料--自定義屬性,或訊號和函式的引數,QML都必須支援其型別.

預設QML支援如下資料型別:

JavaScript陣列和物件

QML內建支援在QVariantList和JavaScript陣列之間,QVariantMap和JavaScript物件間的轉換.

例如,如下定義在QML中的函式需要兩個引數,一個數組一個物件,使用標準的JavaScript語法訪問陣列和物件輸出其中的內容.C++程式碼呼叫了這個函式,傳遞QVariantListQVariantMap引數,將自動轉換為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])
         }
     }
 }

 // C++
 QDeclarativeView view(QUrl::fromLocalFile("MyItem.qml"));

 QVariantList list;
 list << 10 << Qt::green << "bottles";

 QVariantMap map;
 map.insert("language", "QML");
 map.insert("released", QDate(2010, 9, 21));

 QMetaObject::invokeMethod(view.rootObject(), "readValues",
         Q_ARG(QVariant, QVariant::fromValue(list)),
         Q_ARG(QVariant, QVariant::fromValue(map)));

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.qmlbackground.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