用C++编写高级QML扩展

BirthdayParty基础项目

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类保持不变,而BoyGirlC++类是它的简单扩展。它们的数据类型及其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 的通用基类,因此不再可以直接从 QML 中实例化 Person。取而代之,应该显式实例化 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 类信息注释,以指定 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 中来存储受邀请宾客的 RSVP 信息。

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 继承,并使用 QQmlPropertyValueSource 接口实现 Q_INTERFACES 宏。使用 setTarget() 函数定义此来源影响的属性。在这种情况下,值来源写入 BirthdayPartyannouncement 属性,以时分显示歌词。它有一个内部计时器,它将派对 announcement 属性设置为歌词的下一行,并反复设置。

在 QML 中,在 BirthdayParty 内部实例化 HappyBirthdaySong。它的签名中使用 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"
    }
    ...
}

现在设置生日派对的数据对象属性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!

见 also指定QML对象类型的默认属性和父属性分组属性提供附加属性属性值来源,和注册外部类型

© 2024 The Qt Company Ltd. 此处的文档贡献为其各自所有者的版权。提供的文档根据GNU自由文档许可证1.3版条款许可,由自由软件基金会发布。Qt及其相关标志是The Qt Company Ltd.在芬兰和/或其他国家的商标。所有其他商标均为其各自所有者的财产。