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.
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
These packages will also provide
pyuic5 for converting Designer files to Python
files. It also includes many other utilities like
linguist, and more.
If you are using Linux or a system that doesn't have the packages
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
You can test your installation by attempting to run the Hello World application in the next section.
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.
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:
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
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 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
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.
To use QThreads, you can create a subclass of
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_())
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 Q
MainWindow or a
Instead, make it a
# 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())
You can package your PyQt5 app in a number of ways including:
- Python setuputils packaging (for
- PyInstaller to create an
.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.
pyi-makespec to generate a
.spec file based on your
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
# 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
.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
If you add it in the
datas object, it will put the
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.
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.
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.
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.
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.
- Official Qt C++ Documentation
- PyQt5 Reference Guide
- PtQt5 Class Documentation