PyQt ProgressBar
Asked Answered
S

2

12

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.

Stew answered 7/11, 2012 at 12:36 Comment(0)
D
13

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).

Demetrius answered 7/11, 2012 at 17:53 Comment(5)
If I run this the GUI window stalls as well with the "Not responding, force close" error. However if I wait until all my tasks are finished the normal application continues.Stew
@Tuim. My script is just a simple demo based on the code in your question. It isn't a universal solution that's going to work in all situations. You need to update your question with a proper explanation of what you're trying to do. What are these "tasks" you mention? Are they CPU-bound, or IO-bound? Is the application that carries out the tasks something you wrote yourself, and can therefore modify? What language is it written in? Etc, etc.Demetrius
These tasks are installations e.g unpacking zip files, installing msi/deb packages things like that. But this is not very relevant to the case. The application is written in Python aswell and fully customizable. Also I am not expecting a copy-paste-able answer! I am expecting a hint in the right direction and the one you have did not seem to be the right direction for me, I have tried. No offence.Stew
@Tuim. On the contrary: the nature of the tasks is probably the only thing that is relevant to your case. Please see the update to my answer.Demetrius
Thank you for your extensive answer.Stew
P
0

when you do gui programming you need to use some form of multi-threading, you would have a worker thread and one that updates the gui and responds to mouse and keyboard events. There are a number of ways to do that. I recommend you have a look at this QProgressBar tutorial that has an excellent working example of how to use QProgressBar.

In the tutorial they are using QBasicTimer which is way to yield control back to the main thread so that it can respond to GUI events. By using time.sleep in your code you are blocking for one second the only thread that is running.

Pickled answered 7/11, 2012 at 13:2 Comment(5)
I am aware of that. But they aren't making any more use of threading as I am in the example.Stew
@Tuim: Yes they are, don't use time.sleep().Pee
@Stew I updated the answer to further explain why your code and the tutorials are not doing the same thing.Pickled
I think mentioning threading here just confuses things. There's no threading involved, it's just about returning control to the Qt event loop so the app can stay responsive. Moving work onto separate threads is one strategy for achieving this, but it's in no way required.Consonantal
Maybe I need to add some more context. I have a console application which does a set of tasks in a serial way. I simply want to show a progressbar for this and since QT is the preferred GUI library here I am bound to use it. The application does not have any threading model except for the main thread. So what I simply need is a GUI frame (in a new thread or not) to show a progress bar and update this bar whenever a task finishes.Stew

© 2022 - 2024 — McMap. All rights reserved.