Learn how to write an application with PyQt/PySide.
Prerequisites
For the purposes of this tutorial, we will create the application on Linux.
To use Python together with QML, we can use either PySide,
the official Python bindings for the Qt framework, or
PyQt, a project by
Riverbank Computing that allows you to write Qt applications using Python.
You will need Python installed, and that will be the case in any major Linux
distribution. But instead of using pip to install PySide/PyQt and Kirigami, you will
need to install them from your distribution. This ensures PySide/PyQt and
Kirigami will have been built for the same Qt version, allowing you to package
it easily. Any other dependencies can be installed from pip in a
Python virtual environment later.
The application will be a simple Markdown viewer called simplemdviewer.
By the end of the tutorial, the project will look like this:
simplemdviewer/
├── README.md
├── LICENSE.txt
├── MANIFEST.in # To add our QML file
├── pyproject.toml # The main file to manage the project
├── org.kde.simplemdviewer.desktop
├── org.kde.simplemdviewer.json
├── org.kde.simplemdviewer.svg
├── org.kde.simplemdviewer.metainfo.xml
└── src/
├── __init__.py # To import the src/ directory as a package
├── __main__.py # To signal simplemdviewer_app as the entrypoint
├── simplemdviewer_app.py
├── md_converter.py
└── qml/
└── main.qml
All of the metadata will be in the root folder, while the actual code will be
in src/:
To quickly generate this folder structure, just run: mkdir -p simplemdviewer/src/qml/
Setting up the project
The UI will be created in QML and the logic in Python. Users will write some
Markdown text, press a button, and the formatted text will be shown below it.
It is recommended to use a virtual environment. The venv module provides
support for virtual environments with their own site directories,
optionally isolated from system site directories.
Create a directory and a virtual environment for the project:
mkdir simplemdviewer
cd simplemdviewer
python3 -m venv --system-site-packages env/
A new virtual environment will be created in env/, pulling the required Python modules straight from your distribution packages. Activate it using the activate script:
source env/bin/activate
We can verify that we are working in a virtual environment by checking
the VIRTUAL_ENV environment variable with env | grep VIRTUAL_ENV.
It’s time to write some code. At first the application will consist of two files:
a file with the QML description of the user interface, and a Python file that
loads the QML file.
Create a new directory simplemdviewer/src/ and add a new
simplemdviewer_app.py file in this directory:
#!/usr/bin/env python3importosimportsysimportsignalfromPySide6.QtGuiimportQGuiApplicationfromPySide6.QtCoreimportQUrlfromPySide6.QtQmlimportQQmlApplicationEnginedefmain():"""Initializes and manages the application execution"""app=QGuiApplication(sys.argv)engine=QQmlApplicationEngine()"""Needed to close the app with Ctrl+C"""signal.signal(signal.SIGINT,signal.SIG_DFL)"""Needed to get proper KDE style outside of Plasma"""ifnotos.environ.get("QT_QUICK_CONTROLS_STYLE"):os.environ["QT_QUICK_CONTROLS_STYLE"]="org.kde.desktop"base_path=os.path.abspath(os.path.dirname(__file__))url=QUrl(f"file://{base_path}/qml/main.qml")engine.load(url)iflen(engine.rootObjects())==0:quit()app.exec()if__name__=="__main__":main()
#!/usr/bin/env python3importosimportsysimportsignalfromPyQt6.QtGuiimportQGuiApplicationfromPyQt6.QtCoreimportQUrlfromPyQt6.QtQmlimportQQmlApplicationEnginedefmain():"""Initializes and manages the application execution"""app=QGuiApplication(sys.argv)engine=QQmlApplicationEngine()"""Needed to close the app with Ctrl+C"""signal.signal(signal.SIGINT,signal.SIG_DFL)"""Needed to get proper KDE style outside of Plasma"""ifnotos.environ.get("QT_QUICK_CONTROLS_STYLE"):os.environ["QT_QUICK_CONTROLS_STYLE"]="org.kde.desktop"base_path=os.path.abspath(os.path.dirname(__file__))url=QUrl(f"file://{base_path}/qml/main.qml")engine.load(url)iflen(engine.rootObjects())==0:quit()app.exec()if__name__=="__main__":main()
We have just created a
QGuiApplication
object that initializes the application and contains the main event loop. The
QQmlApplicationEngine
object loads the main.qml file.
Create a new src/qml/main.qml file that specifies the UI of the application:
Older distributions such as Debian or Ubuntu LTS that do not have an up-to-date Kirigami might require lowering the Kirigami import version from 3.20 to 3.15 to run.
First test run
We have just created a new QML-Kirigami-Python application. Run it:
python3 simplemdviewer_app.py
At the moment we have not used any interesting Python stuff. In reality,
the application can also run as a standalone QML app:
The MdConverter class contains the _source_text member variable.
The sourceText property exposes _source_text to the QML system
by using a getter/accessor and a setter/modifier.
In the PySide6 case, we use a
Property decorator
(beginning with @) in lines 18 and 22. Note that the name of the
function needs to be the same for both getter and setter: the getter
is marked with @Property and the setter is marked as functionName.setter.
In the PyQt6 case, we use a
Property as a function
by creating a property with the pyqtProperty() function in lines 20-22.
Note that the name of the getter and setter needs to be different here.
When setting the sourceText property, the sourceTextChangedsignal
is emitted to let QML know that the property has changed. Note that
the sourceTextChanged needs to be marked with notify= before it can be
emitted with .emit().
The mdFormat() function returns the Markdown-formatted
text and it has been declared as a
slot
so as to be invokable by the QML code.
The markdown Python package takes care of formatting. Let’s install
it in our virtual environment:
#!/usr/bin/env python3importosimportsysimportsignalfromPySide6.QtGuiimportQGuiApplicationfromPySide6.QtCoreimportQUrlfromPySide6.QtQmlimportQQmlApplicationEnginefrommd_converterimportMdConverter# noqa: F401defmain():"""Initializes and manages the application execution"""app=QGuiApplication(sys.argv)engine=QQmlApplicationEngine()"""Needed to close the app with Ctrl+C"""signal.signal(signal.SIGINT,signal.SIG_DFL)"""Needed to get proper KDE style outside of Plasma"""ifnotos.environ.get("QT_QUICK_CONTROLS_STYLE"):os.environ["QT_QUICK_CONTROLS_STYLE"]="org.kde.desktop"base_path=os.path.abspath(os.path.dirname(__file__))url=QUrl(f"file://{base_path}/qml/main.qml")engine.load(url)iflen(engine.rootObjects())==0:quit()app.exec()if__name__=="__main__":main()
#!/usr/bin/env python3importosimportsysimportsignalfromPyQt6.QtGuiimportQGuiApplicationfromPyQt6.QtCoreimportQUrlfromPyQt6.QtQmlimportQQmlApplicationEngine,qmlRegisterTypefrommd_converterimportMdConverterdefmain():"""Initializes and manages the application execution"""app=QGuiApplication(sys.argv)engine=QQmlApplicationEngine()"""Needed to close the app with Ctrl+C"""signal.signal(signal.SIGINT,signal.SIG_DFL)"""Needed to get proper KDE style outside of Plasma"""ifnotos.environ.get("QT_QUICK_CONTROLS_STYLE"):os.environ["QT_QUICK_CONTROLS_STYLE"]="org.kde.desktop"qmlRegisterType(MdConverter,"org.kde.simplemdviewer",1,0,"MdConverter")base_path=os.path.abspath(os.path.dirname(__file__))url=QUrl(f"file://{base_path}/qml/main.qml")engine.load(url)iflen(engine.rootObjects())==0:quit()app.exec()if__name__=="__main__":main()
The Python import from md_converter import MdConverter in
simplemdviewer_app.py takes care of making both Python and the QML engine
aware of the new MdConverter. In PySide we add # noqa: F401 just so later on
linters don't complain about the unused import. In PyQt the import is used in line 23.
In PyQt, the qmlRegisterType() function registers the MdConverter type in the
QML system, under the import name org.kde.simplemdviewer, version 1.0.
In PySide, this registration is done in the file where the class is defined,
namely through the @QmlElement decorator in md_converter.py. Let's revisit it: