Skip to content
This repository has been archived by the owner on Nov 7, 2024. It is now read-only.

Qt for Python Guide

Dolica Akello-Egwel edited this page Mar 29, 2019 · 12 revisions

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.

QObjects

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

Properties

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

Signals

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

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

Roles

TODO.

QML Validators

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.

Creating Custom Validators

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 }
    }
}