《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型別註冊到特定的型別名稱空間中。然後,客戶端可以匯入該名稱空間以使用該型別。
假設有一個帶有author
和creationDate
屬性的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
函式可以用於訊號處理器表示式。這使得單例型別成為實現樣式化或主題化的理想方式。
假設我們有一個用於主題的單例型別,它已註冊到MyThemeModule
1.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型別使用。它提供當前時間hour
和minute
屬性。
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
獲取更多資訊,請關注作者公眾號:程式設計師練兵場