Monday, December 21, 2020

Real Python: Use PyQt's QThread to Prevent Freezing GUIs

PyQt graphical user interface (GUI) applications have a main thread of execution that runs the event loop and GUI. If you launch a long-running task in this thread, then your GUI will freeze until the task terminates. During that time, the user won’t be able to interact with the application, resulting in a bad user experience. Luckily, PyQt’s QThread class allows you to work around this issue.

In this tutorial, you’ll learn how to:

  • Use PyQt’s QThread to prevent freezing GUIs
  • Create reusable threads with QThreadPool and QRunnable
  • Manage interthread communication using signals and slots
  • Safely use shared resources with PyQt’s locks
  • Use best practices for developing GUI applications with PyQt’s thread support

For a better understanding of how to use PyQt’s threads, some previous knowledge of GUI programming with PyQt and Python multithreaded programming would be helpful.

Free Bonus: Get a sample chapter from Python Basics: A Practical Introduction to Python 3 to see how you can go from beginner to intermediate in Python with a complete curriculum, up-to-date for Python 3.8.

Freezing a GUI With Long-Running Tasks

Long-running tasks occupying the main thread of a GUI application and causing the application to freeze is a common issue in GUI programming that almost always results in a bad user experience. For example, consider the following GUI application:

PyQt Freezing GUI Example

Say you need the Counting label to reflect the total number of clicks on the Click me! button. Clicking the Long-Running Task! button will launch a task that takes a lot of time to finish. Your long-running task could be a file download, a query to a large database, or any other resource-intensive operation.

Here’s a first approach to coding this application using PyQt and a single thread of execution:

import sys
from time import sleep

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.clicksCount = 0
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle("Freezing GUI")
        self.resize(300, 150)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        # Create and connect widgets
        self.clicksLabel = QLabel("Counting: 0 clicks", self)
        self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.stepLabel = QLabel("Long-Running Step: 0")
        self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.countBtn = QPushButton("Click me!", self)
        self.countBtn.clicked.connect(self.countClicks)
        self.longRunningBtn = QPushButton("Long-Running Task!", self)
        self.longRunningBtn.clicked.connect(self.runLongTask)
        # Set the layout
        layout = QVBoxLayout()
        layout.addWidget(self.clicksLabel)
        layout.addWidget(self.countBtn)
        layout.addStretch()
        layout.addWidget(self.stepLabel)
        layout.addWidget(self.longRunningBtn)
        self.centralWidget.setLayout(layout)

    def countClicks(self):
        self.clicksCount += 1
        self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks")

    def reportProgress(self, n):
        self.stepLabel.setText(f"Long-Running Step: {n}")

    def runLongTask(self):
        """Long-running task in 5 steps."""
        for i in range(5):
            sleep(1)
            self.reportProgress(i + 1)

app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())

In this Freezing GUI application, .setupUi() creates all the required graphical components for the GUI. A click on the Click me! button calls .countClicks(), which makes the text of the Counting label reflect the number of button clicks.

Note: PyQt was first developed to target Python 2, which has an exec keyword. To avoid a name conflict on those earlier versions of PyQt, an underscore was added to the end of .exec_().

Even though PyQt5 targets only Python 3, which doesn’t have an exec keyword, the library provides two methods to start an application’s event loop:

  1. .exec_()
  2. .exec()

Both variations of the method work the same, so you can use either one in your applications.

Clicking the Long-Running Task! button calls .runLongTask(), which performs a task that takes 5 seconds to complete. This is a hypothetical task that you coded using time.sleep(secs), which suspends the execution of the calling thread for the given number of seconds, secs.

In .runLongTask(), you also call .reportProgress() to make the Long-Running Step label reflect the progress of the operation.

Does this application work as you intend? Run the application and check out its behavior:

PyQt Freezing GUI Example

When you click the Click me! button, the label shows the number of clicks. However, if you click the Long-Running Task! button, then the application becomes frozen and unresponsive. The buttons no longer respond to clicks and the labels don’t reflect the application’s state.

After five seconds, the application’s GUI gets updated again. The Counting label shows ten clicks, reflecting five clicks that occurred while the GUI was frozen. The Long-Running Step label doesn’t reflect the progress of your long-running operation. It jumps from zero to five without showing the intermediate steps.

Note: Even though your application’s GUI freezes during the long-running task, the application still registers events such as clicks and keystrokes. It’s just unable to process them until the main thread gets released.

The application’s GUI freezes as a result of a blocked main thread. The main thread is busy processing a long-running task and doesn’t immediately respond to the user’s actions. This is an annoying behavior because the user doesn’t know for sure if the application is working correctly or if it’s crashed.

Fortunately, there are some techniques you can use to work around this issue. A commonly used solution is to run your long-running task outside of the application’s main thread using a worker thread.

In the sections below, you’ll learn how to use PyQt’s built-in thread support to solve the issue of unresponsive or frozen GUIs and provide the best possible user experience in your applications.

Multithreading: The Basics

Sometimes you can divide your programs into several smaller subprograms, or tasks, that you can run in several threads. This might make your programs faster, or it might help you improve the user experience by preventing your programs from freezing while executing long-running tasks.

Read the full article at https://realpython.com/python-pyqt-qthread/ »


[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...