How to build your first Desktop Application in Python | by Ampofo Amoh - Gyebi | Analytics Vidhya | Medium
How to build your first Desktop Application in Python
Follow
Dec 12, 2020 · 12 min read
Search the entire internet for uses of the Python programming language and they list them with Desktop Applications marked as not very suited with python. But years ago in 2016 when I was hoping to move on from web development to software development, Google.com told me I should choose python because it is used to build some modern and advanced scientific applications, then they mentioned blender3d. I knew blender3d, it is an awesome 3d creation software. love
A screenshot of blender3d (source: blender.org)
But its not their fault, the ugly things people are using as showcases for python guis are detestable, too old, and too expired looking windows, no young person would like that. I hope to change that notion with this simple desktop app tutorial. Lets get going.
We will be using PyQt (more on that soon) instead of Tkinter which was almost removed from the list of python standard libraries for being outdated.
What is PyQt (pronounced: pie-cute). It is a port of a framework, Qt (pronounced: cute) from C++, . That framework is known as the necessary framework for C++ developers. It is the framework behind blender3d, Tableau, Telegram, Anaconda Navigator, Ipython, Jupyter Notebook, VirtualBox, VLC etc. We will use it instead of the embarrassing Tkinter.
Prerequisites
- You should already know some python basics
- You should know how to install packages or libraries with pip
- You should already have python installed, of course.
Installation
The only thing we will need to install is PyQt. So open up your terminal, on windows it will be the Command Prompt or Powershell.
Type the following command in your terminal
>>> pip install PyQt5
PyQt5 because we are downloading the version 5 of PyQt, version 5.15 to be specific. Wait for the installation to complete it will only take like a minute or two.
Project Files and Folders
Now that we are done with the installation. We should start with our project. Create a project folder for the app, we going to call it: helloApp. Create it anywhere you want on your computer, but its good to be organised.
Lets do a “Hello World”
Open up the main.py, preferably in vscode and enter the following code
main.py
import sysfrom PyQt5.QtGui import QGuiApplicationfrom PyQt5.QtQml import QQmlApplicationEngineapp = QGuiApplication(sys.argv)engine = QQmlApplicationEngine()engine.quit.connect(app.quit)engine.load('./UI/main.qml')sys.exit(app.exec())
The above code calls QGuiApplication and QQmlApplicationEngine Which will use Qml instead of QtWidgets as the UI layer for the Qt Application. It then connects the UI layers quit function with the app’s main quit function. So both can close when the UI has been closed by the user. Next it loads the qml file as the qml code for the Qml UI. The app.exec() is what runs the Application, it is inside the sys.exit because it returns the exit code of the Application which gets passed into the sys.exit which exits the python sytem.
Add this code to the main.qml
main.qml
import QtQuick 2.15import QtQuick.Controls 2.15ApplicationWindow {visible: truewidth: 600height: 500title: "HelloApp" Text {anchors.centerIn: parenttext: "Hello World"font.pixelSize: 24}}
The above code creates a Window, the visible code is very important, without that the UI is going to run but will be invisible,with width and height as specified, with a title of “HelloApp”. And a Text that is centered in the parent(which happens to be the window), the text displayed is “Hello World”, a pixel size of 24px.
If you have the above, you can run it and see your result.
Navigate into your helloApp folder
>>> cd helloApp
Now run it by doing:
>>> python main.py
If your code runs, you should see:
`An app window showing the text “Hello World”
Update UI
Now lets update a UI a little bit, lets add an image as a background and a text that will have a time
import QtQuick 2.15import QtQuick.Controls 2.15ApplicationWindow {visible: truewidth: 400height: 600title: "HelloApp" Rectangle {anchors.fill: parent Image {sourceSize.width: parent.widthsourceSize.height: parent.heightsource: "./images/playas.jpg"fillMode: Image.PreserveAspectCrop } Rectangle {anchors.fill: parentcolor: "transparent" Text {text: "16:38:33"font.pixelSize: 24color: "white"} } }}
The above has an ApplicationWindow type, a Rectangle type inside it, that is actually filling up all the space of the window. There is an Image inside of it, and a another Rectangle that looks like its beside it, but because of the non-existence of a Layout type, its actually on top of the Image type. The Rectangle has a color of transparent since by default Rectangles are white, there is a Text inside of it that reads 16:38:33, to mock up time.
If you run the app the text will appear at the top-left corner of the Window. We do not like that, and so we are going to make it appear at the bottom-left corner with some margins instead.
In your qml code, update the Text type to include anchors as shown below:
... Text {anchors {bottom: parent.bottombottomMargin: 12left: parent.leftleftMargin: 12 }text: "16:38:33"font.pixelSize: 24...} ...
Now run it by doing
>>> python main.py
You should see something similar to this.
Now I would like the time to update
Use real time
Lets use a real time. Python provides us with native functions that give us all kinds of time and date related functions. We want a string with the current time. gmtime provides you with global time struct with all kinds of information and strftime constructs certains portions of the time as a string using the gmtime function
import the strftime, and gmtime functions
main.py
import sysfrom time import strftime, gmtime...
Then construct your time string anywhere in the file
main.py
curr_time = strftime("%H:%M:%S", gmtime())
The %H, %M, %S, tells strftime, that we want to see Hours(24-hour type), minutes, and seconds. (Read more about format codes for strftime here). This variable will be passed on to the qml layer.
Lets create a property in qml that we can use to receive time string. This variable makes it easier to change the time. Lets call this property currTime
main.qml
...ApplicationWindow {...title: "HelloApp" property string currTime: "00:00:00"...
Use this property in the qml, so when this value changes all the other places where it has been used also will change.
main.qml
...Text {...text: currTime // used to be; text: "16:38:33"font.pixelSize: 48color: "white"}...
Now send our curr_time variable we created in python to qml by setting it to the currTime qml property.
main.py
...engine.load('./UI/main.qml')engine.rootObjects()[0].setProperty('currTime', curr_time)...
The above code will set the qml property currTime to the value of the curr_time python property. This is one way we pass information from python to the UI layer.
Run the app and you should see no errors and will also have the current time. Hooray!!! Onward!!!
Update the time
To keep our time updated. We will need to use threads. Threading in python is easy and straightforward, we will use that instead of Qt’s threading. Thread uses functions or thread calls a function. I prefer we use a technique in Qt known as signals, this is a professional method, and studying it know will make your like better and easier. Lets put our current time code into a function, use underscore(_) for the file name. I will explain why later. It is not a requirement or anything, it is just good practice
To use signals we would have to subclass QObject, straightforward.
Create a subclass of QObject, call it whatever you like. I will call it Backend.
main.py
...from from PyQt5.QtCore import QObject, pyqtSignalclass Backend(QObject): def __init__(self):QObject.__init__(self)...
The above code imports QObject and pyqtSignal, in pyside this is called Signal. It is one of the few differences between pyqt and pyside.
Formally, we had a property string that received our curr_time string from python, now we create a property QtObject to receive the Backend object from python. There are not that many types. Qml converts python base types into bool, int, double, string, list, QtObject and var. var can handle every python type, but its the least loved.
main.qml
...property string currTime: "00:00:00"property QtObject backend...
The above code creates a QtObject backend to hold our python object back_end. The names used are mine, feel free to change them to whatever you like
In the python pass it on
main.py
...engine.load('./UI/main.qml')back_end = Backend()engine.rootObjects()[0].setProperty('backend', back_end)...
In the above code an object back_end was created from the class Backend. We then set it to the qml property named backend
In Qml, one QtObject can receive numerous functions (called signals) from python that does numerous things, but they would have to be organised under that QtObject.
Create Connections type and target it to backend. Now inside the Connections type can be functions as numerous as we want to receive for the backend.
main.qml
...Rectangle {anchors.fill: parent Image {...}...}Connections {target: backend}...
Now thats’ how we connect with the python signals.
If we do not use threading our UI will freeze. Its quite emphatic to state that what we need here is threading and not multiprocessing.
Create two functions, one for the threading one for the actually function. Here is where the underscore comes in handy.
main.py
...import threadingfrom 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 = Truet_thread.start() def _bootUp(self):while True:curr_time = strftime("%H:%M:%S", gmtime())print(curr_time)sleep(1)...
The above code has an underscore function that does the work creating an updated time.
Create a pyqtsignal called updated and call it from a function called updater
main.py
...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 the above code the pyqtSignal, updated, has as it arguments parameter the list containing the name of the function ‘updater’. From this updater function, qml shall receive data. In the updater function we call(emit) the signal updated and pass data (curr_time) to it
Update the qml, receive the signal by creating a signal handler, a signal handlers name is the capitalised form of the signal name preceded by ‘on’. So, ‘mySignal’ becomes ‘onMySignal’ and ‘mysignal’ becomes ‘onMysignal’.
main.qml
...target: backend function onUpdated(msg) {currTime = msg;}...
In the above code you can see the signal handler for updated signal is called onUpdated. It is also has the curr_time passed to it as msg.
All is well but we are yet to call the updater function. Having a seperate function to call the signal is not necessary for a small application as this. But in a big application, it is the recommended way to do it. Change the delay seconds to 1/10 of a second. I have found this figure to the best to update time.
main.py
...curr_time = strftime("%H:%M:%S", gmtime())self.updater(curr_time)sleep(0.1)...
The bootUp function should be called immediately after the UI has loaded.
...engine.rootObjects()[0].setProperty('backend', back_end)back_end.bootUp()sys.exit(app.exec())
All is done!!!
Run the code:
>>> python main.py
Your seconds should be updating now
Bonus:
Make the Window Frameless
You can make the window frameless and stick it to the bottom right of the Screen.
main.qml
...height: 600x: screen.desktopAvailableWidth - width - 12y: screen.desktopAvailableHeight - height - 48title: "HelloApp"flags: Qt.FramelessWindowHint | Qt.Window...
The above code sets x, y for the window and add flags, to make the window frameless. The Qt.Window flag ensures that even though the window is frameless, we still get a Taskbutton
Run it, and you should be glad with what you see.
>>> python main.py
At long last, the coding has ended and here are the final codes.
main.py
import sysfrom time import strftime, gmtimeimport threadingfrom time import sleepfrom PyQt5.QtGui import QGuiApplicationfrom PyQt5.QtQml import QQmlApplicationEnginefrom PyQt5.QtCore import QObject, pyqtSignalclass 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 = Truet_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())
main.qml
import QtQuick 2.15import QtQuick.Controls 2.15ApplicationWindow {visible: truewidth: 360height: 600x: screen.desktopAvailableWidth - width - 12y: screen.desktopAvailableHeight - height - 48title: "HelloApp" flags: Qt.FramelessWindowHint | Qt.Window property string currTime: "00:00:00"property QtObject backend Rectangle {anchors.fill: parent Image {sourceSize.width: parent.widthsourceSize.height: parent.heightsource: "./images/playas.jpg"fillMode: Image.PreserveAspectFit} Text {anchors {bottom: parent.bottombottomMargin: 12left: parent.leftleftMargin: 12}text: currTimefont.pixelSize: 48color: "white"} }Connections {target: backend function onUpdated(msg) {currTime = msg;}}}
Apart from the names that you may have changed, everything should be similar.
Build and next steps
Building a pyqt application could be the easiest, since it is widely known.
To build, install pyinstaller, since building is a part of the bonus section, we didn’t install it before.
>>> pip install pyinstaller
We could have easily done run the following code in the applications folder (helloApp) but, we have to take care of the resources that we used.
>>> pyinstaller main.py
Instead, first do:
>>> pyi-makespec main.py
It generates a spec file for you to update first, then you can run pyinstaller again
The datas parameter can be used to include data files in your App or App’s folder. Its a list of tuples, and the tuple always has two items, the target path, we will be including, and the destination path, where it should be stored in the Application’s folder. The destination path must be relative. If you want it placed right there with the app’s executables, you make it an empty string (‘’), if you want it to be in a nested folder within the application’s folder, you specify the nested folder (‘nest/nested/really_nested’)
Update the datas parameter like you see below to match the path to your helloApp’s UI folder on your computer.
Set the console parameter to False, since this is a Gui and we are not testing it.
main.spec
...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')
The name parameter in the EXE call is the name of the executable itself. eg. main.exe, or main.dmg but the name parameter in the COLLECT call is for the folder name in which the executable and all its accompanying files will be stored, both can be changed. But the names were based on the file we used to generate the spec, remember: ‘main.py’
Finally, build your application using
>>> pyinstaller main.spec
Now you should see a folder named ‘dist’ with another folder within it named ‘main’ with the application files. Search for the main.exe, or main executable and run it. TADAAA! And all is well.
Next Steps
Apart from the way that the UI folder was included and used in the application, all of the things we’ve talked about are used in production. Resources are bundle before being deployed in production.
But the Signals, how the background image was used, to the frameless window are all techniques used in production and so to speak, in real-world. Its just that there is more to it. Yes there is more to frameless Windows, you have to handle the titlebar, the resizing and dragging of the window among other things if you are not going to use it as a splash screen, not that complex, but it goes beyond the scope of this tutorial.
Qml is a lot more than Images, Rectangles and Text, and the Layout system are four types. They are easy to study, but practical approach is the best, so I have not bothered to explain them.
Continue with PyQt and Qml, it will lead into a career in software development, embedded systems, and in the future Data visualization. Prefer it over TKinter, its popularity keeps increasing by the day.
More resources can be found on:
- PyQt/PySide — PythonGui-Books, YouTube, Udemy, Qt official docs, Riverbank Computing
- QML — evileg.com, Qt Official docs