RkBlog

Quick start into GUI applications with PyQt5 and PySide2

2020-05-03

Qt is a powerful framework for developing cross platform GUI applications. KDE Linux desktop environment uses it as well as many Open Source and commercial applications.

In this article I'll showcase how to use Qt in Python to make GUI applications (the very basics of it).

PyQt and PySide

For Python developers since long time PyQt binding were available. As of now there is also Qt for Python project that provides PySide2 bindings.

There is a very good comparison on machinekoder.com. In short PyQt bindings existed for much longer and are distributed on GPL or Commercial license. PySide2 is distributed on LGPL or Qt commercial license - which may make a difference in some legal cases. In terms of making simple apps there shouldn't be much differences aside of imports. For more complex ones there may be some.

You may find a lot of tutorials for PyQt due to it age. PyQt4 is the former main version while now we use PyQt5. Some code did change (like signals and slots syntax) but overall the workflow should be the same. If you pick up some older PyQt4 books keep that in mind (and the book may use Python 2 instead of 3).

There are also books and resources for PyQt5, like Create Simple GUI Applications with Python and PyQt5 while also having some online resources on learnpyqt.com.

PyQt and PySide can be installed as any other Python packages via PyPi. You should also install Qt development tools. Qt Designer is the desired application for now. It allows creating user interface of the app in a visual manner. On Linux it can be in a separate package while for macOS or Windows it will likely be with the official Qt development package (macOS package managers like homebrew may also have split packages).

A note on desktop applications

Compared to web development, common in the Python community, the GUI application development is less popular and it can get quite complex much quicker. Before developing an app check if a web variant isn't a better solution (no need to build, distribute, install etc.).

When creating an app with a GUI be sure to make the GUI follow look and feel of other similar apps - a.k.a. you should try not to reinvent styles or some widget placements unless you are making an app with non-standard UI. For each operating system or desktop platform there are Human interface guidelines that describe how applications should look and behave to meet user experience standards.

Qt does follow those guidelines and also tries to make widgets follow current style of the system - if you run your app on Windows or on macOS it will look like other apps of that particular OS as well as some widgets may look different or be placed in different spots of the window.

Kivy uses it own style and will not follow OS style and usability guidelines – everything is in your hands. Tkinter will follow the style to some extent although the widgets won’t look as good as in native apps.

Python standard library versus Qt

In Qt class reference you will find functionalities that are also covered by Python standard library or popular third party packages. For example you can find QFile, QDir classes that represent files and directories and with other related classes implement a lot of filesystem operations. You can even run a SQL query via QSqlQuery and more.

With such overlap one can wonder which solution should be used - Python or PyQt/PySide bindings to C++ classes of Qt? Overall I would advise using Qt path as much as possible. A lot of classes and widgets is inter-compatible - meaning one class can accept an instance of another, like accepting a QFile object or use QSqlQuery to populate a grid and more.

If the Qt usage seems overly complicated and you know you aren't loosing any of that glue then sure, you can also use a Python way of doing things.

Also note that a lot of Qt classes takes operating system differences into account which may not be handled as well by non-Qt Python package. Like if you want to print a file Qt has you covered while when trying pure Python solutions you would likely need more time and testing to see if it's cross-platform (like you even have QPrintDialog in Qt...).

QtDesigner - drawing an UI

QtDesigner is used to create the user interface of your app. It can cooperate with other Qt tools to create translation files for i18n-enabled apps or manage assets like images and other files used by the app.

We will use it to draw UI of our example image viewer, but first some basics:

On start QtDesigner will ask what template to use. Main Window is a good choice for application while you can also pick Widget for smaller app or widgets designed to be used in an app
On start QtDesigner will ask what template to use. Main Window is a good choice for application while you can also pick Widget for smaller app or widgets designed to be used in an app
On the left you have available widgets you can drag and drop onto the window while on the right you have settings of currently selected element
On the left you have available widgets you can drag and drop onto the window while on the right you have settings of currently selected element
You can for example select the window and change it title
You can for example select the window and change it title
Widgets won't be ordered or aligned when drag and dropped
Widgets won't be ordered or aligned when drag and dropped
Qt uses layouts to align widgets. You can set window alignment on the top bar while on the left you have layouts that can be used on selected widgets - you can have layouts inside layouts
Qt uses layouts to align widgets. You can set window alignment on the top bar while on the left you have layouts that can be used on selected widgets - you can have layouts inside layouts. The goal is to have proper alignment and scaling of the UI instead of fixed sizes

Example image viewer

As an example app I will want to make a simple image viewer - a button than when clicked will open a file dialog prompting to select an image file. When a file will be selected I want to display it as well as the path to the file.

I start by dragging one button and two labels - one for image and one for image path
I start by dragging one button and two labels - one for image and one for image path
I put a horizontal layout on the button and path label
I put a horizontal layout on the button and path label
Then I set a horizontal layout for the window using top bar option
Then I set a horizontal layout for the window using top bar option

As you can see label on the bottom and the two widgets at the top have equal height. We want the button and it label to be always on top while the label that will display the image to take the whole remaining space. This is configured on the right panel:

I want the image label to expand vertically so I change it sizePolicy to match the desired effect
I want the image label to expand vertically so I change it sizePolicy to match the desired effect
Same with the button and label. I don't the button to get wider so I tell the label to expand
Same with the button and label. I don't the button to get wider so I tell the label to expand
What's also important is to edit the objectName of each widget you intend to use. Names like label, label_2, label_3 etc aren't very clear what they are so I named each widget appropriately to their intent/function
What's also important is to edit the objectName of each widget you intend to use. Names like label, label_2, label_3 etc aren't very clear what they are so I named each widget appropriately to their intent/function

It's really handy to properly name widgets as an IDE will code complete those names and openImageButton is more clear than pushButton (imagine having many buttons in an app). And of course I set the text used by the button and labels.

Save the file and prepare to convert it to Python code.

Converting UI file to Python code

PyQt5 uses pyuic5 while PySide2 uses pyside2-uic command line apps to convert a ui file to a Python file containing a Python class representing the interface:

pyuic5 image_viewer.ui > image_viewer_pyqt.py
pyside2-uic image_viewer.ui > image_viewer_pyside.py

You can open them and for the most part they will be similar aside of imports. You should not modify those files - anytime you change the UI you have to generate those files again and any change would be lost. They should be used only as an imported class into a skeleton code that launches it and then adds all the logic.

Create a main file for your app, for example run.py and use such boilerplate code:

PyQt5:

import sys

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication

import image_viewer_pyqt as viewer  # this is my file name, yours may be different

PySide2:

import sys

from PySide2 import QtWidgets
from PySide2.QtWidgets import QApplication

import image_viewer_pyside as viewer  # this is my file name, yours may be different

Both then follow with:

class ImageViewer(QtWidgets.QMainWindow, viewer.Ui_ImageViewerWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)


def main():
    app = QApplication(sys.argv)
    viewer = ImageViewer()
    viewer.show()
    app.exec_()


if __name__ == '__main__':
    main()
PyQt5 app
PyQt5 app
PySide2 app
PySide2 app

We import PyQt or PySide, then the generated interface Python class and inherit it with QMainWindow widget as a mixin. The Ui_ImageViewerWindow name will be based on the objectName of the window you made in QtDesigner. If you set it differently the class will have a different name, use appropriately.

You can now run the file to launch the app. You can click on the button but nothing will happen.

Signals and slots - flow of a Qt application

Qt application runs in a run loop that is constantly awaiting events that could be sent by user actions. QPushButton - the widget that makes the button has a defined signal - clicked. You can connect it with your code so that when button gets clicked your code will run - reacting to the click. The thing signal gets connected to is called a slot.

When you are using a widget you can easily check what methods, signals and other properties it has by looking at the Qt class reference - lots of classes there. If you go to QPushButton you can see it methods but no signals. If you look at the table on the top you can notice it inherits QAbstractButton that has that signal.

This is how it would look like:

class ImageViewer(QtWidgets.QMainWindow, image_viewer_pyqt.Ui_ImageViewerWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.openImageButton.clicked.connect(self.open_image_action)

    def open_image_action(self):
    	print('Button clicked')

Whenever you click the button you should see the text in the console.

Codding an app with Qt in Python is heavily based on following the Qt class reference list - checking signals, methods and properties of widgets used or widgets and classes you want to call, create
Codding an app with Qt in Python is heavily based on following the Qt class reference list - checking signals, methods and properties of widgets used or widgets and classes you want to call, create

So let's practice with some additional widgets. We have the UI in the UI file but some widgets or Qt classes will be commonly used pragmatically like the file picker or prompt windows. For this example I want to use QMessageBox to ask a user if he really wants to open a file. Not really practical but I want to show you how widget looks change based on OS and how Qt handles such cases.

QMessageBox can be configured like so:

widget = QtWidgets.QMessageBox(self.ui)
widget.setText('Do you really want to open an image?')
widget.setIcon(widget.Question)
widget.addButton('Yes', widget.AcceptRole)
widget.addButton('No', widget.RejectRole)
widget.setDetailedText('This is just a show-off for OS differences in prompt windows')

We set text, icon and buttons. As you can see it has embedded icons - widget.Question will display system specific question mark icon. Same with buttons. As you can see they have a role assigned - AcceptRole and RejectRole. Based on role they will be displayed differently based on OS. The widget has an accepted signal inherited from QDialog, so we can connect that signal to another method:

import sys

from PyQt5 import QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication

import image_viewer_pyqt


class ImageViewer(QtWidgets.QMainWindow, image_viewer_pyqt.Ui_ImageViewerWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.openImageButton.clicked.connect(self.open_image_action)

    def open_image_action(self):
        prompt = ConfirmationPrompt(self).get_widget()
        prompt.accepted.connect(self.show_image_picker)
        prompt.open()

    def show_image_picker(self):
        print('pick an image here')


class ConfirmationPrompt:
    def __init__(self, ui):
        self.ui = ui

    def get_widget(self):
        widget = QtWidgets.QMessageBox(self.ui)
        widget.setText('Do you really want to open an image?')
        widget.setIcon(widget.Question)
        widget.addButton('Yes', widget.AcceptRole)
        widget.addButton('No', widget.RejectRole)
        widget.setDetailedText('This is just a show-off for OS differences in prompt windows')
        return widget



def main():
    app = QApplication(sys.argv)
    viewer = ImageViewer()
    viewer.show()
    app.exec_()


if __name__ == '__main__':
    main()

Whenever you confirm the prompt the show_image_picker method will be called. So now we can call the QFileDialog that has a fileSelected signal:

def show_image_picker(self):
        picker = QtWidgets.QFileDialog(self)
        picker.setMimeTypeFilters(['image/jpeg', 'image/png', 'image/bmp', 'image/tiff', 'image/gif'])
        picker.fileSelected.connect(self.image_file_selected)
        picker.show()

    def image_file_selected(self, file):
    	print(file)

Wherever you select a file you should see a path printed in the console.

I named the QLabel intended to show the path as currentFile so I can set it value simply by:

self.currentFile.setText(file)

The QLabel I intend to use to display the image I named imageLabel. Image file is not a text so it has to be done differently. QLabel does have a setPixmap method:

picture = QtGui.QPixmap(file)
self.imageLabel.setPixmap(picture)

The final application looks like so:

import sys

from PyQt5 import QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication

import image_viewer_pyqt


class ImageViewer(QtWidgets.QMainWindow, image_viewer_pyqt.Ui_ImageViewerWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.openImageButton.clicked.connect(self.open_image_action)

    def open_image_action(self):
        prompt = ConfirmationPrompt(self).get_widget()
        prompt.accepted.connect(self.show_image_picker)
        prompt.open()
        return prompt

    def show_image_picker(self):
        picker = ImagePicker(self).get_widget()
        picker.fileSelected.connect(self.image_file_selected)
        picker.show()
        return picker

    def image_file_selected(self, file):
        self.currentFile.setText(file)
        picture = QtGui.QPixmap(file)
        self.imageLabel.setPixmap(picture)


class ConfirmationPrompt:
    def __init__(self, ui):
        self.ui = ui

    def get_widget(self):
        widget = QtWidgets.QMessageBox(self.ui)
        widget.setText('Do you really want to open an image?')
        widget.setIcon(widget.Question)
        widget.addButton('Yes', widget.AcceptRole)
        widget.addButton('No', widget.RejectRole)
        widget.setDetailedText('This is just a show-off for OS differences in prompt windows')
        return widget


class ImagePicker:
    def __init__(self, ui):
        self.ui = ui

    def get_widget(self):
        picker = QtWidgets.QFileDialog(self.ui)
        picker.setMimeTypeFilters(['image/jpeg', 'image/png', 'image/bmp', 'image/tiff', 'image/gif', 'image/webp'])
        return picker


def main():
    app = QApplication(sys.argv)
    viewer = ImageViewer()
    viewer.show()
    app.exec_()


if __name__ == '__main__':
    main()

I moved QMessageBox and QFileDialog widgets setup out of the main class to not mix business logic with presentation. With increasing app complexity such abstraction would become bigger and external classes would have more than one method or there would be more of them.

macOS

PyQt app on macOS
PyQt app on macOS
PyQt app on macOS

Windows 10

PyQt on Windows 10
PyQt on Windows 10
PyQt on Windows 10
PyQt on Windows 10

Linux - XFCE

PyQt on Linux XFCE
PyQt on Linux XFCE
PyQt on Linux XFCE

Testing

There is a few way for testing desktop applications and PyQt in particular. You can use pytest with pytest-qt plugin. The qtbot fixture can operate on an PyQt/PySide app. It's not easy to write such tests but doable. For example:

from PyQt5 import QtCore

from app import run_pyqt


class TestImageViewer:
    def test_if_main_window_renders(self, qtbot):
        window = run_pyqt.ImageViewer()
        qtbot.addWidget(window)
        assert window.currentFile.text() == 'No file selected'
        assert window.openImageButton.text() == 'Open image'

You can find more in the github repository. The project there also has pytest configured ready to use.

Deploying applications

In the end you want to build and deploy your applications as stand-alone packages for users to use on different operating systems. Build tools and configurations will differ a bit between PyQt and PySide but overall the idea is the same. You can check PyQt reference and PySide reference on build instructions.

Source code

You can find full source code of this example app for PyQt5 and PySide2 variants on github. It will run on Linux, Windows and macOS if you install Python 3 and all requirements (that's how I made all of the screenshots).

I hope you liked this PyQt5/PySide2 introduction. I have some PyQt4 tutorials here, but those were written more than 10 years ago! They won't work with Python 3 and PyQt5 without modifications (and code style is bad ;)).

You can find more help and support on PyQt mailing list and similar sites, just check both project support pages.

Comment article