Now let’s run the application:
>>> python main.py
You will see a window like this:

UI update
Let’s update the UI a bit, add a background image and time:
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
visible: true
width: 400
height: 600
title: "HelloApp"
Rectangle {
anchors.fill: parent
Image {
sourceSize.width: parent.width
sourceSize.height: parent.height
source: "./images/playas.jpg"
fillMode: Image.PreserveAspectCrop
}
Rectangle {
anchors.fill: parent
color: "transparent"
Text {
text: "16:38:33"
font.pixelSize: 24
color: "white"
}
}
}
}
Inside the ApplicationWindow type is the content of the window, the Rectangle type fills the window space. Inside it is the Image type and another transparent Rectangle that will be displayed over the image.
If you start the application now, the text will appear in the upper left corner. But we want the bottom left corner, so we use padding:
Text {
anchors {
bottom: parent.bottom
bottomMargin: 12
left: parent.left
leftMargin: 12
}
text: "16:38:33"
font.pixelSize: 24
...
}
Once launched, you will see the following:

Show the current time
The gmtime module allows you to use a structure over time, and strftime allows you to convert it to a string. We import them:
import sys
from time import strftime, gmtime
Now we can get a string with the current time:
curr_time = strftime("%H:%M:%S", gmtime())
The string "%H:%M:%S"
means that we will get the time in 24 hour format, with hours, minutes and seconds ( more about strtime ).
Let’s create a property in a qml file to store the time. We’ll call it currTime.
property string currTime: "00:00:00"
Now let’s replace the text with our variable:
Text {
...
text: currTime
font.pixelSize: 48
color: "white"
}
Now, let’s pass the curr_time variable from pyhton to qml:
engine.load('./UI/main.qml')
engine.rootObjects()[0].setProperty('currTime', curr_time)
This is one of the ways to transfer information from Python to UI.
Run the application and you will see the current time.
Time update
In order to update the time, we need to use streams. For this, I suggest using signals.
To use signals we need a QObject subclass. Let’s call it Backend.
...
from PyQt5.QtCore import QObject, pyqtSignal
class Backend(QObject):
def __init__(self):
QObject.__init__(self)
...
We already have properties for the string with curr_time, now let’s create a backend property of type QtObject in the main.qml file.
property string currTime: "00:00:00"
property QtObject backend
Let’s transfer data from Python to qml:
engine.load('./UI/main.qml')
back_end = Backend()
engine.rootObjects()[0].setProperty('backend', back_end)
In a qml file, a single QtObject can receive multiple functions (called signals) from Python.
Let’s create a Connections type and specify the backend in its target. Now inside this type there can be as many functions as we need to get in the backend.
...
Rectangle {
anchors.fill: parent
Image {
...
}
...
}
Connections {
target: backend
}
...
This will link qml and signals from Python.
We use streams to ensure that the UI is up to date. Let’s create two functions, one for managing flows and one for performing actions. It is good practice to use _ in the name of one of the functions.
...
import threading
from time import sleep
...
class Backend(QObject):
def __init__(self):
QObject.__init__(self)
def bootUp(self):
t_thread = threading.Thread(target=self._bootUp)
t_thread.daemon = True
t_thread.start()
def _bootUp(self):
while True:
curr_time = strftime("%H:%M:%S", gmtime())
print(curr_time)
sleep(1)
...
Create pyqtsignal and name it updated, then call it from the updater function.
...
from PyQt5.QtCore import QObject, pyqtSignal
...
def __init__(self):
QObject.__init__(self)
updated = pyqtSignal(str, arguments=['updater'])
def updater(self, curr_time):
self.updated.emit(curr_time)
...
In this code, updated has an arguments parameter, which is a list containing the function name “updater”. Qml will receive data from this function. In the updater function, we call the emit method and pass the time data to it.
Let’s update qml, having received a signal, using a handler, the name of which consists of “on” and the name of the signal:
target: backend
function onUpdated(msg) {
currTime = msg;
}
Now we just need to call the updater function. In our small application, it is not necessary to use a separate function to trigger the signal. But it is recommended to do this in large programs. Change the delay to one tenth of a second.
curr_time = strftime("%H:%M:%S", gmtime())
self.updater(curr_time)
sleep(0.1)
The bootUp function should be called immediately after loading the UI:
engine.rootObjects()[0].setProperty('backend', back_end)
back_end.bootUp()
sys.exit(app.exec())
All is ready
Now you can start the program. The time will update correctly. To remove the border, you can add the following line to the qml file:
flags: Qt.FramelessWindowHint | Qt.Window
This is how the main.py file should look like:
import sys
from time import strftime, gmtime
import threading
from time import sleep
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine
from PyQt5.QtCore import QObject, pyqtSignal
class Backend(QObject):
def __init__(self):
QObject.__init__(self)
updated = pyqtSignal(str, arguments=['updater'])
def updater(self, curr_time):
self.updated.emit(curr_time)
def bootUp(self):
t_thread = threading.Thread(target=self._bootUp)
t_thread.daemon = True
t_thread.start()
def _bootUp(self):
while True:
curr_time = strftime("%H:%M:%S", gmtime())
self.updater(curr_time)
sleep(0.1)
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load('./UI/main.qml')
back_end = Backend()
engine.rootObjects()[0].setProperty('backend', back_end)
back_end.bootUp()
sys.exit(app.exec())
Here is the content of the main.qml file:
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
visible: true
width: 360
height: 600
x: screen.desktopAvailableWidth - width - 12
y: screen.desktopAvailableHeight - height - 48
title: "HelloApp"
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/playas.jpg"
fillMode: Image.PreserveAspectFit
}
Text {
anchors {
bottom: parent.bottom
bottomMargin: 12
left: parent.left
leftMargin: 12
}
text: currTime
font.pixelSize: 48
color: "white"
}
}
Connections {
target: backend
function onUpdated(msg) {
currTime = msg;
}
}
}
Build the application
To build a Python desktop application, we need pyinstaller.
>>> pip install pyinstaller
To add all the necessary resources to the assembly, create a spec file:
>>> pyi-makespec main.py
Spec file settings
The datas parameter can be used to include the file in the application. This is a list of tuples, each of which must have a target path (where to get the files from) and a destination path (where the application will be located). destination path must be relative. Use an empty string to place all resources in the same folder with exe-files.
Change the datas parameter to the path to your UI folder:
a = Analysis(['main.py'],
...
datas=[('I:/path/to/helloApp/UI', 'UI')],
hiddenimports=[],
...
exe = EXE(pyz,
a.scripts,
[],
...
name='main',
debug=False,
...
console=False )
coll = COLLECT(exe,
...
upx_exclude=[],
name='main')
Let’s set the console parameter to false, because we do not have a console application.
The name parameter inside the call to Exe is the name of the executable file. name inside the Collect call, this is the name of the folder in which the finished application will appear. The names are created based on the file for which we created the spec – main.py.
Now you can start the build:
>>> pyinstaller main.spec
The main folder appears in the dist folder. To start the program, just run the main.exe file.
This is how the contents of the folder with the desktop application in Python will look:
