Extending QML (advanced) - Property Value Source#

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

During the party the guests have to sing for the host. It would be handy if the program could display the lyrics customized for the occasion to help the guests. To this end, a property value source is used to generate the verses of the song over time.

13@QmlElement
14class HappyBirthdaySong(QPyQmlPropertyValueSource):
15    name_changed = Signal()
16
17    def __init__(self, parent=None):
18        super().__init__(parent)
19
20        self.m_target = None
21        self.m_name = ""
22        self.m_line = -1
23        self.m_lyrics = []
24
25        self.m_timer = QTimer(self)
26        self.m_timer.timeout.connect(self.advance)
27        self.m_timer.start(1000)
28
29    def setTarget(self, property):
30        self.m_target = property
31
32    @Property(str, notify=name_changed, final=True)
33    def name(self):
34        return self.m_name
35
36    @name.setter
37    def name(self, n):
38        if self.m_name != n:
39            self.m_name = n
40            self.m_lyrics = ["Happy birthday to you,",
41                             "Happy birthday to you,",
42                             f"Happy birthday dear {self.m_name},",
43                             "Happy birthday to you!",
44                             ""]
45
46    @Slot()
47    def advance(self):
48        self.m_line = (self.m_line + 1) % len(self.m_lyrics)
49        self.m_target.write(self.m_lyrics[self.m_line])

The class HappyBirthdaySong is added as a value source. It must inherit from QQmlPropertyValueSource and implement its interface. The setTarget() function is used to define which property this source acts upon. In this case, the value source writes to the announcement property of the BirthdayParty to display the lyrics over time. It has an internal timer that causes the announcement property of the party to be set to the next line of the lyrics repeatedly.

In QML, a HappyBirthdaySong is instantiated inside the BirthdayParty. The on keyword in its signature is used to specify the property that the value source targets, in this case announcement. The name property of the HappyBirthdaySong object is also bound to the name of the host of the party.

6BirthdayParty {
7    HappyBirthdaySong on announcement { name: "Bob Jones" }

The program displays the time at which the party started using the partyStarted signal and then prints the following happy birthday verses over and over:

Happy birthday to you,
Happy birthday to you,
Happy birthday dear Bob Jones,
Happy birthday to you!

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/advanced6-Property-value-source 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
from birthdayparty import BirthdayParty
from happybirthdaysong import HappyBirthdaySong


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

party.startParty()

r = app.exec()

del engine
sys.exit(r)
# 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, QTime, 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):

    announcement_changed = Signal()
    host_changed = Signal()
    guests_changed = Signal()
    partyStarted = Signal(QTime)

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

    def startParty(self):
        self.partyStarted.emit(QTime.currentTime())

    @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()

    @Property(str, notify=announcement_changed, final=True)
    def announcement(self):
        return self._announcement

    @announcement.setter
    def announcement(self, a):
        if self._announcement != a:
            self._announcement = a
            self.announcement_changed.emit()
        print(a)

    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 QTimer, Property, Signal, Slot
from PySide6.QtQml import QmlElement, QPyQmlPropertyValueSource

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


@QmlElement
class HappyBirthdaySong(QPyQmlPropertyValueSource):
    name_changed = Signal()

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

        self.m_target = None
        self.m_name = ""
        self.m_line = -1
        self.m_lyrics = []

        self.m_timer = QTimer(self)
        self.m_timer.timeout.connect(self.advance)
        self.m_timer.start(1000)

    def setTarget(self, property):
        self.m_target = property

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

    @name.setter
    def name(self, n):
        if self.m_name != n:
            self.m_name = n
            self.m_lyrics = ["Happy birthday to you,",
                             "Happy birthday to you,",
                             f"Happy birthday dear {self.m_name},",
                             "Happy birthday to you!",
                             ""]

    @Slot()
    def advance(self):
        self.m_line = (self.m_line + 1) % len(self.m_lyrics)
        self.m_target.write(self.m_lyrics[self.m_line])
# 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 {
    HappyBirthdaySong on announcement { name: "Bob Jones" }

    onPartyStarted: (time) => { console.log("This party started rockin' at " + time); }

    host: Boy {
        name: "Bob Jones"
        shoe_size: 12
    }

    Boy {
        name: "Leo Hodges"
        BirthdayParty.rsvp: "2009-07-06"
    }
    Boy {
        name: "Jack Smith"
    }
    Girl {
        name: "Anne Brown"
        BirthdayParty.rsvp: "2009-07-01"
    }
}
module People
typeinfo coercion.qmltypes
Main 1.0 Main.qml