程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> QML與C++混合編程詳解

QML與C++混合編程詳解

編輯:關於C++

1QMLC++為什麼要混合編程

QML與C++為什麼要混合編程,簡單來說,就是使用QML高效便捷地構建UI,而C++則用來實現業務邏輯和復雜算法,下面介紹了兩者間交互的方法與技巧。

2QML訪問C++概述

Qt集成了QML引擎和Qt元對象系統,使得QML很容易從C++中得到擴展,在一定的條件下,QML就可以訪問QObject派生類的成員,例如信號、槽函數、枚舉類型、屬性、成員函數等。

QML訪問C++有兩個方法:一是在Qt元對象系統中注冊C++類,在QML中實例化、訪問。二是在C++中實例化並設置為QML上下文屬性,在QML中直接使用。與後者相比,前者可以使C++類在QML中作為一個數據類型,例如函數參數類型或屬性類型,也可以使用其枚舉類型、單例等,功能更強大。

3、如何實現可以被QML訪問的C++

C++類要想被QML訪問,首先必須滿足兩個條件:一是派生自QObject類或QObject類的子類,二是使用Q_OBJECT宏。QObject類是所有Qt對象的基類,作為Qt對象模型的核心,提供了信號與槽機制等很多重要特性。Q_OBJECT宏必須在private區(C++默認為private)聲明,用來聲明信號與槽,使用Qt元對象系統提供的內容,位置一般在語句塊首行。下面例子在QtCreator3.1.2中創建,Projects選擇QtQuickApplication,工程名為Gemini,Component選擇QtQuick2.2,然後在自動生成的文件中添磚加瓦。

信號與槽——

(1)添加頭文件Gemini.h

#ifndef GEMINI_H
#define GEMINI_H
// Gemini.h
#include 
#include 
class Gemini : public QObject
{
    Q_OBJECT
signals:
    void begin();
public slots:
    void doSomething() {
        qDebug() << "Gemini::doSomething() called";
    }
};
#endif // GEMINI_H

Gemini類中的信號begin()和槽doSomething()都可以被QML訪問。槽必須聲明為public或protected,信號在C++中使用時要用到emit關鍵字,但在QML中就是個普通的函數,用法同函數一樣,信號處理器形式為on,Signal首字母大寫。信號不支持重載,多個信號的名字相同而參數不同時,能夠被識別的只是最後一個信號,與信號的參數無關。

(2)修改main.cpp

// main.cpp
#include 
#include 
#include 
#include 
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    qmlRegisterType("Union.Lotto.Gemini", 1, 0, "Gemini");
    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
    return app.exec();
}

這裡把Gemini類注冊(qmlRegisterType)到了Qt元對象系統,當然也可以先實例化再設置為QML上下文屬性,相關內容將在後面詳細介紹。

(3)修改main.qml

// main.qml
import QtQuick 2.2
import QtQuick.Window 2.1
import Union.Lotto.Gemini 1.0
Window {
    visible: true
    width: 360; height: 360
    title: "Union Lotto Game"
    color: "white"
    MouseArea {
        anchors.fill: parent
        onClicked: {
            gemini.begin()
        }
    }
    Gemini {
        id: gemini
        onBegin: doSomething()
    }
}

Gemini類注冊到Qt元對象系統後,並且在QML文件中導入(import),關鍵字Gemini就可以在當前QML文件中當作一種QML類型來用了。例子中有個MouseArea,單擊鼠標時會發送begin()信號,進而調用doSomething()槽函數。

枚舉類型——

(1)修改頭文件Gemini.h

#ifndef GEMINI_H
#define GEMINI_H
// Gemini.h
#include 
#include 
class Gemini : public QObject
{
    Q_OBJECT
    Q_ENUMS(BALL_COLOR)
public:
    Gemini() : m_ballColor(BALL_COLOR_YELLOW) {
        qDebug() << "Gemini::Gemini() called";
    }
    enum BALL_COLOR {
        BALL_COLOR_YELLOW,
        BALL_COLOR_RED,
        BALL_COLOR_BLUE,
        BALL_COLOR_ALL
    };
signals:
    void begin();
public slots:
    void doSomething(BALL_COLOR ballColor) {
        qDebug() << "Gemini::doSomething() called with" << ballColor;
        if(ballColor != m_ballColor) {
            m_ballColor = ballColor;
            qDebug() << "ball color changed";
        }
    }
private:
    BALL_COLOR m_ballColor;
};
#endif // GEMINI_H

Gemini類中添加了public的BALL_COLOR枚舉類型,這個枚舉類型要想在QML中使用,就用到了Q_ENUMS()宏。

(2)修改main.qml

// main.qml
import QtQuick 2.2
import QtQuick.Window 2.1
import Union.Lotto.Gemini 1.0
Window {
    visible: true
    width: 360; height: 360
    title: "Union Lotto Game"
    color: "white"
    MouseArea {
        anchors.fill: parent
        onClicked: {
            gemini.begin()
        }
    }
    Gemini {
        id: gemini
        onBegin: doSomething(Gemini.BALL_COLOR_RED)
    }
}

在QML中使用枚舉類型的方式是.,例如Gemini.BALL_COLOR_RED。

成員函數——

(1)修改頭文件Gemini.h

#ifndef GEMINI_H
#define GEMINI_H
// Gemini.h
#include 
#include 
class Gemini : public QObject
{
    Q_OBJECT
    Q_ENUMS(BALL_COLOR)
public:
    Gemini() : m_ballColor(BALL_COLOR_YELLOW) {
        qDebug() << "Gemini::Gemini() called";
    }
    enum BALL_COLOR {
        BALL_COLOR_YELLOW,
        BALL_COLOR_RED,
        BALL_COLOR_BLUE,
        BALL_COLOR_ALL
    };
    Q_INVOKABLE void stop() {
        qDebug() << "Gemini::stop() called";
    }
signals:
    void begin();
public slots:
    void doSomething(BALL_COLOR ballColor) {
        qDebug() << "Gemini::doSomething() called with" << ballColor;
        if(ballColor != m_ballColor) {
            m_ballColor = ballColor;
            qDebug() << "ball color changed";
        }
    }
private:
    BALL_COLOR m_ballColor;
};
#endif // GEMINI_H

Gemini類中添加了成員函數stop(),在QML中訪問的前提是public或protected成員函數,且使用Q_INVOKABLE宏,位置在函數返回類型的前面。

(2)修改main.qml

// main.qml
import QtQuick 2.2
import QtQuick.Window 2.1
import Union.Lotto.Gemini 1.0
Window {
    visible: true
    width: 360; height: 360
    title: "Union Lotto Game"
    color: "white"
    MouseArea {
        anchors.fill: parent
        onClicked: {
            gemini.begin()
            gemini.stop()
        }
    }
    Gemini {
        id: gemini
        onBegin: doSomething(Gemini.BALL_COLOR_RED)
    }
}

在QML中訪問C++的成員函數的形式是.,例如gemini.stop()。支持函數重載,這個與信號不同。

C++類的屬性——

(1)修改頭文件Gemini.h

#ifndef GEMINI_H
#define GEMINI_H
// Gemini.h
#include 
#include 
class Gemini : public QObject
{
    Q_OBJECT
    Q_ENUMS(BALL_COLOR)
    Q_PROPERTY(unsigned int ballNumber READ ballNumber WRITE setBallNumber NOTIFY ballNumberChanged)
public:
    Gemini() : m_ballColor(BALL_COLOR_YELLOW), m_ballNumber(0) {
        qDebug() << "Gemini::Gemini() called";
    }
    enum BALL_COLOR {
        BALL_COLOR_YELLOW,
        BALL_COLOR_RED,
        BALL_COLOR_BLUE,
        BALL_COLOR_ALL
    };
    unsigned int ballNumber() const {
        return m_ballNumber;
    }
    void setBallNumber(const unsigned int &ballNumber) {
        if(ballNumber != m_ballNumber) {
            m_ballNumber = ballNumber;
            emit ballNumberChanged();
        }
    }
    Q_INVOKABLE void stop() {
        qDebug() << "Gemini::stop() called";
    }
signals:
    void begin();
    void ballNumberChanged();
public slots:
    void doSomething(BALL_COLOR ballColor) {
        qDebug() << "Gemini::doSomething() called with" << ballColor;
        if(ballColor != m_ballColor) {
            m_ballColor = ballColor;
            qDebug() << "ball color changed";
        }
    }
private:
    BALL_COLOR m_ballColor;
    unsigned int m_ballNumber;
};
#endif // GEMINI_H

Gemini類中添加了Q_PROPERTY()宏,用來在QObject派生類中聲明屬性,這個屬性如同類的數據成員一樣,但它又有一些額外的特性可通過Qt元對象系統來訪問。

下面是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])

屬性的type、name是必需的,其它是可選項,常用的有READ、WRITE、NOTIFY。屬性的type可以是QVariant支持的任何類型,也可以是自定義類型,包括自定義類、列表類型、組屬性等。另外,屬性的READ、WRITE、RESET是可以被繼承的,也可以是虛函數,這些特性並不常用。

READ:讀取屬性值,如果沒有設置MEMBER的話,它是必需的。一般情況下,函數是個const函數,返回值類型必須是屬性本身的類型或這個類型的const引用,沒有參數。

WRITE:設置屬性值,可選項。函數必須返回void,有且僅有一個參數,參數類型必須是屬性本身的類型或這個類型的指針或引用。

NOTIFY:與屬性關聯的可選信號。這個信號必須在類中聲明過,當屬性值改變時,就可觸發這個信號,可以沒有參數,有參數的話只能是一個類型同屬性本身類型的參數,用來記錄屬性改變後的值。

Q_PROPERTY()的詳細用法可參考如下網址:

http://doc.qt.io/qt-5/properties.html#qt-s-property-system

(2)修改main.qml

// main.qml
import QtQuick 2.2
import QtQuick.Window 2.1
import Union.Lotto.Gemini 1.0
Window {
    visible: true
    width: 360; height: 360
    title: "Union Lotto Game"
    color: "white"
    MouseArea {
        anchors.fill: parent
        onClicked: {
            gemini.begin()
            gemini.stop()
            gemini.ballNumber = 10
        }
    }
    Gemini {
        id: gemini
        onBegin: doSomething(Gemini.BALL_COLOR_RED)
        onBallNumberChanged: console.log("new ball number is", ballNumber) // 10
        Component.onCompleted: console.log("default ball number is", ballNumber) // 0
    }
}

Gemini類中的ballNumber屬性可以在QML中訪問、修改,訪問時調用了ballNumber()函數,修改時調用了setBallNumber()函數,同時還發送了一個信號來自動更新這個屬性值。

4、注冊C++類為QML類型

QObject派生類可以注冊到Qt元對象系統,使得該類在QML中同其它內建類型一樣,可以作為一個數據類型來使用。QML引擎允許注冊可實例化的類型,也可以是不可實例化的類型,常見的注冊函數有:

        qmlRegisterInterface()
        qmlRegisterRevision()
        qmlRegisterSingletonType()
        qmlRegisterType()
        qmlRegisterTypeNotAvailable()
        qmlRegisterUncreatableType()

這些注冊函數各有其用,可根據實際需要選擇,使用時需要包含。常用的為qmlRegisterType(),它有三個重載函數,這裡只介紹其一:

        template
        int qmlRegisterType(const char *uri,
			   int versionMajor,
			   int versionMinor, 
		           const char *qmlName);

這個模板函數注冊C++類到Qt元對象系統中,uri是需要導入到QML中的庫名,versionMajor和versionMinor是其版本數字,qmlName是在QML中可以使用的類型名。例如上面例子main.cpp中的代碼:

    qmlRegisterType("Union.Lotto.Gemini", 1, 0, "Gemini");

main.cpp中將Gemini類注冊為在QML中可以使用的Gemini類型,主版本為1,次版本為0,庫的名字是Union.Lotto.Gemini。main.qml中導入了這個庫,使用Gemini構造了一個對象,id為gemini,這樣就可以借助id來訪問C++了。

注冊動作必須在QML上下文創建之前,否則無效。

另外:QQuickView為QtQuickUI提供了一個窗口,可以方便地加載QML文件並顯示其界面。QApplication派生自QGuiApplication,而QGuiApplication又派生自QCoreApplication,這三個類是常見的管理Qt應用程序的類。QQmlApplicationEngine可以方便地從一個單一的QML文件中加載應用程序,它派生自QQmlEngine,QQmlEngine則提供了加載QML組件的環境,可以與QQmlComponent、QQmlContext等一起使用。

5QML上下文屬性設置

在C++應用程序加載QML對象時,我們可以直接嵌入一些C++數據來給QML使用,這裡需要用到QQmlContext::setContextProperty(),即設置QML上下問屬性,它可以是一個簡單的類型,也可以是任何我們自定義的類對象。

(1)修改main.cpp

// main.cpp
#include 
#include 
#include 
#include 
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    QQuickView view;
    Gemini gemini;
    view.rootContext()->setContextProperty("gemini", &gemini);
    view.setSource(QUrl(QStringLiteral("qrc:///main.qml")));
    view.show();
    return app.exec();
}

徹底修改一下main.cpp吧,這裡使用了QQuickView,注意頭文件的變化,Gemini類先實例化為gemini對象,然後注冊為QML上下文屬性。

(2)修改main.qml

// main.qml
import QtQuick 2.2
Item {
    width: 360; height: 360
    MouseArea {
        anchors.fill: parent
        onClicked: {
            gemini.begin()
            gemini.stop()
        }
    }
    Connections {
        target: gemini
        onBegin:console.log("aaaa")
    }
}

既然main.cpp修改了那麼多東西,main.qml也要做相應的修改,在main.qml中不能使用Gemini類型來實例化了,也不能調用doSomething()槽函數了,因為doSomething()函數中的枚舉類型在QML中是訪問不到的,正確的用法是通過QML上下文屬性“gemini”來訪問C++,可以訪問信號begin()和成員函數stop(),此時的信號處理器就需要用Connections來處理了,如上面例子中所示。

6C++訪問QML

同樣,在C++中也可以訪問QML中的屬性、函數和信號。

在C++中加載QML文件可以用QQmlComponent或QQuickView,然後就可以在C++中訪問QML對象了。QQuickView提供了一個顯示用戶界面的窗口,而QQmlComponent沒有。

QQuickView::rootObject()返回了組件實例,是一個有用的函數。前面的例子中已經使用過QQuickView了,下面的例子介紹QQmlComponent的用法。

使用QQmlComponent——

修改main.cpp

// main.cpp
#include 
#include 
#include 
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    qmlRegisterType("Union.Lotto.Gemini", 1, 0, "Gemini");
    QQmlEngine engine;
    // set qml context property
    // Gemini aGemini;
    // engine.rootContext()->setContextProperty("aGemini", &aGemini);
    QQmlComponent component(&engine, QUrl(QStringLiteral("qrc:///main.qml")));
    component.create();
    return app.exec();
}

例子中注釋的部分是設置QML上下文屬性的方法。

C++中訪問QML中的屬性——

在C++中加載了QML文件並進行組件實例化後,就可以在C++中訪問、修改這個實例的屬性值了,可以是QML內建屬性,也可以是自定義屬性。

(1)修改main.qml

// main.qml
import QtQuick 2.2
import QtQuick.Window 2.1
import Union.Lotto.Gemini 1.0
Window {
    visible: true
    width: 360; height: 360
    title: "Union Lotto Game"
    color: "white"
    Rectangle {
        objectName: "rect"
        anchors.fill: parent
        color: "yellow"
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            gemini.begin()
            gemini.stop()
            gemini.ballNumber = 10
        }
    }
    Gemini {
        id: gemini
        onBegin: doSomething(Gemini.BALL_COLOR_RED)
        onBallNumberChanged: console.log("new ball number is", ballNumber) // 10
        Component.onCompleted: console.log("default ball number is", ballNumber) // 0
    }
}

在main.qml中添加了一個Rectangle,設置objectName屬性值為“rect”,這個值是為了在C++中能夠找到這個Rectangle。

(2)修改main.cpp

// main.cpp
#include 
#include 
#include 
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    qmlRegisterType("Union.Lotto.Gemini", 1, 0, "Gemini");
    QQmlEngine engine;
    // set qml context property
    // Gemini aGemini;
    // engine.rootContext()->setContextProperty("aGemini", &aGemini);
    QQmlComponent component(&engine, QUrl(QStringLiteral("qrc:///main.qml")));
    QObject *object = component.create();
    qDebug() << "width value is" << object->property("width").toInt();
    object->setProperty("width", 500);
    qDebug() << "height value is" << QQmlProperty::read(object, "height").toInt();
    QQmlProperty::write(object, "height", 500);
    QObject *rect = object->findChild("rect");
    if(rect) {
        rect->setProperty("color", "black");
    }
    return app.exec();
}

首先,使用了QObject::property()/setProperty()來讀取、修改width屬性值。

接著,使用了QQmlProperty::read()/write()來讀取、修改height屬性值。

另外,如果某個對象的類型是QQuickItem,例如QQuickView::rootObject()的返回值,這時就可以使用QQuickItem::width/setWidth()來訪問、修改width屬性值了。

有時候,QML組件是一個復雜的樹型結構,包含兄弟組件和孩子組件,我們可以使用QObject::findchild()/findchildren()來查找,如上面例子所示。

C++中訪問QML中的函數與信號——

在C++中,使用QMetaObject::invokeMethod()可以調用QML中的函數,從QML傳遞過來的函數參數和返回值會被轉換為C++中的QVariant類型,成功返回true,參數不正確或被調用函數名錯誤返回false,invokeMethod()共有四個重載函數,用法相似。必須使用Q_ARG()宏來聲明函數參數,用Q_RETURN_ARG()宏來聲明函數返回值,其原型如下:

        QGenericArgument           Q_ARG(Type, const Type & value)
        QGenericReturnArgument     Q_RETURN_ARG(Type, Type & value)

使用QObject::connect()可以連接QML中的信號,connect()共有四個重載函數,它們都是靜態函數。必須使用SIGNAL()宏來聲明信號,SLOT()宏聲明槽函數。

使用QObject::disconnect()可以解除信號與槽函數的連接。

(1)修改main.qml

import QtQuick 2.2
import QtQuick.Window 2.1
import Union.Lotto.Gemini 1.0
Window {
    visible: true
    width: 360; height: 360
    title: "Union Lotto Game"
    color: "white"
    signal qmlSignal(string message)
    onQmlSignal: console.log("qml signal message is", message) // this is a qml signal
    function qmlFunction(parameter) {
        console.log("qml function parameter is", parameter) // Hello from C++
        return "function from qml"
    }
    Rectangle {
        objectName: "rect"
        anchors.fill: parent
        color: "yellow"
    }
    MouseArea {
        anchors.fill: parent
        onClicked: {
            gemini.begin()
            gemini.stop()
            gemini.ballNumber = 10
            qmlSignal("this is a qml signal")
        }
    }
    Gemini {
        id: gemini
        onBegin: doSomething(Gemini.BALL_COLOR_RED)
        onBallNumberChanged: console.log("new ball number is", ballNumber) // 10
        Component.onCompleted: console.log("default ball number is", ballNumber) // 0
    }
}

main.qml中添加了qmlSignal()信號和qmlFunction()函數,信號在QML中發送,函數在C++中調用。

(2)修改Gemini.h

#ifndef GEMINI_H
#define GEMINI_H
// Gemini.h
#include 
#include 
class Gemini : public QObject
{
    Q_OBJECT
    Q_ENUMS(BALL_COLOR)
    Q_PROPERTY(unsigned int ballNumber READ ballNumber WRITE setBallNumber NOTIFY ballNumberChanged)
public:
    Gemini() : m_ballColor(BALL_COLOR_YELLOW), m_ballNumber(0) {
        qDebug() << "Gemini::Gemini() called";
    }
    enum BALL_COLOR {
        BALL_COLOR_YELLOW,
        BALL_COLOR_RED,
        BALL_COLOR_BLUE,
        BALL_COLOR_ALL
    };
    unsigned int ballNumber() const {
        return m_ballNumber;
    }
    void setBallNumber(const unsigned int &ballNumber) {
        if(ballNumber != m_ballNumber) {
            m_ballNumber = ballNumber;
            emit ballNumberChanged();
        }
    }
    Q_INVOKABLE void stop() {
        qDebug() << "Gemini::stop() called";
    }
signals:
    void begin();
    void ballNumberChanged();
public slots:
    void doSomething(BALL_COLOR ballColor) {
        qDebug() << "Gemini::doSomething() called with" << ballColor;
        if(ballColor != m_ballColor) {
            m_ballColor = ballColor;
            qDebug() << "ball color changed";
        }
    }
    void cppSlot(const QString &message) {
        qDebug() << "Called the C++ slot with message:" << message; // this is a qml signal
    }
private:
    BALL_COLOR m_ballColor;
    unsigned int m_ballNumber;
};
#endif // GEMINI_H

Gemini類中添加了cppSlot()槽函數,將要在main.cpp中與QML的信號connect。

(3)修改main.cpp

#include 
#include 
#include 
int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    qmlRegisterType("Union.Lotto.Gemini", 1, 0, "Gemini");
    QQmlEngine engine;
    // set qml context property
    // Gemini aGemini;
    // engine.rootContext()->setContextProperty("aGemini", &aGemini);
    QQmlComponent component(&engine, QUrl(QStringLiteral("qrc:///main.qml")));
    QObject *object = component.create();
    qDebug() << "width value is" << object->property("width").toInt();
    object->setProperty("width", 500);
    qDebug() << "height value is" << QQmlProperty::read(object, "height").toInt();
    QQmlProperty::write(object, "height", 500);
    QObject *rect = object->findChild("rect");
    if(rect) {
        rect->setProperty("color", "black");
    }
    QVariant returnedValue;
    QVariant message = "Hello from C++";
    QMetaObject::invokeMethod(object, "qmlFunction",
                              Q_RETURN_ARG(QVariant, returnedValue),
                              Q_ARG(QVariant, message));
    qDebug() << "returnedValue is" << returnedValue.toString(); // function from qml
    Gemini test;
    QObject::connect(object, SIGNAL(qmlSignal(QString)),
                     &test, SLOT(cppSlot(QString)));
    return app.exec();
}

在main.cpp中添加了QMeta::invokeMethod()和QObject::connect()來分別訪問QML中函數和信號。

7、總結

本文主要介紹了QML與C++混合編程常用的方法與技巧,在使用過程中有幾點值得注意:

自定義類一定要派生自QObject類或其子類。

必須使用Q_OBJECT宏。

注冊自定義類到Qt元對象系統或設置自定義類對象實例為QML上下文屬性是必須的。

兩者交互進行數據傳遞時,要符合QML與C++間數據類型的轉換規則。


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved