Search code examples
pythonpyqtpyqt5splash-screenanimated-gif

PyQt: show animated GIF while other operations are running


I have a loading GIF, and I want to show it as a loading screen till a specific method is finished. My problem is that the GIF won't keep running because there is another function working, and that GIF should keep running till that function is done.

import sys
import time
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QMovie
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow

def wait_till_function_done():
    time.sleep(5)

class Window(QMainWindow):

    def __init__(self):
        super().__init__()


class Splash:

    def showSplash(self):
        label = QLabel()
        label.setWindowFlag(Qt.SplashScreen)

        # label for displaying GIF
        self.movie = QMovie(".../Loading.gif")
        label.setMovie(self.movie)

        self.movie.start()
        label.show()

        # -------
        # Simulate the operation that is talking about.
        # The GIF should keep working and playing till the following simulation is done.
        # -------
        # But The GIF stop running and that is because the following line block Qt event

        wait_function_is_done()  # this line simulate this sentence: "keep showing loading screen till the operation is done"


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main_window = Window()
    splash_obj = Splash()
    # start the Splash screen
    splash_obj.showSplash()
    # when it's done, it will be closed and main_app showen
    main_window.show()
    app.exec_()

I've tried to use QThread with the loading gif, so it will change the gif frames while that method/operation is running (and should be closed when that function is done). But that didn't work out:

  1. The frames are not changing. (I can use app.processEvents() so the frames can be changed).
  2. It is stuck in the while loop.
class GIFThread(QThread):
    signal = pyqtSignal()

    def __init__(self):
        super(GIFThread, self).__init__()

class Splash:

    def showSplash(self):

        .....

        # init thread
        thread = GIFThread()
        thread.signal.connect(self.change_gif_frame)
        thread.start()

        # start changing GIF frames
        thread.signal.emit()

        # -------
        # Simulate the operation.
        time.sleep(5)

        # this should stop the thread and break the while loop
        thread.quit()

    def change_gif_frame(self):
        while True:
            self.movie.jumpToNextFrame()

Solution

  • It's probably best to use QSplashScreen for this, and use QThread to run the app initialisation in the background. If necessary, the splash-screen can be updated with progress messages via a custom signal. Here is a basic demo that shows how to implement that:

    import sys
    from PyQt5.QtCore import pyqtSignal, Qt, QThread
    from PyQt5.QtGui import QMovie
    from PyQt5.QtWidgets import QApplication, QSplashScreen, QMainWindow
    
    class Worker(QThread):
        progressChanged = pyqtSignal(int)
    
        def run(self):
            for count in range(6):
                self.progressChanged.emit(count)
                self.sleep(1)
            self.progressChanged.emit(-1)
    
    class Window(QMainWindow):
        def __init__(self):
            super().__init__()
    
    class SplashScreen(QSplashScreen):
        def __init__(self, filepath, flags=0):
            super().__init__(flags=Qt.WindowFlags(flags))
            self.movie = QMovie(filepath, parent=self)
            self.movie.frameChanged.connect(self.handleFrameChange)
            self.movie.start()
    
        def updateProgress(self, count=0):
            if count == 0:
                message = 'Starting...'
            elif count > 0:
                message = f'Processing... {count}'
            else:
                message = 'Finished!'
            self.showMessage(
                message, Qt.AlignHCenter | Qt.AlignBottom, Qt.white)
    
        def handleFrameChange(self):
            pixmap = self.movie.currentPixmap()
            self.setPixmap(pixmap)
            self.setMask(pixmap.mask())
    
    if __name__ == "__main__":
    
        app = QApplication(sys.argv)
        window = Window()
        splash = SplashScreen('anim.gif', Qt.WindowStaysOnTopHint)
        worker = Worker()
        worker.progressChanged.connect(splash.updateProgress)
        worker.finished.connect(
            lambda: (splash.finish(window), window.show()))
        splash.show()
        worker.start()
        app.exec_()