PyQt ProgressBar

Tuim picture Tuim · Nov 7, 2012 · Viewed 21k times · Source

When using the following code my application stalls after a couple of seconds. And by stalls I mean hangs. I get a window from Windows saying wait or force close.

I might add that this only happens when I click either inside the progress bar window or when I click outside of it so it loses focus. If I start the example and do not touch anything it works like it should.

from PyQt4 import QtCore
from PyQt4 import QtGui


class ProgressBar(QtGui.QWidget):
    def __init__(self, parent=None, total=20):
        super(ProgressBar, self).__init__(parent)
        self.name_line = QtGui.QLineEdit()

        self.progressbar = QtGui.QProgressBar()
        self.progressbar.setMinimum(1)
        self.progressbar.setMaximum(total)

        main_layout = QtGui.QGridLayout()
        main_layout.addWidget(self.progressbar, 0, 0)

        self.setLayout(main_layout)
        self.setWindowTitle("Progress")

    def update_progressbar(self, val):
        self.progressbar.setValue(val)   

Using this like so:

app = QtGui.QApplication(sys.argv)
bar = ProgressBar(total=101)
bar.show()

for i in range(2,100):
    bar.update_progressbar(i)
    time.sleep(1)

Thanks for any help.

Answer

ekhumoro picture ekhumoro · Nov 7, 2012

You need to allow events to be processed whilst the loop is running so that the application can remain responsive.

Even more importantly, for long-running tasks, you need to provide a way for the user to stop the loop once it's started.

One simple way to do this is to start the loop with a timer, and then periodically call qApp.processEvents whilst the loop is running.

Here's a demo script that does that:

import sys, time
from PyQt4 import QtGui, QtCore

class ProgressBar(QtGui.QWidget):
    def __init__(self, parent=None, total=20):
        super(ProgressBar, self).__init__(parent)
        self.progressbar = QtGui.QProgressBar()
        self.progressbar.setMinimum(1)
        self.progressbar.setMaximum(total)
        self.button = QtGui.QPushButton('Start')
        self.button.clicked.connect(self.handleButton)
        main_layout = QtGui.QGridLayout()
        main_layout.addWidget(self.button, 0, 0)
        main_layout.addWidget(self.progressbar, 0, 1)
        self.setLayout(main_layout)
        self.setWindowTitle('Progress')
        self._active = False

    def handleButton(self):
        if not self._active:
            self._active = True
            self.button.setText('Stop')
            if self.progressbar.value() == self.progressbar.maximum():
                self.progressbar.reset()
            QtCore.QTimer.singleShot(0, self.startLoop)
        else:
            self._active = False

    def closeEvent(self, event):
        self._active = False

    def startLoop(self):
        while True:
            time.sleep(0.05)
            value = self.progressbar.value() + 1
            self.progressbar.setValue(value)
            QtGui.qApp.processEvents()
            if (not self._active or
                value >= self.progressbar.maximum()):
                break
        self.button.setText('Start')
        self._active = False

app = QtGui.QApplication(sys.argv)
bar = ProgressBar(total=101)
bar.show()
sys.exit(app.exec_())

UPDATE

Assuming that you're using the C implementation of python (i.e. CPython), the solution to this issue depends entirely on the nature of the task(s) that have to run concurrently with the GUI. More fundamentally, it is determined by CPython having a Global Interpreter Lock (GIL).

I am not going to attempt any explanation of CPython's GIL: instead, I will simply recommend watching this excellent PyCon video by Dave Beazley, and leave it at that.


Generally, when trying to run a GUI concurrently with a background task, the first question to ask is: Is the task IO-bound, or CPU-bound?

If it's IO-bound (e.g. accessing the local file-system, downloading from the internet, etc), then the solution is usually quite straightforward, because CPython always releases the GIL for I/O operations. The background task can simply be done asynchronously, or performed by a worker thread, and nothing special needs to be done to keep the GUI responsive.

The main difficulties occur with CPU-bound tasks, when there is a second question to ask: Can the task be broken down into a series of small steps?

If it can, then the solution is to periodically send requests to the GUI thread to process its current stack of pending events. The demo script above is a crude example of this technique. More usually, the task would be carried out in a separate worker thread, which would emit a gui-update signal as each step of the task is completed. (NB: it's important to ensure that the worker thread never attempts any GUI-related operations itself).

But if the task can't be broken down into small steps, then none of the usual threading-type solutions will work. The GUI will just freeze until the task has been completed, whether threads are used or not.

For this final scenario, the only solution is to use a separate process, rather than a separate thread - i.e. make use of the multiprocessing module. Of course, this solution will only be effective if the target system has multiple CPU cores available. If there's only one CPU core to play with, there's basically nothing that can be done to help (other than switching to a different implementation of Python, or to a different language altogether).