1

I am using Python 3.5, PyQt5 on OSX and I was wondering if there was a possibility to update the QProgressBar without slowing down the whole computing work. Here was my code and if I did just the task without the progressbar update it was soo much faster!!

from PyQt5.QtWidgets import (QWidget, QProgressBar, QPushButton, QApplication)
from jellyfish import levenshtein_distance, jaro_winkler
import sys

class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.pbar = QProgressBar(self)
        self.pbar.setGeometry(30, 40, 200, 25)
        self.btn = QPushButton('Start', self)
        self.btn.move(40, 80)
        self.btn.clicked.connect(self.doAction)
        self.setGeometry(300, 300, 280, 170)
        self.show()


    def doAction(self):

        #setup variables
        step = 0
        m = 1000
        n = 500
        step_val = 100 / (m * n)

        #make task
        for i in range(m):
            for j in range(n):
                jaro_winkler(str(i), str(j))

                #show task
                print(i,j)

                #update progressbar
                step += step_val
                self.pbar.setValue(step)
                QApplication.processEvents()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

Then with help from stackoverflow users I got the hint to make a separate working thread and connect the updating signal to the GUI. I did it and it looks now like the following code. It also works and is much faster, but I can't figure out how to connect the emited signal to the GUI. Can somebody please help me? Many thanks in advance!

from jellyfish import jaro_winkler
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QProgressBar, QMainWindow
import time
import numpy as np

class Main_Window(QMainWindow):

    def __init__(self):
        super(Main_Window,self).__init__()
        self.initUI()

    def initUI(self):
        self.pbar = QProgressBar(self)
        self.pbar.setGeometry(30, 40, 200, 25)
        self.btn = QPushButton('Start', self)
        self.btn.move(40, 80)
        self.btn.clicked.connect(MyThread.doAction)
        self.setGeometry(300, 300, 280, 170)
        self.show()

    def updateProgressBar(self, val):
        self.pbar.setValue.connect(val)


class MySignal(QWidget):
    pbar_signal = QtCore.pyqtSignal(int)


class MyThread(QtCore.QThread):
    def __init__(self):
        super().__init__()

    def doAction(self):
        t = time.time()     #for time measurement

        #setup variables
        step = 0
        m = 1000
        n = 500
        pbar_val = 100 / m
        signal_instance = MySignal()

        #make task
        for i in range(m):
            for j in range(n):
                jaro_winkler(str(i), str(j))
            signal_instance.pbar_signal.emit(pbar_val)

        #measuring task time
        print(np.round_(time.time() - t, 3), 'sec elapsed')


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    w = Main_Window()
    sys.exit(app.exec_())
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
sunwarr10r
  • 4,420
  • 8
  • 54
  • 109
  • How do you call the function without the progressbar? I assume it's not in a nested loop? – UnholySheep Oct 12 '16 at 16:24
  • @UnholySheep Just comment out the 3 line below `#update progressbar` – sunwarr10r Oct 12 '16 at 16:43
  • So the obvious questions are: How did you measure the performance/runtime? Why are you performing heavy work in your UI thread (instead of a separate thread)? – UnholySheep Oct 12 '16 at 16:57
  • 1.) The performance difference is so abvious, after commenting out the progressbar update, that I can measure it with any timer device (e.g. my smartphone). 2.) The reason why I am performing it in the UI is because I am new to Python :-) I didn't know that it would be faster if I put it into another thread. Somehow I thought that it would be even slower because of the additional thread on which Python would have to work additionally. Can you give me a hint how to do it? – sunwarr10r Oct 12 '16 at 19:35

1 Answers1

1

There are three things slowing the code down:

  1. Printing to stdout is very expensive - especially when you do it 500,000 times! On my system, commenting out print(i,j) roughly halves the time doAction takes to run.
  2. It's also quite expensive to call processEvents 500,000 times. Commenting out QApplication.processEvents() reduces the run time by another two-thirds.
  3. Commenting out self.pbar.setValue(step) halves the time again.

Hopefully it should be obvious by now that attempting to update the gui 500,000 times during a task that should take less than a second is massive overkill! Most user's reaction times are about 200ms at best, so you only need to update the gui about once every 100ms.

Given that, one simple fix is to move the updates into the outer loop:

    for i in range(m):
        for j in range(n):
            jaro_winkler(str(i), str(j))

            # show task
            # print(i,j)

            step += step_val

        # update progressbar
        self.pbar.setValue(step)
        QApplication.processEvents()

But an even better solution would be to move the calculations into a separate worker thread and have it periodically emit a custom signal to update the progress bar:

class Main_Window(QMainWindow):
    ...
    def initUI(self):
        ...
        self.btn.clicked.connect(self.doAction)
        self.thread = MyThread()
        self.thread.pbar_signal.connect(self.pbar.setValue)

    def doAction(self):
        if not self.thread.isRunning():
            self.thread.start()    

class MyThread(QtCore.QThread):
    pbar_signal = QtCore.pyqtSignal(int)

    def run(self):
        #for time measurement
        t = time.time()

        #setup variables
        m = 1000
        n = 500
        progress = step = 100 / m

        #make task
        for i in range(m):
            for j in range(n):
                jaro_winkler(str(i), str(j))
            progress += step
            self.pbar_signal.emit(progress)

        #measuring task time
        print(np.round_(time.time() - t, 3), 'sec elapsed')
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • Thank you for the info and commenting out `print()` ist OK, but I need to update and show and update the `pbar` for my application. Otherwise I wouldn't ask the question :-) – sunwarr10r Oct 12 '16 at 19:29
  • Sorry I didn't read the complete answer. The outer loop makes sense, thanks :-) But I have to figure out how to do it with the thread. – sunwarr10r Oct 12 '16 at 19:44