pythonsignals-slotsqthreadpyside6

PySide6 concurrent information window (with delay) from long process


I am working a tiny PySide6 project where I need to run light-to-heavy calculations. Because of the duration of the calculations (0.1 - 600 sec), I want to display a small window indicating "processing ..." when the calculation time goes beyond 3 sec.

I tried to create a separate QThread where I receive a signal to trigger a response after some delay, from a main window/thread where I run the calculation.

Here is my sample code :

from PySide6.QtWidgets import QWidget, QPushButton, QVBoxLayout, QApplication, QLabel
from PySide6.QtCore import Slot, Signal, QObject, QThread

import time
import sys


class AnotherWindow(QWidget):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Processing ...")
        layout.addWidget(self.label)
        self.setLayout(layout)


class MyWorker(QObject):
    display = Signal()

    @Slot()
    def display_trigger(self):
        print('before delay')
        time.sleep(2)
        print('delay done')
        self.display.emit()


class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()

        self.thread = None
        self.worker = None
        self.button = None
        self.w = AnotherWindow()

        self.init_ui()
        self.setup_thread()

    def init_ui(self):
        layout = QVBoxLayout()
        self.button = QPushButton('User input')
        self.button.setEnabled(True)
        layout.addWidget(self.button)
        self.setLayout(layout)
        self.show()

    @Slot()
    def display(self):
        print("display it !")
        self.w.show()

    @Slot()
    def processing(self):
        print("=========")
        for i in range(8):
            print(i)
            time.sleep(1)
        self.w = None

    def closeEvent(self, event):
        # self.w = None
        self.thread.quit()
        del self.thread

    def setup_thread(self):
        self.thread = QThread()
        self.worker = MyWorker()

        self.worker.moveToThread(self.thread)

        self.worker.display.connect(self.display)
        self.button.clicked.connect(self.worker.display_trigger)
        self.button.clicked.connect(self.processing)

        self.thread.start()


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

The problem here is that the 'processing' windows does not appear during the calculation, and then an error occurs obviously because it tries to show it despite it was deleted at the end of the processing method. I want to keep this delete because the window is useless once the processing is done, but I don't understand why the widget does not show up when the signal is triggered. It seems that the signal is blocked while the long process is running. Any idea about this blocked signal in main thread ?


Solution

  • After some tuning, I found a solution thank to a "famous online AI" and previous suggestions. I was doing it wrong : I inverted the problem and the threads. In the next solution, the popup window is simply started from main thread, and the long calculation is started from the other thread. That is the good way to do it.

    Here is the working code with my final tuning:

    import sys
    import time
    from PySide6 import QtWidgets, QtCore, QtGui
    
    
    class ProcessingWindow(QtWidgets.QDialog):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setModal(True)
            self.setWindowTitle("Processing...")
            self.setFixedSize(200, 100)
            layout = QtWidgets.QVBoxLayout(self)
            self.label = QtWidgets.QLabel("Processing...", self)
            self.label.setFont(QtGui.QFont("Arial", 16))
            layout.addWidget(self.label, alignment=QtCore.Qt.AlignmentFlag.AlignCenter)
            self.setWindowFlags(QtCore.Qt.WindowType.SplashScreen)
    
        def center(self):
            # Calculate the center position of the main window
            parent_rect = self.parent().geometry()
            parent_center = parent_rect.center()
    
            # Calculate the position of the dialog
            dialog_rect = self.geometry()
            dialog_rect.moveCenter(parent_center)
            self.setGeometry(dialog_rect)
    
    
    class LongProcessThread(QtCore.QThread):
        finished = QtCore.Signal()
    
        def __init__(self, parent=None, duration=None):
            super().__init__(parent)
            self.parent = parent
            self.duration = duration
    
        def run(self):
            self.parent.simulate_long_process(self.duration)
            self.finished.emit()
    
    
    class MainWindow(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            self.setWindowTitle("Long Process Example")
            self.setFixedSize(500, 300)
            layout = QtWidgets.QVBoxLayout(self)
            self.button = QtWidgets.QPushButton("Run Long Process", self)
            layout.addWidget(self.button, alignment=QtCore.Qt.AlignmentFlag.AlignCenter)
            self.button.clicked.connect(self.run_long_process)
            self.processing_window = None
    
        def run_long_process(self):
            self.button.setEnabled(False)
            long_process_duration = 10  # seconds
            delay_popup = 3000  # milliseconds
            long_process_thread = LongProcessThread(self, long_process_duration)
            long_process_thread.finished.connect(self.enable_button)
            long_process_thread.start()
            QtCore.QTimer.singleShot(delay_popup, lambda: self.show_processing_window(long_process_thread))
    
        @staticmethod
        def simulate_long_process(duration=None):
            print("before")
            if duration:
                time.sleep(duration)  # Simulating a long process
            print(f"after {duration} seconds")
    
        def show_processing_window(self, long_process_thread):
            if not long_process_thread.isFinished():
                self.processing_window = ProcessingWindow(self)
                self.processing_window.center()
                self.processing_window.show()
    
        def enable_button(self):
            if self.processing_window is not None:
                self.processing_window.close()
            self.button.setEnabled(True)
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        main_window = MainWindow()
        main_window.show()
        sys.exit(app.exec())