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:
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_()
"(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 usemoveToThread()
when dealing withQThread
: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
fromself.thread.started.connect
and replaced it withwhich 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,:
lambda
s behave like closures and are evaluated at runtime (see How to Use Python Lambda Functions), andself
) 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 sincemoveToThread()
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 usingmoveToThread()
, passing alambda
to aslot
will cause this "(Not Responding)" error.I would love it if someone could explain this!