In this chapter we will look at how you can combine Python and QML. The most natural way to combine the two worlds is to do as with C++ and QML, i.e. implement the logic in Python and the presentation in QML.

To do this, we need to understand how to combine QML and Python into a single program, and then how to implement interfaces between the two worlds. In the sub-sections below, we will look at how this is done. We will start simple and progress to an example exposing the capabilities of a Python module to QML through a Qt item model.

# Running QML from Python

The very first step is to create a Python program that can host the Hello World QML program shown below.

1
2
3
4
5
6
7
8
import QtQuick
import QtQuick.Window
Window {
width: 640
height: 480
visible: true
title: "Hello Python World!"
}

To do this, we need a Qt mainloop provided by QGuiApplication from the QtGui module. We also need a QQmlApplicationEngine from the QtQml module. In order to pass the reference to the source file to the QML application engine, we also need the QUrl class from the QtCore module.

In the code below we emulate the functionality of the boilerplate C++ code generated by Qt Creator for QML projects. It instanciates the application object, and creates a QML application engine. It then loads the QML and then ensures that the QML was loaded by checking if a root object was created. Finally, it exits and returns the value returned by the exec method of the application object.

1
2
3
4
5
6
7
8
9
10
11
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtCore import QUrl
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())

Executing the example results in a window with the title Hello Python World.

TIP

The example assumes that it is executed from the directory containing the main.qml source file. You can termine the location of the Python file being executed using the __file__ variable. This can be used to locate the QML files relative to the Python file as shown in this blog post.

# Exposing Python Objects to QML

The easiest way to share information between Python and QML is to expose a Python object to QML. This is done by registering a context property through the QQmlApplicationEngine . Before we can do that, we need to define a class so that we have an object to expose.

Qt classes come with a number of features that we want to be able to use. These are: signals, slots and properties. In this first example, we will restrict ourselves to a basic pair of signal and slot. The rest will be covered in the examples further on.

# Signals and Slots

We start with the class NumberGenerator . It has a constructor, a method called updateNumber and a signal called nextNumber . The idea is that when you call updateNumber , the signal nextNumber is emitted with a new random number. You can see the code for the class below, but first we will look at the details.

First of all we make sure to call QObject.__init__ from our constructor. This is very important, as the example will not work without it.

Then we declare a signal by creating an instance of the Signal class from the PySide6.QtCore module. In this case, the signal carries an integer value, hence the int . The signal parameter name, number , is defined in the arguments parameter.

Finally, we decorate the updateNumber method with the @Slot() decorator, thus turning it into a slot. There is not concept of invokables in Qt for Python, so all callable methods must be slots.

In the updateNumber method we emit the nextNumber signal using the emit method. This is a bit different than the syntax for doing so from QML or C++ as the signal is represented by an object instead of being a callable function.

1
2
3
4
5
6
7
8
9
import random
from PySide6.QtCore import QObject, Signal, Slot
class NumberGenerator(QObject):
def __init__(self):
QObject.__init__(self)
nextNumber = Signal(int, arguments=['number'])
@Slot()
def updateNumber(self):
self.nextNumber.emit(random.randint(0, 99))

Next up is to combine the class we just created with the boilerplate code for combining QML and Python from earlier. This gives us the following entry-point code.

The interesting lines are the one where we first instatiate a NumberGenerator . This object is then exposed to QML using the setContextProperty method of the rootContext of the QML engine. This exposes the object to QML as a global variable under the name numberGenerator .

1
2
3
4
5
6
7
8
9
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
number_generator = NumberGenerator()
engine.rootContext().setContextProperty("numberGenerator", number_generator)
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())

Continuing to the QML code, we can see that we’ve created a Qt Quick Controls 2 user interface consisting of a Button and a Label . In the button’s onClicked handler, the numberGenerator.updateNumber() function is called. This is the slot of the object instantiated on the Python side.

To receive a signal from an object that has been instantiated outside of QML we need to use a Connections element. This allows us to attach a signal hanlder to an existing target.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import QtQuick
import QtQuick.Window
import QtQuick.Controls
Window {
id: root
width: 640
height: 480
visible: true
title: "Hello Python World!"
Flow {
Button {
text: "Give me a number!"
onClicked: numberGenerator.updateNumber()
}
Label {
id: numberLabel
text: "no number"
}
}
Connections {
target: numberGenerator
function onNextNumber(number) {
numberLabel.text = number
}
}
}

# Properties

Instead of relying soley on signals and slots, the common way to expose state to QML is through properties. A property is a combination of a setter, getter and notification signal. The setter is optional, as we can also have read-only properties.

To try this out we will update the NumberGenerator from the last example to a property based version. It will have two properties: number , a read-only property holding the last random number, and maxNumber , a read-write property holding the maximum value that can be returned. It will also have a slot, updateNumber that updates the random number.

Before we dive into the details of properties, we create a basic Python class for this. It consists of the relevant getters and setters, but not Qt signalling. As a matter of fact, the only Qt part here is the inheritance from QObject . Even the names of the methods are Python style, i.e. using underscores instead of camelCase.

Take notice of the underscores (“ __ ”) at the beginning of the __set_number method. This implies that it is a private method. So even when the number property is read-only, we provide a setter. We just don’t make it public. This allows us to take actions when changing the value (e.g. emitting the notification signal).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class NumberGenerator(QObject):
def __init__(self):
QObject.__init__(self)
self.__number = 42
self.__max_number = 99
def set_max_number(self, val):
if val < 0:
val = 0
if self.__max_number != val:
self.__max_number = val
if self.__number > self.__max_number:
self.__set_number(self.__max_number)
def get_max_number(self):
return self.__max_number
def __set_number(self, val):
if self.__number != val:
self.__number = val
def get_number(self):
return self.__number

In order to define properties, we need to import the concepts of Signal , Slot , and Property from PySide2.QtCore . In the full example, there are more imports, but these are the ones relevant to the properties.

1
from PySide6.QtCore import QObject, Signal, Slot, Property

Now we are ready to define the first property, number . We start off by declaring the signal numberChanged , which we then invoke in the __set_number method so that the signal is emitted when the value is changed.

After that, all that is left is to instantiate the Property object. The Property contructor takes three arguments in this case: the type ( int ), the getter ( get_number ) and the notification signal which is passed as a named argument ( notify=numberChanged ). Notice that the getter has a Python name, i.e. using underscore rather than camelCase, as it is used to read the value from Python. For QML, the property name, number , is used.

1
2
3
4
5
6
7
8
9
10
11
class NumberGenerator(QObject):
# ...
# number
numberChanged = Signal(int)
def __set_number(self, val):
if self.__number != val:
self.__number = val
self.numberChanged.emit(self.__number)
def get_number(self):
return self.__number
number = Property(int, get_number, notify=numberChanged)

This leads us to the next property, maxNumber . This is a read-write property, so we need to provide a setter, as well as everything that we did for the number property.

First up we declare the maxNumberChanged signal. This time, using the @Signal decorator instead of instantiating a Signal object. We also provide a setter slot, setMaxNumber with a Qt name (camelCase) that simply calls the Python method set_max_number alongside a getter with a Python name. Again, the setter emits the change signal when the value is updated.

Finally we put the pieces together into a read-write property by instantiating a Property object taking the type, getter, setter and notification signal as arguments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class NumberGenerator(QObject):
# ...
# maxNumber
@Signal
def maxNumberChanged(self):
pass
@Slot(int)
def setMaxNumber(self, val):
self.set_max_number(val)
def set_max_number(self, val):
if val < 0:
val = 0
if self.__max_number != val:
self.__max_number = val
self.maxNumberChanged.emit()
if self.__number > self.__max_number:
self.__set_number(self.__max_number)
def get_max_number(self):
return self.__max_number
maxNumber = Property(int, get_max_number, set_max_number, notify=maxNumberChanged)

Now we have properties for the current random number, number , and the maximum random number, maxNumber . All that is left is a slot to produce a new random number. It is called updateNumber and simply sets a new random number.

1
2
3
4
5
class NumberGenerator(QObject):
# ...
@Slot()
def updateNumber(self):
self.__set_number(random.randint(0, self.__max_number))

Finally, the number generator is exposed to QML through a root context property.

1
2
3
4
5
6
7
8
9
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
number_generator = NumberGenerator()
engine.rootContext().setContextProperty("numberGenerator", number_generator)
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())

In QML, we can bind to the number as well as the maxNumber properties of the numberGenerator object. In the onClicked handler of the Button we call the updateNumber method to generate a new random number and in the onValueChanged handler of the Slider we set the maxNumber property using the setMaxNumber method. This is because altering the property directly through Javascript would destroy the bindings to the property. By using the setter method explicitly, this is avoided.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import QtQuick
import QtQuick.Window
import QtQuick.Controls
Window {
id: root
width: 640
height: 480
visible: true
title: "Hello Python World!"
Column {
Flow {
Button {
text: "Give me a number!"
onClicked: numberGenerator.updateNumber()
}
Label {
id: numberLabel
text: numberGenerator.number
}
}
Flow {
Slider {
from: 0
to: 99
value: numberGenerator.maxNumber
onValueChanged: numberGenerator.setMaxNumber(value)
}
}
}
}

# Exposing a Python class to QML

Up until now, we’ve instantiated an object Python and used the setContextProperty method of the rootContext to make it available to QML. Being able to instantiate the object from QML allows better control over object life-cycles from QML. To enable this, we need to expose the class, instead of the object, to QML.

The class that is being exposed to QML is not affected by where it is intantiated. No change is needed to the class definition. However, instead of calling setContextProperty , the qmlRegisterType function is used. This function comes from the PySide2.QtQml module and takes five arguments:

  • A reference to the class, NumberGenerator in the example below.
  • A module name, 'Generators' .
  • A module version consisting of a major and minor number, 1 and 0 meaning 1.0 .
  • The QML name of the class, 'NumberGenerator'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import random
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
from PySide6.QtCore import QUrl, QObject, Signal, Slot
class NumberGenerator(QObject):
def __init__(self):
QObject.__init__(self)
nextNumber = Signal(int, arguments=['number'])
@Slot()
def updateNumber(self):
self.nextNumber.emit(random.randint(0, 99))
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
qmlRegisterType(NumberGenerator, 'Generators', 1, 0, 'NumberGenerator')
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())

In QML, we need to import the module, e.g. Generators 1.0 and then instantiate the class as NumberGenerator { ... } . The instance now works like any other QML element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import Generators
Window {
id: root
width: 640
height: 480
visible: true
title: "Hello Python World!"
Flow {
Button {
text: "Give me a number!"
onClicked: numberGenerator.updateNumber()
}
Label {
id: numberLabel
text: "no number"
}
}
NumberGenerator {
id: numberGenerator
}
Connections {
target: numberGenerator
function onNextNumber(number) {
numberLabel.text = number
}
}
}

# A Model from Python

One of the more interesting types of objects or classes to expose from Python to QML are item models. These are used with various views or the Repeater element to dynamically build a user interface from the model contents.

In this section we will take an existing python utility for monitoring CPU load (and more), psutil , and expose it to QML via a custom made item model called CpuLoadModel . You can see the program in action below:

TIP

The psutil library can be found at https://pypi.org/project/psutil/ .

“psutil (process and system utilities) is a cross-platform library for retrieving information on running processes and system utilization (CPU, memory, disks, network, sensors) in Python.”

You can install psutil using pip install psutil .

We will use the psutil.cpu_percent function (documentation) to sample the CPU load per core every second. To drive the sampling we use a QTimer . All of this is exposed through the CpuLoadModel which is a QAbstractListModel .

Item models are interesting. They allow you to represent a two dimensional data set, or even nested data sets, if using the QAbstractItemModel . The QAbstractListModel that we use allow us to represent a list of items, so a one dimensional set of data. It is possible to implement a nested set of lists, creating a tree, but we will only create one level.

To implement a QAbstractListModel , it is necessary to implement the methods rowCount and data . The rowCount returns the number of CPU cores which we get using the psutil.cpu_count method. The data method returns data for different roles. We only support the Qt.DisplayRole , which corresponds to what you get when you refer to display inside the delegate item from QML.

Looking at the code for the model, you can see that the actual data is stored in the __cpu_load list. If a valid request is made to data , i.e. the row, column and role is correct, we return the right element from the __cpu_load list. Otherwise we return None which corresponds to an uninitialized QVariant on the Qt side.

Every time the update timer ( __update_timer ) times out, the __update method is triggered. Here, the __cpu_load list is updated, but we also emit the dataChanged signal, indicating that all data was changed. We do not do a modelReset as that also implies that the number of items might have changed.

Finally, the CpuLoadModel is exposed to QML are a registered type in the PsUtils module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import psutil
import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
from PySide6.QtCore import Qt, QUrl, QTimer, QAbstractListModel


class CpuLoadModel(QAbstractListModel):
def __init__(self):
QAbstractListModel.__init__(self)
self.__cpu_count = psutil.cpu_count()
self.__cpu_load = [0] * self.__cpu_count
self.__update_timer = QTimer(self)
self.__update_timer.setInterval(1000)
self.__update_timer.timeout.connect(self.__update)
self.__update_timer.start()
# The first call returns invalid data
psutil.cpu_percent(percpu=True)

def __update(self):
self.__cpu_load = psutil.cpu_percent(percpu=True)
# print(self.__cpu_load) # [74.6, 71.9, 85.9, 93.8]
self.dataChanged.emit(self.index(0, 0), self.index(self.__cpu_count - 1, 0))

def rowCount(self, parent):
return self.__cpu_count

def data(self, index, role):
if (
role == Qt.DisplayRole
and index.row() >= 0
and index.row() < len(self.__cpu_load)
and index.column() == 0
):
return self.__cpu_load[index.row()]
else:
return None


if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
qmlRegisterType(CpuLoadModel, "PsUtils", 1, 0, "CpuLoadModel")
engine.load(QUrl("main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())

On the QML side we use a ListView to show the CPU load. The model is bound to the model property. For each item in the model a delegate item will be instantiated. In this case that means a Rectangle with a green bar (another Rectangle ) and a Text element displaying the current load.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import PsUtils

Window {
id: root
width: 640
height: 480
visible: true
title: "CPU Load"
ListView {
anchors.fill: parent
model: CpuLoadModel { }
delegate: Rectangle {
required property int display
width: parent.width
height: 30
color: "#bbb"
border.color: Qt.lighter(color, 1.1)
radius: 15

Rectangle {
id: bar
width: parent.width * delegate.display / 100.0
height: 30
color: "green"
border.color: Qt.lighter(color)
radius: 15
}
Text {
anchors.verticalCenter: parent.verticalCenter
x: Math.min(bar.x + bar.width + 5, parent.width-width)
text: delegate.display + "%"
}
}
}
}

当前内容版权归 Juergen Bocklage Ryannel and Johan Thelin 或其关联方所有,如需对内容或内容相关联开源项目进行关注与资助,请访问 Juergen Bocklage Ryannel and Johan Thelin .

Edited on