PySide6

Signals, Slots & Events

Signals are notifications emitted by widgets when something happens. That something can be any number of things, from pressing a button, to the text of an input box changing, to the text of the window changing. Many signals are initiated by user action, but this is not a rule.

In addition to notifying about something happening, signals can also send data to provide additional context about what happened.

You can also create your own custom signals, which we'll explore later.

Slots is the name Qt uses for the receivers of signals. In Python, any function (or method) in your application can be used as a slot -- simply by connecting the signal to it. If the signal sends data, then the receiving function will receive that data too. Many Qt widgets also have their own built-in slots, meaning you can hook Qt widgets together directly.

Let's take a look at the basics of Qt signals and how you can use them to hook widgets up to make things happen in your apps.

Save the following app outline to a file named app.py:

import sys
from PySide6.QtWidgets import QApplication, QMainWindow

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

QPushButton Signals

Our simple application currently has a QMainWindow with a QPushButton set as the central widget. Let's start by hooking up this button to a custom Python method. Here we create a simple custom slot named the_button_was_clicked() which accepts the clicked signal from the QPushButton object:

import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")
        button.setCheckable(True)
        button.clicked.connect(self.the_button_was_clicked)

        # Set the central widget of the Window.
        self.setCentralWidget(button)

    def the_button_was_clicked(self):
        print("Clicked!")

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

Run it! If you click the button, you'll see the text "Clicked!" on the console:

Clicked!
Clicked!
Clicked!
Clicked!

Receiving data

That's a good start! We've heard already that signals can also send data to provide more information about what has just happened. The clicked signal is no exception, also providing a checked (or toggled) state for the button. For normal buttons, this is always False, so our first slot ignored this data. However, we can make our button checkable and see the effect.

In the following example, we add a second slot that outputs the checkstate:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")
        button.setCheckable(True)
        button.clicked.connect(self.the_button_was_clicked)
        button.clicked.connect(self.the_button_was_toggled)

        self.setCentralWidget(button)

    def the_button_was_clicked(self):
        print("Clicked!")

    def the_button_was_toggled(self, checked):
        print("Checked?", checked)

Run it! If you press the button, you'll see it highlighted as checked. Press it again to release it. Look for the check state in the console:

Clicked!
Checked? True
Clicked!
Checked? False
Clicked!
Checked? True
Clicked!
Checked? False
Clicked!
Checked? True

You can connect as many slots to a signal as you like and can respond to different versions of signals at the same time on your slots.

Storing data

Often it is useful to store the current state of a widget in a Python variable. This allows you to work with the values like any other Python variable without accessing the original widget. You can either store these values as individual variables or use a dictionary if you prefer. In the next example, we store the checked value of our button in a variable called button_is_checked on self:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.button_is_checked = True

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")
        button.setCheckable(True)
        button.clicked.connect(self.the_button_was_toggled)
        button.setChecked(self.button_is_checked)

        self.setCentralWidget(button)

    def the_button_was_toggled(self, checked):
        self.button_is_checked = checked

        print(self.button_is_checked)

First, we set the default value for our variable (to True), then use the default value to set the initial state of the widget. When the widget state changes, we receive the signal and update the variable to match.

You can use this same pattern with any PySide widgets. If a widget does not provide a signal that sends the current state, you will need to retrieve the value from the widget directly in your handler. For example, here we're checking the checked state in a pressed handler:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.button_is_checked = True

        self.setWindowTitle("My App")

        self.button = QPushButton("Press Me!")
        self.button.setCheckable(True)
        self.button.released.connect(self.the_button_was_released)
        self.button.setChecked(self.button_is_checked)

        self.setCentralWidget(self.button)

    def the_button_was_released(self):
        self.button_is_checked = self.button.isChecked()

        print(self.button_is_checked)

We need to keep a reference to the button on self so we can access it in our slot.

The released signal fires when the button is released but does not send the check state, so instead, we use isChecked() to get the check state from the button in our handler.

Changing the interface

So far, we've seen how to accept signals and print output to the console. But how about making something happen in the interface when we click the button? Let's update our slot method to modify the button, changing the text and disabling the button so it is no longer clickable. We'll also turn off the checkable state for now:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        self.button = QPushButton("Press Me!")
        self.button.clicked.connect(self.the_button_was_clicked)

        self.setCentralWidget(self.button)

    def the_button_was_clicked(self):
        self.button.setText("You already clicked me.")
        self.button.setEnabled(False)

        # Also change the window title.
        self.setWindowTitle("My Oneshot App")

Again, because we need to be able to access the button in our the_button_was_clicked method, we keep a reference to it on self. The text of the button is changed by passing a string to setText(). To disable a button, call setEnabled() with False.

Run it! If you click the button, the text will change, and the button will become unclickable.

You're not restricted to changing the button that triggers the signal, you can do anything you want in your slot methods. For example, try adding the following line to the_button_was_clicked() method to also change the window title:

self.setWindowTitle("A new window title")

Most widgets have their own signals -- and the QMainWindow we're using for our window is no exception. In the following more complex example, we connect the windowTitleChanged signal on the QMainWindow to a custom slot method.

In the following example, we connect the windowTitleChanged signal on the QMainWindow to a method slot, the_window_title_changed(). This slot also receives the new window title:

import sys
from random import choice

from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton

window_titles = [
    "My App",
    "My App",
    "Still My App",
    "Still My App",
    "What on earth",
    "What on earth",
    "This is surprising",
    "This is surprising",
    "Something went wrong",
]

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.n_times_clicked = 0

        self.setWindowTitle("My App")

        self.button = QPushButton("Press Me!")
        self.button.clicked.connect(self.the_button_was_clicked)

        self.windowTitleChanged.connect(self.the_window_title_changed)

        self.setCentralWidget(self.button)

    def the_button_was_clicked(self):
        print("Clicked.")
        new_window_title = choice(window_titles)
        print("Setting title:  %s" % new_window_title)
        self.setWindowTitle(new_window_title)

    def the_window_title_changed(self, window_title):
        print("Window title changed: %s" % window_title)

        if window_title == "Something went wrong":
            self.button.setDisabled(True)

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

First, we set up a list of window titles -- we'll select one at random from this list using Python's built-in random.choice(). We hook our custom slot method, the_window_title_changed(), to the window's windowTitleChanged signal.

When we click the button, the window title will change at random. If the new window title equals "Something went wrong" the button will be disabled.

Run it! Click the button repeatedly until the title changes to "Something went wrong", and the button will become disabled.

There are a few things to notice in this example.

Firstly, the windowTitleChanged signal is not always emitted when setting the window title. The signal only fires if the new title is changed from the previous one. If you set the same title multiple times, the signal will only be fired the first time. It is important to double-check the conditions under which signals fire, to avoid being surprised when using them in your app.

Secondly, notice how we are able to chain things together using signals. One thing happening -- a button press -- can trigger multiple other things to happen in turn. These subsequent effects do not need to know what caused them, but simply follow as a consequence of simple rules. This decoupling of effects from their triggers is one of the key concepts to understand when building GUI applications.

In this section, we've covered signals and slots. We've demonstrated some simple signals and how to use them to pass data and state around your application. Next, we'll look at the widgets which Qt provides for use in your applications -- together with the signals they provide.

Connecting widgets together directly

So far, we've seen examples of connecting widget signals to Python methods. When a signal is fired from the widget, our Python method is called and receives the data from the signal. But you don't always need to use a Python function to handle signals -- you can also connect Qt widgets directly to one another.

In the following example, we add a QLineEdit widget and a QLabel to the window. In the __init__() for the window, we connect our line edit textChanged signal to the setText() method on the QLabel. Now any time the text changes in the QLineEdit the QLabel will receive that text to its setText() method:

import sys

from PySide6.QtWidgets import (
    QApplication,
    QLabel,
    QLineEdit,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        self.label = QLabel()

        self.input = QLineEdit()
        self.input.textChanged.connect(self.label.setText)

        layout = QVBoxLayout()
        layout.addWidget(self.input)
        layout.addWidget(self.label)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

Notice that in order to connect the input to the label, the input and label must both be defined. This code adds the two widgets to a layout and sets that on the window. We'll cover layouts in detail later, you can ignore it for now.

Run it! Type some text in the upper box, and you'll see it appear immediately on the label.

Most Qt widgets have slots available, to which you can connect any signal that emits the same type that it accepts. The widget documentation has the slots for each widget listed under "Public Slots". For example, see the QLabel documentation.

Events

Every interaction the user has with a Qt application is an event. There are many types of events, each representing a different type of interaction. Qt represents these events using event objects which package up information about what happened. These events are passed to specific event handlers on the widget where the interaction occurred.

By defining custom, or extended event handlers, you can alter the way your widgets respond to these events. Event handlers are defined just like any other method, but the name is specific for the type of event they handle.

One of the main events which widgets receive is the QMouseEvent. QMouseEvent events are created for each and every mouse movement and button click on a widget. The following event handlers are available for handling mouse events:

For example, clicking on a widget will cause a QMouseEvent to be sent to the mousePressEvent() event handler on that widget. This handler can use the event object to find out information about what happened, such as what triggered the event and where specifically it occurred.

You can intercept events by sub-classing and overriding the handler method on the class. You can choose to filter, modify, or ignore events, passing them up to the normal handler for the event by calling the parent class function with super(). These could be added to your main window class as follows. In each case, e will receive the incoming event:

PYTHON

import sys

from PySide6.QtWidgets import QApplication, QLabel, QMainWindow

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.label = QLabel("Click in this window")
        self.setCentralWidget(self.label)

    def mouseMoveEvent(self, e):
        self.label.setText("mouseMoveEvent")

    def mousePressEvent(self, e):
        self.label.setText("mousePressEvent")

    def mouseReleaseEvent(self, e):
        self.label.setText("mouseReleaseEvent")

    def mouseDoubleClickEvent(self, e):
        self.label.setText("mouseDoubleClickEvent")

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

Run it! Try moving and clicking (and double-clicking) in the window and watch the events appear.

You'll notice that mouse move events are only registered when you have the button pressed down. You can change this by calling self.setMouseTracking(True) on the window. You may also notice that the press (click) and double-click events both fire when the button is pressed down. Only the release event fires when the button is released. Typically, to register a click from a user, you should watch for both the mouse down and the release.

Inside the event handlers, you have access to an event object. This object contains information about the event and can be used to respond differently depending on what exactly has occurred. We'll look at the mouse event objects next.

Mouse events

All mouse events in Qt are tracked with the QMouseEvent object, with information about the event being readable from the following event methods:

You can use these methods within an event handler to respond to different events differently or ignore them completely. The positional methods provide both global and local (widget-relative) position information as QPoint objects, while buttons are reported using the mouse button types from the Qt.MouseButton namespace.

For example, the following allows us to respond differently to a left, right or middle click on the window:

PYTHON

import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.label = QLabel("Click in this window")
        self.setCentralWidget(self.label)

    def mousePressEvent(self, e):
        if e.button() == Qt.MouseButton.LeftButton:
            # handle the left-button press in here
            self.label.setText("mousePressEvent LEFT")

        elif e.button() == Qt.MouseButton.MiddleButton:
            # handle the middle-button press in here.
            self.label.setText("mousePressEvent MIDDLE")

        elif e.button() == Qt.MouseButton.RightButton:
            # handle the right-button press in here.
            self.label.setText("mousePressEvent RIGHT")

    def mouseReleaseEvent(self, e):
        if e.button() == Qt.MouseButton.LeftButton:
            self.label.setText("mouseReleaseEvent LEFT")

        elif e.button() == Qt.MouseButton.MiddleButton:
            self.label.setText("mouseReleaseEvent MIDDLE")

        elif e.button() == Qt.MouseButton.RightButton:
            self.label.setText("mouseReleaseEvent RIGHT")

    def mouseDoubleClickEvent(self, e):
        if e.button() == Qt.MouseButton.LeftButton:
            self.label.setText("mouseDoubleClickEvent LEFT")

        elif e.button() == Qt.MouseButton.MiddleButton:
            self.label.setText("mouseDoubleClickEvent MIDDLE")

        elif e.button() == Qt.MouseButton.RightButton:
            self.label.setText("mouseDoubleClickEvent RIGHT")

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

The button identifiers are defined in the Qt.MouseButton namespace, as follows:

On left-handed mice, the left and right button positions are reversed, i.e. pressing the right-most button will return Qt.MouseButton.LeftButton. This means you don't need to account for the mouse orientation in your code.

Context menus

Context menus are small context-sensitive menus that typically appear when right-clicking on a window. Qt has support for generating these menus, and widgets have a specific event used to trigger them. In the following example, we're going to intercept the contextMenuEvent() in a QMainWindow. This event is fired whenever a context menu is about to be shown and is passed a single value event of type QContextMenuEvent.

To intercept the event, we simply override the object method with our new method of the same name. So, in this case, we can create a method on our MainWindow subclass with the name contextMenuEvent(), and it will receive all events of this type:

PYTHON

import sys

from PySide6.QtGui import QAction
from PySide6.QtWidgets import QApplication, QMainWindow, QMenu

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

    def contextMenuEvent(self, e):
        context = QMenu(self)
        context.addAction(QAction("test 1", self))
        context.addAction(QAction("test 2", self))
        context.addAction(QAction("test 3", self))
        context.exec(e.globalPos())

app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

If you run the above code and right-click within the window, you'll see a context menu appear. You can set up triggered slots on your menu actions as normal (and re-use actions defined for menus and toolbars).

When passing the initial position to the exec() method, this must be relative to the parent passed in while defining. In this case, we pass self as the parent, so we can use the global position.

Just for completeness, there is actually a signal-based approach to creating context menus:

PYTHON

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.show()

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.on_context_menu)

    def on_context_menu(self, pos):
        context = QMenu(self)
        context.addAction(QAction("test 1", self))
        context.addAction(QAction("test 2", self))
        context.addAction(QAction("test 3", self))
        context.exec(self.mapToGlobal(pos))

It's entirely up to you which you choose.

Event hierarchy

In PySide6, every widget is part of two distinct hierarchies: the Python object hierarchy and the Qt layout hierarchy. How you respond or ignore events can affect how your UI behaves.

Python inheritance forwarding

Often you may want to intercept an event and do something with it, yet still trigger the default event-handling behavior. If your object is inherited from a standard widget, it will likely have sensible behavior implemented by default. You can trigger this by calling up the parent's implementation using super().

This is the Python parent class, not the PySide parent() class:

PYTHON

def mousePressEvent(self, event):
    print("Mouse pressed!")
    super().mousePressEvent(event)

The event will continue to behave as normal, yet you've added some non-interfering behavior.

Layout forwarding

When you add a widget to your application, it also gets another parent from the layout. The parent of a widget can be found by calling .parent(). Sometimes you specify these parents manually, such as for QMenu or QDialog, often it is automatic. When you add a widget to your main window, for example, the main window will become the widget's parent.

When events are created for user interaction with the UI, these events are passed to the uppermost widget in the UI. So, if you have a button on a window and click the button, then the button will receive the event first.

If the first widget cannot handle the event or chooses not to, then the event will bubble up to the parent widget, which will be given a turn. This bubbling continues all the way up nested widgets until the event is handled or it reaches the main window.

In your own event handlers, you can choose to mark an event as handled by calling the accept() method:

PYTHON

class CustomButton(QPushButton)
    def mousePressEvent(self, e):
        e.accept()

Alternatively, you can mark it as unhandled by calling ignore() on the event object. In this case, the event will continue to bubble up the hierarchy:

PYTHON

class CustomButton(QPushButton)
    def event(self, e):
        e.ignore()

If you want your widget to appear transparent to events, you can safely ignore events that you've actually responded to in some way. Similarly, you can choose to accept events you are not responding to in order to silence them.

Widgets
QLabel
QCheckBox
QComboBox
QListWidget
QLineEdit
QSpinBox and QDoubleSpinBox
QSlider
QDial

QLabel

We'll start the tour with QLabel, arguably one of the simplest widgets available in the Qt toolbox. This is a simple one-line piece of text that you can position in your application. You can set the text by passing in a str as you create it:

label = QLabel("Hello")

Or, by using the .setText() method:

label_1 = QLabel("1")  # The label is created with the text 1.
label_2.setText("2")   # The label now shows 2.

You can also adjust font parameters, such as the size of the font or the alignment of text in the widget:

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

        self.setWindowTitle("My App")

        label = QLabel("Hello")
        font = label.font()
        font.setPointSize(30)
        label.setFont(font)
        label.setAlignment(
            Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
        )

        self.setCentralWidget(label)

The alignment is specified by using a flag from the Qt.AlignmentFlag namespace. The flags available for horizontal alignment are:

The flags available for vertical alignment are:

You can combine flags together using pipes (|), however, note that you can only use the vertical or horizontal alignment flag at a time:

align_top_left = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop

Finally, there is also a shorthand flag that centers in both directions simultaneously:

Weirdly, you can also use QLabel to display an image using .setPixmap(). This accepts a pixmap object, which you can create by passing an image filename to the QPixmap class. In the example files provided with this book, you can find a file otje.jpg which you can display in your window as follows:

label.setPixmap(QPixmap('otje.jpg'))

QCheckBox

The next widget to look at is QCheckBox(), which presents a checkable box to the user, as the name suggests. However, as with all Qt widgets, there are a number of configurable options to change the widget behaviors:

PYTHON

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

        self.setWindowTitle("My App")

        checkbox = QCheckBox()
        checkbox.setCheckState(Qt.CheckState.Checked)

        # For tristate: widget.setCheckState(Qt.PartiallyChecked)
        # Or: widget.setTriState(True)
        checkbox.stateChanged.connect(self.show_state)

        self.setCentralWidget(checkbox)

    def show_state(self, state):
        print(state == Qt.CheckState.Checked.value)
        print(state)

You can set a checkbox state programmatically using .setChecked() or .setCheckState(). The former accepts either True or False, representing checked or unchecked, respectively. With .setCheckState(), you also specify a particular checked state using a Qt.CheckState namespace:

A checkbox that supports a partially-checked (Qt.CheckState.PartiallyChecked) state is commonly referred to as tri-state, which means the widget is neither on nor off. A checkbox in this state is commonly shown as a greyed-out checkbox and is commonly used in hierarchical checkbox arrangements where sub-items are linked to parent checkboxes.

If you set the value to Qt.CheckState.PartiallyChecked the checkbox will become tristate. You can also set a checkbox to be tri-state without setting the current state to partially checked by using .setTriState(True)

You may notice that when the script is running, the current state number is displayed as an int with checked = 2, unchecked = 0, and partially checked = 1. You don’t need to remember these values, the Qt.CheckState.Checked namespace variable = 2, for example. This is the value of these states' respective flags. This means you can test state using state == Qt.CheckState.Checked.

QComboBox

The QComboBox widget is a drop-down list, closed by default with an arrow to open it. You can select a single item from the list, with the currently selected item being shown as a label on the widget. The combo box is suited to selecting choices from a list of options.

You have probably seen the combo box used for the selection of font faces, or sizes, in word-processing applications. Although Qt actually provides a specific font-selection combo box as QFontComboBox.

You can add items to a QComboBox by passing a list of strings to .addItems(). Items will be added in the order they are provided:

PYTHON

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

        self.setWindowTitle("My App")

        combobox = QComboBox()
        combobox.addItems(["One", "Two", "Three"])

        # The default signal from currentIndexChanged sends the index
        combobox.currentIndexChanged.connect(self.index_changed)

        # The same signal can send a text string
        combobox.currentTextChanged.connect(self.text_changed)

        self.setCentralWidget(combobox)

    def index_changed(self, index):  # index is an int stating from 0
        print(index)

    def text_changed(self, text):  # text is a str
        print(text)

The .currentIndexChanged signal is triggered when the currently selected item is updated, by default passing the index of the selected item in the list. There is also a .currentTextChanged signal, which instead provides the label of the currently selected item. This signal is often more useful.

QComboBox can also be editable, allowing users to enter values not currently in the list and either have them inserted, or simply used as a value. To make the box editable:

combobox.setEditable(True)

You can also set a flag to determine how the insert is handled. These flags are stored on the QComboBox class itself and are listed below:

To use these, apply the flag as follows:

combobox.setInsertPolicy(QComboBox.InsertAlphabetically)

You can also limit the number of items allowed in the box by using the .setMaxCount() method:

combobox.setMaxCount(10)

QListWidget

Next QListWidget. It's very similar to QComboBox, differing mainly in the signals available and in its on-screen appearance. This one is a box with a list of options rather than a drop-down list:

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

        self.setWindowTitle("My App")

        listwidget = QListWidget()
        listwidget.addItems(["One", "Two", "Three"])

        # In QListWidget there are two separate signals for the item, and the str
        listwidget.currentItemChanged.connect(self.index_changed)
        listwidget.currentTextChanged.connect(self.text_changed)

        self.setCentralWidget(listwidget)

    def index_changed(self, index):  # Not an index, index is a QListWidgetItem
        print(index.text())

    def text_changed(self, text):  # text is a str
        print(text)

QListWidget offers a currentItemChanged signal that sends the QListWidgetItem (the element of the list box), and a currentTextChanged signal that sends the text of the item.

QLineEdit

The QLineEdit widget is a simple single-line text editing box, into which users can type input. These are used for form fields, or settings where there is no restricted list of valid inputs. For example, when entering an email address or computer name:

PYTHON

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

        self.setWindowTitle("My App")

        self.lineedit = QLineEdit()
        self.lineedit.setMaxLength(10)
        self.lineedit.setPlaceholderText("Enter your text")

        # widget.setReadOnly(True) # uncomment this to make readonly

        self.lineedit.returnPressed.connect(self.return_pressed)
        self.lineedit.selectionChanged.connect(self.selection_changed)
        self.lineedit.textChanged.connect(self.text_changed)
        self.lineedit.textEdited.connect(self.text_edited)

        self.setCentralWidget(self.lineedit)

    def return_pressed(self):
        print("Return pressed!")
        self.lineedit.setText("BOOM!")

    def selection_changed(self):
        print("Selection changed")
        print(self.lineedit.selectedText())

    def text_changed(self, text):
        print("Text changed...")
        print(text)

    def text_edited(self, text):
        print("Text edited...")
        print(text)

As demonstrated in the above code, you can set a maximum length for the text in a line edit.

The QLineEdit has a number of signals available for different editing events, including when the Enter key is pressed (by the user), and when the user selection is changed. There are also two edit signals, one for when the text in the box has been edited and one for when it has been changed. The distinction here is between user edits and programmatic changes. The textEdited signal is only sent when the user edits text.

Additionally, it is possible to perform input validation using an input mask to define which characters are supported and where. This can be applied to the field as follows:

lineedit.setInputMask('000.000.000.000;_')

QSpinBox and QDoubleSpinBox

QSpinBox provides a small numerical input box with arrows to increase and decrease the value. QSpinBox supports integers, while the related widget, QDoubleSpinBox, supports floats:

PYTHON

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        spinbox = QSpinBox()
        # Or: doublespinbox = QDoubleSpinBox()

        spinbox.setMinimum(-10)
        spinbox.setMaximum(3)
        # Or: doublespinbox.setRange(-10, 3)

        spinbox.setPrefix("$")
        spinbox.setSuffix("c")
        spinbox.setSingleStep(3)  # Or e.g. 0.5 for QDoubleSpinBox
        spinbox.valueChanged.connect(self.value_changed)
        spinbox.textChanged.connect(self.value_changed_str)

        self.setCentralWidget(spinbox)

    def value_changed(self, value):
        print(value)

    def value_changed_str(self, str_value):
        print(str_value)

The demonstration code above shows the various features that are available for the widget.

To set the range of acceptable values, you can use .setMinimum() and .setMaximum(). You can alternatively use .setRange() to set both simultaneously. Annotation of value types is supported with both prefixes and suffixes that can be added to the number, e.g. for currency markers or units using .setPrefix() and .setSuffix(), respectively.

Clicking on the up and down arrows on the widget will increase or decrease the value in the widget by an amount, which can be set using .setSingleStep(). Note that this has no effect on the values that are acceptable to the widget.

Both QSpinBox and QDoubleSpinBox have a .valueChanged signal which fires whenever their value is altered. The raw .valueChanged signal sends the numeric value (either an int or a float) while .textChanged sends the value as a string, including both the prefix and suffix characters.

You can optionally disable text input on the spin box line edit, by setting it to read-only. With this set, the value can only be changed using the controls. This also has the side effect of disabling the flashing cursor:

spinbox.lineEdit().setReadOnly(True)

QSlider

QSlider provides a slide-bar widget, which internally works much like a QDoubleSpinBox. Rather than displaying the current value numerically, the slider defines its value by its handle position along the length of the widget. This is often useful when providing adjustment between two extremes, but where absolute accuracy is not required. The most common use of this type of widget is for volume controls.

There is an additional .sliderMoved signal that is triggered whenever the slider moves position and a .sliderPressed signal that emits whenever the slider is clicked:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        slider = QSlider()

        slider.setMinimum(-10)
        slider.setMaximum(3)
        # Or: widget.setRange(-10,3)

        slider.setSingleStep(3)

        slider.valueChanged.connect(self.value_changed)
        slider.sliderMoved.connect(self.slider_position)
        slider.sliderPressed.connect(self.slider_pressed)
        slider.sliderReleased.connect(self.slider_released)

        self.setCentralWidget(slider)

    def value_changed(self, value):
        print(value)

    def slider_position(self, position):
        print("position", position)

    def slider_pressed(self):
        print("Pressed!")

    def slider_released(self):
        print("Released")

You can also construct a slider with a vertical or horizontal orientation by passing the orientation in as you create it. The orientation flags are defined in the Qt.Orientation namespace. For example:

slider = QSlider(Qt.Orientation.Vertical)

QDial

Finally, the QDial is a rotatable widget that works just like the slider but appears as an analog dial. This looks nice, but from a UI perspective is not particularly user-friendly. However, they are often used in audio applications as a simulation of real-world analog dials:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        dial = QDial()
        dial.setRange(-10, 100)
        dial.setSingleStep(1)

        dial.valueChanged.connect(self.value_changed)
        dial.sliderMoved.connect(self.dial_position)
        dial.sliderPressed.connect(self.dial_pressed)
        dial.sliderReleased.connect(self.dial_released)

        self.setCentralWidget(dial)

    def value_changed(self, value):
        print(value)

    def dial_position(self, position):
        print("position", position)

    def dial_pressed(self):
        print("Pressed!")

    def dial_released(self):
        print("Released")

Layouts

There are 4 basic layouts available in Qt, which are listed in the following table.

  • QHBoxLayout -- Linear horizontal layout

  • QVBoxLayout -- Linear vertical layout

  • QGridLayout -- In indexable grid XxY

  • QStackedLayout -- Stacked (z) in front of one another

As you can see, there are three positional layouts available in Qt. The VBoxLayout, QHBoxLayout and QGridLayout. In addition there is also QStackedLayout which allows you to place widgets one on top of the other within the same space, yet showing only one layout at a time.

Before we start we need a simple application outline. Save the following code in a file named app.py -- we'll modify this application to experiment with different layouts.

import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget
from PySide6.QtGui import QPalette, QColor

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

To make it easier to visualize the layouts, we'll first create a simple custom widget that displays a solid color of our choosing. This will help to distinguish widgets that we add to the layout. Add the following code to your file as a new class at the top level --

class Color(QWidget):

    def __init__(self, color):
        super(Color, self).__init__()
        self.setAutoFillBackground(True)

        palette = self.palette()
        palette.setColor(QPalette.Window, QColor(color))
        self.setPalette(palette)

In this code we subclass QWidget to create our own custom widget Color. We accept a single parameter when creating the widget — color (a str). We first set .setAutoFillBackground to True to tell the widget to automatically fill its background with the window cooler. Next we get the current palette (which is the global desktop palette by default) and change the current QPalette.Window color to a new QColor described by the value color we passed in. Finally we apply this palette back to the widget. The end result is a widget that is filled with a solid color, that we specified when we created it.

If you find the above confusing, don't worry too much. We'll cover custom widgets in more detail later. For now it's sufficient that you understand that calling you can create a solid-filled red widget by doing the following:

Color('red')

First let's test our new Color widget by using it to fill the entire window in a single color. Once it’s complete we can add it to the QMainWindow using .setCentralWidget and we get a solid red window.

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = Color('red')
        self.setCentralWidget(widget)

QVBoxLayout vertically arranged widgets

With QVBoxLayout you arrange widgets one above the other linearly. Adding a widget adds it to the bottom of the column.

Let’s add our widget to a layout. Note that in order to add a layout to the QMainWindow we need to apply it to a dummy QWidget. This allows us to then use .setCentralWidget to apply the widget (and the layout) to the window. Our colored widgets will arrange themselves in the layout, contained within the QWidget in the window. First we just add the red widget as before.

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout = QVBoxLayout()

        layout.addWidget(Color('red'))

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

If you add a few more colored widgets to the layout you’ll notice that they line themselves up vertical in the order they are added.

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout = QVBoxLayout()

        layout.addWidget(Color('red'))
        layout.addWidget(Color('green'))
        layout.addWidget(Color('blue'))

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

QHBoxLayout horizontally arranged widgets

QHBoxLayout is the same, except moving horizontally. Adding a widget adds it to the right hand side.

To use it we can simply change the QVBoxLayout to a QHBoxLayout. The boxes now flow left to right.

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout = QHBoxLayout()

        layout.addWidget(Color('red'))
        layout.addWidget(Color('green'))
        layout.addWidget(Color('blue'))

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

Nesting layouts

For more complex layouts you can nest layouts inside one another using .addLayout on a layout. Below we add a QVBoxLayout into the main QHBoxLayout. If we add some widgets to the QVBoxLayout, they’ll be arranged vertically in the first slot of the parent layout.

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout1 = QHBoxLayout()
        layout2 = QVBoxLayout()
        layout3 = QVBoxLayout()

        layout2.addWidget(Color('red'))
        layout2.addWidget(Color('yellow'))
        layout2.addWidget(Color('purple'))

        layout1.addLayout( layout2 )

        layout1.addWidget(Color('green'))

        layout3.addWidget(Color('red'))
        layout3.addWidget(Color('purple'))

        layout1.addLayout( layout3 )

        widget = QWidget()
        widget.setLayout(layout1)
        self.setCentralWidget(widget)

You can set the spacing around the layout using .setContentMargins or set the spacing between elements using .setSpacing.

layout1.setContentsMargins(0,0,0,0)
layout1.setSpacing(20)

The following code shows the combination of nested widgets and layout margins and spacing. Experiment with the numbers til you get a feel for them.

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout1 = QHBoxLayout()
        layout2 = QVBoxLayout()
        layout3 = QVBoxLayout()

        layout1.setContentsMargins(0,0,0,0)
        layout1.setSpacing(20)

        layout2.addWidget(Color('red'))
        layout2.addWidget(Color('yellow'))
        layout2.addWidget(Color('purple'))

        layout1.addLayout( layout2 )

        layout1.addWidget(Color('green'))

        layout3.addWidget(Color('red'))
        layout3.addWidget(Color('purple'))

        layout1.addLayout( layout3 )

        widget = QWidget()
        widget.setLayout(layout1)
        self.setCentralWidget(widget)

QGridLayout widgets arranged in a grid

As useful as they are, if you try and using QVBoxLayout and QHBoxLayout for laying out multiple elements, e.g. for a form, you’ll find it very difficult to ensure differently sized widgets line up. The solution to this is QGridLayout.

QGridLayout allows you to position items specifically in a grid. You specify row and column positions for each widget. You can skip elements, and they will be left empty.

Usefully, for QGridLayout you don't need to fill all the positions in the grid.

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        layout = QGridLayout()

        layout.addWidget(Color('red'), 0, 0)
        layout.addWidget(Color('green'), 1, 0)
        layout.addWidget(Color('blue'), 1, 1)
        layout.addWidget(Color('purple'), 2, 1)

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

QStackedLayout multiple widgets in the same space

The final layout we’ll cover is the QStackedLayout. As described, this layout allows you to position elements directly in front of one another. You can then select which widget you want to show. You could use this for drawing layers in a graphics application, or for imitating a tab-like interface. Note there is also QStackedWidget which is a container widget that works in exactly the same way. This is useful if you want to add a stack directly to a QMainWindow with .setCentralWidget.

from PySide6.QtWidgets import QStackedLayout  # add this import


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        layout = QStackedLayout()

        layout.addWidget(Color("red"))
        layout.addWidget(Color("green"))
        layout.addWidget(Color("blue"))
        layout.addWidget(Color("yellow"))

        layout.setCurrentIndex(3)

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

QStackedWidget is exactly how tabbed views in applications work. Only one view ('tab') is visible at any one time. You can control which widget to show at any time by using .setCurrentIndex() or .setCurrentWidget() to set the item by either the index (in order the widgets were added) or by the widget itself.

Below is a short demo using QStackedLayout in combination with QButton to to provide a tab-like interface to an application:

import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QPushButton,
    QStackedLayout,
    QVBoxLayout,
    QWidget,
)
from PySide6.QtGui import QPalette, QColor

class Color(QWidget):

    def __init__(self, color):
        super(Color, self).__init__()
        self.setAutoFillBackground(True)

        palette = self.palette()
        palette.setColor(QPalette.Window, QColor(color))
        self.setPalette(palette)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        pagelayout = QVBoxLayout()
        button_layout = QHBoxLayout()
        self.stacklayout = QStackedLayout()

        pagelayout.addLayout(button_layout)
        pagelayout.addLayout(self.stacklayout)

        btn = QPushButton("red")
        btn.pressed.connect(self.activate_tab_1)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("red"))

        btn = QPushButton("green")
        btn.pressed.connect(self.activate_tab_2)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("green"))

        btn = QPushButton("yellow")
        btn.pressed.connect(self.activate_tab_3)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("yellow"))

        widget = QWidget()
        widget.setLayout(pagelayout)
        self.setCentralWidget(widget)

    def activate_tab_1(self):
        self.stacklayout.setCurrentIndex(0)

    def activate_tab_2(self):
        self.stacklayout.setCurrentIndex(1)

    def activate_tab_3(self):
        self.stacklayout.setCurrentIndex(2)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

Helpfully. Qt actually provide a built-in TabWidget that provides this kind of layout out of the box - albeit in widget form. Below the tab demo is recreated using QTabWidget:

import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QTabWidget,
    QWidget,
)

from layout_colorwidget import Color


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        tabs = QTabWidget()
        tabs.setTabPosition(QTabWidget.West)
        tabs.setMovable(True)

        for n, color in enumerate(["red", "green", "blue", "yellow"]):
            tabs.addTab(Color(color), color)

        self.setCentralWidget(tabs)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

As you can see, it's a little more straightforward — and a bit more attractive! You can set the position of the tabs using the cardinal directions, toggle whether tabs are moveable with .setMoveable. You'll notice that the macOS tab bar looks quite different to the others -- by default on macOS tabs take on a pill or bubble style. On macOS this is typically used for tabbed configuration panels. For documents, you can turn on document mode to give slimline tabs similar to what you see on other platforms. This option has no effect on other platforms.

tabs = QTabWidget()
tabs.setDocumentMode(True)

Toolbars & Menus

Next we'll look at some of the common user interface elements, that you've probably seen in many other applications — toolbars and menus. We'll also explore the neat system Qt provides for minimising the duplication between different UI areas — QAction.

Toolbars

One of the most commonly seen user interface elements is the toolbar. Toolbars are bars of icons and/or text used to perform common tasks within an application, for which accessing via a menu would be cumbersome. They are one of the most common UI features seen in many applications. While some complex applications, particularly in the Microsoft Office suite, have migrated to contextual 'ribbon' interfaces, the standard toolbar is usually sufficient for the majority of applications you will create.

We'll start with a simple skeleton application, which we can customize. Save the following code in a file named app.py -- this code all the imports you'll need for the later steps.

import sys
from PySide6.QtWidgets import (
    QMainWindow, QApplication,
    QLabel, QToolBar, QStatusBar
)
from PySide6.QtGui import QAction, QIcon
from PySide6.QtCore import Qt

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()

Creating applications with Qt Designer

QT Designer

Using your generated .ui file

We've created a very simple UI. The next step is to get this into Python and use it to construct a working application.

First save your .ui file — by default it will save at the location you chosen while creating it, although you can choose another location if you like.

The .ui file is in XML format. To use our UI from Python we have two alternative methods available —

  1. load into into a class using the .loadUI() method

  2. convert it to Python using the pyside6-uic tool.

These two approaches are covered below. Personally I prefer to convert the UI to a Python file to keep things similar from a programming & packaging point of view.

Loading the .ui file directly

To load .ui files in PySide6 we first create a QUiLoader instance and then call the loader.load() method to load the UI file.

import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtUiTools import QUiLoader

loader = QUiLoader()
app = QtWidgets.QApplication(sys.argv)
window = loader.load("mainwindow.ui", None)
window.show()
app.exec()

The second parameter to the loader.load() method is for the parent of the widget you're creating.

The PySide6 loader does not allow you to apply a UI layout to an existing widget. This prevents you adding custom code for the initialization of the widget in a class __init__ block. However, you can work around this with a separate init function.

import sys
from PySide6 import QtWidgets
from PySide6.QtUiTools import QUiLoader

loader = QUiLoader()

def mainwindow_setup(w):
    w.setWindowTitle("MainWindow Title")

app = QtWidgets.QApplication(sys.argv)

window = loader.load("mainwindow.ui", None)
mainwindow_setup(window)
window.show()
app.exec_()

You can create a standalone class which handles loading of the UI files, creating and customized the windows and contains any business logic for your application.

Converting your .ui file to Python

Instead of importing your .uic files into your application directly, you can instead convert them into Python code and then import them like any other module. To generate a Python output file run pyside6-uic from the command line, passing the .ui file and the target file for output, with a -o parameter. The following will generate a Python file named MainWindow.py which contains our created UI.

$bash:PySide6 pyside6-uic mainwindow.ui -o MainWindow.py

To create the main window in your application, create a class as normal but subclassing from both QMainWindow and your imported Ui_MainWindow class. Finally, call self.setupUi(self) from within the __init__ to trigger the setup of the interface.

import sys
from PySide6 import QtWidgets

from MainWindow import Ui_MainWindow

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupUi(self)


app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()
app.exec()

QResource

The example below shows a single button, which when pressed changes it's icon.

import sys

from PySide6 import QtGui, QtWidgets

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")
        self.button = QtWidgets.QPushButton("My button")

        icon = QtGui.QIcon("animal-penguin.png")
        self.button.setIcon(icon)
        self.button.clicked.connect(self.change_icon)

        self.setCentralWidget(self.button)

        self.show()

    def change_icon(self):
        icon = QtGui.QIcon("animal-monkey.png")
        self.button.setIcon(icon)



app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec_()

Download and place the animal-penguin.png and animal-monkey.png icons in the same folder as the script, and run it again. These icons are from the Fugue icon set by Yusuke Kamiyamane.

The QRC file

The core of the Qt Resources system is the resource file or QRC. The .qrc file is a simple XML file, which can be opened in any text editor.

Simple QRC example

A very simple resource file is shown below, containing a single resource (a single icon animal-penguin.png we might add to a button).

<!DOCTYPE RCC>
<RCC version="1.0">
    <qresource prefix="icons">
        <file alias="animal-penguin.png">animal-penguin.png</file>
        <file alias="animal-monkey.png">animal-monkey.png</file>
    </qresource>
</RCC>

The name between the <file> </file> tags is the path to the file, relative to the resource file. The alias is the name which this resource will be known by from within your application. You can use this rename icons to something more logical or simpler in your app, while keeping the original name externally.

For example, if we want to use the name penguin.png internally, we can change these lines to.

<file alias="penguin.png">animal-penguin.png</file>
<file alias="monkey.png">animal-monkey.png</file>

Outside this tag is the qresource tag which specifies a prefix. This is a namespace which can be used to group resources together. This is effectively a virtual folder, under which nested resources can all be found. With our current structure, our two icons are grouped under the virtual folder icons/.

Save the following file as resources.qrc, this is our resource file which we'll use from now on.

<!DOCTYPE RCC>
<RCC version="1.0">
    <qresource prefix="icons">
        <file alias="penguin.png">animal-penguin.png</file>
        <file alias="monkey.png">animal-monkey.png</file>
    </qresource>
</RCC>

Using a QRC file

To use a .qrc file in your application you first need to compile it to Python. PySide comes with a command line tool to do this, which takes a .qrc file as input and outputs a Python file containing the compiled data. This can then be imported into your app as for any other Python file or module.

To compile our resources.qrc file to a Python file named resources.py we can use --

pyside6-rcc resources.qrc -o resources.py

To use the resource file in our application we need to make a few small changes. Firstly, we need to import resources at the top of our app, to load the resources into the Qt resource system, and then secondly we need to update the path to the icon file to use the resource path format as follows:

The prefix :/ indicates that this is a resource path. The first name "icons" is the prefix namespace and the filename is taken from the file alias, both as defined in our resources.qrc file.

The updated application is shown below.

import sys

from PySide6 import QtGui, QtWidgets

import resources

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")
        self.button = QtWidgets.QPushButton("My button")

        icon = QtGui.QIcon(":/icons/penguin.png")
        self.button.setIcon(icon)
        self.button.clicked.connect(self.change_icon)

        self.setCentralWidget(self.button)

        self.show()

    def change_icon(self):
        icon = QtGui.QIcon(":/icons/monkey.png")
        self.button.setIcon(icon)


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec()

Threads & Processes

Multithreading with QThreadPool

Background

Applications based on Qt (like most GUI applications) are event based. This means that execution is driven in response to user interaction, signals and timers. In an event-driven application, clicking on a button creates an event which your application subsequently handles to produce some expected output. Events are pushed onto and taken off an event queue and processed sequentially.

app = QApplication([])
window = MainWindow()
app.exec()

The event loop is started by calling .exec_() on your QApplication object and runs within the same thread as your Python code. The thread which runs this event loop — commonly referred to as the GUI thread — also handles all window communication with the host operating system.

By default, any execution triggered by the event loop will also run synchronously within this thread. In practise this means that any time your PySide6 application spends doing something in your code, window communication and GUI interaction are frozen.

If what you're doing is simple, and returns control to the GUI loop quickly, this freeze will be imperceptible to the user. However, if you need to perform longer-running tasks, for example opening/writing a large file, downloading some data, or rendering some complex image, there are going to be problems. To your user the application will appear to be unresponsive (because it is). Because your app is no longer communicating with the OS, on MacOS X if you click on your app you will see the spinning wheel of death. And, nobody wants that.

The solution is simple: get your work out of the GUI thread (and into another thread). PySide6 (via Qt) provides an straightforward interface to do exactly that.

Threads and Processes

If you take a step back and think about what you want to happen in your application, it can probably be summed up with "stuff to happen at the same time as other stuff happens".

There are two main approaches to running independent tasks within a PySide application: threads and processes.

Threads share the same memory space, so are quick to start up and consume minimal resources. The shared memory makes it trivial to pass data between threads, however reading/writing memory from different threads can lead to race conditions or segfaults. In a Python GUI there is the added issue that multiple threads are bound by the same Global Interpreter Lock (GIL) — meaning non-GIL-releasing Python code can only execute in one thread at a time. However, this is not a major issue with PySide where most of the time is spent outside of Python.

Processes use separate memory space (and an entirely separate Python interpreter). This side-steps any potential problems with the GIL, but at the cost of slower start-up times, larger memory overhead and complexity in sending/receiving data.

For simplicity's sake it usually makes sense to use threads, unless you have a good reason to use processes (see caveats later). Subprocesses in Qt are better suited to running and communicating with external programs.

QRunnable and the QThreadPool

Qt provides a very simple interface for running jobs in other threads, which is exposed nicely in PySide. This is built around two classes: QRunnable and QThreadPool. The former is the container for the work you want to perform, while the latter is the method by which you pass that work to alternate threads.

The neat thing about using QThreadPool is that it handles queuing and execution of workers for you. Other than queuing up jobs and retreiving the results there is not very much to do at all.

To define a custom QRunnable you can subclass the base QRunnable class, then place the code you wish you execute within the run() method. The following is an implementation of our long running time.sleep job as a QRunnable. Add the following code to multithread.py, above the MainWindow class definition.

from PySide6.QtCore import QRunnable, Slot, QThreadPool

class Worker(QRunnable):
    '''
    Worker thread
    '''

    @Slot()  # QtCore.Slot
    def run(self):
        '''
        Your code goes in this function
        '''
        print("Thread start")
        time.sleep(5)
        print("Thread complete")

Executing our function in another thread is simply a matter of creating an instance of the Worker and then pass it to our QThreadPool instance and it will be executed automatically.

Next add the following within the __init__ block, to set up our thread pool.

self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())

Finally, add the following lines to our oh_no function.

def oh_no(self):
    worker = Worker()
    self.threadpool.start(worker)

Now, clicking on the button will create a worker to handle the (long-running) process and spin that off into another thread via thread pool. If there are not enough threads available to process incoming workers, they'll be queued and executed in order at a later time.

Try it out and you'll see that your application now handles you bashing the button with no problems.

Check what happens if you hit the button multiple times. You should see your threads executed immediately up to the number reported by .maxThreadCount. If you hit the button again after there are already this number of active workers, the subsequent workers will be queued until a thread becomes available.

Improved QRunnables

If you want to pass custom data into the execution function you can do so via the init, and then have access to the data via self. from within the run slot.

class Worker(QRunnable):
    '''
    Worker thread

    :param args: Arguments to make available to the run code
    :param kwargs: Keywords arguments to make available to the run code

    '''

    def __init__(self, *args, **kwargs):
        super(Worker, self).__init__()
        self.args = args
        self.kwargs = kwargs

    @Slot()  # QtCore.Slot
    def run(self):
        '''
        Initialise the runner function with passed self.args, self.kwargs.
        '''
        print(args, kwargs)

In fact, we can take avantage of the fact that in Python functions are objects and pass in the function to execute rather than subclassing each time. In the following construction we only require a single Worker class to handle all of our execution jobs.

class Worker(QRunnable):
    '''
    Worker thread

    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

    :param callback: The function callback to run on this worker thread. Supplied args and
                     kwargs will be passed through to the runner.
    :type callback: function
    :param args: Arguments to pass to the callback function
    :param kwargs: Keywords to pass to the callback function

    '''

    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs

    @Slot()  # QtCore.Slot
    def run(self):
        '''
        Initialise the runner function with passed args, kwargs.
        '''
        self.fn(*self.args, **self.kwargs)

You can now pass in any Python function and have it executed in a separate thread.

def execute_this_fn(self):
    print("Hello!")

def oh_no(self):
    # Pass the function to execute
    worker = Worker(self.execute_this_fn) # Any other args, kwargs are passed to the run function

    # Execute
    self.threadpool.start(worker)

Thread IO

Sometimes it's helpful to be able to pass back state and data from running workers. This could include the outcome of calculations, raised exceptions or ongoing progress (think progress bars). Qt provides the signals and slots framework which allows you to do just that and is thread-safe, allowing safe communication directly from running threads to your GUI frontend. Signals allow you to .emit values, which are then picked up elsewhere in your code by slot functions which have been linked with .connect.

Below is a simple WorkerSignals class defined to contain a number of example signals.

import traceback, sys

class WorkerSignals(QObject):
    '''
    Defines the signals available from a running worker thread.

    Supported signals are:

    finished
        No data

    error
        tuple (exctype, value, traceback.format_exc() )

    result
        object data returned from processing, anything

    '''
    finished = Signal()  # QtCore.Signal
    error = Signal(tuple)
    result = Signal(object)

In this example we've defined 3 custom signals:

  1. finished signal, with no data to indicate when the task is complete.

  2. error signal which receives a tuple of Exception type, Exception value and formatted traceback.

  3. result signal receiving any object type from the executed function.

You may not find a need for all of these signals, but they are included to give an indication of what is possible. In the following code we're going to implement a long-running task that makes use of these signals to provide useful information to the user.

class Worker(QRunnable):
    '''
    Worker thread

    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

    :param callback: The function callback to run on this worker thread. Supplied args and
                     kwargs will be passed through to the runner.
    :type callback: function
    :param args: Arguments to pass to the callback function
    :param kwargs: Keywords to pass to the callback function

    '''

    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

    @Slot()  # QtCore.Slot
    def run(self):
        '''
        Initialise the runner function with passed args, kwargs.
        '''

        # Retrieve args/kwargs here; and fire processing using them
        try:
            result = self.fn(
                *self.args, **self.kwargs
                status=self.signals.status
                progress=self.signals.progress
            )
        except:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)  # Return the result of the processing
        finally:
            self.signals.finished.emit()  # Done

You can connect your own handler functions to these signals to receive notification of completion (or the result) of threads.

def execute_this_fn(self):
    for n in range(0, 5):
        time.sleep(1)
    return "Done."

def print_output(self, s):
    print(s)

def thread_complete(self):
    print("THREAD COMPLETE!")

def oh_no(self):
    # Pass the function to execute
    worker = Worker(self.execute_this_fn) # Any other args, kwargs are passed to the run function
    worker.signals.result.connect(self.print_output)
    worker.signals.finished.connect(self.thread_complete)

    # Execute
    self.threadpool.start(worker)

You also often want to receive status information from long-running threads. This can be done by passing in callbacks to which your running code can send the information. You have two options here: either define new signals (allowing the handling to be performed using the event loop) or use a standard Python function.

In both cases you'll need to pass these callbacks into your target function to be able to use them. The signal-based approach is used in the completed code below, where we pass an int back as an indicator of the thread's % progress.

The complete code

from PySide6.QtWidgets import QVBoxLayout, QLabel, QPushButton, QWidget, QMainWindow, QApplication
from PySide6.QtCore import QTimer, QRunnable, Slot, Signal, QObject, QThreadPool

import sys
import time
import traceback


class WorkerSignals(QObject):
    '''
    Defines the signals available from a running worker thread.

    Supported signals are:

    finished
        No data

    error
        tuple (exctype, value, traceback.format_exc() )

    result
        object data returned from processing, anything

    progress
        int indicating % progress

    '''
    finished = Signal()
    error = Signal(tuple)
    result = Signal(object)
    progress = Signal(int)




class Worker(QRunnable):
    '''
    Worker thread

    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

    :param callback: The function callback to run on this worker thread. Supplied args and
                     kwargs will be passed through to the runner.
    :type callback: function
    :param args: Arguments to pass to the callback function
    :param kwargs: Keywords to pass to the callback function

    '''

    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()

        # Store constructor arguments (re-used for processing)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

        # Add the callback to our kwargs
        self.kwargs['progress_callback'] = self.signals.progress

    @Slot()
    def run(self):
        '''
        Initialise the runner function with passed args, kwargs.
        '''

        # Retrieve args/kwargs here; and fire processing using them
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)  # Return the result of the processing
        finally:
            self.signals.finished.emit()  # Done



class MainWindow(QMainWindow):


    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.counter = 0

        layout = QVBoxLayout()

        self.l = QLabel("Start")
        b = QPushButton("DANGER!")
        b.pressed.connect(self.oh_no)

        layout.addWidget(self.l)
        layout.addWidget(b)

        w = QWidget()
        w.setLayout(layout)

        self.setCentralWidget(w)

        self.show()

        self.threadpool = QThreadPool()
        print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())

        self.timer = QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.recurring_timer)
        self.timer.start()

    def progress_fn(self, n):
        print("%d%% done" % n)

    def execute_this_fn(self, progress_callback):
        for n in range(0, 5):
            time.sleep(1)
            progress_callback.emit(n*100/4)

        return "Done."

    def print_output(self, s):
        print(s)

    def thread_complete(self):
        print("THREAD COMPLETE!")

    def oh_no(self):
        # Pass the function to execute
        worker = Worker(self.execute_this_fn) # Any other args, kwargs are passed to the run function
        worker.signals.result.connect(self.print_output)
        worker.signals.finished.connect(self.thread_complete)
        worker.signals.progress.connect(self.progress_fn)

        # Execute
        self.threadpool.start(worker)


    def recurring_timer(self):
        self.counter +=1
        self.l.setText("Counter: %d" % self.counter)


app = QApplication(sys.argv)
window = MainWindow()
app.exec()

QProcess

Using QProcess to execute external applications.

Executing external programs is fairly straightforward with QProcess. First you create a QProcess object and then call .start() passing in the command to execute and a list of string arguments.

p = QProcess()
p.start("<program>", [<arguments>])

For our example we're running the custom dummy_script.py script with Python, so our executable is python (or python3) and our arguments are just dummy_script.py.

p = QProcess()
p.start("python3", ['dummy_script.py'])

If you are running another command line program you'd need to specify arguments for it. For example, using ffmpeg to extract information from a video file.

p = QProcess()
p.start("ffprobe", ['-show_format', '-show_streams', 'a.mp4.py'])

Use this same approach with your own command line program, remembering to split the arguments up into individual items in the list.

We can take the p.start("python3", ['dummy_script.py']) example and add it to our application skeleton as follows. We also add a helper method message() to write messages into our text box in the UI.

This is because if you press the button while a process is already running, creating the new process replaces the reference to the existing QProcess object in self.p, deleting it. We can avoid this by checking the value of self.p before executing a new process, and hooking up a finished signal to reset it back to None, e.g.

from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton, QPlainTextEdit,
                                QVBoxLayout, QWidget)
from PySide6.QtCore import QProcess
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.p = None

        self.btn = QPushButton("Execute")
        self.btn.pressed.connect(self.start_process)
        self.text = QPlainTextEdit()
        self.text.setReadOnly(True)

        l = QVBoxLayout()
        l.addWidget(self.btn)
        l.addWidget(self.text)

        w = QWidget()
        w.setLayout(l)

        self.setCentralWidget(w)

    def message(self, s):
        self.text.appendPlainText(s)

    def start_process(self):
        if self.p is None:  # No process running.
            self.message("Executing process")
            self.p = QProcess()  # Keep a reference to the QProcess (e.g. on self) while it's running.
            self.p.finished.connect(self.process_finished)  # Clean up once complete.
            self.p.start("python3", ['dummy_script.py'])

    def process_finished(self):
        self.message("Process finished.")
        self.p = None


app = QApplication(sys.argv)

w = MainWindow()
w.show()

app.exec()

Getting data from the QProcess

So far we've executed an external program and been notified when it started and stopped, but know nothing about what it's doing. This is fine in some cases, where you just want the job to run, but often you'll want some more detailed feedback. Helpfully, QProcess provides a number of signals which can be used to track the progress and state of processes.

If you're familiar with running external processes using subprocess in Python, you may be familiar with streams. These are file-like objects you use to retrieve data from a running process. The two standard streams are standard output and standard error. The former receives result data the application is outputting, while the second receives diagnostic or error messages. Depending on what you're interested in, both of these can be useful -- many programs (like our dummy_script.py output progress information to the standard error stream.

In Qt land, the same principles apply. The QProcess object has two signals .readyReadStandardOutput and .readyReadStandardError which are used to notify when data is available in the respective streams. We can then read from the process to get the latest data.

Below is an example setup for a QProcess, which connects up readyReadStandardOutput and .readyReadStandardError as well as tracking state changes and finish signals.

p = QProcess()
p.readyReadStandardOutput.connect(self.handle_stdout)
p.readyReadStandardError.connect(self.handle_stderr)
p.stateChanged.connect(self.handle_state)
p.finished.connect(self.cleanup)
p.start("python", ["dummy_script.py"])

The .stateChanged signal fires whenever the process status changes. Valid values -- defined in the QProcess.ProcessState enum -- are shown below.

Putting that into our example and implementing the handler methods for each gives us the following complete code.

from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton, QPlainTextEdit,
                                QVBoxLayout, QWidget)
from PySide6.QtCore import QProcess
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.p = None

        self.btn = QPushButton("Execute")
        self.btn.pressed.connect(self.start_process)
        self.text = QPlainTextEdit()
        self.text.setReadOnly(True)

        l = QVBoxLayout()
        l.addWidget(self.btn)
        l.addWidget(self.text)

        w = QWidget()
        w.setLayout(l)

        self.setCentralWidget(w)

    def message(self, s):
        self.text.appendPlainText(s)

    def start_process(self):
        if self.p is None:  # No process running.
            self.message("Executing process")
            self.p = QProcess()  # Keep a reference to the QProcess (e.g. on self) while it's running.
            self.p.readyReadStandardOutput.connect(self.handle_stdout)
            self.p.readyReadStandardError.connect(self.handle_stderr)
            self.p.stateChanged.connect(self.handle_state)
            self.p.finished.connect(self.process_finished)  # Clean up once complete.
            self.p.start("python3", ['dummy_script.py'])

    def handle_stderr(self):
        data = self.p.readAllStandardError()
        stderr = bytes(data).decode("utf8")
        self.message(stderr)

    def handle_stdout(self):
        data = self.p.readAllStandardOutput()
        stdout = bytes(data).decode("utf8")
        self.message(stdout)

    def handle_state(self, state):
        states = {
            QProcess.NotRunning: 'Not running',
            QProcess.Starting: 'Starting',
            QProcess.Running: 'Running',
        }
        state_name = states[state]
        self.message(f"State changed: {state_name}")

    def process_finished(self):
        self.message("Process finished.")
        self.p = None


app = QApplication(sys.argv)

w = MainWindow()
w.show()

app.exec()

If you run this, you'll see the standard output, standard error, state changes and start/stop messages all being printed to the text box. Note that we convert the states back to friendly strings before output (using a dict to map from the enum values).

The output handling is a bit tricky and deserves a closer look.

data = self.p.readAllStandardError()
stderr = bytes(data).decode("utf8")
self.message(stderr)

This is necessary because the QProcess.readAllStandardError and QProcess.readAllStandardOutput return data as bytes, wrapped in a Qt object. We must first convert this to a Python bytes() object, and then decode that bytestream to a string (here using UTF8 encoding).

Parsing data from process output

Currently we're just dumping the output from the program into the text box, but what if we wanted to extract some specific data from it? A common use case for this is to track progress from a running program, so we can show a progress bar as it completes.

In this example, our demo script dummy_script.py return a series of strings, including lines on standard error which show the current progress complete percentage -- e.g. Total complete: 50%. We can process these lines to a progress, and show this on the statusbar.

In the example below, we extract this using a custom regular expression. The simple_percent_parser function matches the standard error stream content and extracts a number between 00-100 for the progress. This value is used to update the progress bar added to the UI.

from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton, QPlainTextEdit,
                                QVBoxLayout, QWidget, QProgressBar)
from PySide6.QtCore import QProcess
import sys
import re

# A regular expression, to extract the % complete.
progress_re = re.compile("Total complete: (\d+)%")

def simple_percent_parser(output):
    """
    Matches lines using the progress_re regex,
    returning a single integer for the % progress.
    """
    m = progress_re.search(output)
    if m:
        pc_complete = m.group(1)
        return int(pc_complete)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.p = None

        self.btn = QPushButton("Execute")
        self.btn.pressed.connect(self.start_process)
        self.text = QPlainTextEdit()
        self.text.setReadOnly(True)

        self.progress = QProgressBar()
        self.progress.setRange(0, 100)

        l = QVBoxLayout()
        l.addWidget(self.btn)
        l.addWidget(self.progress)
        l.addWidget(self.text)

        w = QWidget()
        w.setLayout(l)

        self.setCentralWidget(w)

    def message(self, s):
        self.text.appendPlainText(s)

    def start_process(self):
        if self.p is None:  # No process running.
            self.message("Executing process")
            self.p = QProcess()  # Keep a reference to the QProcess (e.g. on self) while it's running.
            self.p.readyReadStandardOutput.connect(self.handle_stdout)
            self.p.readyReadStandardError.connect(self.handle_stderr)
            self.p.stateChanged.connect(self.handle_state)
            self.p.finished.connect(self.process_finished)  # Clean up once complete.
            self.p.start("python3", ['dummy_script.py'])

    def handle_stderr(self):
        data = self.p.readAllStandardError()
        stderr = bytes(data).decode("utf8")
        # Extract progress if it is in the data.
        progress = simple_percent_parser(stderr)
        if progress:
            self.progress.setValue(progress)
        self.message(stderr)

    def handle_stdout(self):
        data = self.p.readAllStandardOutput()
        stdout = bytes(data).decode("utf8")
        self.message(stdout)

    def handle_state(self, state):
        states = {
            QProcess.NotRunning: 'Not running',
            QProcess.Starting: 'Starting',
            QProcess.Running: 'Running',
        }
        state_name = states[state]
        self.message(f"State changed: {state_name}")

    def process_finished(self):
        self.message("Process finished.")
        self.p = None


app = QApplication(sys.argv)

w = MainWindow()
w.show()

app.exec()

Graphics & Plotting

PyQtGraph

To be able to use PyQtGraph with PySide you first need to install the package to your Python environment. You can do this as

$pip install pyqtgraph

Creating a PyQtGraph widget

In PyQtGraph all plots are created using the PlotWidget widget. This widget provides a contained canvas on which plots of any type can be added and configured. Under the hood, this plot widget uses Qt native QGraphicsScene meaning it fast and efficient yet simple to integrate with the rest of your app. You can create a PlotWidget as for any other widget.

from PySide6.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
import sys

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature = [30,32,34,32,33,31,29,32,35,45]

        # plot data: x, y values
        self.graphWidget.plot(hour, temperature)


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()

Styling plots

PyQtGraph uses Qt's QGraphicsScene to render the graphs. This gives us access to all the standard Qt line and shape styling options for use in plots. However, PyQtGraph provides an API for using these to draw plots and manage the plot canvas.

Below we'll go through the most common styling features you'll need to create and customize your own plots.

Background Colour

Beginning with the app skeleton above, we can change the background colour by calling .setBackground on our PlotWidget instance (in self.graphWidget). The code below will set the background to white, by passing in the string 'w'.

self.graphWidget.setBackground('w')

You can set (and update) the background colour of the plot at any time.

from PySide6.QtWidgets import QMainWindow, QApplication
import pyqtgraph as pg
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature = [30,32,34,32,33,31,29,32,35,45]

        self.graphWidget.setBackground('w')
        self.graphWidget.plot(hour, temperature)


app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

There are a number of simple colours available using single letters, based on the standard colours used in matplotlib. They're pretty unsurprising -- r is red, b is blue -- except that 'k' is used for black. In addition to these single letter codes, you can also set more complex colours using hex notation eg. #672922 as a string.

self.graphWidget.setBackground('#bbccaa')     

RGB and RGBA values can be passed in as a 3-tuple or 4-tuple respectively, using values 0-255.

self.graphWidget.setBackground((100,50,255))      # RGB each 0-255
self.graphWidget.setBackground((100,50,255,25))   # RGBA (A = alpha opacity)

Line Colour, Width & Style

Lines in PyQtGraph are drawn using standard Qt QPen types. This gives you the same full control over line drawing as you would have in any other QGraphicsScene drawing. To use a pen to plot a line, you simply create a new QPen instance and pass it into the plot method.

Below we create a QPen object, passing in a 3-tuple of int values specifying an RGB value (of full red). We could also define this by passing 'r', or a QColor object. Then we pass this into plot with the pen parameter.

pen = pg.mkPen(color=(255, 0, 0))
self.graphWidget.plot(hour, temperature, pen=pen)

The standard Qt line styles can all be used, including Qt.SolidLine, Qt.DashLine, Qt.DotLine, Qt.DashDotLine and Qt.DashDotDotLine. Examples of each of these lines are shown in the image below, and you can read more in the Qt Documentation.

Line Markers

For many plots it can be helpful to place markers in addition or instead of lines on the plot. To draw a marker on the plot, pass the symbol to use as a marker when calling .plot as shown below.

self.graphWidget.plot(hour, temperature, symbol='+')

In addition to symbol you can also pass in symbolSize, symbolBrush and symbolPen parameters. The value passed as symbolBrush can be any colour, or QBrush type, while symbolPen can be passed any colour or a QPen instance. The pen is used to draw the outline of the shape, while brush is used for the fill.

For example the below code will give a blue cross marker of size 30, on a thick red line.

pen = pg.mkPen(color=(255, 0, 0), width=15, style=QtCore.Qt.DashLine)
self.graphWidget.plot(hour, temperature, pen=pen, symbol='+', symbolSize=30, symbolBrush=('b'))

Plot Titles

Chart titles are important to provide context to what is shown on a given chart. In PyQtGraph you can add a main plot title using the setTitle() method on the PlotWidget, passing in your title string.

self.graphWidget.setTitle("Your Title Here")

You can apply text styles, including colours, font sizes and weights to your titles (and any other labels in PyQtGraph) by passing additional arguments. The available style arguments are shown below.

The code below sets the color to blue with a font size of 30px.

self.graphWidget.setTitle("Your Title Here", color="b", size="30pt")

Axis Labels

Similar to titles, we can use the setLabel() method to create our axis titles. This requires two parameters, position and text. The position can be any one of 'left,'right','top','bottom' which describe the position of the axis on which the text is placed. The 2nd parameter text is the text you want to use for the label.

You can pass additional style parameters into the method. These differ slightly than for the title, in that they need to be valid CSS name-value pairs. For example, the size is now font-size. Because the name font-size has a hyphen in it, you cannot pass it directly as a parameter, but must use the **dictionary method.

styles = {'color':'r', 'font-size':'20px'}
self.graphWidget.setLabel('left', 'Temperature (°C)', **styles)
self.graphWidget.setLabel('bottom', 'Hour (H)', **styles)

Legends

In addition to the axis and plot titles you will often want to show a legend identifying what a given line represents. This is particularly important when you start adding multiple lines to a plot. Adding a legend to a plot can be accomplished by calling .addLegend on the PlotWidget, however before this will work you need to provide a name for each line when calling .plot().

The example below assigns a name "Sensor 1" to the line we are plotting with .plot(). This name will be used to identify the line in the legend.

self.graphWidget.plot(hour, temperature, name = "Sensor 1",  pen = NewPen, symbol='+', symbolSize=30, symbolBrush=('b'))
self.graphWidget.addLegend()

Background Grid

Adding a background grid can make your plots easier to read, particularly when trying to compare relative x & y values against each other. You can turn on a background grid for your plot by calling .showGrid on your PlotWidget. You can toggle x and y grids independently.

The following with create the grid for both the X and Y axis.

self.graphWidget.showGrid(x=True, y=True)

Plotting multiple lines

It is common for plots to involve more than one line. In PyQtGraph this is as simple as calling .plot() multiple times on the same PlotWidget. In the following example we're going to plot two lines of similar data, using the same line styles, thicknesses etc. for each, but changing the line colour.

To simplify this we can create our own custom plot method on our MainWindow. This accepts x and y parameters to plot, the name of the line (for the legend) and a colour. We use the colour for both the line and marker colour.

 def plot(self, x, y, plotname, color):
        pen = pg.mkPen(color=color)
        self.graphWidget.plot(x, y, name=plotname, pen=pen, symbol='+', symbolSize=30, symbolBrush=(color))

Code

from PySide6.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature_1 = [30,32,34,32,33,31,29,32,35,45]
        temperature_2 = [50,35,44,22,38,32,27,38,32,44]

        #Add Background colour to white
        self.graphWidget.setBackground('w')
        # Add Title
        self.graphWidget.setTitle("Your Title Here", color="b", size="30pt")
        # Add Axis Labels
        styles = {"color": "#f00", "font-size": "20px"}
        self.graphWidget.setLabel("left", "Temperature (°C)", **styles)
        self.graphWidget.setLabel("bottom", "Hour (H)", **styles)
        #Add legend
        self.graphWidget.addLegend()
        #Add grid
        self.graphWidget.showGrid(x=True, y=True)
        #Set Range
        self.graphWidget.setXRange(0, 10, padding=0)
        self.graphWidget.setYRange(20, 55, padding=0)

        self.plot(hour, temperature_1, "Sensor1", 'r')
        self.plot(hour, temperature_2, "Sensor2", 'b')

    def plot(self, x, y, plotname, color):
        pen = pg.mkPen(color=color)
        self.graphWidget.plot(x, y, name=plotname, pen=pen, symbol='+', symbolSize=30, symbolBrush=(color))


app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

Clearing the plot

Finally, sometimes you might want to clear and refresh the plot periodically. You can easily do that by calling .clear().

self.graphWidget.clear()

Matplotlib

The following minimal example sets up a Matplotlib canvas FigureCanvasQTAgg which creates the Figure and adds a single set of axes to it. This canvas object is also a QWidget and so can be embedded straight into an application as any other Qt widget.

import sys
import matplotlib

matplotlib.use('Qt5Agg')

from PySide6.QtWidgets import QMainWindow, QApplication

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # Create the maptlotlib FigureCanvas object,
        # which defines a single set of axes as self.axes.
        sc = MplCanvas(self, width=5, height=4, dpi=100)
        sc.axes.plot([0,1,2,3,4], [10,1,20,3,40])
        self.setCentralWidget(sc)

        self.show()


app = QApplication(sys.argv)
w = MainWindow()
app.exec()

In this case we're adding our MplCanvas widget as the central widget on the window with .setCentralWidget(). This means it will take up the entirety of the window and resize together with it. The plotted data [0,1,2,3,4], [10,1,20,3,40] is provided as two lists of numbers (x and y respectively) as required by the .plot method.

Plot controls

Plots from Matplotlib displayed in PySide are actually rendered as simple (bitmap) images by the Agg backend. The FigureCanvasQTAgg class wraps this backend and displays the resulting image on a Qt widget. The effect of this architecture is that Qt is unaware of the positions of lines and other plot elements — only the x, y coordinates of any clicks and mouse movements over the widget.

However, support for handling Qt mouse events and transforming them into interactions on the plot is built into Matplotlib. This can be controlled through a custom toolbar which can be added to your applications alongside the plot. In this section we'll look at adding these controls so we can zoom, pan and get data from embedded Matplotlib plots.

The complete code, importing the toolbar widget NavigationToolbar2QT and adding it to the interface within a QVBoxLayout, is shown below —

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PySide6.QtWidgets import QMainWindow, QVBoxLayout, QWidget, QApplication

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        sc = MplCanvas(self, width=5, height=4, dpi=100)
        sc.axes.plot([0,1,2,3,4], [10,1,20,3,40])

        # Create toolbar, passing canvas as first parament, parent (self, the MainWindow) as second.
        toolbar = NavigationToolbar(sc, self)

        layout = QVBoxLayout()
        layout.addWidget(toolbar)
        layout.addWidget(sc)

        # Create a placeholder widget to hold our toolbar and canvas.
        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        self.show()


app = QApplication(sys.argv)
w = MainWindow()
app.exec()

First we import the toolbar widget from matplotlib.backends.backend_qt5agg.NavigationToolbar2QT renaming it with the simpler name NavigationToolbar. We create an instance of the toolbar by calling NavigationToolbar with two parameters, first the canvas object sc and then the parent for the toolbar, in this case our MainWindow object self. Passing in the canvas links the created toolbar to it, allowing it to be controlled. The resulting toolbar object is stored in the variable toolbar.

We need to add two widgets to the window, one above the other, so we use a QVBoxLayout. First we add our toolbar widget toolbar and then the canvas widget sc to this layout. Finally, we set this layout onto our simple widget layout container which is set as the central widget for the window.

Running the above code will produce the following window layout, showing the plot at the bottom and the controls on top as a toolbar.

The buttons provided by NavigationToolbar2QT allow the following actions —

  • Home, Back/Forward, Pan & Zoom which are used to navigate through the plots. The Back/Forward buttons can step backwards and forwards through navigation steps, for example zooming in and then clicking Back will return to the previous zoom. Home returns to the initial state of the plot.

  • Plot margin/position configuration which can adjust the plot within the window.

  • Axis/curve style editor, where you can modify plot titles and axes scales, along with setting plot line colours and line styles. The colour selection uses the platform-default colour picker, allowing any available colours to be selected.

  • Save, to save the resulting figure as an image (all Matplotlib supported formats).

Updating plots

Quite often in applications you'll want to update the data shown in plots, whether in response to input from the user or updated data from an API. There are two ways to update plots in Matplotlib, either

  1. clearing and redrawing the canvas (simpler, but slower) or,

  2. by keeping a reference to the plotted line and updating the data.

If performance is important to your app it is recommended you do the latter, but the first is simpler.

Clear and redraw

We start with the simple clear-and-redraw method first below —

import sys
import random
import matplotlib
matplotlib.use('Qt5Agg')

from PySide6.QtWidgets import QMainWindow, QApplication
from PySide6.QtCore import QTimer

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


class MplCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        self.setCentralWidget(self.canvas)

        n_data = 50
        self.xdata = list(range(n_data))
        self.ydata = [random.randint(0, 10) for i in range(n_data)]
        self.update_plot()

        self.show()

        # Setup a timer to trigger the redraw by calling update_plot.
        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.update_plot)
        self.timer.start()

    def update_plot(self):
        # Drop off the first y element, append a new one.
        self.ydata = self.ydata[1:] + [random.randint(0, 10)]
        self.canvas.axes.cla()  # Clear the canvas.
        self.canvas.axes.plot(self.xdata, self.ydata, 'r')
        # Trigger the canvas to update and redraw.
        self.canvas.draw()


app = QApplication(sys.argv)
w = MainWindow()
app.exec()

In this example we've moved the plotting to a update_plot method to keep it self-contained. In this method we take our ydata array and drop off the first value with [1:] then append a new random integer between 0 and 10. This has the effect of scrolling the data to the left.

To redraw we simply call axes.cla() to clear the axes (the entire canvas) and the axes.plot(…) to re-plot the data, including the updated values. The resulting canvas is then redrawn to the widget by calling canvas.draw().

The update_plot method is called every 100 msec using a QTimer. The clear-and-refresh method is fast enough to keep a plot updated at this rate, but as we'll see shortly, falters as the speed increases.

In-place redraw

The changes required to update the plotted lines in-place are fairly minimal, requiring only an addition variable to store and retrieve the reference to the plotted line. The updated MainWindow code is shown below.

class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        self.setCentralWidget(self.canvas)

        n_data = 50
        self.xdata = list(range(n_data))
        self.ydata = [random.randint(0, 10) for i in range(n_data)]

        # We need to store a reference to the plotted line
        # somewhere, so we can apply the new data to it.
        self._plot_ref = None
        self.update_plot()

        self.show()

        # Setup a timer to trigger the redraw by calling update_plot.
        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.update_plot)
        self.timer.start()

    def update_plot(self):
        # Drop off the first y element, append a new one.
        self.ydata = self.ydata[1:] + [random.randint(0, 10)]

        # Note: we no longer need to clear the axis.
        if self._plot_ref is None:
            # First time we have no plot reference, so do a normal plot.
            # .plot returns a list of line <reference>s, as we're
            # only getting one we can take the first element.
            plot_refs = self.canvas.axes.plot(self.xdata, self.ydata, 'r')
            self._plot_ref = plot_refs[0]
        else:
            # We have a reference, we can use it to update the data for that line.
            self._plot_ref.set_ydata(self.ydata)

        # Trigger the canvas to update and redraw.
        self.canvas.draw()

Embedding plots from Pandas

Pandas is a Python package focused on working with table (data frames) and series data structures, which is particularly useful for data analysis workflows. It comes with built-in support for plotting with Matplotlib and here we'll take a quick look at how to embed these plots into PySide. With this you will be able to start building PySide data-analysis applications built around Pandas.

Pandas plotting functions are directly accessible from the DataFrame objects. The function signature is quite complex, giving a lot of options to control how the plots will be drawn.

DataFrame.plot(
    x=None, y=None, kind='line', ax=None, subplots=False,
    sharex=None, sharey=False, layout=None, figsize=None,
    use_index=True, title=None, grid=None, legend=True, style=None,
    logx=False, logy=False, loglog=False, xticks=None, yticks=None,
    xlim=None, ylim=None, rot=None, fontsize=None, colormap=None,
    table=False, yerr=None, xerr=None, secondary_y=False,
    sort_columns=False, **kwargs
)

The parameter we're most interested in is ax which allows us to pass in our own matplotlib.Axes instance on which Pandas will plot the DataFrame.

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PySide6.QtWidgets import QMainWindow, QApplication

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure

import pandas as pd


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # Create the maptlotlib FigureCanvas object,
        # which defines a single set of axes as self.axes.
        sc = MplCanvas(self, width=5, height=4, dpi=100)

        # Create our pandas DataFrame with some simple
        # data and headers.
        df = pd.DataFrame([
           [0, 10], [5, 15], [2, 20], [15, 25], [4, 10],
        ], columns=['A', 'B'])

        # plot the pandas DataFrame, passing in the
        # matplotlib Canvas axes.
        df.plot(ax=sc.axes)

        self.setCentralWidget(sc)
        self.show()


app = QApplication(sys.argv)
w = MainWindow()
app.exec()

The key step here is passing the canvas axes in when calling the plot method on the DataFrameon the line df.plot(ax=sc.axes). You can use this same pattern to update the plot any time, although bear in mind that Pandas clears and redraws the entire canvas, meaning that it is not ideal for high performance plotting.

The resulting plot generated through Pandas is shown below —

Packaging and distribution

PyInstaller & InstallForge

PyInstaller to build first simple, and then increasingly complex PySide6 applications into distributable EXE files on Windows. You can choose to follow it through completely, or skip ahead to the examples that are most relevant to your own project.

We finish off by using InstallForge to create a distributable Windows installer.

Requirements

PyInstaller works out of the box with Qt for Python PySide6 and, as of writing, current versions of PyInstaller are compatible with Python 3.6+. Whatever project you're working on, you should be able to package your apps.

You can install PyInstaller using pip.

BASH

pip3 install PyInstaller

If you experience problems packaging your apps, your first step should always be to update your PyInstaller and hooks package the latest versions using

BASH

pip3 install --upgrade PyInstaller pyinstaller-hooks-contrib

The hooks module contains package-specific packaging instructions for PyInstaller which is updated regularly.

Getting Started

It's a good idea to start packaging your application from the very beginning so you can confirm that packaging is still working as you develop it. This is particularly important if you add additional dependencies. If you only think about packaging at the end, it can be difficult to debug exactly where the problems are.

For this example we're going to start with a simple skeleton app, which doesn't do anything interesting. Once we've got the basic packaging process working, we'll extend the application to include icons and data files. We'll confirm the build as we go along.

from PySide6 import QtWidgets

import sys

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")
        l = QtWidgets.QLabel("My simple app.")
        l.setMargin(10)
        self.setCentralWidget(l)
        self.show()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    app.exec_()

Building a basic app

Open your terminal (command prompt) and navigate to the folder containing your project. You can now run the following command to run the PyInstaller build.

$ pyinstaller app.py

You'll see a number of messages output, giving debug information about what PyInstaller is doing. These are useful for debugging issues in your build, but can otherwise be ignored. The output that I get for running the command on Windows 11 is shown below.

(base) PS B:\PySide> pyinstaller app.py
913 INFO: PyInstaller: 6.3.0
913 INFO: Python: 3.10.9 (conda)
921 INFO: Platform: Windows-10-10.0.22621-SP0
922 INFO: wrote B:\PySide\app.spec
11900 INFO: Loading module hook 'hook-PySide6.QtWidgets.py' from 'D:\\miniconda3\\lib\\site-packages\\PyInstaller\\hooks'...
12476 INFO: Loading module hook 'hook-PySide6.QtCore.py' from 'D:\\miniconda3\\lib\\site-packages\\PyInstaller\\hooks'...
13158 INFO: Loading module hook 'hook-PySide6.QtGui.py' from 'D:\\miniconda3\\lib\\site-packages\\PyInstaller\\hooks'...
13711 INFO: Performing binary vs. data reclassification (114 entries)
15045 INFO: Looking for ctypes DLLs
15049 INFO: Analyzing run-time hooks ...
15051 INFO: Including run-time hook 'D:\\miniconda3\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py'
15052 INFO: Including run-time hook 'D:\\miniconda3\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pyside6.py'
15054 INFO: Processing pre-find module path hook _pyi_rth_utils from 'D:\\miniconda3\\lib\\site-packages\\PyInstaller\\hooks\\pre_find_module_path\\hook-_pyi_rth_utils.py'.
15056 INFO: Loading module hook 'hook-_pyi_rth_utils.py' from 'D:\\miniconda3\\lib\\site-packages\\PyInstaller\\hooks'...
15065 INFO: Looking for dynamic libraries
15413 INFO: Extra DLL search directories (AddDllDirectory): ['D:\\miniconda3\\Lib\\site-packages\\shiboken6', 'D:\\miniconda3\\lib\\site-packages\\numpy\\.libs']
15413 INFO: Extra DLL search directories (PATH): ['D:\\miniconda3\\Lib\\site-packages\\PySide6']
20196 INFO: Warnings written to B:\PySide\build\app\warn-app.txt
20233 INFO: Graph cross-reference written to B:\PySide\build\app\xref-app.html
20308 INFO: checking PYZ
20308 INFO: Building PYZ because PYZ-00.toc is non existent
20308 INFO: Building PYZ (ZlibArchive) B:\PySide\build\app\PYZ-00.pyz
20636 INFO: Building PYZ (ZlibArchive) B:\PySide\build\app\PYZ-00.pyz completed successfully.
20733 INFO: checking PKG
20733 INFO: Building PKG because PKG-00.toc is non existent
20734 INFO: Building PKG (CArchive) app.pkg
20761 INFO: Building PKG (CArchive) app.pkg completed successfully.
20764 INFO: Bootloader D:\miniconda3\lib\site-packages\PyInstaller\bootloader\Windows-64bit-intel\run.exe
20764 INFO: checking EXE
20765 INFO: Building EXE because EXE-00.toc is non existent
20765 INFO: Building EXE from EXE-00.toc
20766 INFO: Copying bootloader EXE to B:\PySide\build\app\app.exe
20851 INFO: Copying icon to EXE
20949 INFO: Copying 0 resources to EXE
20949 INFO: Embedding manifest in EXE
21033 INFO: Appending PKG archive to EXE
21038 INFO: Fixing EXE headers
21391 INFO: Building EXE from EXE-00.toc completed successfully.
21395 INFO: checking COLLECT
21396 INFO: Building COLLECT because COLLECT-00.toc is non existent
21397 INFO: Building COLLECT COLLECT-00.toc
23829 INFO: Building COLLECT COLLECT-00.toc completed successfully.

The build folder is used by PyInstaller to collect and prepare the files for bundling, it contains the results of analysis and some additional logs. For the most part, you can ignore the contents of this folder, unless you're trying to debug issues.

The dist (for "distribution") folder contains the files to be distributed. This includes your application, bundled as an executable file, together with any associated libraries (for example PySide6) and binary .dll files.

Everything necessary to run your application will be in this folder, meaning you can take this folder and "distribute" it to someone else to run your app.

You can try running your app yourself now, by running the executable file, named app.exe from the dist folder. After a short delay you'll see the familiar window of your application pop up as shown below.

The Spec file

The .spec file contains the build configuration and instructions that PyInstaller uses to package up your application. Every PyInstaller project has a .spec file, which is generated based on the command line options you pass when running pyinstaller.

When we ran pyinstaller with our script, we didn't pass in anything other than the name of our Python application file. This means our spec file currently contains only the default configuration. If you open it, you'll see something similar to what we have below.

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='app')

The first thing to notice is that this is a Python file, meaning you can edit it and use Python code to calculate values for the settings. This is mostly useful for complex builds, for example when you are targeting different platforms and want to conditionally define additional libraries or dependencies to bundle.

Once a .spec file has been generated, you can pass this to pyinstaller instead of your script to repeat the previous build process. Run this now to rebuild your executable.

pyinstaller app.spec

The resulting build will be identical to the build used to generate the .spec file (assuming you have made no changes). For many PyInstaller configuration changes you have the option of passing command-line arguments, or modifying your existing .spec file. Which you choose is up to you.

Tweaking the build

So far we've created a simple first build of a very basic application. Now we'll look at a few of the most useful options that PyInstaller provides to tweak our build. Then we'll go on to look at building more complex applications.

Naming your app

One of the simplest changes you can make is to provide a proper "name" for your application. By default the app takes the name of your source file (minus the extension), for example main or app. This isn't usually what you want.

You can provide a nicer name for PyInstaller to use for the executable (and dist folder) either by editing the .spec file to add a name= under the app block.

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='Hello World',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False  # False = do not show console.
         )

Alternatively, you can re-run the pyinstaller command and pass the -n or --name configuration flag along with your app.py script.

pyinstaller -n "Hello World" app.py
# or
pyinstaller --name "Hello World" app.py

Hiding the console window

When you run your packaged application you will notice that a console window runs in the background. If you try and close this console window your application will also close. You almost never want this window in a GUI application and PyInstaller provides a simple way to turn this off.

You can fix this in one of two ways. Firstly, you can edit the previously created .spec file setting console=False under the EXE block as shown below.

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False  # False = do not show console.
         )

Alternatively, you can re-run the pyinstaller command and pass the -w, --noconsole or --windowed configuration flag along with your app.py script.

pyinstaller -w app.py
# or
pyinstaller --windowed app.py
# or
pyinstaller --noconsole app.py

One File Build

On Windows PyInstaller has the ability to create a one-file build, that is, a single EXE file which contains all your code, libraries and data files in one. This can be a convenient way to share simple applications, as you don't need to provide an installer or zip up a folder of files.

To specify a one-file build provide the --onefile flag at the command line.

pyinstaller --onefile app.py

Note that while the one-file build is easier to distribute, it is slower to execute than a normally built application. This is because every time the application is run it must create a temporary folder to unpack the contents of the executable. Whether this trade-off is worth the convenience for your app is up to you!

Setting an application Icon

By default PyInstaller EXE files come with the following icon in place.

You will probably want to customize this to make your application more recognisable. This can be done easily using the --icon=<filename> command-line switch to PyInstaller. On Windows the icon should be provided as an .ico file.

pyinstaller --windowed --icon=hand.ico app.py

Or, by adding the icon= parameter to your .spec file.

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='blarh',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False,
          icon='hand.ico')
from PySide6 import QtWidgets, QtGui
import sys


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")
        l = QtWidgets.QLabel("My simple app.")
        l.setMargin(10)
        self.setCentralWidget(l)
        self.show()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setWindowIcon(QtGui.QIcon('hand.ico'))
    w = MainWindow()
    app.exec()

Here we've added the .setWindowIcon call to the app instance. This defines a default icon to be used for all windows of our application. You can override this on a per-window basis if you like, by calling .setWindowIcon on the window itself.

If you run the above application you should now see the icon appears on the window.

Last updated