使用 C++ 编写高级 QML 扩展
“生日派对”基础项目
extending-qml-advanced/advanced1-Base-project
本教程以生日派对为例,演示 QML 的部分功能。下文所述各项功能的代码均基于此生日派对项目,并借鉴了关于QML 扩展的首个教程中的部分内容。随后将以此简单示例为基础进行扩展,以说明下文所述的各种 QML 扩展。 代码中每个新增扩展的完整代码,可在各章节标题下指定的位置查阅相关教程,或通过本页末尾的代码链接获取。
基础项目定义了Person 类和BirthdayParty 类,它们分别表示派对的参与者和派对本身。
class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged FINAL)
Q_PROPERTY(int shoeSize READ shoeSize WRITE setShoeSize NOTIFY shoeSizeChanged FINAL)
QML_ELEMENT
...
QString m_name;
int m_shoeSize = 0;
};
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged FINAL)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests NOTIFY guestsChanged FINAL)
QML_ELEMENT
...
Person *m_host = nullptr;
QList<Person *> m_guests;
};然后,所有关于派对的信息都可以存储在相应的 QML 文件中。
BirthdayParty {
host: Person {
name: "Bob Jones"
shoeSize: 12
}
guests: [
Person { name: "Leo Hodges" },
Person { name: "Jack Smith" },
Person { name: "Anne Brown" }
]
}main.cpp 文件创建了一个简单的 shell 应用程序,用于显示是谁的生日以及谁被邀请参加了派对。
QQmlEngine engine;
QQmlComponent component(&engine);
component.loadFromModule("People", "Main");
std::unique_ptr<BirthdayParty> party{ qobject_cast<BirthdayParty *>(component.create()) };该应用程序会输出以下派对摘要。
"Bob Jones" is having a birthday!
They are inviting:
"Leo Hodges"
"Jack Smith"
"Anne Brown"以下各节将详细介绍如何通过继承和强制转换,在支持Person 的基础上,同时支持Boy 和Girl 中的“参加者”(attendees),如何利用默认属性将派对的“参加者”隐式设为“宾客”,如何将属性按组而非逐个分配, 如何使用附加对象来跟踪受邀嘉宾的响应,如何使用属性值源随时间推移显示《生日快乐》歌曲的歌词,以及如何将第三方对象暴露给 QML。
继承与强制转换
extending-qml-advanced/advanced2-Inheritance-and-coercion
目前,每位出席者都被建模为“人”。这有些过于笼统,如果能更详细地了解出席者会更好。通过将他们细分为男孩和女孩,我们就能更清楚地知道谁会来参加。
为此,引入了Boy 和Girl 两个类,它们都继承自Person 。
class Boy : public Person
{
Q_OBJECT
QML_ELEMENT
public:
using Person::Person;
};
class Girl : public Person
{
Q_OBJECT
QML_ELEMENT
public:
using Person::Person;
};Person 类保持不变,而Boy 和Girl 这两个 C++ 类则是对其的简单扩展。通过QML_ELEMENT 方法,将这些类型及其 QML 名称注册到 QML 引擎中。
请注意,BirthdayParty 中的host 和guests 属性仍然接受Person 的实例。
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged FINAL)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests NOTIFY guestsChanged FINAL)
QML_ELEMENT
...
};Person 类本身的实现并未改变。但是,由于Person 类已被重新定位为Boy 和Girl 的公共基类,因此不应再直接从 QML 中实例化Person 。取而代之,应显式实例化Boy 或Girl 。
class Person : public QObject
{
...
QML_ELEMENT
QML_UNCREATABLE("Person is an abstract base class.")
...
};虽然我们希望禁止在 QML 内部实例化Person ,但仍需将其注册到 QML 引擎中,以便将其用作属性类型,并允许其他类型被强制转换为该类型。这就是QML_UNCREATABLE 宏的作用。 由于这三种类型——Person 、Boy 和Girl ——均已在 QML 系统中注册,因此在赋值时,QML 会自动(且类型安全地)将Boy 和Girl 对象转换为Person 。
随着这些更改的实施,我们现在可以如下所示,通过关于参加者的额外信息来指定生日聚会。
BirthdayParty {
host: Boy {
name: "Bob Jones"
shoeSize: 12
}
guests: [
Boy { name: "Leo Hodges" },
Boy { name: "Jack Smith" },
Girl { name: "Anne Brown" }
]
}默认属性
extending-qml-advanced/advanced3-Default-properties
目前,在 QML 文件中,每个属性都是显式赋值的。例如,host 属性被赋予一个Boy ,而guests 属性被赋予一个Boy 或Girl 的列表。虽然这样做很简单,但针对这个特定的用例,还可以进一步简化。 与其显式地为 `guests ` 属性赋值,我们不妨直接在 `party` 对象内部添加 `Boy ` 和 `Girl ` 对象,并将其隐式地赋值给 `guests `。这样处理合乎逻辑:我们指定的所有与会者,只要不是主持人,都应被视为嘉宾。这一改动纯属语法层面的调整,但在许多情况下能带来更自然的体验。
guests 属性可被指定为BirthdayParty 的默认属性。这意味着在BirthdayParty 内部创建的每个对象都会被隐式地追加到默认属性guests 中。生成的QML代码如下所示。
BirthdayParty {
host: Boy {
name: "Bob Jones"
shoeSize: 12
}
Boy { name: "Leo Hodges" }
Boy { name: "Jack Smith" }
Girl { name: "Anne Brown" }
}要启用此行为,只需对BirthdayParty 添加DefaultProperty 类信息注解,以将guests 指定为其默认属性。
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged FINAL)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests NOTIFY guestsChanged FINAL)
Q_CLASSINFO("DefaultProperty", "guests")
QML_ELEMENT
...
};您可能已经熟悉这一机制。在 QML 中,Item 的所有子类默认属性均为data 属性。所有未显式添加到Item 属性中的元素都将被添加到data 中。这使得结构清晰,并减少了代码中的不必要冗余。
分组属性
extending-qml-advanced/advanced4-Grouped-properties
我们需要获取更多关于宾客鞋履的信息。除了尺码外,我们还希望存储鞋子的颜色、品牌和价格。这些信息存储在ShoeDescription 类中。
class ShoeDescription : public QObject
{
Q_OBJECT
Q_PROPERTY(int size READ size WRITE setSize NOTIFY shoeChanged FINAL)
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY shoeChanged FINAL)
Q_PROPERTY(QString brand READ brand WRITE setBrand NOTIFY shoeChanged FINAL)
Q_PROPERTY(qreal price READ price WRITE setPrice NOTIFY shoeChanged FINAL)
...
};现在,每个人都有两个属性:一个是name ,另一个是鞋的描述shoe 。
class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged FINAL)
Q_PROPERTY(ShoeDescription *shoe READ shoe WRITE setShoe NOTIFY shoeChanged FINAL)
...
};为鞋类描述的每个元素指定值虽然可行,但略显重复。
Girl {
name: "Anne Brown"
shoe.size: 7
shoe.color: "red"
shoe.brand: "Job Macobs"
shoe.price: 99.99
}分组属性提供了一种更优雅的方式来赋值。无需逐个为每个属性赋值,而是将各个值作为一个组传递给shoe 属性,从而使代码更易于阅读。启用此功能无需任何更改,因为它是QML的默认功能。
host: Boy {
name: "Bob Jones"
shoe { size: 12; color: "white"; brand: "Bikey"; price: 90.0 }
}关联属性
extending-qml-advanced/advanced5-Attached-properties
现在到了主人发送邀请的时候了。为了跟踪哪些宾客已回复邀请以及回复时间,我们需要一个地方来存储这些信息。将这些信息存储在BirthdayParty 对象本身中并不合适。更好的方法是将回复作为附加对象存储在party对象上。
首先,我们声明用于存储宾客回复的BirthdayPartyAttached 类。
class BirthdayPartyAttached : public QObject
{
Q_OBJECT
Q_PROPERTY(QDate rsvp READ rsvp WRITE setRsvp NOTIFY rsvpChanged FINAL)
QML_ANONYMOUS
...
};然后将其附加到BirthdayParty 类上,并定义qmlAttachedProperties() 方法以返回该附加对象。
class BirthdayParty : public QObject
{
...
QML_ATTACHED(BirthdayPartyAttached)
...
static BirthdayPartyAttached *qmlAttachedProperties(QObject *);
};现在,可以在QML中使用这些关联对象来存储受邀客人的回复信息。
BirthdayParty {
Boy {
name: "Robert Campbell"
BirthdayParty.rsvp: Date.fromLocaleString(Qt.locale(), "2023-03-01", "yyyy-MM-dd")
}
Boy {
name: "Leo Hodges"
shoe { size: 10; color: "black"; brand: "Reebok"; price: 59.95 }
BirthdayParty.rsvp: Date.fromLocaleString(Qt.locale(), "2023-03-03", "yyyy-MM-dd")
}
host: Boy {
name: "Jack Smith"
shoe { size: 8; color: "blue"; brand: "Puma"; price: 19.95 }
}
}最后,可以通过以下方式访问这些信息。
QDate rsvpDate;
QObject *attached = qmlAttachedPropertiesObject<BirthdayParty>(guest, false);
if (attached)
rsvpDate = attached->property("rsvp").toDate();程序会输出即将举行的派对的以下摘要。
"Jack Smith" is having a birthday!
He is inviting:
"Robert Campbell" RSVP date: "Wed Mar 1 2023"
"Leo Hodges" RSVP date: "Mon Mar 6 2023"属性值来源
extending-qml-advanced/advanced6-Property-value-source
在派对期间,宾客们必须为主持人演唱歌曲。如果程序能显示为该场合量身定制的歌词以帮助宾客,那将非常方便。为此,使用属性值源随时间生成歌曲的歌词。
class HappyBirthdaySong : public QObject, public QQmlPropertyValueSource
{
Q_OBJECT
Q_INTERFACES(QQmlPropertyValueSource)
...
void setTarget(const QQmlProperty &) override;
};将类 `HappyBirthdaySong ` 作为值源添加。它必须继承自 `QQmlPropertyValueSource `,并使用 `Q_INTERFACES ` 宏实现 `QQmlPropertyValueSource ` 接口。使用 `setTarget() ` 函数来定义该源作用于哪个属性。 在此示例中,该值源向BirthdayParty 的announcement 属性写入数据,以随时间推移显示歌词。它具有一个内部计时器,该计时器会反复将party的announcement 属性设置为歌词的下一行。
在 QML 中,HappyBirthdaySong 是在BirthdayParty 内部实例化的。其签名中的on 关键字用于指定值源的目标属性,在本例中为announcement 。HappyBirthdaySong 对象的name 属性也绑定到了派对主持人的姓名。
BirthdayParty {
id: party
HappyBirthdaySong on announcement {
name: party.host.name
}
...
}该程序通过partyStarted 信号显示派对开始的时间,然后反复打印以下生日祝福语。
Happy birthday to you,
Happy birthday to you,
Happy birthday dear Bob Jones,
Happy birthday to you!外部对象集成
extending-qml-advanced/advanced7-Foreign-objects-integration
与会者不希望仅仅将歌词打印到控制台,而是希望使用支持彩色显示的更炫酷的界面。他们希望将其集成到项目中,但目前无法通过 QML 配置屏幕,因为该功能来自第三方库。 为解决此问题,需要将必要的类型暴露给 QML 引擎,以便直接在 QML 中修改其属性。
可以通过ThirdPartyDisplay 类控制显示器。该类具有用于定义显示内容的属性,以及文本的前景色和背景色属性。
class Q_DECL_EXPORT ThirdPartyDisplay : public QObject
{
Q_OBJECT
Q_PROPERTY(QString content READ content WRITE setContent NOTIFY contentChanged FINAL)
Q_PROPERTY(QColor foregroundColor READ foregroundColor WRITE setForegroundColor NOTIFY colorsChanged FINAL)
Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY colorsChanged FINAL)
...
};要将该类型暴露给 QML,我们可以使用 `QML_ELEMENT` 将其注册到引擎中。但是,由于该类无法被直接修改,因此无法直接向其添加 `QML_ELEMENT `。 要将该类型注册到引擎中,必须从外部进行注册。这就是QML_FOREIGN 的用途。当在类型中与其他 QML 宏结合使用时,这些宏所作用的并非其所在的类型,而是由QML_FOREIGN 指定的外部类型。
class ForeignDisplay : public QObject
{
Q_OBJECT
QML_NAMED_ELEMENT(ThirdPartyDisplay)
QML_FOREIGN(ThirdPartyDisplay)
};这样一来,BirthdayParty 现在就拥有了一个带有显示效果的新属性。
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged FINAL)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests NOTIFY guestsChanged FINAL)
Q_PROPERTY(QString announcement READ announcement WRITE setAnnouncement NOTIFY announcementChanged FINAL)
Q_PROPERTY(ThirdPartyDisplay *display READ display WRITE setDisplay NOTIFY displayChanged FINAL)
...
};此外,在 QML 中,可以显式设置第三个花哨显示屏上文本的颜色。
BirthdayParty {
display: ThirdPartyDisplay {
foregroundColor: "black"
backgroundColor: "white"
}
...
}现在,设置 BirthdayParty 的announcement 属性会将消息发送到花哨的显示屏上,而不是由它自己打印出来。
void BirthdayParty::setAnnouncement(const QString &announcement)
{
if (m_announcement != announcement) {
m_announcement = announcement;
emit announcementChanged();
}
m_display->setContent(announcement);
}输出结果与上一节类似,会反复显示如下内容。
[Fancy ThirdPartyDisplay] Happy birthday to you,
[Fancy ThirdPartyDisplay] Happy birthday to you,
[Fancy ThirdPartyDisplay] Happy birthday dear Bob Jones,
[Fancy ThirdPartyDisplay] Happy birthday to you!另请参阅 “为 QML 对象类型指定默认属性和父属性”、“分组属性”、“提供附加属性”、“属性值源”以及“注册外部类型”。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.