Using QThread to display QProgressDialog in PyQt5

2.3k views Asked by At

I am using PyQt5 to write an app that manages Sales Orders. When creating an Order or deleting itI want to display a marqee style progress dialog to indicate that the app is working. I have visited a lot of posts where the answer involved using QThread.I have tried to implement it but it seems I am missing something. This is my threading class.

class Worker(QThread):
    finished = Signal()

def run(self):
    self.x = QProgressDialog("Please wait..",None,0,0)
    self.x.show()

def stop(self):
    self.x.close()

In the Main window's init I create self.worker=Worker()

Now the code for deleting an entry is for example:

msg = MsgBox("yn", "Delete Order", "Are you sure you want to delete this order?") # Wrapper for the QMessageBox
if msg == 16384:
    self.worker.start()   ## start the worker thread, hoping to start the progress dialog
    session.delete(order) ##delete order from db
    session.commit()      ##commit to db
    self.change_view("Active", 8) ##func. clean up the table.
    self.worker.finished.emit()   ##emit the finished signal to close the progress dialog

The result is no progress dialog being displayed. The gui just freezes for a second or two and then the entry deletes without any progress dialog being displayed.

Sorry my code is quite long so I couldn't include it all here, I just wanted to see if I got something terribly wrong.

1

There are 1 answers

2
musicamante On BEST ANSWER

There are two main problems with your code:

  1. GUI elements (everything inherited or related to a QWidget subclass) must be created and accessed only from the main Qt thread.
  2. assuming that what takes some amount of time is the delete/commit operations, it's those operation that must go in the thread while showing the progress dialog from the main thread, not the other way around. Also, consider that QThread already has a finished() signal, and you should not overwrite it.

This is an example based on your code:

class Worker(QThread):
    def __init__(self, session, order):
        super.__init__()
        self.session = session
        self.order = order

    def run(self):
        self.session.delete(self.order)
        self.session.commit()


class Whatever(QMainWindow):
    def __init__(self):
        super().__init__()
        # ...
        self.progressDialog = QProgressDialog("Please wait..", None, 0, 0, self)

    def deleteOrder(self, session, order):
        msg = MsgBox("yn", "Delete Order", 
            "Are you sure you want to delete this order?")
        if msg == MsgBox.Yes: # you should prefer QMessageBox flags
            self.worker = Worker(session, order)
            self.worker.started(self.progressDialog.show())
            self.worker.finished(self.deleteCompleted)
            self.worker.start()

    def deleteCompleted(self):
        self.progressDialog.hide()
        self.change_view("Active", 8)

Since the progress dialog should stay open while processing, you should also prevent the user to be able to close it. To do that you can install an event filter on it and ensure that any close event gets accepted; also, since QProgressDialog inherits from QDialog, the Esc key should be filtered out, otherwise it will not close the dialog, but would reject and hide it.

class Whatever(QMainWindow):
    def __init__(self):
        super().__init__()
        # ...
        self.progressDialog = QProgressDialog("Please wait..", None, 0, 0, self)
        self.progressDialog.installEventFilter(self)

    def eventFilter(self, source, event):
        if source == self.progressDialog:
            # check for both the CloseEvent *and* the escape key press
            if event.type() == QEvent.Close or event == QKeySequence.Cancel:
                event.accept()
                return True
        return super().eventFilter(source, event)