在 C++ 中定义 QML 类型
使用 C++ 代码扩展 QML 时,可以使用 QML 类型系统注册 C++ 类,使 C++ 类能够用作 QML 代码中的数据类型。虽然任何 QObject 派生类的属性、方法和信号都可以从 QML 访问,但如 向 QML 公开 C++ 类型的属性 中所述,只有注册的的类才能用作 QML 的数据类型。此外,注册还可以提供其他的功能,比如允许将类用作 QML 中的可实例化 QML 对象类型,或者允许 QML 导入和使用该类的 singleton instance。
此外,Qt QML 模块还提供了用于实现 QML 特定功能的机制,例如 C++ 中的 附加属性 和 默认属性。
(请注意,使用 C++ 编写 QML 扩展 教程演示了本文档中涵盖的许多重要概念)
注意:所有声明 QML 类型的头文件都需要从项目的 include 路径中不带任何前缀地被访问。
有关 C++ 和 QML 集成方法的更多信息,请参阅 概述 —— QML 与 C++ 混合编程页面。
使用 QML 类型系统注册 C++ 类型
QObject 派生类可以在 QML 类型系统中注册,以使该类型能够用作 QML 代码中的数据类型。
QML引擎允许注册可实例化类型和不可实例化类型:
注册可实例化类型使 C++ 类可以用作 QML 对象类型的定义,从而允许在 QML 代码的对象声明中使用它来创建这种类型的对象。注册还为引擎提供了额外的类型元数据,使类型(以及类声明的任何枚举)能够用作在 QML 和 C++ 之间交换的属性值、方法参数和返回值以及信号参数的数据类型。
注册不可实例化的类型也是以这种方式将类注册为数据类型,但该类型不能作为 QML 对象类型从 QML 实例化。这是有用的,例如,如果一个类型有枚举,应该公开给 QML,但该类型本身不应该被实例化。
有关选择向 QML 公开 C++ 类型的正确方法的快速指南,请参阅 选择正确的方式集成 C++ 和 QML。
前提条件
下面提到的所有宏都可以从 qqmlregistration.h
头文件中获得。你需要将以下代码添加到使用它们的文件中,以使宏可用:
#include <QtQml/qqmlregistration.h>
此外,类声明必须位于可通过项目 include 路径访问的头文件中。声明用于在编译时生成注册代码,而注册代码需要包含声明的头文件。
注册一个可实例化的对象类型
任何 QObject 派生的 C++ 类都可以注册为 QML 对象类型 的定义。一旦在 QML 类型系统中注册了一个类,就可以像 QML 代码中对象类型一样声明和实例化该类。创建后,可以在 QML 操作类实例,正如 向 QML 公开 C++ 类型的属性 所解释的那样,任何 QObject 派生类的属性、方法和信号都可以从 QML 代码访问。
要将 QObject 派生类注册为可实例化的 QML 对象类型,add QML_ELEMENT
or QML_NAMED_ELEMENT(<name>)
to the class declaration and CONFIG += qmltypes
, a QML_IMPORT_NAME
, and a QML_IMPORT_MAJOR_VERSION
to your project file. This will register the class into the type namespace under the given major version, using either the class name or an explicitly given name as QML type name. The minor version(s) will be derived from any revisions attached to properties, methods, or signals. The default minor version is 0
. You can explicitly restrict the type to be available only from specific minor versions by adding the QML_ADDED_IN_MINOR_VERSION()
macro to the class declaration. Clients can import suitable versions of the namespace in order to use the type.
For example, suppose there is a Message
class with author
and creationDate
properties:
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) QML_ELEMENT public: // ... };
This type can be registered by adding an appropriate type namespace and version number to the project file. For example, to make the type available in the com.mycompany.messaging
namespace with version 1.0:
CONFIG += qmltypes QML_IMPORT_NAME = com.mycompany.messaging QML_IMPORT_MAJOR_VERSION = 1
If the header the class is declared in is not accessible from your project's include path, you may have to amend the include path so that the generated registration code can be compiled:
INCLUDEPATH += com/mycompany/messaging
The type can be used in an object declaration from QML, and its properties can be read and written to, as per the example below:
import com.mycompany.messaging 1.0 Message { author: "Amelie" creationDate: new Date() }
Registering Value Types
Any type with a Q_GADGET macro can the registered as a QML value type}. Once such a type is registered with the QML type system it can be used as property type in QML code. Such an instance can be manipulated from QML; as Exposing Attributes of C++ Types to QML explains, the properties and methods of any value type are accessible from QML code.
In contrast to object types, value types require lower case names. The preferred way to register them is using the QML_VALUE_TYPE or QML_ANONYMOUS macros. There is no equivalent to QML_ELEMENT as your C++ classes are typically going to have upper case names. Otherwise the registration is very similar to the registration of object types.
For example, suppose you want to register a value type person
that consists of two strings for first and last name:
class Person { Q_GADGET Q_PROPERTY(QString firstName READ firstName WRITE setFirstName) Q_PROPERTY(QString lastName READ lastName WRITE setLastName) QML_VALUE_TYPE(person) public: // ... };
There are some further limitations on what you can do with value types:
- Value types cannot be singletons.
- Value types need to be default-constructible and copy-constructible.
- Using QProperty as a member of a value type is problematic. Value types get copied, and you would need to decide what to do with any bindings on the QProperty at that point. You should not use QProperty in value types.
- Value types cannot provide attached properties.
- The API to define extensions to value types (QML_EXTENDED) is not public and subject to future changes.
Registering Non-Instantiable Types
Sometimes a QObject-derived class may need to be registered with the QML type system but not as an instantiable type. For example, this is the case if a C++ class:
- is an interface type that should not be instantiable
- is a base class type that does not need to be exposed to QML
- declares some enum that should be accessible from QML, but otherwise should not be instantiable
- is a type that should be provided to QML through a singleton instance, and should not be instantiable from QML
The Qt QML module provides several macros for registering non-instantiable types:
- QML_ANONYMOUS registers a C++ type that is not instantiable and cannot be referred to from QML. This enables the engine to coerce any inherited types that are instantiable from QML.
- QML_INTERFACE registers an existing Qt interface type. The type is not instantiable from QML, and you cannot declare QML properties with it. Using C++ properties of this type from QML will do the expected interface casts, though.
- QML_UNCREATABLE(reason) combined with with QML_ELEMENT or QML_NAMED_ELEMENT registers a named C++ type that is not instantiable but should be identifiable as a type to the QML type system. This is useful if a type's enums or attached properties should be accessible from QML but the type itself should not be instantiable. The parameter should be an error message to be emitted if an attempt at creating an instance of the type is detected.
- QML_SINGLETON combined with QML_ELEMENT or QML_NAMED_ELEMENT registers a singleton type that can be imported from QML, as discussed below.
Note that all C++ types registered with the QML type system must be QObject-derived, even if they are non-instantiable.
Registering Singleton Objects with a Singleton Type
A singleton type enables properties, signals and methods to be exposed in a namespace without requiring the client to manually instantiate an object instance. QObject singleton types in particular are an efficient and convenient way to provide functionality or global property values.
Note that singleton types do not have an associated QQmlContext as they are shared across all contexts in an engine. QObject singleton type instances are constructed and owned by the QQmlEngine, and will be destroyed when the engine is destroyed.
A QObject singleton type can be interacted with in a manner similar to any other QObject or instantiated type, except that only one (engine constructed and owned) instance will exist, and it must be referenced by type name rather than id. Q_PROPERTYs of QObject singleton types may be bound to, and Q_INVOKABLE functions of QObject module APIs may be used in signal handler expressions. This makes singleton types an ideal way to implement styling or theming, and they can also be used instead of ".pragma library" script imports to store global state or to provide global functionality.
Once registered, a QObject singleton type may be imported and used like any other QObject instance exposed to QML. The following example assumes that a QObject singleton type was registered into the "MyThemeModule" namespace with version 1.0, where that QObject has a QColor "color" Q_PROPERTY:
import MyThemeModule 1.0 as Theme Rectangle { color: Theme.color // binding. }
A QJSValue may also be exposed as a singleton type, however clients should be aware that properties of such a singleton type cannot be bound to.
See QML_SINGLETON for more information on how implement and register a new singleton type, and how to use an existing singleton type.
Note: Enum values for registered types in QML should start with a capital.
Final properties
Properties declared final using the FINAL
modifier to Q_PROPERTY cannot be overridden. This means that any properties or functions of the same name, declared either in QML or in C++ on derived types, are ignored by the QML engine. You should declare properties FINAL
when possible, in order to avoid accidental overrides. An override of a property is visible not only in derived classes, but also to QML code executing the context of the base class. Such QML code, typically expects the original property, though. This is a frequent source of mistakes.
Properties declared FINAL
can also not be overridden by functions in QML, or by Q_INVOKABLE methods in C++.
Type Revisions and Versions
Many of the type registration functions require versions to be specified for the registered type. Type revisions and versions allow new properties or methods to exist in the new version while remaining compatible with previous versions.
Consider these two QML files:
// main.qml import QtQuick 1.0 Item { id: root MyType {} }
// MyType.qml import MyTypes 1.0 CppType { value: root.x }
where CppType
maps to the C++ class CppType
.
If the author of CppType adds a root
property to CppType in a new version of their type definition, root.x
now resolves to a different value because root
is also the id
of the top level component. The author could specify that the new root
property is available from a specific minor version. This permits new properties and features to be added to existing types without breaking existing programs.
The REVISION tag is used to mark the root
property as added in revision 1 of the type. Methods such as Q_INVOKABLE's, signals and slots can also be tagged for a revision using the Q_REVISION(x)
macro:
class CppType : public BaseType { Q_OBJECT Q_PROPERTY(int root READ root WRITE setRoot NOTIFY rootChanged REVISION 1) QML_ELEMENT signals: Q_REVISION(1) void rootChanged(); };
The revisions given this way are automatically interpreted as minor versions to the major version given in the project file. In this case, root
is only available when MyTypes
version 1.1 or higher is imported. Imports of MyTypes
version 1.0 remain unaffected.
For the same reason, new types introduced in later versions should be tagged with the QML_ADDED_IN_MINOR_VERSION macro.
This feature of the language allows for behavioural changes to be made without breaking existing applications. Consequently QML module authors should always remember to document what changed between minor versions, and QML module users should check that their application still runs correctly before deploying an updated import statement.
Revisions of a base class that your type depends upon are automatically registered when registering the type itself. This is useful when deriving from base classes provided by other authors, e.g. when extending classes from the Qt Quick module.
Note: The QML engine does not support revisions for properties or signals of grouped and attached property objects.
Registering Extension Objects
When integrating existing classes and technology into QML, APIs will often need tweaking to fit better into the declarative environment. Although the best results are usually obtained by modifying the original classes directly, if this is either not possible or is complicated by some other concerns, extension objects allow limited extension possibilities without direct modifications.
Extension objects add additional properties to an existing type. An extended type definition allows the programmer to supply an additional type, known as the extension type, when registering the class. Its members are transparently merged with the original target class when used from within QML. For example:
QLineEdit { leftMargin: 20 }
The leftMargin
property is a new property added to an existing C++ type, QLineEdit, without modifying its source code.
The QML_EXTENDED(extension) macro is for registering extended types. The argument is the name of another class to be used as extension.
You can also use QML_EXTENDED_NAMESPACE(namespace) to register a namespace, and especially the enumerations declared within, as an extension to a type. If the type you are extending is itself a namespace, you need to use QML_NAMESPACE_EXTENDED(namespace) instead.
An extension class is a regular QObject, with a constructor that takes a QObject pointer. However, the extension class creation is delayed until the first extended property is accessed. The extension class is created and the target object is passed in as the parent. When the property on the original is accessed, the corresponding property on the extension object is used instead.
The Extension Objects Example demonstrates a usage of extension objects.
Registering Foreign Types
There may be C++ types that cannot be modified to hold the above mentioned macros. Those may be types from 3rdparty libraries, or types that need to fulfill some contract that contradicts the presence of those macros. You can still expose those types to QML, though, using the QML_FOREIGN macro. In order to do this, create a separate struct that consists entirely of the registration macros, like this:
// Contains class Immutable3rdParty #include <3rdpartyheader.h> struct Foreign { Q_GADGET QML_FOREIGN(Immutable3rdParty) QML_NAMED_ELEMENT(Accessible3rdParty) QML_ADDED_IN_VERSION(2, 4) // QML_EXTENDED, QML_SINGLETON ... };
From this code, you get a QML type with the methods and properties of Immutable3rdParty, and the QML traits (e.g.: singleton, extended) specified in Foreign.
Defining QML-Specific Types and Attributes
Providing Attached Properties
In the QML language syntax, there is a notion of attached properties and attached signal handlers, which are additional attributes that are attached to an object. Essentially, such attributes are implemented and provided by an attaching type, and these attributes may be attached to an object of another type. This contrasts with ordinary object properties which are provided by the object type itself (or the object's inherited type).
For example, the Item below uses attached properties and attached handlers:
import QtQuick 2.0 Item { width: 100; height: 100 focus: true Keys.enabled: false Keys.onReturnPressed: console.log("Return key was pressed") }
Here, the Item object is able to access and set the values of Keys.enabled
and Keys.onReturnPressed
. This allows the Item object to access these extra attributes as an extension to its own existing attributes.
Steps for Implementing Attached Objects
When considering the above example, there are several parties involved:
- There is an instance of an anonymous attached object type, with an
enabled
and areturnPressed
signal, that has been attached to the Item object to enable it to access and set these attributes. - The Item object is the attachee, to which the instance of the attached object type has been attached.
- Keys is the attaching type, which provides the attachee with a named qualifier, "Keys", through which it may access the attributes of the attached object type.
When the QML engine processes this code, it creates a single instance of the attached object type and attaches this instance to the Item object, thereby providing it with access to the enabled
and returnPressed
attributes of the instance.
The mechanisms for providing attached objects can be implemented from C++ by providing classes for the attached object type and attaching type. For the attached object type, provide a QObject-derived class that defines the attributes to be made accessible to attachee objects. For the attaching type, provide a QObject-derived class that:
- implements a static qmlAttachedProperties() with the following signature:
static <AttachedPropertiesType> *qmlAttachedProperties(QObject *object);
This method should return an instance of the attached object type.
The QML engine invokes this method in order to attach an instance of the attached object type to the attachee specified by the
object
parameter. It is customary, though not strictly required, for this method implementation to parent the returned instance toobject
in order to prevent memory leaks.This method is called at most once by the engine for each attachee object instance, as the engine caches the returned instance pointer for subsequent attached property accesses. Consequently the attachment object may not be deleted until the attachee
object
is destroyed. - is declared as an attaching type, by adding the QML_ATTACHED(attached) macro to the class declaration. The argument is the name of the attached object type
Implementing Attached Objects: An Example
For example, take the Message
type described in an earlier example:
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) QML_ELEMENT public: // ... };
Suppose it is necessary to trigger a signal on a Message
when it is published to a message board, and also track when the message has expired on the message board. Since it doesn't make sense to add these attributes directly to a Message
, as the attributes are more relevant to the message board context, they could be implemented as attached attributes on a Message
object that are provided through a "MessageBoard" qualifier. In terms of the concepts described earlier, the parties involved here are:
- An instance of an anonymous attached object type, which provides a
published
signal and an expired property. This type is implemented byMessageBoardAttachedType
below - A
Message
object, which will be the attachee - The
MessageBoard
type, which will be the attaching type that is used byMessage
objects to access the attached attributes
Following is an example implementation. First, there needs to be an attached object type with the necessary properties and signals that will be accessible to the attachee:
class MessageBoardAttachedType : public QObject { Q_OBJECT Q_PROPERTY(bool expired READ expired WRITE setExpired NOTIFY expiredChanged) QML_ANONYMOUS public: MessageBoardAttachedType(QObject *parent); bool expired() const; void setExpired(bool expired); signals: void published(); void expiredChanged(); };
Then the attaching type, MessageBoard
, must declare a qmlAttachedProperties()
method that returns an instance of the attached object type as implemented by MessageBoardAttachedType. Additionally, MessageBoard
must be declared as an attaching type via the QML_ATTACHED() macro:
class MessageBoard : public QObject { Q_OBJECT QML_ATTACHED(MessageBoardAttachedType) QML_ELEMENT public: static MessageBoardAttachedType *qmlAttachedProperties(QObject *object) { return new MessageBoardAttachedType(object); } };
Now, a Message
type can access the properties and signals of the attached object type:
Message { author: "Amelie" creationDate: new Date() MessageBoard.expired: creationDate < new Date("January 01, 2015 10:45:00") MessageBoard.onPublished: console.log("Message by", author, "has been published!") }
Additionally, the C++ implementation may access the attached object instance that has been attached to any object by calling the qmlAttachedPropertiesObject() function.
For example:
Message *msg = someMessageInstance(); MessageBoardAttachedType *attached = qobject_cast<MessageBoardAttachedType*>(qmlAttachedPropertiesObject<MessageBoard>(msg)); qDebug() << "Value of MessageBoard.expired:" << attached->expired();
Property Modifier Types
A property modifier type is a special kind of QML object type. A property modifier type instance affects a property (of a QML object instance) which it is applied to. There are two different kinds of property modifier types:
- property value write interceptors
- property value sources
A property value write interceptor can be used to filter or modify values as they are written to properties. Currently, the only supported property value write interceptor is the Behavior type provided by the QtQuick
import.
A property value source can be used to automatically update the value of a property over time. Clients can define their own property value source types. The various property animation types provided by the QtQuick
import are examples of property value sources.
Property modifier type instances can be created and applied to a property of a QML object through the "<ModifierType> on <propertyName>" syntax, as the following example shows:
import QtQuick 2.0 Item { width: 400 height: 50 Rectangle { width: 50 height: 50 color: "red" NumberAnimation on x { from: 0 to: 350 loops: Animation.Infinite duration: 2000 } } }
This is commonly referred to as "on" syntax.
Clients can register their own property value source types, but currently not property value write interceptors.
Property Value Sources
Property value sources are QML types that can automatically update the value of a property over time, using the <PropertyValueSource> on <property>
syntax. For example, the various property animation types provided by the QtQuick
module are examples of property value sources.
A property value source can be implemented in C++ by subclassing QQmlPropertyValueSource and providing an implementation that writes different values to a property over time. When the property value source is applied to a property using the <PropertyValueSource> on <property>
syntax in QML, it is given a reference to this property by the engine so that the property value can be updated.
For example, suppose there is a RandomNumberGenerator
class to be made available as a property value source, so that when applied to a QML property, it will update the property value to a different random number every 500 milliseconds. Additionally, a maxValue can be provided to this random number generator. This class can be implemented as follows:
class RandomNumberGenerator : public QObject, public QQmlPropertyValueSource { Q_OBJECT Q_INTERFACES(QQmlPropertyValueSource) Q_PROPERTY(int maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged); QML_ELEMENT public: RandomNumberGenerator(QObject *parent) : QObject(parent), m_maxValue(100) { QObject::connect(&m_timer, SIGNAL(timeout()), SLOT(updateProperty())); m_timer.start(500); } int maxValue() const; void setMaxValue(int maxValue); virtual void setTarget(const QQmlProperty &prop) { m_targetProperty = prop; } signals: void maxValueChanged(); private slots: void updateProperty() { m_targetProperty.write(QRandomGenerator::global()->bounded(m_maxValue)); } private: QQmlProperty m_targetProperty; QTimer m_timer; int m_maxValue; };
When the QML engine encounters a use of RandomNumberGenerator
as a property value source, it invokes RandomNumberGenerator::setTarget()
to provide the type with the property to which the value source has been applied. When the internal timer in RandomNumberGenerator
triggers every 500 milliseconds, it will write a new number value to that specified property.
Once the RandomNumberGenerator
class has been registered with the QML type system, it can be used from QML as a property value source. Below, it is used to change the width of a Rectangle every 500 milliseconds:
import QtQuick 2.0 Item { width: 300; height: 300 Rectangle { RandomNumberGenerator on width { maxValue: 300 } height: 100 color: "red" } }
In all other respects, property value sources are regular QML types that can have properties, signals methods and so on, but with the added capability that they can be used to change property values using the <PropertyValueSource> on <property>
syntax.
When a property value source object is assigned to a property, QML first tries to assign it normally, as though it were a regular QML type. Only if this assignment fails does the engine call the setTarget() method. This allows the type to also be used in contexts other than just as a value source.
Specifying Default and Parent Properties for QML Object Types
Any QObject-derived type that is registered as an instantiable QML object type can optionally specify a default property for the type. A default property is the property to which an object's children are automatically assigned if they are not assigned to any specific property.
The default property can be set by calling the Q_CLASSINFO() macro for a class with a specific "DefaultProperty" value. For example, the MessageBoard
class below specifies its messages
property as the default property for the class:
class MessageBoard : public QObject { Q_OBJECT Q_PROPERTY(QQmlListProperty<Message> messages READ messages) Q_CLASSINFO("DefaultProperty", "messages") QML_ELEMENT public: QQmlListProperty<Message> messages(); private: QList<Message *> m_messages; };
This enables children of a MessageBoard
object to be automatically assigned to its messages
property if they are not assigned to a specific property. For example:
MessageBoard { Message { author: "Naomi" } Message { author: "Clancy" } }
If messages
was not set as the default property, then any Message
objects would have to be explicitly assigned to the messages
property instead, as follows:
MessageBoard { messages: [ Message { author: "Naomi" }, Message { author: "Clancy" } ] }
(Incidentally, the Item::data property is its default property. Any Item objects added to this data
property are also added to the list of Item::children, so the use of the default property enables visual children to be declared for an item without explicitly assigning them to the children property.)
Additionally, you can declare a "ParentProperty" Q_CLASSINFO() to inform the QML engine which property should denote the parent object in the QML hierarchy. For example, the Message type might be declared as follows:
class Message : public QObject { Q_OBJECT Q_PROPERTY(QObject* board READ board BINDABLE boardBindable) Q_PROPERTY(QString author READ author BINDABLE authorBindable) Q_CLASSINFO("ParentProperty", "board") QML_ELEMENT public: Message(QObject *parent = nullptr) : QObject(parent) { m_board = parent; } QObject *board() const { return m_board.value(); } QBindable<QObject *> boardBindable() { return QBindable<QObject *>(&m_board); } QString author() const { return m_author.value(); } QBindable<QString> authorBindable() { return QBindable<QString>(&m_author); } private: QProperty<QObject *> m_board; QProperty<QString> m_author; };
Defining the parent property affords qmllint and other tools better insight into the intention of your code and avoids false positive warnings on some property accesses.
Defining Visual Items with the Qt Quick Module
When building user interfaces with the Qt Quick module, all QML objects that are to be visually rendered must derive from the Item type, as it is the base type for all visual objects in Qt Quick. This Item type is implemented by the QQuickItem C++ class, which is provided by the Qt Quick module. Therefore, this class should be subclassed when it is necessary to implement a visual type in C++ that can be integrated into a QML-based user interface.
See the QQuickItem documentation for more information. Additionally, the Writing QML Extensions with C++ tutorial demonstrates how a QQuickItem-based visual item can be implemented in C++ and integrated into a Qt Quick-based user interface.
Receiving Notifications for Object Initialization
For some custom QML object types, it may be beneficial to delay the initialization of particular data until the object has been created and all of its properties have been set. For example, this may be the case if the initialization is costly, or if the initialization should not be performed until all property values have been initialized.
The Qt QML module provides the QQmlParserStatus to be subclassed for these purposes. It defines a number of virtual methods that are invoked at various stages during component instantiation. To receive these notifications, a C++ class should inherit QQmlParserStatus and also notify the Qt meta system using the Q_INTERFACES() macro.
For example:
class MyQmlType : public QObject, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) QML_ELEMENT public: virtual void componentComplete() { // Perform some initialization here now that the object is fully created } };