Extending QML (advanced) - Grouped Properties

This is the fourth 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.

More information is needed about the shoes of the guests. Aside from their size, we also want to store the shoes’ color, brand, and price. This information is stored in a ShoeDescription class.

14
15@QmlAnonymous
16class ShoeDescription(QObject):
17    brand_changed = Signal()
18    size_changed = Signal()
19    price_changed = Signal()
20    color_changed = Signal()
21
22    def __init__(self, parent=None):
23        super().__init__(parent)
24        self._brand = ''
25        self._size = 0
26        self._price = 0
27        self._color = QColor()
28
29    @Property(str, notify=brand_changed, final=True)
30    def brand(self):
31        return self._brand
32
33    @brand.setter
34    def brand(self, b):
35        if self._brand != b:
36            self._brand = b
37            self.brand_changed.emit()
38
39    @Property(int, notify=size_changed, final=True)
40    def size(self):
41        return self._size
42
43    @size.setter
44    def size(self, s):
45        if self._size != s:
46            self._size = s
47            self.size_changed.emit()
48
49    @Property(float, notify=price_changed, final=True)
50    def price(self):
51        return self._price
52
53    @price.setter
54    def price(self, p):
55        if self._price != p:
56            self._price = p
57            self.price_changed.emit()
58
59    @Property(QColor, notify=color_changed, final=True)
60    def color(self):
61        return self._color
62
63    @color.setter
64    def color(self, c):
65        if self._color != c:
66            self._color = c

Each person now has two properties, a name and a shoe description shoe.

69
70@QmlAnonymous
71class Person(QObject):
72    name_changed = Signal()
73
74    def __init__(self, parent=None):
75        super().__init__(parent)
76        self._name = ''
77        self._shoe = ShoeDescription()
78
79    @Property(str, notify=name_changed, final=True)
80    def name(self):
81        return self._name
82
83    @name.setter
84    def name(self, n):
85        if self._name != n:
86            self._name = n
87            self.name_changed.emit()
88
89    @Property(ShoeDescription, final=True)
90    def shoe(self):

Specifying the values for each element of the shoe description works but is a bit repetitive.

26    Girl {
27        name: "Anne Brown"
28        shoe.size: 7
29        shoe.color: "red"
30        shoe.brand: "Job Macobs"
31        shoe.price: 699.99
32    }

Grouped properties provide a more elegant way of assigning these properties. Instead of assigning the values to each property one-by-one, the individual values can be passed as a group to the shoe property making the code more readable. No changes are required to enable this feature as it is available by default for all of QML.

 9    host: Boy {
10        name: "Bob Jones"
11        shoe { size: 12; color: "white"; brand: "Bikey"; price: 90.0 }
12    }

Download this example

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

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

from pathlib import Path
import sys

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

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


if __name__ == '__main__':
    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:")
    best_shoe = None
    for g in range(party.guestCount()):
        guest = party.guest(g)
        name = guest.name
        print(f"    {name}")
        if not best_shoe or best_shoe.shoe.price < guest.shoe.price:
            best_shoe = guest
    if best_shoe:
        print(f"{best_shoe.name} is wearing the best shoes!")
    del engine
    sys.exit(0)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import QObject, ClassInfo, Property, Signal
from PySide6.QtQml import 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


@QmlElement
@ClassInfo(DefaultProperty="guests")
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()

    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 __future__ import annotations

from PySide6.QtCore import QObject, Property, Signal
from PySide6.QtGui import QColor
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 ShoeDescription(QObject):
    brand_changed = Signal()
    size_changed = Signal()
    price_changed = Signal()
    color_changed = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self._brand = ''
        self._size = 0
        self._price = 0
        self._color = QColor()

    @Property(str, notify=brand_changed, final=True)
    def brand(self):
        return self._brand

    @brand.setter
    def brand(self, b):
        if self._brand != b:
            self._brand = b
            self.brand_changed.emit()

    @Property(int, notify=size_changed, final=True)
    def size(self):
        return self._size

    @size.setter
    def size(self, s):
        if self._size != s:
            self._size = s
            self.size_changed.emit()

    @Property(float, notify=price_changed, final=True)
    def price(self):
        return self._price

    @price.setter
    def price(self, p):
        if self._price != p:
            self._price = p
            self.price_changed.emit()

    @Property(QColor, notify=color_changed, final=True)
    def color(self):
        return self._color

    @color.setter
    def color(self, c):
        if self._color != c:
            self._color = c
            self.color_changed.emit()


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

    def __init__(self, parent=None):
        super().__init__(parent)
        self._name = ''
        self._shoe = ShoeDescription()

    @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(ShoeDescription, final=True)
    def shoe(self):
        return self._shoe


@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 QtQuick

import People

BirthdayParty {
    host: Boy {
        name: "Bob Jones"
        shoe { size: 12; color: "white"; brand: "Bikey"; price: 90.0 }
    }

    Boy {
        name: "Leo Hodges"
        shoe { size: 10; color: "black"; brand: "Thebok"; price: 59.95 }
    }
    Boy { name: "Jack Smith"
        shoe {
            size: 8
            color: "blue"
            brand: "Luma"
            price: 19.95
        }
    }
    Girl {
        name: "Anne Brown"
        shoe.size: 7
        shoe.color: "red"
        shoe.brand: "Job Macobs"
        shoe.price: 699.99
    }
}
module People
typeinfo coercion.qmltypes
Main 1.0 Main.qml