1
self.dialogBox.close() 
self.state = "processing"
self.__clearLayout(self.layout)
print(1)
self.controller.process()

I am running the following code. The dialog box is a PyQt dialog box, self.state is just a current state, self.__clearLayout clears the current layout of the PyQt widget, print(1) is there for debugging purposes, and self.controller.process() is what processes things and it takes a while. My goal is to clear the UI and then have my program process, but it is out of order. It prints 1 and begins processing, which is telling me that it is in order but the UI isn't setting until after the process is done. Any tips on how to fix this?

def processUi(self):
    self.dialogBox.close()
    self.state = "processing"
    self.__clearLayout(self.layout)

    #label
    processingLabel = QtWidgets.QLabel()
    processingLabel.setMaximumSize(500, 500)
    processingLabel.setFont(QtGui.QFont("Mono", 16))
    processingLabel.setText("Processing...may take a few minutes.")

    #set the current layout
    currentLayout = QtWidgets.QVBoxLayout()
    currentLayout.setContentsMargins(0,0,0,0)
    currentLayout.addStretch()
    currentLayout.addWidget(processingLabel)
    currentLayout.addStretch()

    self.layout.addStretch()
    self.layout.addLayout(currentLayout)
    self.layout.addStretch()

    #thread for processing
    processing = multiprocessing.Process(target=self.controller.process, args=())
    processing.start()
    processing.join()
    self.finishedUi()

This is the full code. The goal is to set the ui code you see while the processing is working and once the process finishes, to call finishedUi.

Aleks
  • 23
  • 6

1 Answers1

0

I assume your clear-layout method is the same as the one I gave in this answer.

If so, the problem is with deleteLater, which delays deletion until control returns to the event-loop. There is no way to force processing of delayed deletion-events (e.g. QApplication.processEvents() will have no effect). Instead, it will be necessary to delete the widgets directly using the sip module:

import sip

def clearLayout(self, layout):
    if layout is not None:
        while layout.count():
            item = layout.takeAt(0)
            widget = item.widget()
            if widget is not None:
                sip.delete(widget)
            else:
                self.clearLayout(item.layout())

If multi-threading is used, there is no need to bother with sip.delete, because any pending deletion events will be processed as soon as the thread starts. Below is a simple demo that shows how to use a thread to do non-blocking processing:

from PyQt5 import QtCore, QtWidgets

class Worker(QtCore.QThread):
    progressChanged = QtCore.pyqtSignal(int)

    def run(self):
        for tick in range(10):
            self.msleep(500)
            self.progressChanged.emit(tick + 1)

class Window(QtWidgets.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.button = QtWidgets.QPushButton('Start', self)
        self.button.clicked.connect(self.handleButton)
        self.frame = QtWidgets.QFrame(self)
        self.frame.setMinimumHeight(100)
        self.frame.setLayout(QtWidgets.QVBoxLayout())
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.frame)
        layout.addWidget(self.button)

    def handleButton(self):
        self.button.setEnabled(False)
        self.clearLayout(self.frame.layout())
        label = QtWidgets.QLabel('Starting')
        self.frame.layout().addWidget(label)
        def progress(tick):
            label.setText('Processing... Count = %d' % tick)
        def finish():
            label.setText('Finished')
            self.button.setEnabled(True)
            thread.deleteLater()
        thread = Worker(self)
        thread.finished.connect(finish)
        thread.progressChanged.connect(progress)
        thread.start()

    def clearLayout(self, layout):
        if layout is not None:
            while layout.count():
                item = layout.takeAt(0)
                widget = item.widget()
                if widget is not None:
                    widget.deleteLater()
                else:
                    self.clearLayout(item.layout())

if __name__ == '__main__':

    app = QtWidgets.QApplication(['test'])
    window = Window()
    window.setGeometry(600, 100, 300, 200)
    window.show()
    app.exec_()
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • You are correct, I was using the previous one you had posted and I changed it to this version, however the behavior did not change. The ui was still frozen. – Aleks Jun 17 '17 at 00:45
  • @Aleks. The behaviour **does** change. If you put `QApplication.processEvents()` after clearing the layout, the ui **will** update immediately. However, your processing function blocks, so it still won't be possible to interact with the ui afterwards. To achieve that, you would need to do the processsing in a separate thread. That is a completely different question, though - your current question only asks how to fix the execution order when clearing the layout. – ekhumoro Jun 17 '17 at 12:49
  • Thank you, I am aware of using different processes, and I am using them, I only simplified the code for this question. The behavior changed after I added the QApplication.processEvents() which I was not aware of. However, now it blocks my whole ui during and after the process is complete. When I click anywhere on the ui, computer just makes blocking noise. – Aleks Jun 17 '17 at 16:00
  • @Aleks. I have solved the specific problem stated in your original question. Of course your ui will block: as I said, you need to do the processing in a separate thread if you want to avoid that. – ekhumoro Jun 17 '17 at 16:50
  • As I stated before, my code shown is only a simplified version of what I am doing. The self.controller.process() is actually being called as a separate process. My UI after the process is done stays blocked after I added the QApplication.processEvents() line. Without this line, my ui is unblocked at least after the process finished. Once again, thanks for all the help. – Aleks Jun 17 '17 at 17:52
  • @Aleks. The `QApplication.processEvents()` line is irrelevant. It is only intended for debugging, and is not part of my answer. It merely proves that my solution works: i.e. that the ui is updated correctly (which is all your question asks for). The code you posted, whether simplified or not, clearly *does not* use multi-threading or multi-processing. It is just a crude sequence of commands called one after the other - so it is all executed *in the same thread*. You need to do the processing *separately*, so that there is *more than one thread of execution*. Then the ui will not freeze. – ekhumoro Jun 17 '17 at 19:15
  • I have added a full code to show you that in fact I am doing separate processes and the solution does not work as intended. My ui is not updated until the process is complete still. – Aleks Jun 17 '17 at 23:39
  • @Aleks. That code is **not** asynchronous. Calling `join()` will block until the target function completes. You need to do the processing in a **completely separate** worker thread, so that control can immediately return to the event-loop (in the main thread). I have added a crude demo script to my answer to illustrate the basic principles. – ekhumoro Jun 18 '17 at 11:52
  • Thanks for all the help, I really appreciate it, I guess I was thinking that by starting the separate process it'll run concurrently with what is in my function and join will make finishedUi wait until the separate process is done. I will give this a try. – Aleks Jun 18 '17 at 12:29
  • I have not been able to actually implement it yet due to other work being in the way but when I do, I will for sure. – Aleks Jun 24 '17 at 15:18
  • I have actually up voted you not sure if it showed due to me having a low reputation on here, but still thank you I've implemented it and it works as expected. – Aleks Jul 06 '17 at 00:04