Python3 Qt5 (PyQt5) Tutorial

Advertisement

Advertisement

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:

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

Advertisement

Advertisement