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

 

 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 使用。

實現可以導出的 C++ 類

前提條件   

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

信號,槽

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

 

我們首先來看一個完整類的實現。

 

LogicMaker.h

 

#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.cpp

 

 

#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"; } }