1. 程式人生 > 其它 >《Qt MOOC系列教程》第五章第三節:建立新的QML型別

《Qt MOOC系列教程》第五章第三節:建立新的QML型別

技術標籤:Qt MOOC系列教程qtc++guiqml軟體開發

到目前為止,我們已經討論瞭如何將物件例項公開給QML上下文。有時我們還希望在QML中可以使用註冊類本身。註冊允許將類當作QML中的資料型別來使用。此外,註冊還可以提供其他功能,比如允許在QML中將類用作可例項化的QML物件型別,或者允許在QML中匯入和使用類的單例例項。

通常我們使用Q_OBJECT巨集註冊從QObject派生的類,也可以用Q_GADGET巨集宣告一個比QObject“更輕”的版本。在這些更輕的類中,我們可以訪問它們的屬性、列舉和可呼叫的方法,但不能使用訊號槽系統,我們稍後會進行介紹。

1. 註冊

1.1 註冊例項化型別

要將QObject派生的類註冊為可例項化的QML物件型別,請呼叫qmlRegisterType()將類作為QML型別註冊到特定的型別名稱空間中。然後,客戶端可以匯入該名稱空間以使用該型別。

假設有一個帶有authorcreationDate屬性的Message類:

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
    Q_PROPERTY(QDateTime creationDate READ creationDate WRITE setCreationDate NOTIFY creationDateChanged)
public:
    // ...
};

可以通過使用合適的型別名稱空間和版本號呼叫qmlRegisterType()來註冊此型別。比如使該型別在com.mycompany.messaging版本1.0的名稱空間中可用:

qmlRegisterType<Message>("com.mycompany.messaging", 1, 0, "Message");

然後在QML的物件宣告中可以使用該型別,並且可以按以下示例讀取和寫入其屬性:

import com.mycompany.messaging 1.0

Message {
    author: "Amelie"
    creationDate: new Date()
}

1.2 註冊非例項化型別

有時,可能需要向QML型別系統註冊QObject派生類時不希望將其註冊為可例項化型別。比如一個C++類:

  • 是不應例項化的介面型別
  • 是不需要暴露給QML的基類型別
  • 宣告一些能從QML訪問的列舉,但自己不應該被例項化
  • 一個應該通過單例例項提供給QML的型別,並且不應該從QML例項化

Qt QML模組提供了幾種註冊非例項化型別的方法:

  • qmlRegisterType()(不帶引數)註冊一個不可例項化且無法從QML引用的C++型別。這使引擎能夠強制執行可從QML例項化的所有繼承型別。
  • qmlRegisterInterface()使用特定的QML型別名稱註冊Qt的介面型別。型別不能從QML例項化,但可以通過其型別名引用。
  • qmlRegisterUncreatableType()註冊一個C++型別,該型別不可例項化,但應可識別為QML型別系統的型別。如果型別的列舉或附加屬性應該可以從QML訪問,但是型別本身不應該是可例項化的,那麼這很有用。
  • qmlRegisterSingletonType() 註冊可以從QML匯入的單例型別。

下面我們討論一下單例型別。

QObject單例型別可以與任何其他QObject或例項化型別進行互動,但只有一個例項存在,而且必須通過型別名稱而不是id進行引用。QObject單例型別的Q_PROPERTY可以用於屬性繫結,QObject模組API的Q_INVOKABLE函式可以用於訊號處理器表示式。這使得單例型別成為實現樣式化或主題化的理想方式。

假設我們有一個用於主題的單例型別,它已註冊到MyThemeModule1.0版本的名稱空間中,其中QObject具有一個QColor color屬性。然後我們可以簡單地使用它:

import MyThemeModule 1.0 as Theme

Rectangle {
    color: Theme.color // binding.
}

另一個例子是我們有一個只用於列舉的類。請注意,在這種情況下,我們可以使用前面提到的更輕量級的Q_GADGET巨集(因為我們不需要訊號和槽):

// modeclass.h
#include <QObject>

class ModeClass
{
    Q_GADGET
public:
    explicit ModeClass();
    enum Mode { Slow, Normal, Fast, UsainBolt };
    Q_ENUM(Mode)
};    

註冊:

qmlRegisterUncreatableType<ModeClass>("com.mycompany.modes", 1, 0, "ModeClass", "Message");

2. 建立自定義QObject

讓我們通過一個例子來看看如何建立一個自定義的QObject派生類然後在QML中使用:

// cppperson.h
class CppPerson : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
    Q_PROPERTY(int shoeSize READ shoeSize WRITE setShoeSize NOTIFY shoeSizeChanged)
public:
    CppPerson(QObject *parent = nullptr);

    QString name() const;
    void setName(const QString &name);

    int shoeSize() const;
    void setShoeSize(int size);

signals:
    void nameChanged();
    void shoeSizeChanged();

private:
    QString m_name;
    int m_shoeSize;
};

// cppperson.cpp
#include "cppperson.h"

CppPerson::CppPerson(QObject *parent) :
    QObject(parent), m_shoeSize(0)
{
}

QString CppPerson::name() const
{
    return m_name;
}

void CppPerson::setName(const QString &name)
{
    if (m_name != name) {
        m_name = name;
        emit nameChanged();
    }
}

int CppPerson::shoeSize() const
{
    return m_shoeSize;
}

void CppPerson::setShoeSize(int size)
{
    if (m_shoeSize != size) {
        m_shoeSize = size;
        emit shoeSizeChanged();
    }
}

// main.cpp
...
qmlRegisterType<CppPerson>("People", 1,0, "Person");
...

// main.qml
import QtQuick 2.9
import QtQuick.Window 2.3
import People 1.0

Window {
    width: 640; height: 480
    visible: true

    Rectangle {
        anchors.fill: parent

        // Person is implemented in C++
        Person {
            id: person
            name: "Bob Jones"
            shoeSize: 12
            onNameChanged: {
                console.log("New name: " + name)
            }
            onShoeSizeChanged: {
                console.log("New shoe size: " + shoeSize)
            }
        }

        Column {
            anchors.fill:  parent
            spacing: 20
            Text {
                font.bold: true
                font.pixelSize: 26
                text: "Person name: " + person.name
            }
            Text {
                font.bold: true
                font.pixelSize: 26
                text: "Person shoe size: " + person.shoeSize
            }
        }

        MouseArea {
            anchors.fill: parent
            onClicked: {
                person.name = "John Doe"
                person.shoeSize = 9
            }
        }
    }
}

C++端現在應該開始變得熟悉,我們使用Q_PROPERTY在標頭檔案中聲明瞭屬性及其訪問器和訊號,並在cppperson.cpp檔案中實現了getter和setter。(不要忘記在setter中發出xxxChanged訊號!)

我們在QML裡匯入已註冊的型別,然後就可以像使用其他QML型別一樣使用它。屬性繫結與通常一樣是按名稱進行的,訊號的槽自動存在,遵循命名約定onXxxChanged

3. QStandardItemModel

一個常見的需求是在C++中實現一個模型來儲存資料,然後在QML中顯示。更專業的用例通常要求我們編寫自己的模型,例如在下一節中,我們將通過子類化QAbstractTableModel(繼承自QAbstractItemModel)來實現我們自己的表格模型。Qt還提供了一個簡單的通用模型來儲存自定義資料,稱為QStandardItemModel

// mymodel.h
#include <QStandardItemModel>

class MyModel : public QStandardItemModel
{
    Q_OBJECT
public:
    enum Roles {
        BrandRole = Qt::UserRole + 1,
        ModelRole
    };

    explicit MyModel(QObject *parent = nullptr);

    QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;

    Q_INVOKABLE void addCar(const QString &brand, const QString &model);
};

// mymodel.cpp
#include "mymodel.h"

MyModel::MyModel(QObject *parent) : QStandardItemModel(parent)
{
}

QHash<int, QByteArray> MyModel::roleNames() const
{
    QHash<int, QByteArray> mapping = QStandardItemModel::roleNames();
    mapping[BrandRole] = "brand";
    mapping[ModelRole] = "model";
    return mapping;
}

void MyModel::addCar(const QString &brand, const QString &model)
{
    QStandardItem *item = new QStandardItem;
    item->setData(brand, BrandRole);
    item->setData(model, ModelRole);
    appendRow(item);
}

這裡要注意的最重要部分是重寫的roleNames()函式,其中指定的角色被對映到數字(列舉)。這使得我們能夠使用名稱“brand”和“model”來引用QML中的資料。

另外,不要忘記用Q_INVOKABLE宣告addCar方法,以允許它可以在QML中使用。

// main.cpp
qmlRegisterType<MyModel>("org.mooc.cars", 1, 0, "MyModel");

// main.qml
import QtQuick 2.9
import QtQuick.Window 2.2
import org.mooc.cars 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Cars")

    MyModel {
        id: cars
    }

    ListView {
        anchors.fill: parent
        model: cars
        delegate: Text {
            text: "The brand is " + brand + " and the model is " + model
        }
    }

    // Some way to populate the model, cars.addCar("brand", "model")
    ...
}

4. QML外掛

匯入自定義QML元件時,它會首先載入到記憶體中。為了縮短啟動時間,您可以將元件改為外掛,在這種情況下,一旦建立了物件,它就會被動態載入。

要編寫QML擴充套件外掛,我們需要:

  • 子類化QQmlExtensionPlugin
  • 為外掛編寫一個專案檔案
  • 建立一個qmldir檔案描述此外掛

假設我們有一個C++類TimeModel,需要將其作為新的QML型別使用。它提供當前時間hourminute屬性。

class TimeModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int hour READ hour NOTIFY timeChanged)
    Q_PROPERTY(int minute READ minute NOTIFY timeChanged)
    ...

現在,我們建立一個名為QExampleQmlPlugin的類,它繼承至QmlExtensionPlugin

class QExampleQmlPlugin : public QQmlExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)

public:
    void registerTypes(const char *uri) override
    {
        Q_ASSERT(uri == QLatin1String("TimeExample"));
        qmlRegisterType<TimeModel>(uri, 1, 0, "Time");
    }
};
  • 我們使用Q_PLUGIN_METADATA()巨集將外掛註冊到具有唯一識別符號的元物件系統。
  • 重寫了registerTypes()方法,用qmlRegisterType註冊TimeModel型別。
  • 這裡的Q_ASSERT不是必須的,但是我們可以使用它來確保使用此外掛的任何QML元件都能正確匯入型別名稱空間。

接下來,我們需要一個.pro專案檔案:

TEMPLATE = lib
CONFIG += qt plugin
QT += qml

DESTDIR = imports/TimeExample
TARGET = qmlqtimeexampleplugin
SOURCES += qexampleqmlplugin.cpp

它將專案定義為外掛庫,指定構建目錄,並註冊外掛目標名稱。

最後,我們需要一個qmldir檔案來描述外掛:

module TimeExample
plugin qmlqtimeexampleplugin

Source

獲取更多資訊,請關注作者公眾號:程式設計師練兵場
在這裡插入圖片描述