Chris Aung
Chris Aung

Reputation: 9532

How to display progress without multi-threading

Let's say I have a PyQt program that goes through a given directory, looks for *JPEG images, and does some processing every time it finds one. Depending on the size of the selected directory, this may take from some seconds to minutes.

I would like to keep my user updated with the status - preferably with something like "x files processed out of y files" . If not, a simple running pulse progress bar by setting progressbar.setRange(0,0) works too.

From my understanding, in order to prevent my GUI from freezing, I will need a seperate thread that process the images, and the original thread that updates the GUI every interval.

But I am wondering if there is any possible way for me to do both in the same thread?

Upvotes: 1

Views: 1337

Answers (3)

ekhumoro
ekhumoro

Reputation: 120768

Yes, you can easily do this using processEvents, which is provided for this exact purpose.

I have used this technique for implementing a simple find-in-files dialog box. All you need to do is launch the function that processes the files with a single-shot timer, and then periodically call processEvents in the loop. This is should be good enough to update a counter with the number of files processed, and also allow the user to cancel the process, if necessary.

The only real issue is deciding on how frequently to call processEvents. The more often you call it, the more responsive the GUI will be - but this comes at the cost of considerably slowing the processing of the files. So you may have to experiment a little bit in order to find an acceptable compromise.

UPDATE:

Here's a simple demo that shows how the code could be structured:

import sys, time
from PyQt5 import QtWidgets, QtCore

class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.button = QtWidgets.QPushButton('Start')
        self.progress = QtWidgets.QLabel('0')
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.button)
        layout.addWidget(self.progress)
        self.button.clicked.connect(self.test)
        self._stop = False
        self._stopped = True

    def test(self):
        if self._stopped:
            self._stop = False
            self.progress.setText('0')
            self.button.setText('Stop')
            QtCore.QTimer.singleShot(1, self.process)
        else:
            self._stop = True

    def process(self):
        self._stopped = False
        for index in range(1, 1000):
            time.sleep(0.01)
            self.progress.setText(str(index))
            if not index % 20:
                QtWidgets.qApp.processEvents(
                    QtCore.QEventLoop.AllEvents, 50)
            if self._stop:
                break
        self._stopped = True
        self.button.setText('Start')

if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

Upvotes: 3

You could just use the python threading module and emit a signal in your threaded routine.

Here's a working example

from PyQt4 import QtGui, QtCore
import threading
import time

class MyWidget(QtGui.QWidget):

    valueChanged = QtCore.pyqtSignal(int)

    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent)
        self.computeButton = QtGui.QPushButton("Compute", self)
        self.progressBar = QtGui.QProgressBar()
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.computeButton)
        layout.addWidget(self.progressBar)
        self.computeButton.clicked.connect(self.compute)
        self.valueChanged.connect(self.progressBar.setValue)   

    def compute(self):
        nbFiles = 10
        self.progressBar.setRange(0, nbFiles)

        def inner(): 
            for i in range(1, nbFiles+1):
                time.sleep(0.5)  # Process Image
                self.valueChanged.emit(i)  # Notify progress 

        self.thread = threading.Thread(target = inner)
        self.thread.start()


if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    widget = MyWidget()
    widget.show()
    sys.exit(app.exec_())

Upvotes: 0

VP.
VP.

Reputation: 16765

I could not achieve the thing you need without multi threading and this is not possible because gui can be only updated in main thread. Below is an algorithm how I did this with multithreading.

Let's say you have your application processing images. Then there are the following threads:

  1. Main thread (that blocks by GUI/QApplication-derived classes.exec())
  2. Timer with, for example, 1 second interval which updates a variable and calls a slot in GUI thread which updates a variable in user interface.
  3. A thread which is processing images on your pc.

    def process(self):
        self._status = "processing image 1"
        ....
    
    def _update(self):
        self.status_label.setText(self._status)
    
    def start_processing(self, image_path):
        # create thread for process and run it
        # create thread for updating by using QtCore.QTimer()
        # connect qtimer triggered signal to and `self._update()` slot
        # connect image processing thread (use connect signal to any slot, in this example I'll stop timer after processing thread finishes)
        @pyqtSlot()
        def _stop_timer():
            self._qtimer.stop()
            self._qtimer = None
        _update_thread.finished.connect(_stop_timer)
    

In pyqt5 it is possible to assign a pyqtvariable from a one nested thread(first level). So you can make your variable a pyqtvariable with setter and getter and update gui in a setter or think how you can do this by yourself.

Upvotes: 0

Related Questions