用 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"

下面的章节将介绍如何通过使用继承和强制来添加对BoyGirl 出席者的支持,而不仅仅是Person ;如何使用默认属性来隐式地将参加聚会的人指定为客人;如何将属性指定为组,而不是一个一个地指定;如何使用附加对象来跟踪受邀客人的回复;如何使用属性值源来随着时间的推移显示生日快乐歌的歌词;以及如何将第三方对象暴露给 QML。

继承和强制

extending-qml-advanced/advanced2-Inheritance-and-coercion

现在,每个服务员都是一个人。这有点过于笼统,如果能更多地了解出席者就更好了。通过将他们特殊化为男孩和女孩,我们已经可以更好地了解谁会来。

为此,我们引入了BoyGirl 类,它们都继承于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 类保持不变,而BoyGirl C++ 类则是其微不足道的扩展。这些类型及其 QML 名称通过QML_ELEMENT 向 QML 引擎注册。

请注意,BirthdayParty 中的hostguests 属性仍然使用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 类被重新用作BoyGirl 的共同基础,Person 不再能直接从 QML 实例化。取而代之的是一个明确的BoyGirl 实例。

class Person : public QObject
{
    ...
    QML_ELEMENT
    QML_UNCREATABLE("Person is an abstract base class.")
    ...
};

虽然我们不允许在 QML 中实例化Person ,但它仍然需要在 QML 引擎中注册,这样它才能被用作属性类型,其他类型才能被强制使用。这就是QML_UNCREATABLE 宏的作用。由于PersonBoyGirl 这三种类型都已在 QML 系统中注册,因此在赋值时,QML 会自动(并在类型安全的情况下)将BoyGirl 对象转换成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 属性分配了一个BoyGirl 的列表。这很简单,但对于这个特定的用例,可以做得更简单一些。我们可以不显式分配guests 属性,而是直接在聚会中添加BoyGirl 对象,并将它们隐式分配给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 class info 注解,将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 对象本身并不合适。更好的方法是将回复作为聚会对象的附加对象来存储。

首先,我们声明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() 函数用于定义该值源作用于哪个属性。在本例中,值源写入BirthdayPartyannouncement 属性,以显示随时间变化的歌词。它有一个内部定时器,使announcement 属性重复设置为歌词的下一行。

在 QML 中,HappyBirthdaySong 被实例化在BirthdayParty 中。其签名中的on 关键字用于指定值源所针对的属性,在本例中是announcementHappyBirthdaySong 对象的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 对象类型指定默认属性和父属性分组属性提供附加属性属性值来源注册外来类型

© 2025 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.