Extending QML - Creating a New Type

This is the first of a series of 6 examples forming a tutorial about extending QML with Python.

The Qt QML module provides a set of APIs for extending QML through Python extensions. You can write extensions to add your own QML types, extend existing Qt types, or call Python functions that are not accessible from ordinary QML code.

This tutorial shows how to write a QML extension using Python that includes core QML features, including properties, signals and bindings. It also shows how extensions can be deployed through plugins.

A common task when extending QML is to provide a new QML type that supports some custom functionality beyond what is provided by the built-in Qt Quick types. For example, this could be done to implement particular data models, or provide types with custom painting and drawing capabilities, or access system features like network programming that are not accessible through built-in QML features.

In this tutorial, we will show how to use the C++ classes in the Qt Quick module to extend QML. The end result will be a simple Pie Chart display implemented by several custom QML types connected together through QML features like bindings and signals, and made available to the QML runtime through a plugin.

To begin with, let’s create a new QML type called PieChart that has two properties: a name and a color. We will make it available in an importable type namespace called Charts, with a version of 1.0.

We want this PieChart type to be usable from QML like this:

import Charts 1.0

PieChart {
    width: 100; height: 100
    name: "A simple pie chart"
    color: "red"
}

To do this, we need a C++ class that encapsulates this PieChart type and its two properties. Since QML makes extensive use of Qt’s Meta-Object System this new class must:

Class Implementation

Here is our PieChart class, defined in basics.py:

21
22@QmlElement
23class PieChart (QQuickPaintedItem):
24
25    nameChanged = Signal()
26
27    def __init__(self, parent=None):
28        QQuickPaintedItem.__init__(self, parent)
29        self._name = u''
30        self._color = QColor()
31
32    def paint(self, painter):
33        pen = QPen(self.color, 2)
34        painter.setPen(pen)
35        painter.setRenderHints(QPainter.RenderHint.Antialiasing, True)
36        painter.drawPie(self.boundingRect().adjusted(1, 1, -1, -1), 90 * 16, 290 * 16)
37
38    @Property(QColor, final=True)
39    def color(self):
40        return self._color
41
42    @color.setter
43    def color(self, value):
44        self._color = value
45
46    @Property(str, notify=nameChanged, final=True)
47    def name(self):
48        return self._name
49
50    @name.setter
51    def name(self, value):

The class inherits from QQuickPaintedItem because we want to override paint() to perform drawing operations with the QPainter API. If the class just represented some data type and was not an item that actually needed to be displayed, it could simply inherit from QObject. Or, if we want to extend the functionality of an existing QObject-based class, it could inherit from that class instead. Alternatively, if we want to create a visual item that doesn’t need to perform drawing operations with the QPainter API, we can just subclass QQuickItem.

The PieChart class defines the two properties, name and color, with the Property decorator, and overrides QQuickPaintedItem.paint(). The PieChart class is registered using the @QmlElement decorator, to allow it to be used from QML. If you don’t register the class, app.qml won’t be able to create a PieChart.

QML Usage

Now that we have defined the PieChart type, we will use it from QML. The app.qml file creates a PieChart item and displays the pie chart’s details using a standard QML Text item:

 7Item {
 8    width: 300; height: 200
 9
10    PieChart {
11        id: aPieChart
12        anchors.centerIn: parent
13        width: 100; height: 100
14        name: "A simple pie chart"
15        color: "red"
16    }
17
18    Text {
19        anchors {
20            bottom: parent.bottom;
21            horizontalCenter: parent.horizontalCenter;
22            bottomMargin: 20
23        }
24        text: aPieChart.name
25    }
26}

Notice that although the color is specified as a string in QML, it is automatically converted to a QColor object for the PieChart color property. Automatic conversions are provided for various other QML value types. For example, a string like “640x480” can be automatically converted to a QSize value.

We’ll also create a main function that uses a QQuickView to run and display app.qml. Here is the application basics.py:

54
55if __name__ == '__main__':
56    app = QGuiApplication(sys.argv)
57
58    view = QQuickView()
59    view.setResizeMode(QQuickView.SizeRootObjectToView)
60    qml_file = os.fspath(Path(__file__).resolve().parent / 'app.qml')
61    view.setSource(QUrl.fromLocalFile(qml_file))
62    if view.status() == QQuickView.Status.Error:
63        sys.exit(-1)
64    view.show()
65    res = app.exec()
66    # Deleting the view before it goes out of scope is required to make sure all child QML instances
67    # are destroyed in the correct order.
68    del view

Note

You may see a warning Expression … depends on non-NOTIFYable properties: PieChart.name. This happens because we add a binding to the writable name property, but haven’t yet defined a notify signal for it. The QML engine therefore cannot update the binding if the name value changes. This is addressed in the following chapters.

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/tutorials/extending-qml/chapter1-basics example from Qt v5.x"""

import os
from pathlib import Path
import sys

from PySide6.QtCore import Property, Signal, QUrl
from PySide6.QtGui import QGuiApplication, QPen, QPainter, QColor
from PySide6.QtQml import QmlElement
from PySide6.QtQuick import QQuickPaintedItem, QQuickView

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


@QmlElement
class PieChart (QQuickPaintedItem):

    nameChanged = Signal()

    def __init__(self, parent=None):
        QQuickPaintedItem.__init__(self, parent)
        self._name = u''
        self._color = QColor()

    def paint(self, painter):
        pen = QPen(self.color, 2)
        painter.setPen(pen)
        painter.setRenderHints(QPainter.RenderHint.Antialiasing, True)
        painter.drawPie(self.boundingRect().adjusted(1, 1, -1, -1), 90 * 16, 290 * 16)

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

    @color.setter
    def color(self, value):
        self._color = value

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

    @name.setter
    def name(self, value):
        self._name = value


if __name__ == '__main__':
    app = QGuiApplication(sys.argv)

    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView)
    qml_file = os.fspath(Path(__file__).resolve().parent / 'app.qml')
    view.setSource(QUrl.fromLocalFile(qml_file))
    if view.status() == QQuickView.Status.Error:
        sys.exit(-1)
    view.show()
    res = app.exec()
    # Deleting the view before it goes out of scope is required to make sure all child QML instances
    # are destroyed in the correct order.
    del view
    sys.exit(res)
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import Charts
import QtQuick

Item {
    width: 300; height: 200

    PieChart {
        id: aPieChart
        anchors.centerIn: parent
        width: 100; height: 100
        name: "A simple pie chart"
        color: "red"
    }

    Text {
        anchors {
            bottom: parent.bottom;
            horizontalCenter: parent.horizontalCenter;
            bottomMargin: 20
        }
        text: aPieChart.name
    }
}