Linux applications with QML and Python? Why not? Python is a popular programming language. QML offers an intuitive way to create user interfaces. Kirigami provides useful UI components and implements UI/UX patterns for mobile and desktop. Let's fit these technologies together and create a simple application.
Prerequisites
Obviously, we need Python. Most Linux distributions provide a recent Python. In the rare case that you have to install it by yourself, you may find here some tips. For QML and Kirigami, check the Kirigami introduction guide.
Between Python and QML there is Qt for Python. It offers Python bindings for Qt, and it consists of:
- PySide2, so that you can use Qt5 APIs in Python
- Shiboken2, a binding generator tool which exposes C++ to Python
On Ubuntu and the like, python3-pyside2.qtcore
, python3-pyside2.qtqml
and python3-pyside2.qtwidgets
are enough for the code of this article. Different distributions may package or name the various bindings differently. E.g., on postmarketOS, everything we need is found in the py3-pyside2
package.
Development
Let's create a simple program to see these technologies in action: a markdown viewer. 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.
Nothing fancy.
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. On Ubuntu flavors and derivatives, it is provided by the python3.8-venv
package.
- Create a directory and a virtual environment for the project:
mkdir simple-md-viewer-project
cd simple-md-viewer-project
python3 -m venv --system-site-packages env/
- 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.
It's time to write some code. A PySide2/QML application consists, at least, 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
simplemdviewer
directory intosimple-md-viewer-project
, and add a newsimplemdviewer_app.py
file in this directory:
import os
from PySide2.QtGui import QGuiApplication
from PySide2.QtCore import QUrl
from PySide2.QtQml import QQmlApplicationEngine, qmlRegisterType
def main():
"""Initializes and manages the application execution"""
app = QGuiApplication()
engine = QQmlApplicationEngine()
base_path = os.path.abspath(os.path.dirname(__file__))
url = QUrl(f'file://{base_path}/qml/main.qml')
engine.load(url)
if len(engine.rootObjects()) == 0:
quit()
app.exec_()
if __name__ == "__main__":
main()
simplemdviewer_app.py
We have just created a QGuiApplication object that, among others, initializes the application and contains the main event loop. The QQmlApplicationEngine object loads the QML main.qml
file.
- Create a new
qml
directory in thesimplemdviewer
one and add a newmain.qml
file that specifies the UI of the application:
import QtQuick 2.12
import QtQuick.Controls 2.12 as Controls
import org.kde.kirigami 2.12 as Kirigami
import QtQuick.Layouts 1.12
Kirigami.ApplicationWindow {
id: root
title: qsTr("Simple markdown viewer")
minimumWidth: Kirigami.Units.gridUnit * 20
minimumHeight: Kirigami.Units.gridUnit * 20
pageStack.initialPage: initPage
Component {
id: initPage
Kirigami.Page {
title: qsTr("Markdown Viewer")
ColumnLayout {
anchors {
top: parent.top
left: parent.left
right: parent.right
}
Controls.TextArea {
id: sourceArea
placeholderText: qsTr("Write here some markdown code")
wrapMode: Text.WrapAnywhere
Layout.fillWidth: true
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
}
RowLayout {
Layout.fillWidth: true
Controls.Button {
text: qsTr("Format")
onClicked: formattedText.text = sourceArea.text
}
Controls.Button {
text: qsTr("Clear")
onClicked: {
sourceArea.text = ""
formattedText.text = ""
}
}
}
Text {
id: formattedText
textFormat: Text.RichText
wrapMode: Text.WordWrap
text: sourceArea.text
Layout.fillWidth: true
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
}
}
}
}
}
main.qml
We have just created a new QML-Kirigami-Python application.
Cool.
- Run it:
python3 simplemdviewer_app.py
It shows a page split into two sections. On the top one you write some markdown code. Upon clicking on Format, the formated text will be displayed on the bottom of the page.
A simple Python QML application
At the moment we have not used any Python interesting stuff. In reality, the application can also run as a standalone QML one:
qmlscene qml/main.qml
It does not format anything; if we click on Format it just spits the unformatted text into a text element.
Nothing special
OK, let's add some Python logic: a simple markdown converter in a Python, QObject derivative class.
- Create a new
md_converter.py
file in thesimplemdviewer
directory:
from markdown import markdown
from PySide2.QtCore import QObject, Signal, Slot, Property
class MdConverter(QObject):
"""A simple markdown converter"""
def __init__(self):
QObject.__init__(self)
self._source_text = ''
def readSourceText(self):
return self._source_text
def setSourceText(self, val):
self._source_text = val
self.sourceTextChanged.emit()
@Signal
def sourceTextChanged(self):
pass
@Slot(result=str)
def mdFormat(self):
return markdown(self._source_text)
sourceText = Property(str, readSourceText, setSourceText, notify=sourceTextChanged)
md_converter.py
The MdConverter class contains the source_text member variable. The sourceText property exposes source_text to the QML system through the readSouceText getter and the setSourceText setter functions. When setting the sourceText, the sourceTextChanged signal is emitted to let QML know that the property has changed. The mdFormat function returns the markdown formatted text and it has been declared as 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.
- Execute:
pip install markdown
It is time to register the new MdConverter type.
- Update the
simplemdviewer_app.py
file to:
import os
from PySide2.QtGui import QGuiApplication
from PySide2.QtCore import QUrl
from PySide2.QtQml import QQmlApplicationEngine, qmlRegisterType
from md_converter import MdConverter
def main():
"""Initializes and manages the application execution"""
app = QGuiApplication()
engine = QQmlApplicationEngine()
qmlRegisterType(MdConverter, 'org.mydomain.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)
if len(engine.rootObjects()) == 0:
quit()
app.exec_()
if __name__ == "__main__":
main()
simplemdviewer_app.py
The qmlRegisterType function has registered the MdCoverter type in the QML system, in the library org.mydomain.simplemdviewer, version 1.0.
- Change the
qml/main.qml
file to:
import QtQuick 2.12
import QtQuick.Controls 2.12 as Controls
import org.kde.kirigami 2.12 as Kirigami
import QtQuick.Layouts 1.12
import org.mydomain.simplemdviewer 1.0
Kirigami.ApplicationWindow {
id: root
title: qsTr("Simple markdown viewer")
minimumWidth: Kirigami.Units.gridUnit * 20
minimumHeight: Kirigami.Units.gridUnit * 20
pageStack.initialPage: initPage
Component {
id: initPage
Kirigami.Page {
title: qsTr("Markdown Viewer")
MdConverter {
id: mdconverter
sourceText: sourceArea.text
}
ColumnLayout {
anchors {
top: parent.top
left: parent.left
right: parent.right
}
Controls.TextArea {
id: sourceArea
placeholderText: qsTr("Write here some markdown code")
wrapMode: Text.WrapAnywhere
Layout.fillWidth: true
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
}
RowLayout {
Layout.fillWidth: true
Controls.Button {
text: qsTr("Format")
onClicked: formattedText.text = mdconverter.mdFormat()
}
Controls.Button {
text: qsTr("Clear")
onClicked: {
sourceArea.text = ""
formattedText.text = ""
}
}
}
Text {
id: formattedText
textFormat: Text.RichText
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
}
}
}
}
}
main.qml
The updated QML code: 1. imports the org.mydomain.simplemdviewer library 2. creates an MdConverter object 3. updates the onClicked signal handler of the Format button to call the mdFormat function of the converter object
- Change directory to
simplemdviewer
and execute:
python3 simplemdviewer_app.py
- Play with adding some markdown text: Simple Markdown viewer in action
Hooray.
Package the application
To distribute the application to the users we have to package it. We are going to use the setuptools library.
- Create an empty
__init__.py
file in thesimplemdviewer
directory.
This file is required to import a directory as a package. The project structure should look like this:
simple-md-viewer-project/
├── env
└── simplemdviewer
├── __init__.py
├── md_converter.py
├── qml
│ └── main.qml
└── simplemdviewer_app.py
- Create a
pyproject.toml
file in thesimple-md-viewer-project
directory, to tell build tools what is needed to build our project:
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
pyproject.toml
Time to add the configuration file for setuptools.
- Create a
setup.py
into thesimple-md-viewer-project
directory:
from setuptools import setup
setup()
setup.py
- Add a new
setup.cfg
file into thesimple-md-viewer-project
directory:
[metadata]
name = org.mydomain.simplemdviewer
version = 0.1
url = https://mydomain.org/simplemdviewer
author= Example Author
author_email = example@author.org
maintainer = Example Author
maintainer_email = example@author.org
description = A simple markdown viewer
long_description = file: README.md
long_description_content_type = text/markdown
classifiers =
Development Status :: 5 - Production/Stable
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Intended Audience :: End Users/Desktop
Topic :: Utilities
Programming Language :: Python
Operating System :: POSIX :: Linux
keywords= viewer converter markdown
[options]
packages = simplemdviewer
include_package_data = True
install_requires =
requests
importlib; python_version >= "3.8"
markdown
setup.cfg
In the metadata section we have provided information about the application. The options section contains the project dependencies and the import package that our distribution package is going to provide. There is a lot of options and classifiers available. For more details on dependencies management in setuptools, check here.
It is good to have a README.md
file as well.
- Create a
README.md
file in thesimple-md-viewer-project
directory:
# Simple Markdown Viewer
A simple markdown viewer created with Kirigami, QML and Python
README.md
Another important piece is the license of our project.
- Create a
LICENCE.txt
file and add the text of the license of our project.
Apart from the Python stuff, we have to add the QML code to the distribution package as well.
- Create a
MANIFEST.in
file in thesimple-md-viewer-project
directory, and add:
include simplemdviewer/qml/*.qml
MANIFEST.in
Some last pieces and we are ready to build. We are going to add:
- the appstream metadata to be used by the software centers
- a desktop entry file to add the application to the application launcher
-
an application icon
-
Create a new
org.mydomain.simplemdviewer.desktop
file into thesimple-md-viewer-project
directory:
[Desktop Entry]
Version=0.1
Type=Application
Name=Simple Markdown Viewer
Name[x-test]=xxSimple Markdown Viewerxx
GenericName=Markdown Viewer
GenericName[x-test]=xxMarkdown Viewerxx
Comment=A simple markdown viewer application
Comment[x-test]=xxA simple markdown viewer applicationxx
Exec=simplemdviewer
Icon=org.mydomain.simplemdviewer
Terminal=false
Categories=Office;
X-KDE-FormFactor=desktop;tablet;handset;
org.mydomain.simplemdviewer.desktop
- Add a new
org.mydomain.simplemdviewer.metainfo.xml
file into thesimple-md-viewer-project
directory:
<?xml version="1.0" encoding="utf-8"?>
<component type="desktop">
<id>org.mydomain.simplemdviewer</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Simple Markdown Viewer</name>
<summary>A simple markdown viewer application</summary>
<description>
<p>Simple Markdown Viewer is a showcase application for QML with Python development</p>
</description>
<url type="homepage">https://mydomain.org/simplemdviewer</url>
<releases>
<release version="0.1" date="2022-02-25">
<description>
<p>First release</p>
</description>
</release>
</releases>
<provides>
<binary>simplemdviewer</binary>
</provides>
</component>
org.mydomain.simplemdviewer.metainfo.xml
For our tutorial the well known markdown icon is OK.
- Get the markdown icon and save it as
org.mydomain.simplemdviewer.svg
into thesimple-md-viewer-project
directory.
Now we have to let setup.cfg
know about the new files. Let's also provide an easy way to open the application from the console by just typing simplemdviewer
.
- Update the
setup.cfg
to:
[metadata]
name = org.mydomain.simplemdviewer
version = 0.1
url = https://mydomain.org/simplemdviewer
author= Example Author
author_email = example@author.org
maintainer = Example Author
maintainer_email = example@author.org
description = A simple markdown viewer
long_description = file: README.md
long_description_content_type = text/markdown
classifiers =
Development Status :: 5 - Production/Stable
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Intended Audience :: End Users/Desktop
Topic :: Utilities
Programming Language :: Python
Operating System :: POSIX :: Linux
keywords= viewer converter markdown
[options]
packages = simplemdviewer
include_package_data = True
install_requires =
requests
importlib; python_version >= "3.8"
markdown
[options.data_files]
share/applications =
org.mydomain.simplemdviewer.desktop
share/icons/hicolor/scalable/apps =
org.mydomain.simplemdviewer.svg
share/metainfo =
org.mydomain.simplemdviewer.metainfo.xml
[options.entry_points]
console_scripts =
simplemdviewer = simplemdviewer.simplemdviewer_app:main
setup.cfg (updated)
Last step is to tinker with the way we import modules.
- Update the
simplemdviewer_app.py
file to:
import os
from PySide2.QtGui import QGuiApplication
from PySide2.QtCore import QUrl
from PySide2.QtQml import QQmlApplicationEngine, qmlRegisterType
from simplemdviewer.md_converter import MdConverter
def main():
"""Initializes and manages the application execution"""
app = QGuiApplication()
engine = QQmlApplicationEngine()
qmlRegisterType(MdConverter, 'org.mydomain.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)
if len(engine.rootObjects()) == 0:
quit()
app.exec_()
if __name__ == "__main__":
main()
simplemdviewer_app.py (updated)
- Create a
__main__.py
file into thesimplemdviewer
directory:
from . import simplemdviewer_app
simplemdviewer_app.main()
This last step will facilitate the excecution of the package during development.
- Execute from inside the
simple-md-viewer-project
a last check before packaging:
python3 -m simplemdviewer
It's time to generate the distribution package for our import package.
- Make sure that the latest version of
build
is installed:
python3 -m pip install --upgrade build
- Execute from inside the
simple-md-viewer-project
directory:
python3 -m build
As soon as the build completes, two archives will be created in the dist
directory:
- the
org.mydomain.simplemdviewer-0.1.tar.gz
source archive -
the
org.mydomain.simplemdviewer-0.1-py3-none-any.whl
built distribution package -
Install the package into the virtual environment:
python3 -m pip install dist/org.mydomain.simplemdviewer-0.1.tar.gz
- Run:
simplemdviewer
At this point we can tag and release the source code. The Linux distributions will package it and the application will be added to their software repositories.
Well done.
We can also take care of distributing it. Although the PyPI is probably the standard way to distribute a Python package, I believe that Flatpak is more compatible with the Linux applications ecoystem.
- Create an
org.mydomain.simplemdviewer.json
file in thesimple-md-viewer-project
directory:
{
"id": "org.mydomain.simplemdviewer",
"runtime": "org.kde.Platform",
"runtime-version": "5.15",
"sdk": "org.kde.Sdk",
"command": "simplemdviewer",
"finish-args": [
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland",
"--device=dri",
"--socket=pulseaudio"
],
"modules": [
"python3-markdown.json",
{
"name": "PySide2",
"buildsystem": "cmake-ninja",
"builddir": true,
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DBUILD_TESTS=OFF"
],
"sources": [
{
"type": "archive",
"url": "https://download.qt.io/official_releases/QtForPython/pyside2/PySide2-5.15.2-src/pyside-setup-opensource-src-5.15.2.tar.xz",
"sha256": "b306504b0b8037079a8eab772ee774b9e877a2d84bab2dbefbe4fa6f83941418"
},
{
"type": "shell",
"commands": [
"mkdir -p /app/include/qt5tmp && cp -R /usr/include/Qt* /app/include/qt5tmp # https://bugreports.qt.io/broswse/PYSIDE-787",
"sed -i 's|--include-paths=|--include-paths=/app/include/qt5tmp:|' sources/pyside2/cmake/Macros/PySideModules.cmake"
]
}
]
},
{
"name": "simplemdviewer",
"buildsystem" : "simple",
"build-commands" : [
"python3 setup.py build",
"python3 setup.py install --prefix=/app --root=/"
],
"sources": [
{
"type": "archive",
"path": "dist/org.mydomain.simplemdviewer-0.1.tar.gz"
}
]
}
]
}
org.mydomain.simplemdviewer.json
This file reads that we use the markdown module and the build info is provided by the python3-markdown.json
manifest file. We are going to create this manifest using flatpak-pip-generator
.
- Get the flatpak-pip-generator, save it into the
env/bin/
directory, and run:
python3 env/bin/flatpak-pip-generator markdown
- Install org.kde.Sdk and org.kde.Platform, version 5.15, from flathub:
flatpak install org.kde.Platform/x86_64/5.15 org.kde.Sdk/x86_64/5.15
- Install the flatpak-builder package using the package manager of your distribution and run:
flatpak-builder --verbose --force-clean flatpak-build-dir org.mydomain.simplemdviewer.json
- Test the flatpak build:
flatpak-builder --env=QT_QPA_PLATFORM=wayland --run flatpak-build-dir org.mydomain.simplemdviewer.json simplemdviewer
- Build the flatpak bundle:
flatpak-builder --install-deps-from=flathub flatpak-build-dir --repo=simplemdviewer-master --force-clean --ccache org.mydomain.simplemdviewer.json
flatpak build-bundle simplemdviewer-master simplemdviewer.flatpak org.mydomain.simplemdviewer
Now we can either distribute simplemdviewer.flatpak
directly to the users, or submit the application to a flatpak repository, e.g., on flathub.
Great.
I suggest that you also consider: - Signing the source archive with a detached signature - Providing copyright and licensing information for each file with reuse
The code of the article can be found here.
If you have found this article interesting, share it with your friends on your favorite social platform. I hope this platform is Mastodon.
Happy hacking.