2

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.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Dschoni
  • 3,714
  • 6
  • 45
  • 80

1 Answers1

1

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_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Wow. That's way over my head. Could you explain this approach please? – Dschoni Aug 02 '19 at 12:15
  • @Dschoni In this case, the percent variable that uses long_running_function only requests as a requirement that it can be compared with an integer and also that it allows an integer to be added, so that does not necessarily imply that percent is an integer but that it allows such operations and for that purpose implement the \__add__ and \__lt__ methods, so for that I created a QObject that allows me to use signals and added the methods indicated above – eyllanesc Aug 02 '19 at 12:32
  • I'm gonna try that, but leave this question open a little more, to see if there are other approaches. In my use-case, `long_running_function` starts subprocesses etc. so this approach could get tricky. But that's for another question ;) – Dschoni Aug 02 '19 at 12:34
  • 1
    @Dschoni My answer is based on your MRE since each solution has limitations so if they are other conditions I invite you to publish a new question with an appropriate MRE. – eyllanesc Aug 02 '19 at 12:36
  • @Dschoni some feedback? – eyllanesc Aug 20 '19 at 04:35
  • Yeah, this particular solution only works, if the wrapped function can be called in `partial`. I tried to indicate in my question with the `#do something here` that I need to trigger the update from within my function, so probably the `percent` parameter was misleading. I'll update my question. – Dschoni Aug 20 '19 at 14:34