3

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.

ahendawy
  • 79
  • 7
  • One thing to note, not related to the freeze you experience, is that you never update the progress bar with something like `self.x.setValue(progress)`. – Guimoute Sep 17 '20 at 12:25
  • 1
    That's because I just want it to give that busy indication until the db update finishes. This is a marquee style bar so minimum and maximum are set to zero. – ahendawy Sep 17 '20 at 12:36
  • Oh ok. Then I believe the solution lies in one of two things: 1- you need to keep a reference to your progress dialog "alive" and stored in `self`, and I mean the `self` from your program interface and not the one from your class. Or 2- the progress dialog needs to have a parent which is your program interface. In both cases you need to find a way to pass self as an argument to the QThread (call it `zelf` or something). – Guimoute Sep 17 '20 at 12:47
  • 1
    please provide a [mcve] – S. Nick Sep 17 '20 at 12:54
  • You should send the action for deleting the order to the thread, not the creation of the QDialog.The dialog should be created on the main thread and shown/hidden using the `QThread.started` and `QThread.finished` signals. – Heike Sep 17 '20 at 12:59
  • @Heike I thought it better to separate it to another thread so I can reuse it with other functions of the main window, like say creating a new order or editing an order. But theoretically there should be nothing stopping it from executing. – ahendawy Sep 17 '20 at 13:34
  • @Guimoute I already create an instance of the worker thread in the main window __init__ method. Is that what you mean by point 1? – ahendawy Sep 17 '20 at 13:35
  • No, I mean that the main window init method could have something like `self.progress_dialog = QProgressDialog("Please wait...", None, 0, 0, parent=self)` then the thread is provided (somehow) the ref to this widget. – Guimoute Sep 17 '20 at 13:40

1 Answers1

2

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)
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • This was super helpful in understanding threading. However, following this philosophy means that I have to create threads for each function that will deal with the database. So in the end the main window functionality would be divided into various separate threads. Is this a good practice? – ahendawy Sep 20 '20 at 11:24
  • Any operation that is potentially blocking is usually moved to separate threads. Note that you should try to optimize their usage, avoid unnecessary creation (you can reuse a thread, you just have to set it up correctly each time). Also, have a look at QRunnable and QThreadPool. – musicamante Sep 20 '20 at 12:45