Extending QML (advanced) - Attached Properties#

This is the fifth of a series of 6 examples forming a tutorial using the example of a birthday party to demonstrate some of the advanced features of QML.

The time has come for the host to send out invitations. To keep track of which guests have responded to the invitation and when, we need somewhere to store that information. Storing it in the BirthdayParty object iself would not really fit. A better way would be to store the responses as attached objects to the party object.

First, we declare the BirthdayPartyAttached class which holds the guest reponses.

16@QmlAnonymous
17class BirthdayPartyAttached(QObject):
18    rsvp_changed = Signal()
19
20    def __init__(self, parent=None):
21        super().__init__(parent)
22        self._rsvp = QDate()
23
24    @Property(QDate, notify=rsvp_changed, final=True)
25    def rsvp(self):
26        return self._rsvp
27
28    @rsvp.setter
29    def rsvp(self, d):
30        if self._rsvp != d:
31            self._rsvp = d
32            self.rsvp_changed.emit()

And we attach it to the BirthdayParty class and define qmlAttachedProperties() to return the attached object.

34
35@QmlElement
36@ClassInfo(DefaultProperty="guests")
37@QmlAttached(BirthdayPartyAttached)
38class BirthdayParty(QObject):
67    @staticmethod
68    def qmlAttachedProperties(self, o):
69        return BirthdayPartyAttached(o)

Now, attached objects can be used in the QML to hold the rsvp information of the invited guests.

 6BirthdayParty {
 7    Boy {
 8        name: "Robert Campbell"
 9        BirthdayParty.rsvp: "2009-07-01"
10    }
11
12    Boy {
13        name: "Leo Hodges"
14        shoe_size: 10
15        BirthdayParty.rsvp: "2009-07-06"
16    }
17
18    host: Boy {
19        name: "Jack Smith"
20        shoe_size: 8
21    }
22}

Finally, the information can be accessed in the following way.

36    name = guest.name
37
38    rsvp_date = None
39    attached = qmlAttachedPropertiesObject(BirthdayParty, guest, False)

The program outputs the following summary of the party to come:

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

Download this example

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

"""PySide6 port of the
   qml/examples/qml/tutorials/extending-qml-advanced/advanced5-Attached-properties example
   from Qt v6.x"""

from pathlib import Path
import sys

from PySide6.QtCore import QCoreApplication
from PySide6.QtQml import QQmlComponent, QQmlEngine, qmlAttachedPropertiesObject

from person import Boy, Girl  # noqa: F401
from birthdayparty import BirthdayParty  # noqa: F401


app = QCoreApplication(sys.argv)
engine = QQmlEngine()
engine.addImportPath(Path(__file__).parent)
component = QQmlComponent(engine)
component.loadFromModule("People", "Main")
party = component.create()
if not party:
    print(component.errors())
    del engine
    sys.exit(-1)
host = party.host
print(f"{host.name} is having a birthday!")
if isinstance(host, Boy):
    print("He is inviting:")
else:
    print("She is inviting:")
for g in range(party.guestCount()):
    guest = party.guest(g)
    name = guest.name

    rsvp_date = None
    attached = qmlAttachedPropertiesObject(BirthdayParty, guest, False)
    if attached:
        rsvp_date = attached.rsvp.toString()
    if rsvp_date:
        print(f"    {name} RSVP date: {rsvp_date}")
    else:
        print(f"    {name} RSVP date: Hasn't RSVP'd")

del engine
sys.exit(0)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import QDate, QObject, ClassInfo, Property, Signal
from PySide6.QtQml import QmlAnonymous, QmlAttached, QmlElement, ListProperty

from person import Person


# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "People"
QML_IMPORT_MAJOR_VERSION = 1


@QmlAnonymous
class BirthdayPartyAttached(QObject):
    rsvp_changed = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self._rsvp = QDate()

    @Property(QDate, notify=rsvp_changed, final=True)
    def rsvp(self):
        return self._rsvp

    @rsvp.setter
    def rsvp(self, d):
        if self._rsvp != d:
            self._rsvp = d
            self.rsvp_changed.emit()


@QmlElement
@ClassInfo(DefaultProperty="guests")
@QmlAttached(BirthdayPartyAttached)
class BirthdayParty(QObject):
    host_changed = Signal()
    guests_changed = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self._host = None
        self._guests = []

    @Property(Person, notify=host_changed, final=True)
    def host(self):
        return self._host

    @host.setter
    def host(self, h):
        if self._host != h:
            self._host = h
            self.host_changed.emit()

    def guest(self, n):
        return self._guests[n]

    def guestCount(self):
        return len(self._guests)

    def appendGuest(self, guest):
        self._guests.append(guest)
        self.guests_changed.emit()

    @staticmethod
    def qmlAttachedProperties(self, o):
        return BirthdayPartyAttached(o)

    guests = ListProperty(Person, appendGuest, notify=guests_changed, final=True)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import QObject, Property, Signal
from PySide6.QtQml import QmlAnonymous, QmlElement

# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "People"
QML_IMPORT_MAJOR_VERSION = 1


@QmlAnonymous
class Person(QObject):
    name_changed = Signal()
    shoe_size_changed = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self._name = ''
        self._shoe_size = 0

    @Property(str, notify=name_changed, final=True)
    def name(self):
        return self._name

    @name.setter
    def name(self, n):
        if self._name != n:
            self._name = n
            self.name_changed.emit()

    @Property(int, notify=shoe_size_changed, final=True)
    def shoe_size(self):
        return self._shoe_size

    @shoe_size.setter
    def shoe_size(self, s):
        self._shoe_size = s


@QmlElement
class Boy(Person):
    def __init__(self, parent=None):
        super().__init__(parent)


@QmlElement
class Girl(Person):
    def __init__(self, parent=None):
        super().__init__(parent)
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import People

BirthdayParty {
    Boy {
        name: "Robert Campbell"
        BirthdayParty.rsvp: "2009-07-01"
    }

    Boy {
        name: "Leo Hodges"
        shoe_size: 10
        BirthdayParty.rsvp: "2009-07-06"
    }

    host: Boy {
        name: "Jack Smith"
        shoe_size: 8
    }
}
module People
typeinfo coercion.qmltypes
Main 1.0 Main.qml