Launching your QML application from Python

Installing PySide6

$ cd path/to/my/project
$ python3 -m venv env
$ . ./env/bin/activate
$ (env) pip install PySide6

That's it! You now have the officially endorsed Python bindings [1] which include everything we'll need to start:

  • The Qt framework, wrapped in a Python API,
  • QML, a declarative language that feels as if HTML, CSS and JavaScript had all been rolled into one compelling package,
  • QtQuick Components, a Qt Widgets alternative for QML.

Defining the entry points

Combining Python and QML requires us to define several entry points:

  1. One for the Python interpreter,
  2. one for the Qt framework,
  3. and one for QML.

For Python, we set the entry point like so:

if __name__ == "__main__":
    ...

For Qt, we need to initialize the framework and then kick off the main loop:

app = QGuiApplication(argv)
exit(app.exec())

For QML, we create the engine and point it to the component it should load as its root element:

engine = QQmlApplicationEngine()
engine.loadFromModule("MyApp.UI", "Main")

Here's the full launcher script (main.py), with code comments:

 1 import signal
 2 from os import fspath
 3 from sys import argv, exit
 4 from pathlib import Path
 5 
 6 from PySide6.QtQml import QQmlApplicationEngine
 7 from PySide6.QtCore import Qt
 8 from PySide6.QtGui import QGuiApplication
 9 from PySide6.QtQuickControls2 import QQuickStyle
10 
11 
12 def run():
13     # Shuts down the app when pressing Ctrl-C in the terminal.
14     # For proper handling, check matplotlib's `_allow_interrupt(...)`
15     # implementation.
16     signal.signal(signal.SIGINT, signal.SIG_DFL)
17 
18     # We need to set the style early, or else the engine complains.
19     QQuickStyle.setStyle("Universal")
20 
21     # Also parses Qt-specific command line arguments.
22     app = QGuiApplication(argv)
23     # The engine is responsible for executing QML code.
24     engine = QQmlApplicationEngine()
25     engine.objectCreationFailed.connect(app.quit, Qt.QueuedConnection)
26     # Search for QML modules in the same directory as our `main.py`.
27     engine.addImportPath(fspath(Path(__file__).parent))
28     # The module name "MyApp.UI" is translated into a path.
29     # "Main" is defined in "MyApp/UI/Main.qml" and will be loaded as the
30     # engine's root element.
31     engine.loadFromModule("MyApp.UI", "Main")
32 
33     # Yield control from the Python interpreter to the Qt event loop.
34     return app.exec()
35 
36 
37 if __name__ == "__main__":
38     exit(run())

This allows us to run the app from the command line like so:

$ (env) python3 main.py
QQmlApplicationEngine failed to load component
<Unknown File>: No module named "MyApp.UI" found

The Python interpreter complains about the missing QML module. Let's fix that:

$ (env) mkdir -p MyApp/UI
$ (env) touch MyApp/UI/qmldir
$ (env) touch MyApp/UI/Main.qml

qmldir is QML's way of declaring modules [2]. In that file, we list every QML file belonging to the module. Each QML module requires its own qmldir. One QML module can span across multiple (sub-) directories.

In our case, the module contains a single file, Main.qml, but we need to map it to its component name, which is just Main. We could have chosen other names but Main fits the theme of defining entry points.

Here's our MyApp/UI/qmldir file:

1 module MyApp.UI
2 Main 1.0 Main.qml

1.0 is the version of the component. QtQuick Components make heavy use of that feature, but app developers can safely ignore it and just always keep the initial version for their own components.

We now know what QML modules are and how to point to them from Python. We've also seen the majority of what QML modules have to offer, and why qmldir files exist.

Anyway, let's try running our app again!

$ (env) python3 main.py
QQmlApplicationEngine failed to load component
file:///full/path/to/my/project/MyApp/UI/Main.qml: File is empty

Despite the new error message, we should notice that the QML engine got much farther this time: It found the module, read the qmldir and loaded Main.qml as it expects to find the definition of the Main component in there. But empty QML files make no valid components.

The ApplicationWindow

The root element of your QML app should almost always be the ApplicationWindow [3] itself. Here's our Main.qml:

 1 import QtQuick
 2 import QtQuick.Controls
 3 
 4 
 5 ApplicationWindow {
 6     id: root
 7 
 8     title: "My first QML app"
 9     width: 1280
10     height: 800
11     // Everybody forgets to set `visible` to `true` the first time. Right, Trin?
12     visible: true
13 
14     Text {
15         anchors.centerIn: parent
16         text: "It works! Our application window size is "
17             + `${root.width}x${root.height} pixels.`
18     }
19 }

Launching the app should now look similar to this:

Screenshot of the application's main window

The text property inside the Text component uses a mix of static text and dynamically computed text. Resize the window to see the text update in real-time.

We haven't added a real Python backend yet, nor have we created a practical layout for extending the UI. We do however have a robust app template [4] with an extendable project structure and can start experimenting with QML.

References

[1]Qt for Python mainly focuses on Qt Widget applications whereas we focus on QML
[2]qmldir: module definition file
[3]QML's ApplicationWindow
[4]The app template used in this blog post
[5]Code example on GitHub