Search code examples
pythonpython-3.xpyqt5python-3.10

How do I multithread in the MVP architectural pattern?


I am programming a desktop app for a small local hotel using python.
As is so often the case, I also have a heavy-duty process/method in the background. During that time a progressbar should pop up and fill itself. The progress bar should block any further input and serves as user information: "Data/tasks are being processed"

My issue:
I am unable to achieve both: running the heavy_load_method in the background and showing&filling the progessbar at the same time.

I assume that I implement my progressbar incorrectly for multithreading/multiprocessing.

Since my failure is possibly related to my current implementation of the MVP, some code snippets:

# main.py
# importing all modules (presenter, model and all views/formulars (including the progressbar view/form)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    mainwindow = Mainwindow()
    progressbar = ProgressBarPopup()
    model = Model()
    ...
    presenter = Presenter(model, mainwindow, progressbar, ...)
    sys.exit(app.exec_())
# presenter.py
# imports modules required for data manipulation

class Presenter:
    def __init__(self, model, mainwindow, progressbar, ...)
        self.model = model
        self.mainwindow = mainwindow
        self.progressbar = progressbar
        ...

    def heavy_load_method(self):
        # does operations like:
        #  - getting (minor) amounts of data from form/view (related view/form method calls)
        #  - processes already cached `self.data` (via Presenter method calls)
        #  - calling and calculating data from the model/database
        #  - calling functions from imported moduls like "myfilemanagement.py"
        #  - ...
# MyProgressbar.py
from PyQt5.QtWidgets import QProgressBar, QDialog, QVBoxLayout, QLabel
from PyQt5.QtCore import Qt, QTimer

class ProgressBarPopup(QDialog):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Progressbar")
        self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
        self.setFixedSize(300, 100)
        self.setWindowModality(Qt.ApplicationModal)
        layout = QVBoxLayout()
        self.label = QLabel("Processing...", self)
        self.label.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.label)
        self.progressbar = QProgressBar(self)
        self.progressbar.setRange(0, 99)
        layout.addWidget(self.progressbar)
        self.setLayout(layout)

        # Progressbar visual update timer
        self.step = 0
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_progress)

        # Progressbar exit/close timer
        self.close_timer = QTimer(self)
        self.close_timer.timeout.connect(self.close_progressbar)


    def close_progressbar(self):
        self.close_timer.stop()
        self.close()

    def update_progress(self):
        self.step += 1
        if self.step <= 98:
            self.progressbar.setValue(self.step)
        if self.step > 98:
            self.timer.stop()
            self.label.setText("This takes longer then expected.")


    def setup_progressbar_and_show(self):
        self.step = 0
        self.progressbar.setValue(self.step)
        self.timer.start(70)
        self.close_timer.start(10000)
        self.label.setText("Processing...") 
        self.show()

I have tried to multithread or multiprocess the method heavy_load_method. My previous attempts failed because I could not multithread/multiprocess the progressbar.


Solution

  • I'm a bit embarrassed that it took me so long but:

    Even if you intuitively believe that you have to QThread the progressbar (as this is a pyqt5 object), it is actually the other way round. heavy_duty_method must be detached from the presenter and placed in a separate QThread sub-class. An object is created from this during runtime, which can be integrated and run as below.

    # presenter.py
    # imports modules required for data manipulation and 'Create_Heavy_Duty_Thread' class
    
    class Presenter:
        def __init__(self, model, mainwindow, progressbar, ...)
            self.model = model
            self.mainwindow = mainwindow
            self.progressbar = progressbar
            ...
    
        def prepare_for_load(self)
            if conditions_are_met:
                self.worker = Create_Heavy_Duty_Thread(self.model, invoice_dict, bookings, isClassic)
                self.worker.started.connect(self.progressbar.setup_progressbar_and_show)
                self.worker.finished.connect(self.progressbar.close_progressbar)
                self.worker.start()
    
    class Create_Heavy_Duty_Thread(QThread):
        def __init__(self, model, invoice_dict, bookings, isClassic):
            super().__init__()
            self.model = model
            self.invoice_dict = invoice_dict
            self.bookings = bookings
            self.isClassic = isClassic
    
        ... # all methods needed to perform heavy_duty_method
    
        def heavy_duty_method(self):
            # heavy duty is done here 
            ...
    
        def run(self):
            self.heavy_duty_method()