Search code examples
pythonpyqt6

QT Waiting Spinner not showing


I am trying to use qwaitingspinner.py (QtWaitingSpinner on GitHub) module in my project. The spinner's parent is an existing, visible QTabWidget page. When I add a page (a process whose initialization takes 5 to 10 seconds during which the 'tabVisible' attribute of the new page is kept at False), I start the spinner. This one is not displaying as I expect. However, it becomes visible and functional once the new page has been added and made visible (voluntarily, I don't stop the spinner to see what happens). I understand Python is busy executing the while loop in the example. And Qt's event loop also seems to be impacted since I don't see the spinner while the while loop is executing. So how to make the spinner functional while executing the loop in the example?

import sys
from time import monotonic

from PyQt6.QtWidgets import (
    QApplication,
    QMainWindow,
    QPushButton,
    QTabWidget,
    QVBoxLayout,
    QWidget,
)


from waitingspinnerwidget import QtWaitingSpinner

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.resize(400, 400)
        self.setWindowTitle("Spinner test")
        layout = QVBoxLayout()
        self._tab = QTabWidget(self)

        _page_1 = QWidget()
        self._page_index = self._tab.addTab(_page_1, "Page with the spinner")
        layout.addWidget(self._tab)
       
        btn_add_page = QPushButton()
        btn_add_page.setText("Add a page")
        btn_add_page.clicked.connect(self.add_new_page)
        layout.addWidget(btn_add_page)
        
        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        self.spinner = None
        
    def add_new_page(self):
        _new_index = self._page_index + 1
        widget = QWidget()
        widget.setObjectName(f"page_{_new_index}")
        self.start_spinner()

        self._page_index = self._tab.addTab(widget, f"Page no {_new_index}")
        self._tab.setTabVisible(self._page_index, False)

        t = monotonic()
        while monotonic() - t < 5.0:
            # The purpose of this loop is to simulate time-consuming by the project constructor of a new page.
            continue

        self._tab.setTabVisible(self._page_index, True)
        self._tab.setCurrentIndex(self._page_index)
        self.stop_spinner()

    def start_spinner(self):
        self.spinner = QtWaitingSpinner(parent=self._tab.widget(self._tab.count() - 1))
        self.spinner.start()

Solution

  • The issue you're running into is due to the fact that the UI thread is being blocked by your long-running construction task, and cannot enter the event loop. You might want to consider partially moving this task to another thread, to avoid freezing your UI (you'll notice that while it's running, the window is completely unresponive -- it's not just the spinner that is failing to show up).

    One possible approach might be to look into QThreads. Beware though: this can lead to crashes if not handled properly. The main thing to remember is that the UI can only be modified from within the UI thread, so once you have finished the long-running part of your setup (i.e. loading data, preparing lables, calculating layout, etc...), you should pass the results back to the main thread using Qt Signals and Slots.

    An example below:

    import sys
    from time import monotonic, sleep
    
    from PyQt6.QtCore import (
        pyqtSlot, pyqtSignal,
        QThread
    )
    
    from PyQt6.QtWidgets import (
        QApplication,
        QMainWindow,
        QPushButton,
        QTabWidget,
        QVBoxLayout,
        QWidget,
    )
    
    
    from QtWaitingSpinner.waitingspinnerwidget import QtWaitingSpinner
    
    
    class MyPageCreatorThread(QThread):
        mySignal = pyqtSignal(str)
    
        def run(self):
    
            print("started")
    
            t = monotonic()
            while monotonic() - t < 5.0:
                # The purpose of this loop is to simulate time-consuming by the project constructor of a new page.
                # however, you cannot do any operations directly on the UI in this thread (only the main thread is allowed to touch the UI).
                # Information must be passed back out of your thread using signals & slots.
                self.mySignal.emit(f"{monotonic():.2f}s")
                sleep(1)
    
            print("finished")
    
    
    class MyWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            self.resize(400, 400)
            self.setWindowTitle("Spinner test")
            layout = QVBoxLayout()
            self._tab = QTabWidget(self)
    
            _page_1 = QWidget()
            self._page_index = self._tab.addTab(_page_1, "Page with the spinner")
            layout.addWidget(self._tab)
    
            self.btn_add_page = QPushButton()
            self.btn_add_page.setText("Add a page")
            self.btn_add_page.clicked.connect(self.add_new_page)
            layout.addWidget(self.btn_add_page)
    
            widget = QWidget()
            widget.setLayout(layout)
            self.setCentralWidget(widget)
    
            # set the spinner over the tab widget, so its always visible
            self.spinner = QtWaitingSpinner(self._tab)
    
            # create the contructor thread. this will do the long-running initialization of the new widgets
            self.constructor_thread = MyPageCreatorThread(self)
            self.constructor_thread.mySignal.connect(
                self._page_creation_update)
            self.constructor_thread.finished.connect(
                self._page_creation_finished)
    
        def add_new_page(self):
            # start spinner
            self.start_spinner()
    
            # don't allow multiple pages to be started at once, it'll cause chaos
            self.btn_add_page.setDisabled(True)
    
            # create the widget
            _new_index = self._page_index + 1
            widget = QWidget()
            widget.setObjectName(f"page_{_new_index}")
            widget.setLayout(QVBoxLayout())
            self._page_index = self._tab.addTab(widget, f"Page no {_new_index}")
            self._tab.setTabVisible(self._page_index, False)
    
            # run the constructor thread
            self.constructor_thread.start()
    
        @pyqtSlot(str)
        def _page_creation_update(self, text):
            '''callback to update the page during a long-running initialization'''
            widget = self._tab.widget(self._page_index)
            new_btn = QPushButton(f"this button was created at {text}")
            widget.layout().addWidget(new_btn)
    
        @pyqtSlot()
        def _page_creation_finished(self):
            '''callback to finalize the page after a long-running initialization'''
    
            self._tab.setTabVisible(self._page_index, True)
            self._tab.setCurrentIndex(self._page_index)
    
            self.stop_spinner()
    
            # allow new pages to be created again
            self.btn_add_page.setEnabled(True)
    
        def start_spinner(self):
            self.spinner.start()
    
        def stop_spinner(self):
            self.spinner.stop()
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        mw = MyWindow()
        mw.show()
        app.exec()