Search code examples
pythonpython-3.xpyside2

QWidget not loading child elements when QRunnable is also called


I'm working in a data processing desktop application with Python 3.7 and PySide2 on which requires me to load data from several large (approx 250k rows) excel files into the program's processing library. For this I've set up in my application a simple popup (called LoadingPopup) which includes a rotating gif and a simple caption, and also some code that loads the data from the excel files into a global object using pandas. Both of these things work as intended when run on their own, but if I happen to create a loading dialog and a QRunnable worker in the same scope of my codebase, the widgets contained in loading widget (a gif and a simple caption) will simply not show.

I've tried changing the parent type for my widget from QDialog to QWidget, or initializing the popup (the start() function) both outside and inside the widget. I'm not very experienced with Qt5 so I don't know what else to do.


import sys, time, traceback

from PySide2.QtWidgets import *
from PySide2.QtCore import *
from PySide2.QtGui import *

from TsrUtils import PathUtils


class WorkerSignals(QObject):
    finished = Signal()
    error = Signal(tuple)
    result = Signal(object)

class TsrWorker(QRunnable):

    def __init__(self, fn, *args, **kwargs):
        super(TsrWorker, self).__init__()
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]

            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)
        finally:
            self.signals.finished.emit()

class LoadingPopup(QWidget):
    def __init__(self):
        super().__init__()

        self.message = "Cargando"
        self.setMinimumSize(350,300)

        self.setWindowIcon(\
            QIcon(PathUtils.ruta_absoluta('resources/icons/tsr.png')))

        self.setWindowTitle(self.message)

        self.layout = QVBoxLayout(self)
        self.setLayout(self.layout)

        self.movie = QMovie(self)
        self.movie.setFileName("./resources/img/spinner.gif")
        self.movie.setCacheMode(QMovie.CacheAll)
        self.movie.start()

        self.loading = QLabel(self)
        self.loading.setMovie(self.movie)
        self.loading.setAlignment(Qt.AlignCenter)
        self.layout.addWidget(self.loading)

        self.lbl = QLabel(self.message, self)
        self.lbl.setAlignment(Qt.AlignCenter)
        self.lbl.setStyleSheet("font: 15pt")
        self.layout.addWidget(self.lbl)

class MyMainApp(QApplication):
    def __init__(self, args):
        super().__init__()

        self.l = LoadingPopup()
        self.l.show()

        w = TsrWorker(time.sleep, 5)
        w.signals.finished.connect(self.terminado)
        w.run()

    def terminado(self):
        print('timer finished')
        self.l.hide()

if __name__ == '__main__':

    app = MyMainApp(sys.argv)

    sys.exit(app.exec_())

I've changed the actual data loading part of the application in the example with a time.sleep function. MY expected results are that I should be able to have the LoadingPopup show up with a gif moving, and then it should close once the QRunnable finishes.


Solution

  • You should not call the run method directly since you will have the heavy task run on the GUI thread freezing it. You must launch it using QThreadPool:

    class MyMainApp(QApplication):
        def __init__(self, args):
            super().__init__()
            self.l = LoadingPopup()
            self.l.show()
            w = TsrWorker(time.sleep, 5)
            w.signals.finished.connect(self.terminado)
            # w.run()
            QThreadPool.globalInstance().start(w) # <---
    
        def terminado(self):
            print('timer finished')
            self.l.hide()