Introduction
Qt is a robust cross-platform framework that works on Windows, Linux, Mac, Android, and more. It allows you to create GUI applications as well as provides libraries for networking,
In this tutorial, we will focus on some of the very core aspects of using PyQt5 and how to package it for distribution.
Here is an example project I created using Pyqt5: https://github.com/DevDungeon/PyQt5-Bitcoin-Price-Checker and the live coding of that project https://www.devdungeon.com/content/live-coding-pyqt5-guis-w-python.
Install pyqt5
You need the pyqt5
package for Python and Qt Designer. You can get
both of these by running:
# The primary package with all dependencies
pip install pyqt5
# Optional; In Windows, you can get the designer tools from this package
pip install pyqt5-tools
You will find the designer in the site-packages/pyqt5_tools
directory of
your Python environment and should be available in your PATH
as designer
or pyqt5designer
.
On Mac, I found it can be run from your venv with venv/bin/qt5-tools designer
.
These packages will also provide pyuic5
for converting Designer files to Python
files. It also includes many other utilities like qdbus
, qdoc
, qmlscene
, linguist
, and more.
If you are using Linux or a system that doesn't have the packages
for pyqt5-tools
you can usually find the Designer and other apps
in your system's package repositories. For example, in Fedora
there is a package named qt5-designer
.
You can test your installation by attempting to run the Hello World application in the next section.
Documentation
You can get the local documentation on your computer by running the pydoc server and visiting it in your browser.
python -m pydoc -p 8888
Alternatively, you can read the online documentation at https://www.riverbankcomputing.com/static/Docs/PyQt5.
Hello World
Here is a simple example that demonstrates how to initialize a PyQt5 application. It also includes a simple window with nothing in it. A window is not technically required for a PyQt5 application, but generally the primary reason Qt is used is to create GUI windows.
# hello.py
import sys
from PyQt5.QtWidgets import QApplication, QWidget
# Create the main Qt app, passing command line arguments
app = QApplication(sys.argv)
win = QWidget()
win.setWindowTitle('Hello')
win.resize(250, 250)
win.show()
# Run the app, passing it's exit code back through `sys.exit()`
# The app will exit when the close button is pressed on the main window.
sys.exit(app.exec_())
Run the application like normal using Python:
python hello.py
In Windows, you can use the .pyw
extension or use pythonw
to run the application without the command prompt showing.
Using Qt Designer and .ui files
Using the Designer is optional, but it can be a very helpful way to layout interfaces. I highly recommend using the Designer.
Once you have created a .ui
file in the Designer, you can either
convert it to a Python file which will create the interface programmatically,
or you can load the .ui
file directly. The next sections will cover both options.
Convert UI file to Python code
From the terminal, you can convert a QtDesigner .ui file to a Python file. This method works fine, however it becomes difficult to iterate rapidly on changes with this extra step. You also cannot modify the .py file if you ever want to regenerate it. I recommend using the method in the next section, where you load the .ui file directly.
pyuic5 my_design.ui -o my_window.py
python my_window.py
The Python file output will have all the code needed to recreate the interface created in Designer.
Load the UI file in Python
My preferred option for loading interfaces, is to the load the .ui
file
directly. This means you will need to include the .ui
file with your
distribution, but it means you can easily make changes in the Designer
and quickly test them in Python w/o the extra step of converting the file.
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
# If you saved the template in `templates/main_window.ui`
ui = uic.loadUi("templates/main_window.ui")
ui.show()
# Then you can access the objects from the UI
# For example, if you had a label named label1
ui.label1.setText('new text')
sys.exit(app.exec_())
Signals
Signals are a critical concept to understand when using the Qt framework. Signals and events are similar to channels in Go. Every signal you create must have a data type associated with it. The signal can then emit events that contain data of that type.
For example, if you create a string signal, you can emit strings to anyone listening for the events. You can create an integer signal that spits out integers to listeners. Signals are thread safe.
Alternatively, you can use a decorator @pyqtSlot()
. You can learn more
about that method at
https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html.
Some widgets come with signals already.
For example, a button comes with a clicked
signal that can be connected.
The next section on threading will provide a working example of how to create a custom signal, connect it to a callback function, and emit events.
Threading
To use QThreads, you can create a subclass of QThread
.
Be sure to call the parent class constructor, and create
any signals that will be used to pass data.
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import pyqtSignal, QThread
import sys
class MyTask(QThread):
done_signal = pyqtSignal(str)
def __init__(self):
QThread.__init__(self)
def run(self):
# Do some work here
self.done_signal.emit('some string')
def process_done_signal(result):
print(result)
sys.exit()
if __name__ == '__main__':
app = QApplication(sys.argv)
task = MyTask()
task.done_signal.connect(process_done_signal)
task.start()
# This will continue to run forever, except we are killing the app
# in the process_done_signal() function.
sys.exit(app.exec_())
Button clicks
Buttons provide some signals out of the box.
For example, the clicked
event is a signal that can be connected
to a callback. For example:
# Assuming you have loaded a `.ui` and stored it in an object named `ui`
# and there is a button named `button1`
ui.button1.clicked.connect(some_function)
One important thing to keep in mind is that the callback function will block your main application thread, unless it runs the operations in its own thread. See the section above about how to create QThreads. In this example, some_function should kick off a thread to perform operations.
System Tray and Notifications
Creating a system tray icon for your application can be very useful. A common task is to keep an application running even when the main window is closed while leaving an icon in the system tray.
This example will demonstrate how to:
- Create a system tray icon
- Add a right-click context menu
- Add custom action handlers to the context menu items
- Capture window close event, ignore, and hide window (exit to system tray)
- Add an Exit option to the context menu of the system tray
- Generate a desktop notification
# Example modified from https://evileg.com/en/post/68/
# https://www.youtube.com/watch?v=1_4jfqYOi6w
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QSystemTrayIcon, QAction, QMenu, QStyle, qApp
from PyQt5 import QtGui
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.tray_icon = QSystemTrayIcon(self)
# Set icon to a standard or custom icon
self.tray_icon.setIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
# self.tray_icon.setIcon(QtGui.QIcon("icons/devdungeon32x32.png"))
exit_action = QAction("Exit", self)
exit_action.triggered.connect(self.exit_app)
tray_menu = QMenu()
tray_menu.addAction(exit_action)
self.tray_icon.setContextMenu(tray_menu) # Set right-click menu
self.tray_icon.show()
def notify(self, message):
"""Generate a desktop notification"""
self.tray_icon.showMessage("Pssst!",
message,
QSystemTrayIcon.Information,
3000)
def exit_app(self):
self.tray_icon.hide() # Do this or icon will linger until you hover after exit
qApp.quit()
def closeEvent(self, event):
"""
By overriding closeEvent, we can ignore the event and instead
hide the window, effectively performing a "close-to-system-tray"
action. To exit, the right-click->Exit option from the system
tray must be used.
"""
event.ignore()
self.hide()
self.notify("App minimized to system tray.")
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec())
You can also create application that doesn't have a main window
at all and lives entirely in the sytem tray. In that case,
don't make your main widget a QMainWindow
or a QWidget
.
Instead, make it a QSystemTray
# Example modified from https://evileg.com/en/post/68/
# https://www.youtube.com/watch?v=1_4jfqYOi6w
import sys
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QAction, QMenu, QStyle, qApp
from PyQt5 import QtGui
class TrayOnlyApp(QSystemTrayIcon):
def __init__(self):
QSystemTrayIcon.__init__(self)
self.setIcon(QtGui.QIcon("icons/devdungeon32x32.png"))
exit_action = QAction("Exit", self)
exit_action.triggered.connect(self.exit_app)
tray_menu = QMenu()
tray_menu.addAction(exit_action)
self.setContextMenu(tray_menu) # Set right-click menu
self.show()
self.notify('Now running...')
def notify(self, message):
"""Generate a desktop notification"""
self.showMessage("Pssst!",
message,
QSystemTrayIcon.Information,
3000)
def exit_app(self):
self.tray_icon.hide() # Do this or icon will linger until you hover after exit
qApp.quit()
if __name__ == "__main__":
app = QApplication(sys.argv)
tray_app = TrayOnlyApp()
sys.exit(app.exec())
Packaging
You can package your PyQt5 app in a number of ways including:
- Python setuputils packaging (for
pip
and pypi.org). - PyInstaller to create an
.exe
,.app
, or Linux executable
We will only look at using PyInstaller here since regular Python packaging is already well documented. You can also check out my PyInstaller Tutorial.
PyInstaller can be used to create .exe files for Windows, .app files for Mac, and distributable packages for Linux. Optionally, it can create a single file which is more convenient for distributing, but takes slightly longer to start because it unzip itself.
Use pyi-makespec
to generate a .spec
file based on your .py
application.
You can find more details about spec files at https://pythonhosted.org/PyInstaller/spec-files.html.
# Create a spec file that specifies no console and custom icon
# The icon is optional
pyi-makespec myapp.py --noconsole --icon=path/to/icon.ico
# Or, if you want to create a single file use:
pyi-makespec myapp.py --onefile --noconsole --icon=path/to/icon.ico
After generating the spec file, you can customize it to suit your needs.
For example, to add template files, modify the datas
variable.
# Modify the line in the .spec file that has datas=[]
# and include the files to copy over. Put each pair in a tuple.
datas=[('templates/*.ui', 'templates')],
Note that starting in PqyQt 5.13.0 there is an additional build step
for the .dll
files to be found. When packaging, it expects
the libraries to be in PyQt5\Qt\bin\
but they get put in the root
directory. You can manually move the files in to that directory after
build or you can set it in the --add-data
flag or datas
object.
If you add it in the datas
object, it will put the .dll
files
in the right spot, but it still ALSO leave the same files in the root
of the output directory. You will need to clean up the duplicates.
For example:
datas=[
('templates', 'templates'),
('C:\\opt\\python37\\Lib\\site-packages\\PyQt5\\Qt\\bin', 'PyQt5/Qt/bin')
],
Then, after the spec file is complete, you can build it using PyInstaller.
pyinstaller myapp.spec
Alternatively, instead of using a .spec
file, you can specify
all the options at the command line. For example:
pyinstaller myapp.py --windowed --add-data "templates;templates" --add-data "C:\\opt\\python37\\Lib\\site-packages\\PyQt5\\Qt\\bin;PyQt5/Qt/bin"
When a Hello World type application is packaged with PyInstaller, it comes out to about 100MB on disk, and uses about 13MB or RAM when running.
More examples
You can find many examples in this GitHub repository: https://github.com/baoboa/pyqt5/tree/master/examples/ and a few examples in the DevDungeon Cookbook.
Conclusion
After working through this tutorial you should have an understanding of how to make basic PyQt5 applications and package them for distribution. PyQt5 is capable of a whole lot more, like system tray icons, dialogs, taking screenshots, drag-and-drop and much more. This tutorial should be enough to just get you started and able to explore more of the available widgets and tools.
References
- Official Qt C++ Documentation
- PyQt5 Reference Guide
- PtQt5 Class Documentation
- https://www.qt.io/
- https://github.com/DevDungeon/PyQt5-Bitcoin-Price-Checker
- https://www.devdungeon.com/content/live-coding-pyqt5-guis-w-python
- https://www.qt.io/download
- https://github.com/baoboa/pyqt5/tree/master/examples
- https://www.riverbankcomputing.com/static/Docs/PyQt5
- https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html
- https://github.com/baoboa/pyqt5/tree/master/examples/
- https://github.com/DevDungeon/Cookbook/tree/master/python/pyqt5_examples
- https://setuptools.readthedocs.io/en/latest/
- https://www.pyinstaller.org/