Search code examples
dialogqthreadpyvistaqtpy

`QProgressDialog` Not Responding


I am using pyvistaqt and want display a progress bar window when I load data. I have success without using pyvista with PyQt (see this SO post), however it isn't working when I add vtk.

I think something is still blocking the main thread, but I don't know what. Either the progress bar won't show at all, or if it does, half way through the bar stops loading and stops responding. Any help would be much appreciated:

Setup:

python 3.8.10
pyvista 0.32.1
qtpy 1.11.3

Output:
demo

MRE

from pyvistaqt import MainWindow, QtInteractor
from qtpy import QtCore, QtGui, QtWidgets

class Worker(QtCore.QObject):

    update = QtCore.Signal(int)
    done = QtCore.Signal()

    def __init__(self):
        super().__init__()

    def load(self):
        for num in range(100):
            for i in range(200000):
                continue  # Simulate long-running task
            self.update.emit(num)

        self.done.emit()

class Controller(object):

    def __init__(self):
        self.view = View(controller=self)

    def on_load(self):
        self.thread = QtCore.QThread()
        self.worker = Worker()

        self.worker.moveToThread(self.thread)

        self.view.show_progress_dialog()

        self.thread.started.connect(lambda: self.worker.load())
        self.worker.update.connect(self.view.progress_dialog.on_update)

        def _on_finish():
            self.view.hide_progress_dialog()
            self.thread.quit()

        self.worker.done.connect(_on_finish)
        self.thread.finished.connect(self.worker.deleteLater)

        self.thread.start()

class ProgressDialog(QtWidgets.QDialog):

    def __init__(self, parent=None, title=None):
        super().__init__(parent)
        self.setWindowTitle(title)

        self.pbar = QtWidgets.QProgressBar(self)

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(self.pbar)
        self.setLayout(layout)

        self.setWindowFlag(QtCore.Qt.WindowContextHelpButtonHint, False)

        self.resize(500, 50)
        self.hide()

    def on_update(self, value):
        self.pbar.setValue(value)

class View(MainWindow):
    def __init__(self, controller):
        super().__init__()
        self.controller = controller

        self.container = QtWidgets.QFrame()

        self.layout_ = QtWidgets.QGridLayout()
        self.layout_.setContentsMargins(0, 0, 0, 0)

        self.container.setLayout(self.layout_)
        self.setCentralWidget(self.container)

        self.progress_dialog = ProgressDialog(self)

        self.btn = QtWidgets.QPushButton(self)
        self.btn.setText("Load")

        self.btn.clicked.connect(self.controller.on_load)

    def show_progress_dialog(self):
        self.progress_dialog.setModal(True)
        self.progress_dialog.show()

    def hide_progress_dialog(self):
        self.progress_dialog.hide()
        self.progress_dialog.setModal(False)
        self.progress_dialog.pbar.reset()
        self.progress_dialog.title = None

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    root = Controller()
    root.view.show()
    app.exec_()

Solution

  • "(Not Responding)" in Windows is usually the consequence of a deadlock. Historically, a lot of confusion has surfaced over whether to override run() or to use moveToThread() when dealing with QThread:

    1. You're doing it wrong...
    2. You were not doing so wrong
    3. QThread documentation: do not discourage the reimplementation of QThread

    Though both methods are accepted, I chose to use moveToThread() because I learned it was best used when you have threads that need to interact with one another through signals and slots (see QThreads: Are You Using Them Wrong?)

    After careful consideration, I removed the lambda from self.thread.started.connect and replaced it with

    self.thread.started.connect(self.worker.load)
    

    which fixed the problem. If arguments do need to be passed, an appropriate alternative would be to use functools.partial.

    I'm still not 100% sure why this fixed the problem, but I think it did because, in Python,:

    1. lambdas behave like closures and are evaluated at runtime (see How to Use Python Lambda Functions), and
    2. creating a signal to a slot with a reference to itself (self) doesn't get garbage collected (see this SO post)

    However, the above being the case, I'm not sure why the progress bar can run part way through (up to 51%) before hitting a deadlock...?

    Alternatively, overriding QThread.run() works, even though technically the thread affinity would be towards the main thread since moveToThread() was not used, so I'm not sure how the signals and slots communicate without issue in this case.

    Regardless of overriding QThread.run() or using moveToThread(), passing a lambda to a slot will cause this "(Not Responding)" error.

    I would love it if someone could explain this!