DreyFax
DreyFax

Reputation: 427

PyQt Dialog not responsible while thread running

I want to show loading progress via a modal QDialog. So I create a thread to load the data and call exec() on the dialog.

loading_progress_dialog = LoadingProgressDialog(len(filenames))
loadingWorker = analyzer.LoadingWorker(filenames, loading_progress_dialog.apply_progress)
workingThread = QThread()

workingThread.started.connect(loadingWorker.process)
loadingWorker.finished.connect(workingThread.quit)
workingThread.finished.connect(loading_progress_dialog.accept)

loadingWorker.moveToThread(workingThread)
workingThread.start()

loading_progress_dialog.exec()

I want the dialog to be responsible but it freezes and I'm not able to move it around on the screen while the loading thread is running.

class LoadingProgressDialog(QLoadingProgressDialog, Ui_LoadingDialog):
    def __init__(self, maxFiles):
        super(LoadingProgressDialog, self).__init__()
        self.setupUi(self)

        self.progressBar.setMaximum(maxFiles)
        self.setWindowTitle('Loading files...')

    def apply_progress(self, delta_progress):
        self.progressBar.setValue(delta_progress + self.progressBar.value())

class LoadingWorker(QtCore.QObject):
    def __init__(self, file_names, progress_made):
        super(LoadingWorker, self).__init__()
        self._file_names = file_names
        self._progress_made = progress_made

    finished = QtCore.pyqtSignal()

    def process(self):
        print("Thread started")
        # load_csv_data(self._file_names, self._progress_made)    
        QtCore.QThread.sleep(5)
        self.finished.emit()

Am I fighting with GIL or is it another problem? And second thing I am worried about is race-condition between self.finished.emit() and loading_progress_dialog.exec(). If the working thread is finished faster than gui thread runs exec(), the dialog will not close. Is there any way to ensure that everything is in right order?

Upvotes: 2

Views: 1596

Answers (1)

Alexander Lutsenko
Alexander Lutsenko

Reputation: 2160

  1. Your GUI freezes because it executes in the same thread as your worker - in main thread! How is this possible if you moved worker to different thread? Well, let's take a look at what exactly you've done:

    # This connects signal to the instance of worker located in main thread
    workingThread.started.connect(loadingWorker.process)
    
    # Creates a copy of worker in the different thread
    loadingWorker.moveToThread(workingThread)
    
    # Signal reaches the instance of worker it was connected to - 
    # the instance belonging to main thread!
    workingThread.start()
    

    The fix is straightforward: move worker before attaching signals to it.

  2. Race-condition is impossible if it is guaranteed that progress dialog receives command to show before closing one:

    class LoadingWorker(QtCore.QObject):
        [...]
        def process(self):
            self.ready.emit()
            [...]
            self.finished.emit() 
    
    loadingWorker.ready.connect(loading_progress_dialog.exec)
    loadingWorker.finished.connect(loading_progress_dialog.close)
    

So, the simple program which updates UI by the order of the different thread may look like this:

from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import QThread
from time import sleep

class LoadingProgressDialog(QtGui.QDialog):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Loading files...')

    def show_progress(self, p):
        self.setWindowTitle('Loading files... {}%'.format(p))

class LoadingWorker(QtCore.QObject):
    finished = QtCore.pyqtSignal()
    ready = QtCore.pyqtSignal()
    report_progress = QtCore.pyqtSignal(object)

    def process(self):
        print('Worker thread ID: %s' % int(QThread.currentThreadId()))
        print("Worker started")
        self.ready.emit()

        for p in range(0, 100, 10):
            self.report_progress.emit(p)
            sleep(0.2)

        print("Worker terminates...")
        self.finished.emit()


if __name__ == '__main__':
    import sys
    app = QtGui.QApplication([])

    print('Main thread ID: %s' % int(QThread.currentThreadId()))

    workingThread = QThread()
    loadingWorker = LoadingWorker()
    loading_progress_dialog = LoadingProgressDialog()

    loadingWorker.ready.connect(loading_progress_dialog.exec)
    loadingWorker.report_progress.connect(loading_progress_dialog.show_progress)
    loadingWorker.finished.connect(workingThread.quit)
    loadingWorker.finished.connect(loading_progress_dialog.close)

    loadingWorker.moveToThread(workingThread)

    workingThread.started.connect(loadingWorker.process)
    workingThread.start()

    sys.exit(app.exec_())

Upvotes: 3

Related Questions