sunwarr10r
sunwarr10r

Reputation: 4787

Python3, PyQt5: QProgressBar updating makes actual task very slow

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_())

Upvotes: 1

Views: 2297

Answers (1)

ekhumoro
ekhumoro

Reputation: 120578

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

Upvotes: 2

Related Questions