-
Notifications
You must be signed in to change notification settings - Fork 1
Qt for Python Guide
This page contains definitions and examples of how to use several features in Qt for Python. Data can be passed around by Python and QML by using Properties, Signals, Slots and Roles.
A QObject is the base class of everything in Qt, including abstract types like QAbstractList
. You can inherit from QObject and use your own class directly in QML by registering a type on startup and using it in the QML like so:
Person.py:
from PySide2.QtCore import QObject
class Person(QObject):
def __init__(self):
super().__init__()
self.current_mood = "Happy"
Person
can be registered as part of a module by using qmlRegisterType
where the application is started. This module can then be imported in QML and our class can be used as a QML type.
main.py:
from PySide2.QtWidgets import QApplication
from PySide2.QtQuick import QQuickView
from PySide2.QtCore import QUrl
from PySide2.QtQml import qmlRegisterType
from person import Person
VERSION_MAJOR = 1
VERSION_MINOR = 0
# Register our Person object as part of a module so it can be used in QML.
qmlRegisterType(Person, "MyModule", VERSION_MAJOR, VERSION_MINOR, "Person")
app = QApplication([])
view = QQuickView()
url = QUrl("view.qml")
view.setSource(url)
view.show()
app.exec_()
In the case there are multiple QML files involved, you can use a resource folder, but in this case we don't need to use that as we're only using one QML file: view.qml
The Person
class shown above has a string property called "mood", defined like this:
mood = Property(str, getMood, setMood, notify=moodChanged)
This requires a getter and setter method, and a signal to notify QML when the value has changed. We have to do this manually in the setting method like so:
from PySide2.QtCore import QObject, Signal, Property
class Person(QObject):
def __init__(self):
super().__init__()
self.current_mood = "Happy"
def getMood(self):
return self.current_mood
def setMood(self, mood):
if mood != self.current_mood and mood != "":
self.current_mood = mood
# Call the notifying signal. We are not passing data here so don't need to emit anything in particular.
self.moodChanged.emit()
# Notifying signal - to be used in qml as "onMoodChanged"
moodChanged = Signal()
# Mood property to be accessed to and from QML and Python.
mood = Property(str, getMood, setMood, notify=moodChanged)
Note: Lambdas could also be used for your "getter" and "setter" functions.
This little QML application consists of:
- A text label, containing the
Person
's current mood, - A text field, to change the
Person
's mood - A button, to submit the new mood.
To use our Person
class in QML, we must use the module generated by qmlRegisterType
like this:
import MyModule 1.0
As we also want a button and some text fields, we will also need QtQuick
and QtQuick.Controls
import QtQuick 2.3
import QtQuick.Controls 2.0
import MyModule 1.0
An easy way of creating a basic application is by just using a Rectangle
as the root type. We will give it a width and height, and a unique id
.
import QtQuick 2.3
import QtQuick.Controls 2.0
import MyModule 1.0
Rectangle {
id: page
width: 300
height: 300
} // Rectangle
We can then put components inside page
, such as our Person
object, the text fields and our submit button:
import QtQuick 2.3
import QtQuick.Controls 2.0
import MyModule 1.0
Rectangle {
id: page
width: 300
height: 300
Person {
id: person
} // Person
Label {
id:label
anchors.bottom: parent.bottom
text: "Mood \:"
TextField {
id: field
focus: true
anchors.bottom: parent.bottom
anchors.left: parent.right
Button {
id: button
text: "submit"
anchors.bottom: parent.bottom
anchors.left: field.right
} // Button
} // TextField
} // Label
} // Rectangle
Setting up a property in QML is even easier, and can be used to bind different objects together.
We can add one here to our Text
object like so, setting the initial value to the Person's default mood:
Text {
property var mood: person.mood
id: currentMood
anchors.top: parent.top
text: "Current mood: " + mood
} // Text
this can then be accessed (and set!) from anywhere in the QML (even if in a different file) by using currentMood.mood
A Signal is a way to tell QML that something has happened or that some data should be passed around. Most QML native types, such as button, have their own signals already bundled in. In Button
's case, there is an onClicked
signal which is triggered when the button is clicked. We can use this signal to define behaviour for another component.
in our case, we want to submit what's in our text field like this:
Button {
id: button
text: "submit"
anchors.bottom: parent.bottom
anchors.left: field.right
onClicked: {
person.mood=field.text
} // onClicked
} // Button
we are then calling the setter we defined earlier in python for the mood
Property in Person
.
In this setter, our notifier is also being called by emitting. This emit can be given data to pass to qml, but in our case we don't need anything other than to just tell QML that the text has changed.
def setMood(self, mood):
if mood != self.current_mood and mood != "":
self.current_mood = mood
# Call the notifying signal. We are not passing data here so don't need to emit anything in particular.
self.moodChanged.emit()
We need a signal for when moodChanged()
gets called. To do this, add this into the Person
object in qml, much like the onClicked
functionality that Button
uses.
Person {
id: person
onMoodChanged: {
// Set the text's "mood" property to Person's
currentMood.mood = person.mood
} // onMoodChanged
} // Person
Our application now changes the Person
's mood to whatever we want.
Full code for this example is available here
Slots can be connected with Signals through QML and Python so that data can be passed around between files without having to bind properties together. They are essentially equivalent to callbacks, except the caller(Signal) doesn't care about the function(Slot) as they are loosely coupled - A Slot is connected to a Signal, but not the other way around.
They can also be used to call Python functions on QObjects, by decorating the function with @Slot()
and filling the parameters with qml types, such as int, string, "QVariant".
@Slot(str)
def print_something(text):
print(text)
SomeObject {
id: something
onCompleted: print_something("hello")
} // SomeObject
TODO.
The input given to a LabeledTextField
can be checked before it is accepted by using custom Python Validators. We define our Validator classes in validators.py
and place their tests tested in test_validators.py
A Validator may return one of the following three inputs to inform QML if the input should be accepted:
Acceptable
Intermediate
Invalid
An explanation of when to use these different return values can be found here.
The example below shows what a custom validator might look like. All Validators must implement a validate
function where the first argument is the input provided by the user.
class MyCustomValidator(QValidator):
"""
A custom validator
"""
def __init__(self):
super().__init__()
self.object_used_for_validating_input = AUsefulValidationTool()
def validate(self, input: str, pos: int):
valid_input = self.object_used_for_validating_input(input)
if not valid_input:
self.validationFailure.emit()
return QValidator.Intermediate
else:
self.validationSuccess.emit()
return QValidator.Acceptable
validationSuccess = Signal()
validationFailed = Signal()
The constructor can be used for creating any important objects/variables that are used to help validate the input. In addition, a developer may wish to create further helper functions for checking input or import them from elsewhere. In the above example, we return QValidator.Intermediate
rather than QValidator.Invalid
as this prevents our LabeledTextField
from "freezing" as soon as it has any valid input. This can be helpful when the text field has a default value and attempting to replace it with something else causes the input to become invalid. Should a validator return QValidator.Invalid
in this case, QML will attempt to preserve the default input and effectively freeze the LabeledTextField
.
In addition to returning Acceptable
/Invalid
/Intermediate
we also make use of custom signals. Such signals are necessary when you wish to program a response to the user input in QML (such as displaying a message if input is unsuitable). These signals are not always required.
Note that the signals are declared outside the constructor but are still accessed with the self
prefix.
A test for the custom validator may look like the example below:
def test_custom_validator():
custom_validator = MyCustomValidator()
valid_inputs = [valid1, valid2, ...]
invalid_inputs = [invalid1, invalid2, ...]
for input in valid_inputs:
assert custom_validator.validate(input, 0) == QValidator.Acceptable
for input in invalid_inputs:
assert custom_validator.validate(input, 0) == QValidator.Intermediate
Once a new validator has been created it must be registered in order for QML to recognise it. This is done by placing some statements in application.py
similar to the ones below:
from nexus_constructor.validators import MyCustomValidator
qmlRegisterType(MyCustomValidator, 'MyValidators', 1, 0, 'MyCustomValidator')
This then makes it possible to access your new custom validator (along with the other validators) by using the following import statement at the top of a QML file:
import MyValidators 1.0
Once imported, a custom Validator can then be used within a LabeledTextField
by using the assignment validatior: MyCustomValidator
. The example below shows how this is done in the case of a field that checks for valid unit import using the UnitValidator
.
LabeledTextField {
id: unitInput
editorText: units
Layout.fillWidth: true
anchoredEditor: true
onEditingFinished: units = editorText
validator: UnitValidator {
id: meshUnitValidator
onValidationFailed: { ValidUnits.validMeshUnits = false }
onValidationSuccess: { ValidUnits.validMeshUnits = true }
}
}