Joseph Mawa A very passionate open source contributor and technical writer

How to build a GUI with PyQt

14 min read 3936

Introduction

A Graphical User Interface, better known as GUI, is a characteristic feature of most personal computers today. It provides an intuitive experience to users of varying levels of computing skills. Though they may use more resources, GUI applications are generally user-friendly due to their point-and-click nature.

PyQt is one of the toolkits you can use to develop cross-platform GUI applications in Python. It is powerful and easy to learn if you already have a firm grasp of this language.

This article will introduce you to the basics of building a GUI with PyQt. It requires you to have a working knowledge of the fundamentals of Python and object-oriented programming. Instead of explaining Python concepts, our focus will mainly be on PyQt.

What is PyQt?

PyQt is Python binding for the cross-platform application development framework, Qt. Using PyQt gives you the benefit of developing GUI applications using a simple but powerful language like Python. It exposes all the functionalities of the Qt API.

Riverbank Computing is the company behind the development and maintenance of PyQt. Its latest stable release is PyQt6. The release cycle of the PyQt major version appears to be in sync with that of Qt, judging from the release history.

In this article, we shall use PyQt5. It requires Python v3.5 or later, though you can also build with earlier versions of Python. Note, however, that PyQt6 requires Python v3.6.1 or later.

Before diving headfirst into building GUI apps, you should be aware that PyQt is dual-licensed. These licenses are GPL v3 and the Riverbank commercial license.

It is a requirement for you to distribute your code under a compatible license if you obtain PyQt under the GPL v3 license. Similarly, your PyQt license should be in harmony with your Qt license.

What I have provided here is a high-level overview of the PyQt licensing requirements. I recommend you acquaint yourself with the licensing requirement of the specific version of PyQt you want to use.

How to install PyQt

For the commercially licensed version of PyQt, you must first acquire the license before installation. To install the GPL licensed version, run the commands below to create a virtual environment and install PyQt5. Though you can install PyQt globally, it is recommended you use a virtual environment.

We made a custom demo for .
No really. Click here to check it out.

# Create virtual environment
python3 -m venv env

# Activate virtual environment
source env/bin/activate

# Install PyQt5
pip install PyQt5

For detailed installation instructions, you should check the documentation for the specific version of PyQt you want to use. It has instructions for installing both the GPL and commercial versions. The documentation also has troubleshooting tips in case you run into some errors.

Building a simple GUI with PyQt

Let us get a taste of PyQt by building a simple “hello world” GUI. Building this simple app will make things a lot easier in the subsequent sections.

Before we get started, it is worth mentioning here that PyQt uses camelCase for method and property names. Throughout this article, we shall use camelCase when naming variables and functions for consistency instead of the recommended naming convention in Python.

We shall intentionally keep things simple and minimal at the moment. I am assuming you have an app.py file created in your project directory; you can add the lines of code in each step to your app.py file as you follow the steps below.

Step 1: Import the required classes

PyQt comes with several built-in modules. However, the module you will interact with most regularly when building a GUI is the QtWidgets module. It has classes you will use for creating your GUI.

Because our goal is to create the most basic “hello world” GUI, we shall use the QApplication and QWidgets classes only. Start by importing them like so:

from PyQt.QtWidgets import QApplication, QWidgets

You can import the other classes you want to use in your application the same way.

Step 2: Initialize the application

We need to initialize the application by creating an instance of QApplication. It is responsible for managing the application’s main settings and control flow. Therefore, you should instantiate this class before creating any other object related to the user interface.

application = QApplication([])

For more insights about the responsibilities of the QApplication class, check the Qt documentation.

In the code above, we passed an empty array to QApplication, but you can also pass sys.argv instead if you want the app to receive arguments from the command line. Be sure to import sys if you are passing sys.argv as an argument.

Step 3: Create the main window

The main window, also referred to as a top-level window, is a widget that doesn’t have a parent. Every GUI must have a main window.

At the moment, we shall create an instance of QWidget and make it our main window like so:

mainWindow = QWidget()
mainWindow.setGeometry(0, 0, 350, 400)
mainWindow.setWindowTitle('Hello World')

After creating an instance of Qwidget, there are several other methods you can invoke. For our simple GUI, we have invoked the setGeometry method and the setWindowTitle methods.

The setGeometry method is for positioning the GUI on the screen and setting its dimensions. Its function signature is setGeometry(x, y, width, height). The first two arguments specify the x and y coordinates of the window on the screen, and the width and height are for setting the width and height of the window, respectively.

The setWindowTitle method, like its name suggests, is for setting the title of the application. You can pass the title as a string argument. The window won’t have a title if you don’t set it yourself.

Step 4: Show the main window

The window we have created in the previous step is not visible by default. We need to show it by invoking the show method:

mainWindow.show()

Step 5: Start the event loop

Finally, you need to fire up the event loop by invoking the application.exec method:

application.exec()

You can also use application.exec_() instead to start the event loop.

After following all five steps outlined above, your app.py file should have the following code:

from PyQt5.QtWidgets import QWidget, QApplication

application = QApplication([])

mainWindow = QWidget()

mainWindow.setGeometry(0, 0, 350, 400)
mainWindow.setWindowTitle('Hello World')

mainWindow.show()

application.exec()

Like any other Python script, you need to run app.py by using the command python3 app.py. You should be able to see the window displayed. The appearance of the window largely depends on your system. On Linux, it should look similar to the image below.

Basic hello world app

Main concepts in PyQt

We have just created our very first “hello world” GUI. Let us now look at some of the main concepts that will broaden our knowledge of PyQt. Some level of familiarity with these concepts is necessary for building production-level GUIs.

Widgets

Like most GUI toolkits out there, widgets are the building blocks of PyQt GUIs. You can use a widget to display data, receive user input, or use it as a container for grouping other related widgets.

Most widgets are nested within other widgets, however, there is always a widget that does not have a parent. As already mentioned, a widget that does not have a parent is referred to as a window.

The main class for creating widgets in PyQt is the QWidgets class. All the elements for creating UIs in PyQt are either sub-classes of the QWidgets class or are used in connection with the QWidgets class.

There are several widget classes that you can read about in the PyQt or Qt documentation. We cannot mention all of them here. Some of the basic widget classes include:

  • QLabel for displaying text and images
  • QPushButton for creating command buttons
  • QLineEdit for creating a one-line text editor
  • QRadioButton for creating a radio button with a text label

Let us add a simple pushButton widget to our “hello world” GUI:

from PyQt5.QtWidgets import QWidget, QApplication, QPushButton

application = QApplication([])

mainWindow = QWidget()
mainWindow.setGeometry(0, 0, 350, 400)
mainWindow.setWindowTitle('Button Widget')

pushButton = QPushButton(parent=mainWindow, text='Click me')

mainWindow.show()
application.exec()

Running the code above will create a window similar to the image below.

button widget that says "click me"

Event loop

Most GUI toolkits are event-driven. PyQt is no exception. An event can originate from user interaction with the app like a button click, filling an input text, clicking a link, or closing the window. An event can also be from the window system or other sources. It is the responsibility of the event loop to manage these events.

Invoking the .exec method will fire off the event loop, as we highlighted while building the “hello world” GUI. The loop waits for events to occur and responds when they do. It terminates and exits the application if it receives the Terminate event.

It is through this event-response functionality that you can add interactivity to the GUI using signals and slots. We shall learn about signals and slots in the section below.

Signals and slots

We looked at how to use widgets to create the visible components of the GUI in one of the previous sections. It is through signals and slots that you can add interactivity to your GUI. We added a push-button widget to our “hello world” GUI, but clicking the button doesn’t do anything at the moment.

Ordinarily, a button click should trigger an action such as opening another widget, closing a widget, or logging in. You need signals and slots to respond to such user actions or changes in the state of a widget.

A signal is a notification the widget emits whenever something happens. It can be a button click, mouse move, or a change in a text input field. Different widgets emit different signals. For example, a push-button widget emits the clicked signal when clicked. The push-button widget also emits other lesser-known signals such as the pressed, released, and toggled signals. To know what signal a specific widget emits, you need to read the documentation for the widget’s corresponding class.

A slot is a function or method that is invoked after a widget emits a specific signal. Several widgets come with predefined slots. However, you can also define slots to handle signals of interest.

To illustrate what we have just learned, let us add a slot to our push-button widget so that it runs in response to a clicked signal:

from PyQt5.QtWidgets import QWidget, QApplication, QPushButton

application = QApplication([])

mainWindow = QWidget()
mainWindow.setGeometry(0, 0, 350, 400)
mainWindow.setWindowTitle('Slot and Signal')

def clickedSlot():
    print('The button has been clicked')

pushButton = QPushButton(parent=mainWindow, text='Click me')
pushButton.clicked.connect(clickedSlot)

mainWindow.show()
application.exec()

After running the code above and clicking the button, you should see the text The button has been clicked on the terminal.

PyQT layout management

Up to this moment, we have looked at only the most basic components of PyQt. In a real-world app, you will be dealing with multiple widgets within the same window. Fortunately, Qt has several functionalities for managing widget layout in your application’s UI. You can use these functionalities to describe how to arrange widgets. The layouts automatically resize and position widgets whenever the space changes. As a result, the UI remains usable.

Though PyQt has several forms of layouts, we shall look at the horizontal, vertical, grid, and form layouts in this article. You can read about the others in the PyQt or Qt documentation. Each of the mentioned layouts has a corresponding built-in layout class. These classes are:

  • QHBoxLayout
  • QVBoxLayout
  • QGridLayout
  • QFormLayout

Horizontal layout

You can use the built-in QHBoxLayout class to lay out widgets horizontally, usually from left to right. It can also arrange widgets from right to left for right-to-left languages.

In the code below, I have modified the “hello world” GUI to display five images in a horizontal layout. To avoid repetition, I have used a for loop to add the images to the layout. Make sure you have an image in the cat.jpg file before running this code:

from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (
    QLabel,
    QWidget,
    QApplication,
    QHBoxLayout,
)

application = QApplication([])

mainWindow = QWidget()

mainWindow.setGeometry(0, 0, 350, 400)
mainWindow.setWindowTitle('Horizontal Layout')

horizontalLayout = QHBoxLayout()

for num in range(6):
    label = QLabel()
    pixmap = QPixmap('cat.jpg')
    label.setPixmap(pixmap)
    horizontalLayout.addWidget(label)

mainWindow.setLayout(horizontalLayout)
mainWindow.show()

application.exec()

In PyQt, you can render images using the QLabel widget. You start by creating an instance of QPixmap class after importing it from the QtGui module. Use the setPixmap method of the QLabel class to set it on the label widget as shown in the for loop above.

You can play with the code before moving to the next section. Instead of laying out images, you can lay out other widgets like push buttons.

The code above should create a GUI similar to the image below.

GUI with horizontal line of cats

Vertical layout

Unlike the QHBoxLayout class, which lays out widgets horizontally from left to right or from right to left, the QVBoxLayout class lays widgets vertically from top to bottom.

The code below shows how you use the QVBoxLayout class for vertical layout management. It is very similar to the horizontal layout:

from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (
    QLabel,
    QWidget,
    QApplication,
    QVBoxLayout,
)

application = QApplication([])

mainWindow = QWidget()

mainWindow.setGeometry(0, 0, 350, 400)
mainWindow.setWindowTitle('Vertical Layout')

verticalLayout = QVBoxLayout()

for num in range(6):
    label = QLabel()
    pixmap = QPixmap('cat.jpg')
    label.setPixmap(pixmap)
    verticalLayout.addWidget(label)

mainWindow.setLayout(verticalLayout)
mainWindow.show()

application.exec()

Running the code above should create a GUI with images arranged from top to bottom similar to the image below.

GUI with vertical cat photos

Grid layout

Grid layout management involves laying out widgets in a two dimensional grid. The QGridLayout is a handy built-in class for doing just that. In a grid layout, an item can occupy two grids. You can also nest another layout within a grid item. It makes building more complex GUIs a lot easier.

You can create an instance of QGridLayout class and add widgets to it using the addWidget method. Like in the previous sections, I am adding images to the grid in a loop to avoid repetition in the code below. It is also possible to skip a grid while populating the grid with widgets:

from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (
    QLabel,
    QWidget,
    QApplication,
    QGridLayout,
)

application = QApplication([])

mainWindow = QWidget()

mainWindow.setGeometry(0, 0, 350, 400)
mainWindow.setWindowTitle('Grid Layout')

gridLayout = QGridLayout()

for row in range(3):
    for col in range(3):
        label = QLabel()
        pixmap = QPixmap('cat.jpg')
        label.setPixmap(pixmap)
        gridLayout.addWidget(label, row, col)

mainWindow.setLayout(gridLayout)
mainWindow.show()

application.exec()

Because you are placing the widgets in a two dimensional grid, you need to specify the location of each widget when adding it to the layout.

Your “hello world” GUI should look like the image below after running the code above.

GUI with cat pics in a grid

Form layout

The form layout is primarily for managing input widgets and their associated labels. It comprises rows of widgets laid out as label-field pairs. You need to use the QFormLayout class to arrange widgets in a form layout as illustrated in the code below:

from PyQt5.QtWidgets import (
    QGroupBox,
    QLabel,
    QLineEdit,
    QPlainTextEdit,
    QRadioButton,
    QSpinBox,
    QVBoxLayout,
    QWidget,
    QApplication,
    QFormLayout,
)

application = QApplication([])

mainWindow = QWidget()

mainWindow.setGeometry(0, 0, 350, 400)
mainWindow.setWindowTitle('Form Layout')

formLayout = QFormLayout()

nameLabel = QLabel('Name')
nameField = QLineEdit()

ageLabel = QLabel('Age')
ageField = QSpinBox()
ageField.setMinimum(0)
ageField.setMaximum(130)


sexLabel = QLabel('Sex')
sexGroup = QGroupBox()
verticalLayout = QVBoxLayout()

for sex in ['Male', 'Female', 'Other']:
    radioButton = QRadioButton(sex)
    verticalLayout.addWidget(radioButton)

sexGroup.setLayout(verticalLayout)

commentLabel = QLabel('Comments')
commentField = QPlainTextEdit()


formLayout.addRow(nameLabel, nameField)
formLayout.addRow(ageLabel, ageField)
formLayout.addRow(sexLabel, sexGroup)
formLayout.addRow(commentLabel, commentField)

mainWindow.setLayout(formLayout)
mainWindow.show()

application.exec()

Run the code above to see a GUI similar to the image below. You can see label-field pairs of widgets. Each row has a label widget and the corresponding field widget to the right.

Form layout GUI

The layouts mentioned above are by no means exhaustive. You can read about other forms of layout in the PyQt or Qt documentation.

The main window framework

With PyQt, you can use an arbitrary widget to create the main window; that is what we did for the illustrations in the previous sections in order to keep things simple and understandable. However, that is not what you do in a real-world application. PyQt comes with the QMainWindow class for managing an application’s main window.

The QMainWindow class provides layouts for adding widgets such as menu bars, toolboxes, and status bars out of the box. It is common practice to create a subclass of QMainWindow class and use it as the main window. And that is what we shall do.

Below is the refactored code for the “hello world” GUI:

from PyQt5.QtWidgets import QMainWindow, QApplication

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setWindowTitle('Hello Wordl')
        self.setGeometry(0, 0, 350, 400)

if (__name__ == '__main__'):
    application = QApplication([])
    mainWindow = MainWindow()
    mainWindow.show()
    application.exec()

You will notice how our MainWindow class inherits from the QMainWindow class. When you run the code above, the window it creates will be the same as our first “hello world” GUI. From now on, we shall always create a subclass of the QMainWindow class.

How to create menu bar

Menus are characteristic features of most GUI applications. Desktop GUIs usually have menus at the top of the main window. Some dropdown menus have sub-menus that open on hover. The image below shows what a typical menu looks like on a desktop GUI.

desktop GUI with menu and submenu

As noted in the previous section, the QMainWindow class provides a layout for adding a menu bar to your GUI out of the box. To create a menu bar, you need to use the menuBar method.

Below is the code for creating the dropdown menu in the image above. I have used the QAction class to create actions and added them to the corresponding menu to create a dropdown menu:

from PyQt5.QtWidgets import QMainWindow, QApplication, QAction

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setWindowTitle('Dropdown Menu')
        self.setGeometry(0, 0, 350, 400)
        self.addMenu()

    def addMenu(self):

        # Create menu bar
        menuBar = self.menuBar()

        # Add menu items
        fileMenu = menuBar.addMenu('File')
        helpMenu = menuBar.addMenu('Help')

        # Create actions

        visitWebsiteAction = QAction('Visit Our Website', self)
        fileBugReportAction = QAction('File a Bug Report', self)

        # Add dropdown menu items on the Help menu

        helpMenu.addAction(visitWebsiteAction)
        helpMenu.addAction(fileBugReportAction)

        # Add 'Follow Us' dropdown menu item on the Help menu

        followUs = helpMenu.addMenu('Follow Us')

        # Social media actions

        twitterAction = QAction('Twitter', self)
        githubAction = QAction('GitHub', self)

        # Add actions

        followUs.addAction(twitterAction)
        followUs.addAction(githubAction)



if (__name__ == '__main__'):
    application = QApplication([])
    mainWindow = MainWindow()
    mainWindow.show()
    application.exec()

Building a standard GUI with PyQt

Hopefully, the above sections have introduced you to the basics of PyQt. Let us now put the knowledge we have just acquired to use by building a standard GUI.

The image below shows the login screen for the desktop version of a password management app. It has a menu bar and a password field. We have looked at most of the widgets in the GUI in the previous sections. However, I will briefly explain some of the methods shortly.

I have kept it simple by just focusing on the basics. If you are interested, you can push yourself further by adding more features and interactivity to it.

password manager GUI

Below is the corresponding code for the above GUI. I have left comments on most sections to explain what is happening. I urge you to read through the code before reading my explanation:

from PyQt5.QtWidgets import (QAction, QApplication, QFormLayout, QGroupBox,
                             QLabel, QPushButton, QVBoxLayout, QWidget,
                             QMainWindow, QLineEdit)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.createUI()
        self.createActions()
        self.creatMenu()
    def createUI(self):
        # Create window
        self.setWindowTitle('Password Manager')
        self.resize(800, 500)
        self.setMinimumSize(500, 450)
        # Create central widget and layout
        self._centralWidget = QWidget()
        self._verticalLayout = QVBoxLayout()
        self._centralWidget.setLayout(self._verticalLayout)
        # Set central widget
        self.setCentralWidget(self._centralWidget)
        # Vertically center widgets
        self._verticalLayout.addStretch(1)
        # Add lock image
        self.addLockImage()
        self.addText()
        self.addInputText()
        # Vertically center widgets
        self._verticalLayout.addStretch(1)
        # Add Copyright
        self.addCopyRight()
    def addLockImage(self):
        imageLabel = QLabel()
        pixmap = QPixmap('lock.png')
        imageLabel.setPixmap(pixmap)
        self._verticalLayout.addWidget(imageLabel, alignment=Qt.AlignCenter)
    def addText(self):
        messageLabel = QLabel(
            'Hi there 👋. Your vault is locked. Verify your master password to continue.'
        )
        messageLabel.setAlignment(Qt.AlignCenter)
        messageLabel.setFixedWidth(350)
        messageLabel.setMinimumHeight(50)
        messageLabel.setWordWrap(True)
        self._verticalLayout.addWidget(messageLabel, alignment=Qt.AlignCenter)

    def addCopyRight(self):
        copyRight = QLabel(
            'Copyright © <a href="https://logrocket.com/">LogRocket</a> 2021')
        copyRight.setOpenExternalLinks(True)
        self._verticalLayout.addWidget(copyRight, alignment=Qt.AlignCenter)

    def addInputText(self):
        groupBox = QGroupBox()
        groupBox.setFixedWidth(350)

        formLayout = QFormLayout()

        passwordLabel = QLabel('Master Password')
        passwordField = QLineEdit()
        passwordField.setTextMargins(3, 0, 3, 0)
        passwordField.setMinimumWidth(200)
        passwordField.setMaximumWidth(300)
        passwordField.setEchoMode(QLineEdit.Password)
        passwordField.setClearButtonEnabled(True)

        submitLabel = QLabel('Open Your Vault')
        submitField = QPushButton()

        formLayout.addRow(passwordLabel, passwordField)
        formLayout.addRow(submitLabel, submitField)

        groupBox.setLayout(formLayout)
        self._verticalLayout.addWidget(groupBox, alignment=Qt.AlignCenter)

    def creatMenu(self):
        # Create menu bar
        menuBar = self.menuBar()
        # Add menu items
        fileMenu = menuBar.addMenu('File')
        editMenu = menuBar.addMenu('Edit')
        accountMenu = menuBar.addMenu('Account')
        helpMenu = menuBar.addMenu('Help')
        # Add sub-items under Help menu item
        helpMenu.addAction(self.sendEmailAction)
        helpMenu.addAction(self.visitWebsiteAction)
        helpMenu.addAction(self.fileBugReportAction)
        # Add horizontal line
        helpMenu.addSeparator()
        # Add 'Follow Us' sub-item under Help menu item
        # Use addMenu method because it contains sub-items
        followUs = helpMenu.addMenu('Follow Us')
        followUs.addAction(self.twitterAction)
        followUs.addAction(self.facebookAction)
        followUs.addAction(self.githubAction)

    def createActions(self):
        # Help menu actions
        self.sendEmailAction = QAction('Email Us', self)
        self.visitWebsiteAction = QAction('Visit Our Website', self)
        self.fileBugReportAction = QAction('File a Bug Report', self)
        # Social media actions
        self.twitterAction = QAction('Twitter', self)
        self.facebookAction = QAction('Facebook', self)
        self.githubAction = QAction('GitHub', self)

if (__name__ == '__main__'):
    application = QApplication([])
    mainWindow = MainWindow()
    mainWindow.show()
    application.exec()

In the code above, I have declared the createUI method for creating the GUI. I have extracted the functionalities for creating the other widgets to separate methods. We have encountered most of the widgets that make up the UI in the previous sections, and I have added comments in the code to explain what is happening. Therefore, I will not explain all of them here. However, I will talk about how the UI is organized.

The GUI comprises four widgets in a vertical layout. I have added a stretchable space at the start of the vertical layout using the addStretch method. Similarly, there is another stretchable space after the group box. The stretchable spaces help displace the copyright text to the bottom of the GUI and vertically center the rest of the widgets.

The previous sections introduced you to the basics of building a GUI app programmatically. There is an alternative drag-and-drop interface for building GUIs referred to as Qt Designer. It will significantly increase your productivity. For more, you can read the Qt designer manual.

Conclusion

If you are looking to develop cross-platform GUI applications using Python, PyQt is a handy toolkit. You can use the various built-in classes to create widgets and then add interactivity using signals and slots. Using it with Qt Designer can significantly reduce development time and increase productivity.

Hopefully, you found this article helpful. What we have covered here are just the basics, and PyQt has tons of classes and methods. Nonetheless, I hope it has given you enough background knowledge to read the documentation and start using PyQt.

Unfortunately, there are lots of missing sections in the PyQt5 and PyQt6 documentation at the time of writing this article. Therefore, using the PyQt documentation without other resources might not be very helpful. You need to use the PyQt and the Qt documentation concurrently.

However, be aware that the Qt documentation is C++-oriented. You can also use the documentation for earlier versions of PyQt like PyQt4 instead of the most recent versions like PyQt5 or PyQt6. Some sections of the API haven’t changed much.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Joseph Mawa A very passionate open source contributor and technical writer

Leave a Reply