Why is QProgressDialog always showing at app startup?

254 views Asked by At

In a PyQt GUI, I will have several workers (QObjects associated to QThread), and 1 QProgressDialog per worker. Each one may have a different lifespan than the other.

I naively made the following example, where I create all necessary QProgressDialog at GUI init time.

However, each QProgressDialog defined at init time shows up at startup, even if I explicitly set visibility to False. I made several attempts to play with QProgressDialog, and it seems that the working versions are all based on a new QProgressDialog instance being created each time we need it.

Why is the QProgressDialog showing at startup ? Is it preferrable to instantiate on-the-fly ? (I am not looking for opinions, but for formal elements coming from either Qt code or Qt doc I may have missed)

There are various questions dealing with progress dialog/progress bar management, but none of them seem to address my question. The PyQt QProgressDialog documentation has no explanation for this, neither has QDialog documentation.


MCVE:

import sys
import time
from PyQt5.QtCore import QThread, Qt, QObject, pyqtSignal
from PyQt5.QtWidgets import (QApplication, QMainWindow, QProgressDialog, QPushButton)


class WorkerA(QObject):
    finished = pyqtSignal()
    def do_it(self):
        time.sleep(5)
        self.finished.emit()


class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumSize(250, 250)

        self.button = QPushButton(self)
        self.button.setText("Start worker A")
        self.button.clicked.connect(self.start_worker_A)

        self.thread_A = QThread()
        self.thread_A.setObjectName("ThreadA")
        self.worker_A = WorkerA()

        self.progress_dialog_A = QProgressDialog("Work A in progress", None, 0, 0, self)
        self.progress_dialog_A.setWindowModality(Qt.ApplicationModal)
        self.progress_dialog_A.setCancelButton(None)
        self.progress_dialog_A.setWindowTitle("Work A")
        self.progress_dialog_A.setVisible(False)
        self.progress_dialog_A.hide()

        self.thread_B = QThread()
        self.thread_B.setObjectName("ThreadB")
        self.worker_B = None

        self.progress_dialog_B = QProgressDialog("Work B in progress", None, 0, 0, self)
        self.progress_dialog_B.setWindowModality(Qt.ApplicationModal)
        self.progress_dialog_B.setCancelButton(None)
        self.progress_dialog_B.setWindowTitle("Work B")
        self.progress_dialog_B.setVisible(False)
        self.progress_dialog_B.hide()

    def start_worker_A(self):
        if not self.thread_A.isRunning():
            self.button.setEnabled(False)

            self.worker_A.moveToThread(self.thread_A)
            self.thread_A.started.connect(self.worker_A.do_it)
            self.thread_A.started.connect(self.progress_dialog_A.show)
            self.worker_A.finished.connect(self.thread_A.quit)
            self.worker_A.finished.connect(self.progress_dialog_A.hide)
            self.worker_A.finished.connect(self.enable_button)
            self.thread_A.start()
        else:
            pass

    def enable_button(self):
        self.button.setEnabled(True)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = Window()
    win.show()
    sys.exit(app.exec())

The code above always shows the QProgressDialog at app startup, even if their visibility is never explicitly invoked.


I tried the following variant, which works fine, but I don't understand the lifetime logic of QProgressDialog.

class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setMinimumSize(250, 250)

        self.button = QPushButton(self)
        self.button.setText("Start worker A")
        self.button.clicked.connect(self.start_worker_A)

        self.thread_A = QThread()
        self.thread_A.setObjectName("ThreadA")
        self.worker_A = WorkerA()

        self.progress_dialog_A : QProgressDialog = None
        # obviously no further operations here on self.progress_dialog_A
        # rest of init as first code sample above

    def start_worker_A(self):
        if not self.thread_A.isRunning():
            self.button.setEnabled(False)
            # QProgressDialog instantiated only when needed
            # any previous reference to QProgressDialog is garbaged collected ?
            # and a new instance created on the fly
            self.progress_dialog_A = QProgressDialog("Work A in progress", None, 0, 0, self)
            self.progress_dialog_A.setWindowModality(Qt.ApplicationModal)
            self.progress_dialog_A.setCancelButton(None)
            self.progress_dialog_A.setWindowTitle("Work A")
            self.progress_dialog_A.setVisible(False)
            self.progress_dialog_A.hide()

            self.worker_A.moveToThread(self.thread_A)
            self.thread_A.started.connect(self.worker_A.do_it)
            self.thread_A.started.connect(self.progress_dialog_A.show)
            self.worker_A.finished.connect(self.thread_A.quit)
            self.worker_A.finished.connect(self.progress_dialog_A.hide)
            self.worker_A.finished.connect(self.enable_button)
            self.thread_A.start()
        else:
            pass
# rest of code unchanged

I also tried overriding the default window show() with no success:

def show(self):
    super(Window, self).show()
    self.progress_dialog_A.hide()
    self.progress_dialog_B.hide()
1

There are 1 answers

0
musicamante On BEST ANSWER

This is the expected behavior, and while it might not be very clear at first, it's explained in the documentation.

From the detailed description:

it estimates the time the operation will take (based on time for steps), and only shows itself if that estimate is beyond minimumDuration()

Then, minimumDuration():

This property holds the time that must pass before the dialog appears.

If the expected duration of the task is less than the minimumDuration, the dialog will not appear at all. This prevents the dialog popping up for tasks that are quickly over. For tasks that are expected to exceed the minimumDuration, the dialog will pop up after the minimumDuration time or as soon as any progress is set.

If set to 0, the dialog is always shown as soon as any progress is set. The default is 4000 milliseconds.

This is achieved by an internal QTimer that calls forceShow() as soon as it times out with the given minimumDuration() (4 seconds by default).

A possible solution is to stop the timer as soon as the dialog instance is created:

self.progress_dialog_A.findChild(QTimer).stop()
self.progress_dialog_B.findChild(QTimer).stop()