Qt Quick 技術的引入,使得你可以快速構建 UI ,具備動畫、各類絢麗效果的 UI 都不在話下。但它不是萬能的,也有不少侷限性,原來 Qt 的一些技術,好比低階的網絡編程如 QTcpSocket ,多線程,又如 XML 文檔處理類庫 QXmlStreamReader / QXmlStreamWriter 等等,在 QML 中要麼不可用,要麼用起來不方便,因此呢,不少時候咱們是會基於這樣的原則來混合使用 QML 和 C++: QML 構建界面, C++ 實現非界面的業務邏輯和複雜運算。react

 

 QML 的不少基本類型本來就是在 C++ 中實現的,好比 Item 對應 QQuickItem , Image 對應 QQuickImage , Text 對應 QQuickText  ,……這樣看來,在 QML 中訪問 C++ 對象必然不成問題。然也!反過來,在 C++ 中其實也可使用 QML 對象。編程

 

 

在 QML 中使用 C++ 類和對象

    咱們知道, QML 實際上是對 JavaScript 的擴展,融合了 Qt Object 系統,它是一種新的解釋型的語言, QML 引擎雖然由 Qt C++ 實現,但 QML 對象的運行環境,說到底和 C++ 對象的上下文環境是不一樣的,是平行的兩個世界。若是你想在 QML 中訪問 C++ 對象,那麼必然要找到一種途徑來在兩個運行環境之間創建溝通橋樑。網絡

    Qt 提供了兩種在 QML 環境中使用 C++ 對象的方式:多線程

  1. 在 C++ 中實現一個類,註冊到 QML 環境中, QML 環境中使用該類型建立對象
  2. 在 C++ 中構造一個對象,將這個對象設置爲 QML 的上下文屬性,在 QML 環境中直接使用改屬性

  無論哪一種方式,對要導出的 C++ 類都有要求,不是一個類的全部方法、變量均可以被 QML 使用,所以咱們先來看看怎樣讓一個方法或屬性能夠被 QML 使用。app

實現能夠導出的 C++ 類

前提條件   

     要想將一個類或對象導出到 QML 中,下列前提條件必須知足:
  •     從 QObject 或 QObject 的派生類繼承
  •     使用 Q_OBJECT 宏
    看起來好像和使用信號與槽的前提條件同樣……沒錯,的確是同樣的。這兩個條件是爲了讓一個類可以進入 Qt 強大的元對象系統(meta-object system)中,只有使用元對象系統,一個類的某些方法或屬性纔可能經過字符串形式的名字來調用,才具備了在 QML 中訪問的基礎條件。
    一旦你導出了一個類,在 QML 中就必然要訪問該類的實例的屬性或方法來達到某種目的,不然我真想不來你要幹什麼……而具備什麼特徵的屬性或方法才能夠被 QML 訪問呢?

信號,槽

    只要是信號或者槽,均可以在 QML 中訪問,你能夠把 C++ 對象的信號鏈接到 QML 中定義的方法上,也能夠把 QML 對象的信號鏈接到 C++ 對象的槽上,還能夠直接調用 C++ 對象的槽或信號……因此,這是最簡單好用的一種途徑。

 

咱們首先來看一個完整類的實現。函數

 

LogicMaker.hpost

 

#include <QObject> class LogicMaker:public QObject { Q_OBJECT Q_ENUMS(kGameType) //直接調用函數,非槽函數 Q_INVOKABLE void qmlCallCfunction(); Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged) public: LogicMaker(QObject *p); LogicMaker(){ } enum kGameType{ TYPE_DOTA=2, TYPE_WAR3, TYPE_RPG, }; int width(); void setWidth(int); signals: void widthChanged(int newwidth); public slots: void qmlCallCSlotfunction(kGameType type); private: int _width; QObject* pparent; };

LogicMaker.cpp動畫

 

 

#include "logicmaker.h" #include<QDebug> #include <QQuickView> #include <QQuickItem> LogicMaker::LogicMaker(QObject *obj) { _width=0; pparent=obj; qDebug()<<"parent"<<pparent; } void LogicMaker::qmlCallCSlotfunction(kGameType type){ qDebug()<<"qml call C++ slots function"<<type; setWidth(5); qDebug()<<"=======parent"<< this->parent(); QObject *quitButton = this->parent()->findChild<QObject*>("qmlbtn2");//要在qml中設置其對應的objname if(quitButton!=NULL) { QObject::connect(quitButton, SIGNAL(clicked()), this, SLOT(qmlCallCfunction())); //setText這個必定會調用失敗,由於並無setText這個屬性 bool bRet = QMetaObject::invokeMethod(quitButton, "setText", Q_ARG(QString, "world hello")); qDebug() << "call setText return - " << bRet; quitButton->setProperty("width", 200); quitButton->setProperty("text", QString(tr("hello,world"))); } else { qDebug()<<"get button failed"; } } void LogicMaker::qmlCallCfunction(){ qDebug()<<"qml call C++ function"; } int LogicMaker::width(){ return _width; } void LogicMaker::setWidth(int w){ _width=w; emit widthChanged(w); }

main.cppui

 

 

#include <QGuiApplication> #include <QQuickView> #include <QQuickItem> #include <QQmlContext> include "logicmaker.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); // QQmlApplicationEngine engine; //engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); //在qml環境準備好以前,註冊好qml類型 qmlRegisterType<LogicMaker>("seanyxie.qt.logicMaker", 1, 0,"LogicMaker"); QQuickView viwer; viwer.setSource(QUrl(QStringLiteral("qrc:///main.qml"))); // QObject *rootItem=NULL; QQuickItem *rootItem = viwer.rootObject(); qDebug()<<rootItem; viwer.rootContext()->setContextProperty("cpplogicMaker",new LogicMaker(rootItem)); viwer.show(); return app.exec(); }

main.qml

 

import QtQuick 2.2 import QtQuick.Window 2.1 import QtQuick.Controls 1.1 import seanyxie.qt.logicMaker 1.0 Rectangle { visible: true width: 360 height: 360 id:rect MouseArea { anchors.fill: parent onClicked: { Qt.quit(); } } Text { text: qsTr("Hello World") anchors.centerIn: parent } LogicMaker{ id:qml2Cmaker; } Row{ Button{ id:btn1 onClicked: { qml2Cmaker.qmlCallCSlotfunction(LogicMaker.TYPE_RPG); // cpplogicMaker.qmlCallCSlotfunction(LogicMaker.TYPE_RPG); } } Button{ id:btn2 objectName:"qmlbtn2" onClicked: { // qml2Cmaker.qmlCallCfunction(); } } } //這個對象是用來綁定一個從C++來的信號,對應的曹函數 Connections{ target:qml2Cmaker onWidthChanged: { btn1.width = newwidth // console.log("width change slot %d",newwidth); } } }

第一步,在main.cpp裏面咱們註冊了一個類,能夠在qml中直接被使用,就是這段代碼

 

 

qmlRegisterType<LogicMaker>("seanyxie.qt.logicMaker", 1, 0,"LogicMaker");

這個過程大概分四個步驟:

 

 

  1. 實現 C++ 類
  2. 註冊 QML 類型
  3. 在 QML 中導入類型
  4. 在 QML 建立由 C++ 導出的類型的實例並使用

要註冊一個 QML 類型,有多種方法可用,如 qmlRegisterSingletonType() 用來註冊一個單例類型, qmlRegisterType() 註冊一個非單例的類型, qmlRegisterTypeNotAvailable() 註冊一個類型用來佔位, qmlRegisterUncreatableType() 一般用來註冊一個具備附加屬性的附加類型

 

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);
先說模板參數 typename ,它就是你實現的 C++ 類的類名。
    qmlRegisterType() 的第一個參數 uri ,讓你指定一個惟一的包名,相似 Java 中的那種,一是用來避免名字衝突,而是能夠把多個相關類聚合到一個包中方便引用。好比咱們常寫這個語句 「import QtQuick.Controls 1.1」 ,其中的 「QtQuick.Controls」 就是包名 uri ,而 1.1 則是版本,是 versionMajor 和 versionMinor 的組合。 qmlName 則是 QML 中可使用的類名。

因此這裏註冊的類,在qml中使用的話,就要先

import seanyxie.qt.logicMaker 1.0

在 QML 中建立 C++ 導入類型的實例

    引入包後,你就能夠在 QML 中建立 C++ 導入類型的對象了,與 QML 內建類型的使用徹底同樣。以下是建立一個 LogicMaker 實例的代碼:

 

 

 LogicMaker{
 id:qml2Cmaker;
 
       
 }
 
       

咱們看到,LogicMaker和Rectangle等用法沒有什麼不一樣,指定一個id,就能夠在qml中直接使用這個對象。

 

咱們在LogicMaker中定義了槽函數qmlCallCSlotfunction(),能夠直接在qml中使用qml2Cmaker對象來調用這個槽函數,可是還有一個參數,這個參數是C++類裏的枚舉,這時候須要要QENUM宏來導出這組枚舉。

 

Q_ENUMS

    若是你要導出的類定義了想在 QML 中使用枚舉類型,可使用 Q_ENUMS 宏將該枚舉註冊到元對象系統中。

一旦你使用 Q_ENUMS 宏註冊了你的枚舉類型,在 QML 中就能夠用 ${CLASS_NAME}.${ENUM_VALUE} 的形式來訪問,好比 LogicMaker.TYPE_DOTA,上節展現的 QML 代碼片斷已經使用了導出的枚舉類型。

 

 

 

Q_INVOKABLE 宏

    在定義一個類的成員函數時使用 Q_INVOKABLE 宏來修飾,就可讓該方法被元對象系統調用。這個宏必須放在返回類型前面。

 

 

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 width READ width WRITE setWidth NOTIFY widthChanged)
    type name 這兩個字段想必不用細說了吧? type 是屬性的類型,能夠是 int / float / QString / QObject / QColor / QFont 等等, name 就是屬性的名字。
    其實咱們在實際使用中,不多可以用全 Q_PROPERTY 的全部選項,就往 QML 導出類這種場景來講,比較經常使用的是 READ / WRITE / NOTIFY 三個選項。咱們來看看都是什麼含義。
  • READ 標記,若是你沒有爲屬性指定 MEMBER 標記,則 READ 標記必不可少;聲明一個讀取屬性的函數,該函數通常沒有參數,返回定義的屬性。
  • WRITE 標記,可選配置。聲明一個設定屬性的函數。它指定的函數,只能有一個與屬性類型匹配的參數,必須返回 void 。
  • NOTIFY 標記,可選配置。給屬性關聯一個信號(該信號必須是已經在類中聲明過的),當屬性的值發生變化時就會觸發該信號。信號的參數,通常就是你定義的屬性。

因此上述定義的width屬性,能夠用width,setWidth來讀寫,而且在width發生變化時候,關聯一個信號widthChanged的信號。

 

 

因此這段代碼的表現效果是:

點擊了btn1按鈕後,會經過LogicMaker的對象qml2Cmaker調用C++裏LogicMaker的槽函數qmlCallCSlotFunction,而且帶一個枚舉類型的參數。而後在qmlCallCSlotFunction方法裏,調用setWidth來設置width屬性,而且發射出信號widthChanged的信號。

 

這個信號將會被qml捕獲處理,在qml中有下面一段代碼:

 

Connections{ target:qml2Cmaker onWidthChanged: { btn1.width = newwidth // console.log("width change slot %d",newwidth); } }

Connections的解釋:

 

 

A Connections object creates a connection to a QML signal.

When connecting to signals in QML, the usual way is to create an 「on<Signal>」 handler that reacts when a signal is received, like this:

MouseArea { onClicked: { foo(parameters) } }

就是用來綁定一個QML信號的處理對象,它的槽函數使用on+信號名的格式,因此qml中onWidthChanged,在設定了target屬性後,就會綁定qml2Cmaker對象的信號WidthChanged,這個信號在C++中發出,並在qml中處理。從而修改了btn1按鈕的寬度。

 

 

  好啦,如今再來看看怎樣導出一個對象到 QML 中。

導出一個 C++ 對象爲 QML 的屬性

    上面看了怎樣導出一個 QML 類型在 QML 文檔中使用,你還能夠把 C++ 中建立的對象做爲屬性傳遞到 QML 環境中,而後在 QML 環境中訪問。咱們仍是以 LogciMaker 爲例,對其代碼作適當修改來適應本節的內容。

 

咱們看main.cpp裏代碼

 

 QQuickItem *rootItem = viwer.rootObject();
 qDebug()<<rootItem;
 viwer.rootContext()->setContextProperty("cpplogicMaker",new LogicMaker(rootItem));
 
       
 正式這行代碼從堆上分配了一個 LogicMaker 對象,而後註冊爲 QML 上下文的屬性,起了個名字就叫 cpplogicMaker 。
    viewer.rootContext() 返回的是 QQmlContext 對象。 QQmlContext 類表明一個 QML 上下文,它的 setContextProperty() 方法能夠爲該上下文設置一個全局可見的屬性。要注意的是,你 new 出來的對象, QQmlContext 只是使用,不會幫你刪除,你須要本身找一個合適的時機來刪除它。

    還有一點要說明,由於咱們去掉了 qmlRegisterType() 調用,因此在 main.qml 中不能再訪問LogicMaker 類了,好比你不能經過類名來引用它定義的枚舉類,也不能定義LogicMaker對象了。

 

而後咱們就能夠在qml中使用這個全局可見對象cpplogicMaker了。

 

 

如今來看如何在 QML 中使用咱們導出的屬性

在 QML 中使用關聯到 C++ 對象的屬性

    一旦調用 setContextProperty() 導出了屬性,就能夠在 QML 中使用了,不須要 import 語句哦。下面
cpplogicMaker.qmlCallCSlotfunction(LogicMaker.TYPE_RPG);

固然這裏由於處處了LogicMaker類,因此才能訪問LogicMaker.TYPE_RPG枚舉。

 

 

 

 

 

 

 

在 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;
   示例 1 :
  1. QPushButton *button = parentWidget->findChild<QPushButton *>(「button1」);  

    查找 parentWidget 的名爲 「button1」 的類型爲 QPushButton 的孩子。

    示例 2 :
  1. 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);

因此在LogicMaker類的函數中能夠這樣來調用qml中的對象

 

 

void LogicMaker::qmlCallCSlotfunction(kGameType type){ qDebug()<<"qml call C++ slots function"<<type; setWidth(5); qDebug()<<"=======parent"<< this->parent(); QObject *quitButton = this->parent()->findChild<QObject*>("qmlbtn2");//要在qml中設置其對應的objname if(quitButton!=NULL) { QObject::connect(quitButton, SIGNAL(clicked()), this, SLOT(qmlCallCfunction())); //setText這個必定會調用失敗,由於並無setText這個屬性 bool bRet = QMetaObject::invokeMethod(quitButton, "setText", Q_ARG(QString, "world hello")); qDebug() << "call setText return - " << bRet; quitButton->setProperty("width", 200); quitButton->setProperty("text", QString(tr("hello,world"))); } else { qDebug()<<"get button failed"; } }