pythonpyqtpyqt5python-importqprogressbar

Connect an imported function to Qt5 progress bar without dependencies


I'm writing a set of small python applications, that are aimed to be run via CLI. Some of the functions should be bundled together in a PyQT5 GUI to be easier usable. Now, I have one function inside my package, that tends to run quite long, so I would like to display a progress bar. However, the function itself needs to be able to be run without QT5 present. I'm looking for a way to have the progress from my long running imported function to be shown in the QT GUI without making QT a dependency of my package.

Simple example:

Somewhere inside my package:

import time
percent = 0
def long_running_function(percent):
  while percent < 100:
    percent+=1
    #do something here to update percentage in QT
    time.sleep(1) #just to indicate, that the function might be a blocking call

My simple GUI:

from my_package import long_running_function

from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import (QApplication, QDialog,
                             QProgressBar, QPushButton)

class Actions(QDialog):
    """
    Simple dialog that consists of a Progress Bar and a Button.
    Clicking on the button results in running my external function and
    updates the progress bar.
    """
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle('Progress Bar')
        self.progress = QProgressBar(self)
        self.progress.setGeometry(0, 0, 300, 25)
        self.progress.setMaximum(100)
        self.button = QPushButton('Start', self)
        self.button.move(0, 30)
        self.show()

        self.button.clicked.connect(self.onButtonClick)

    def onButtonClick(self):
        long_running_function(0)
        self.progress.setValue(value) #probably somewhere

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Actions()
    sys.exit(app.exec_())

I know, that I could solve this, by emitting a pyqtsignal in each iteration of the loop inside long_running_function, but that would make QT a dependency of my package, which I would like to circumvent.


Solution

  • One possible solution is to create a QObject by implementing the __add__ and __lt__ operators to be the percent of the function:

    from functools import partial
    
    from PyQt5.QtCore import QObject, QThread, QTimer, pyqtSignal, pyqtSlot
    from PyQt5.QtWidgets import QApplication, QDialog, QProgressBar, QPushButton
    
    from my_package import long_running_function
    
    
    class PercentageWorker(QObject):
        started = pyqtSignal()
        finished = pyqtSignal()
        percentageChanged = pyqtSignal(int)
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self._percentage = 0
    
        def __add__(self, other):
            if isinstance(other, int):
                self._percentage += other
                self.percentageChanged.emit(self._percentage)
                return self
            return super().__add__(other)
    
        def __lt__(self, other):
            if isinstance(other, int):
                return self._percentage < other
            return super().__lt__(other)
    
        def start_task(self, callback, initial_percentage):
            self._percentage = initial_percentage
            wrapper = partial(callback, self)
            QTimer.singleShot(0, wrapper)
    
        @pyqtSlot(object)
        def launch_task(self, wrapper):
            self.started()
            wrapper()
            self.finished()
    
    
    class Actions(QDialog):
        """
        Simple dialog that consists of a Progress Bar and a Button.
        Clicking on the button results in running my external function and
        updates the progress bar.
        """
    
        def __init__(self):
            super().__init__()
            self.initUI()
    
        def initUI(self):
            self.setWindowTitle("Progress Bar")
            self.progress = QProgressBar(self)
            self.progress.setGeometry(0, 0, 300, 25)
            self.progress.setMaximum(100)
            self.button = QPushButton("Start", self)
            self.button.move(0, 30)
            self.show()
    
            self.button.clicked.connect(self.onButtonClick)
    
            thread = QThread(self)
            thread.start()
            self.percentage_worker = PercentageWorker()
            self.percentage_worker.moveToThread(thread)
            self.percentage_worker.percentageChanged.connect(self.progress.setValue)
            self.percentage_worker.started.connect(self.onStarted)
            self.percentage_worker.finished.connect(self.onFinished)
    
        @pyqtSlot()
        def onStarted(self):
            self.button.setDisabled(True)
    
        @pyqtSlot()
        def onFinished(self):
            self.button.setDisabled(False)
    
        @pyqtSlot()
        def onButtonClick(self):
            self.percentage_worker.start_task(long_running_function, 0)
    
    
    if __name__ == "__main__":
        import sys
    
        app = QApplication(sys.argv)
        window = Actions()
        sys.exit(app.exec_())