Create Applications with QtQuick in PySide2

Build modern desktop applications with declarative QML and Python

PySide2 Tutorial QtQuick & QML in PySide2

Heads up! You've already completed this tutorial.

In previous tutorials we've used the Qt Widgets API for building our applications. This has been the standard method for building applications since Qt was first developed. However, Qt provides another API for building user interfaces: Qt Quick. This is a modern, mobile-focused API for app development, with which you can create dynamic and highly customizable user interfaces.

In this tutorial, you'll learn how to combine PySide2 with QML to build a live digital clock desktop widget, covering everything from loading QML files in Python to passing data between Python and QML using properties and signals.

You can download the source code for all our articles. The code for this article is in the folder pyside2/tutorials/pyside2-qml-qtquick-python-application

What is QtQuick?

Qt Quick uses a declarative scripting language — the Qt Modeling Language (QML) — to define user interfaces. With it you can build completely custom UIs, with dynamic graphical elements and fluid transitions, effects, and animations. UIs built with QML have more in common with mobile apps than traditional desktop applications, reflecting its origin at Nokia, but Qt Quick can be used on all platforms supported by Qt.

QML syntax also supports embedded JavaScript, which can be used to handle application logic — in simple applications the entire app can be implemented in QML! But using PySide you can also write your application code in Python and hook this up to your QML. This has the advantage of keeping your UI design (QML) and business logic (Python) implementation properly isolated, and gives you access to all the Python libraries to power the backend of your app.

Before starting this tutorial you will need to install PySide, see the installation guides.

For building QML applications you can use PySide2 or PySide6. If using Qt 6 you will need v6.1 or later.

Getting started with PySide2 and QML

In this tutorial we will be using PySide with the Qt Quick/QML API. If you've used Qt Widgets before, many of the Qt Quick concepts will seem familiar. While QML does not make use of QtWidgets classes, all the other parts of Qt (QtCore, QtGui, etc.) are still available.

Before we start writing our application, we can set up our project folder with the files we'll need in the right structure below. You can also download a zip file containing these files.

  • Create a project folder for the app, in our example we will call it: clock
  • Inside your clock folder create an empty file named main.py
  • Create a file alongside main.py named main.qml, to hold our UI definition in QML
  • Create an empty folder alongside main.py and main.qml called images

Creating a "Hello World" QML app with Python

Open up main.py in your editor and add the following skeleton code. This is the bare minimum code required to load a QML file and display it using the QML application engine.

main.py

python
import sys

from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine


app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load("main.qml")

sys.exit(app.exec_())

The above code creates a QGuiApplication and a QQmlApplicationEngine, which uses QML (rather than Qt Widgets) as the UI layer for the Qt application. It then connects the engine's quit signal to the application's quit slot, so the app exits cleanly when the QML window is closed. Next it loads the QML file. Finally, app.exec_() starts the Qt event loop and launches the application, just as in Qt Widgets.

Here the call to app.exec_() is wrapped inside sys.exit() to return the exit code to the calling process in case of errors, but this isn't strictly necessary.

Next, add the following code to main.qml.

main.qml

qml
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 600
    height: 500
    title: "HelloApp"

    Text {
        anchors.centerIn: parent
        text: "Hello World"
        font.pixelSize: 24
    }
}

The above code creates a window with the specified width and height, a title of "HelloApp", and a Text object centered in the window. The text displayed is "Hello World", with a pixel size of 24px.

visible: true is very important — without it the UI will be created but will be invisible!

Once you have entered the above code into the two files and saved them, you can run it and see the result. You can run the code like any other Python script — navigate into the folder containing the main.py script and run it using python (or python3 depending on your system).

shell
$ cd clock
$ python main.py

When the application launches you should see a window which looks like the following.

Hello World shown in an application

Success! We have a QML application, although it's very basic to start with. Next we'll modify the UI to make it a little more interesting and build towards a simple clock.

Updating the QML UI design

First, let's add an image as a background.

Place this image in the folder we created earlier named images. This will be the background for our application window.

A simple background image with a gradient effect A simple background image with a gradient effect

If you like, you can substitute any other image you have. We'll be placing white text over it, so dark, simple images will work better.

main.qml

qml
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 400
    height: 600
    title: "Clock"

    Rectangle {
        anchors.fill: parent

        Image {
            anchors.fill: parent
            source: "./images/background.png"
            fillMode: Image.PreserveAspectCrop
        }

        Rectangle {
            anchors.fill: parent
            color: "transparent"

            Text {
                text: "16:38:33"
                font.pixelSize: 24
                color: "white"
            }
        }
    }
}

In this QML file we've defined our main application window using the ApplicationWindow object type. Within this we've defined a Rectangle and an Image which holds our background image, filling the parent. The fillMode defines how the image will be sized. In this example we've set the image to fill the parent window using anchors.fill: parent while preserving aspect ratio and cropping. This ensures the image fills the window area without being deformed.

You can also control the size of the image in memory by setting the sourceSize property, e.g.

qml
Image {
    sourceSize.width: parent.width
    sourceSize.height: parent.height
    source: "./images/background.png"
    fillMode: Image.PreserveAspectCrop
}

This approach allows you some more control — for example, you could scale an image to half the size of the parent window by dividing the sizes by two and use this to tile multiple images.

qml
Image {
    sourceSize.width: parent.width / 2
    sourceSize.height: parent.height / 2
    source: "./images/background.png"
    fillMode: Image.PreserveAspectCrop
}

Alongside the Image we've also defined a transparent Rectangle which also fills the window. Since the rectangle is defined after the Image, it is drawn on top of it. In QML, items are stacked in the order they are declared unless you use a layout.

By default Rectangle objects have a white background.

Finally, inside the rectangle, we've defined a Text object with the text "16:38:33" to mock up a standard time display.

If you run the app now, the text will appear at the top-left corner of our application window.

shell
$ python main.py

By default text appears in the top left By default text appears in the top left

Let's move it somewhere else — down to the bottom-left, with some margins to make it look nicer. In your QML code, update the Text object to include position anchors and change the size of the font.

main.qml

qml
Text {
    anchors {
        bottom: parent.bottom
        bottomMargin: 12
        left: parent.left
        leftMargin: 12
    }
    text: "16:38:33"
    font.pixelSize: 48
    color: "white"
}

Run the application again as before.

shell
$ python main.py

You will see the text has now moved to the bottom left.

Application window with text in the bottom left Application window with text in the bottom left

So far, our time display is just a fixed text string — it doesn't update, and unless you run it at the right time, it's going to be wrong. Not the most useful of clocks! Next we'll add some Python code to get the current system time and update our clock display automatically.

Getting the time from Python

The Python standard library provides functions for handling time and date, including a number of options for getting the current time. For example, the Python function time.gmtime() provides a struct containing the current GMT time, while time.localtime() will give the time in your current local timezone.

Once you have a time struct you can pass this to the time.strftime() function to get a properly formatted string. The strftime function accepts two arguments — first a time format string, and second the time struct to use. The time format string uses tokens such as %H to place specific parts of the time/date in a specific format.

For example, if you enter the following in a Python shell you'll get the current GMT (UTC) time output.

python
from time import strftime, gmtime
strftime("%H:%M:%S", gmtime())

The %H, %M and %S tokens tell strftime to insert the hours (24 hour, zero padded), minutes (zero padded) and seconds (zero padded) into the string.

You can read more about format codes for strftime in the Python documentation.

For local time, you can use the localtime method instead of gmtime.

python
from time import strftime, localtime
strftime("%H:%M:%S", localtime())

This adjusts to your computer's local time settings and should output the same time displayed on your computer's clock.

If you're used to working with datetime objects, every datetime has a .strftime() method, which uses the same format strings and returns the same output. For example, the following will give the same output as the localtime example above.

python
from datetime import datetime
datetime.now().strftime("%H:%M:%S")

Passing data from Python to QML with properties

To pass our formatted time string from Python to QML we can use QML properties. First, let's define a property on our QML ApplicationWindow called currTime. Update your QML code in main.qml as follows:

main.qml

qml
...
ApplicationWindow {
    ...
    title: "Clock"
    property string currTime: "00:00:00"

    ...

The ... marks indicate where existing code should be left as it is.

Next, modify the Text object to use the currTime property as its text value. When the currTime property is modified, the text label will update automatically (along with any other places it is used).

main.qml

qml
...
Text {
    ...
    text: currTime  // used to be: text: "16:38:33"
    font.pixelSize: 48
    color: "white"
}
...

Finally, we need to send the current time, stored in the curr_time variable, from our Python code through to QML. Modify the Python code to add the time formatting code, using localtime(), and then set this property onto the QML object. The following code will set the QML property currTime to the value of the Python variable curr_time.

main.py

python
import sys
from time import localtime, strftime

from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load("main.qml")

# Pass the current time to QML.
curr_time = strftime("%H:%M:%S", localtime())
engine.rootObjects()[0].setProperty("currTime", curr_time)

sys.exit(app.exec_())

The code engine.rootObjects()[0] gets the root objects from the QML engine as a list. Our ApplicationWindow object is a root object because it appears at the top of the hierarchy. Next we use [0] to select the first item in that list — in our case, there is only one item, our ApplicationWindow. The .setProperty() method is then called on that object.

If you run the application now, you should see the correct time displayed in the window. This means the time will be correct when the application starts — try closing it and re-running it to see the time update. But you'll notice that the time doesn't update yet — we'll do that next.

The correct time (at least it was when I took the screenshot) The correct time (at least it was when I took the screenshot)

Updating the time using QTimer

To update the time we need to run our time fetching and formatting code on a regular interval (every second). There are two options for implementing this:

  1. using a timer which fires regularly, triggering our update method
  2. using a long-running thread, which calculates the time with a delay (sleep) between each update

In Qt, timers are handled on the GUI thread event loop. That means each time the timer fires, the GUI is briefly blocked while the connected function runs. If that function is long-running this can become noticeable in the UI. In that case, using a thread makes more sense. But here, our time fetching and formatting code is very quick — there will be no noticeable delay. For that reason, in this example we'll use a simple timer.

Using setProperty with a timer

Based on the code we have so far, the simplest approach to updating the time automatically is to take our update code, wrap it inside a function, and then call that function repeatedly using a timer. The following code shows this approach in action, using a QTimer with an interval of 100 ms (1/10th second).

main.py

python
import sys
from time import localtime, strftime

from PySide2.QtCore import QTimer
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load("main.qml")


def update_time():
    # Pass the current time to QML.
    curr_time = strftime("%H:%M:%S", localtime())
    engine.rootObjects()[0].setProperty("currTime", curr_time)


timer = QTimer()
timer.setInterval(100)  # 100 ms = 1/10th sec
timer.timeout.connect(update_time)
timer.start()

sys.exit(app.exec_())

If you run this you'll see the time updating correctly.

You may also notice that when the application is first run the time displays as 00:00:00 (the default value) for a moment. That is because the UI is rendered before the first timer timeout fires. You can avoid this by adding a call to update_time() just before app.exec_() is called, e.g.

python
update_time()  # initial startup
sys.exit(app.exec_())

Now, when the app launches it will be showing the correct time immediately.

Using signals to communicate between Python and QML

While this approach of setting properties from the Python code works well for this small example, it's not ideal as your applications grow in size. By having your Python code change specific properties in your QML, you are tying your Python code to the structure of the UI. That makes it easy to break things when restructuring your application.

Just like in the Qt Widgets API, you can use Qt signals to avoid this problem: your code can emit signals without needing to know where they will be received, or how they will be used — and you can even hook a single signal up to multiple receivers. This keeps your logic and UI code nicely decoupled.

If you're not familiar with Qt signals, take a look at our Signals, Slots & Events tutorials.

Let's rework our example to make use of Qt signals in Python and QML to achieve the same result.

First we must define our signals in the main.py file. Signals can only be defined on objects that are subclassed from QObject, so we'll need to implement a small class. This is also a logical place to put our time-handling code to keep things nicely self-contained. We'll also define our signal for passing the current time to QML.

Multiple signals can be handled under a single QObject class and it often makes sense to use a single class for simplicity.

main.py

python
import sys
from time import localtime, strftime

from PySide2.QtCore import QObject, QTimer, Signal
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load("main.qml")


class Backend(QObject):
    updated = Signal(str, arguments=["time"])

    def __init__(self):
        super().__init__()

        # Define timer.
        self.timer = QTimer()
        self.timer.setInterval(100)  # 100 ms = 1/10th sec
        self.timer.timeout.connect(self.update_time)
        self.timer.start()

    def update_time(self):
        # Pass the current time to QML.
        curr_time = strftime("%H:%M:%S", localtime())
        self.updated.emit(curr_time)


# Define our backend object, which we pass to QML.
backend = Backend()

engine.rootObjects()[0].setProperty("backend", backend)

# Initial call to trigger first update. Must be after setProperty() so QML can connect signals.
backend.update_time()

sys.exit(app.exec_())

While this looks like a lot of changes, the majority of the code is exactly the same, just reorganized to put everything under the container class. Everything in __init__() will be run when we create an instance of the Backend class using backend = Backend().

The signal definition (repeated below) creates a signal which accepts a single parameter — a string. This will be sent with the signal to the receivers. The arguments= parameter is used to define the names under which the arguments will be known in QML (if using keyword arguments).

python
updated = Signal(str, arguments=["time"])

You'll also notice that we pass our backend object through to a QML property (also named backend). This allows the signal we've just implemented to be used from the QML code and hooked up to an appropriate target.

python
engine.rootObjects()[0].setProperty("backend", backend)

As before we need to implement the property in QML which this will set. Previously when defining our property to receive the formatted time string, we used a string type. This isn't appropriate for the Backend object, as it's not a string. To receive the Backend object (which is a QObject) from Python we need to use the QtObject type.

main.qml

qml
...
property string currTime: "00:00:00"
property QtObject backend
...

There are not that many types. QML converts Python base types into bool, int, double, string, list, QtObject and var. The var type is a generic handler which can handle any Python type.

To receive the signal itself, we need to define a Connections object, setting its target as our backend property (in QML).

main.qml

qml
ApplicationWindow {
    ...

    Connections {
        target: backend
    }

    ...
}

We can now implement any other logic we like inside this Connections object to handle the signals on the backend object. Let's create a signal handler to handle our updated signal. Signal handlers are automatically named using the capitalized form of the signal name we chose in Python, preceded by lowercase on. Underscores and existing capitalization are ignored.

Python name QML name
mySignal onMySignal
mysignal onMysignal
my_signal onMy_signal

main.qml

qml
ApplicationWindow {
    ...

    Connections {
        target: backend

        function onUpdated(msg) {
            currTime = msg;
        }
    }

    ...
}

The above code shows the signal handler for the updated signal, named onUpdated. This receives the current time as a string (named msg) and sets that onto the QML property currTime. As before, setting this property automatically updates the associated text label.

If you run the application now, you'll see the time updating exactly the same as before!

We could replace the Python time formatting with formatting code in JavaScript inside QML if we wanted, and send a timestamp as a signal. In fact, you can get the time and define timers in QML too!

Creating a frameless desktop widget

To create a desktop-widget-like application you can hide the window decorations on your QML app. This removes the title bar and buttons for closing/minimizing the app. However, you can still close the window from the taskbar if you need to. Make the following changes to the top of the QML file, setting the flags property and positioning the widget into the bottom right of the display.

main.qml

qml
...
ApplicationWindow {
    visible: true
    width: 400
    height: 600
    x: screen.desktopAvailableWidth - width - 12
    y: screen.desktopAvailableHeight - height - 48
    title: "Clock"
    flags: Qt.FramelessWindowHint | Qt.Window
...

The code sets x and y for the window and adds the flag Qt.FramelessWindowHint to make the window frameless. The Qt.Window flag ensures that even though the window is frameless, we still get an entry on the taskbar. Run it, and you will see the window we've created.

The final view with the updating clock and no window decorations The final view with the updating clock and no window decorations

In the next tutorial we'll expand on this simple clock by using image manipulations, transitions and animations to build a fully functional analog clock.

The complete final code

Below is the complete final code for the PySide2 QtQuick clock application.

main.py

python
import sys
from time import localtime, strftime

from PySide2.QtCore import QObject, QTimer, Signal
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load("main.qml")


class Backend(QObject):
    updated = Signal(str, arguments=["time"])

    def __init__(self):
        super().__init__()

        # Define timer.
        self.timer = QTimer()
        self.timer.setInterval(100)  # 100 ms = 1/10th sec
        self.timer.timeout.connect(self.update_time)
        self.timer.start()

    def update_time(self):
        # Pass the current time to QML.
        curr_time = strftime("%H:%M:%S", localtime())
        self.updated.emit(curr_time)


# Define our backend object, which we pass to QML.
backend = Backend()

engine.rootObjects()[0].setProperty("backend", backend)

# Initial call to trigger first update. Must be after setProperty() so QML can connect signals.
backend.update_time()

sys.exit(app.exec_())

main.qml

qml
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 400
    height: 600
    x: screen.desktopAvailableWidth - width - 12
    y: screen.desktopAvailableHeight - height - 48
    title: "Clock"
    flags: Qt.FramelessWindowHint | Qt.Window
    property string currTime: "00:00:00"
    property QtObject backend

    Rectangle {
        anchors.fill: parent

        Image {
            sourceSize.width: parent.width
            sourceSize.height: parent.height
            source: "./images/background.png"
            fillMode: Image.PreserveAspectCrop
        }

        Rectangle {
            anchors.fill: parent
            color: "transparent"

            Text {
                anchors {
                    bottom: parent.bottom
                    bottomMargin: 12
                    left: parent.left
                    leftMargin: 12
                }
                text: currTime  // used to be: text: "16:38:33"
                font.pixelSize: 48
                color: "white"
            }
        }
    }

    Connections {
        target: backend

        function onUpdated(msg) {
            currTime = msg;
        }
    }
}

Now you have your basic QML application, you should experiment with customizing and changing the behavior. Try changing the background image, modifying the text color, or sending different (or multiple) bits of information from Python to your app.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick

(PySide6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

Amoh Gyebi Ampofo

Create Applications with QtQuick in PySide2 was written by Amoh-Gyebi Ampofo with contributions from Leo Well.

Amoh is a Python GUI developer from Ghana.