Search code examples
pythonpyqtpyqt5

How can I fix issues with my PyQt5 splash screen implementation?


How can I fix issues with my PyQt5 splash screen implementation?

I am working on a project that requires a splash screen displaying a GIF file while loading menus. Each menu can have a different number of items, and the time needed to load each menu can vary. I want to include two progress bars: one tracking the progress of the menu loading and one displaying the overall progress of the entire process. The first progress bar should clear its value after a time delay, even when the menu loading completed before that time. The splash screen should only close after the second progress bar reaches 100%.

Currently, my implementation has the following problems:

  • The second progress bar (displaying the overall progress of the entire process) only shows 33% progress after the first menu loaded. The subsequent menu loading does not update the second progress bar.
  • The first progress bar (tracking the progress of the menu loading) only reaches 78% when loading the third menu before the splash screen closes.

Here is the code I am using:

import sys
import time
from PyQt5.QtWidgets import QApplication, QWidget, QDialog, QProgressBar, QMainWindow, QVBoxLayout
from PyQt5.QtCore import Qt, QTimer, QEventLoop, QCoreApplication


class SplashScreen(QDialog):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Splash Screen Example')
        self.setFixedSize(800, 400)
        self.setWindowFlag(Qt.FramelessWindowHint)
        self.setWindowFlag(Qt.WindowStaysOnTopHint)
        self.initUI()

    def initUI(self):
        layout = QVBoxLayout(self)

        self.progressBarMenu = QProgressBar()
        self.progressBarProcess = QProgressBar()

        layout.addWidget(self.progressBarMenu)
        layout.addWidget(self.progressBarProcess)

        self.setStyleSheet('''
            QProgressBar {
                background-color: #DA7B93;
                color: rgb(200, 200, 200);
                border-style: none;
                border-radius: 10px;
                text-align: center;
                font-size: 20px;
            }

            QProgressBar::chunk {
                border-radius: 10px;
                background-color: qlineargradient(spread:pad x1:0, x2:1, y1:0.511364, y2:0.523, stop:0 #1C3334, stop:1 #376E6F);
            }
        ''')


class MyApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.valueMenu = 0
        self.valueProcess = 0
        self.window_width, self.window_height = 800, 600
        self.setMinimumSize(self.window_width, self.window_height)

        self.splash_screen = SplashScreen()
        self.setCentralWidget(self.splash_screen)

        self.menu_count = 3
        self.current_menu = 1

        self.timerMenu = QTimer()
        self.timerMenu.timeout.connect(self.load_next_menu)
        self.timerMenu.start(1000)

        self.timerProcess = QTimer()
        self.timerProcess.timeout.connect(self.update_process_bar)
        self.timerProcess.start(1000)

        self.start_time = time.time()
        self.estimated_time = 10  # Estimated time in seconds for each menu
        self.remaining_time = self.estimated_time

        self.overall_progress = 0
        self.is_menu_loaded = False

    def load_next_menu(self):
        self.valueMenu = 0

        if self.current_menu == 1:
            self.load_menu_one()
        elif self.current_menu == 2:
            self.load_menu_two()
        elif self.current_menu == 3:
            self.load_menu_three()

        self.update_menu_progress(self.valueMenu)
        self.current_menu += 1

        if self.current_menu > self.menu_count:
            self.timerMenu.stop()
            self.update_menu_progress(100)
            self.is_menu_loaded = True

    def load_menu_one(self):
        menu_one_items = 10  # Number of items in menu one

        for i in range(menu_one_items):
            time.sleep(0.5)
            self.valueMenu += int(100 / menu_one_items)
            self.update_menu_progress(self.valueMenu)
            QCoreApplication.processEvents()

    def load_menu_two(self):
        menu_two_items = 4  # Number of items in menu two

        for i in range(menu_two_items):
            time.sleep(0.5)
            self.valueMenu += int(100 / menu_two_items)
            self.update_menu_progress(self.valueMenu)
            QCoreApplication.processEvents()

    def load_menu_three(self):
        menu_three_items = 40  # Number of items in menu three

        for i in range(menu_three_items):
            time.sleep(0.5)
            self.valueMenu += int(100 / menu_three_items)
            self.update_menu_progress(self.valueMenu)
            QCoreApplication.processEvents()

    def update_menu_progress(self, value):
        self.splash_screen.progressBarMenu.setValue(value)

    def update_process_bar(self):
        elapsed_time = int(time.time() - self.start_time)
        self.remaining_time = self.estimated_time - elapsed_time

        if self.current_menu > self.menu_count:
            self.overall_progress = 100
        else:
            self.overall_progress = int((self.current_menu - 1) / self.menu_count * 100)

        if self.remaining_time <= 0:
            self.remaining_time = 0
            self.timerProcess.stop()

        self.valueProcess = int(self.overall_progress + (self.valueMenu / self.menu_count))
        self.splash_screen.progressBarProcess.setValue(self.valueProcess)

        # print(f"Elapsed Time: {elapsed_time}s")
        # print(f"Estimated Time: {self.estimated_time}s")
        # print(f"Remaining Time: {self.remaining_time}s")

        if self.current_menu > self.menu_count and self.is_menu_loaded:
            self.timerProcess.stop()
            self.splash_screen.close()
            self.show_main_window()

    def show_main_window(self):
        pass


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MyApp()
    window.show()
    sys.exit(app.exec_())

Solution

  • There are several issues with your implementation.

    The problem(s) at hand

    1. You are using a QTimer to start loading menu items, but QTimer can only work properly with an event loop that is practically never blocked, which is not your case (I'm assuming that you used time.sleep to simulate loading);
    2. You are assuming an estimated time for loading that is completely arbitrary; the result if that if the process requires more time, the overall_progress will always be 100 even if loading is not completed, which is exactly your case: since you are loading 10+4+40 items with a basic 0.5s delay for each item, resulting in 27 seconds;

    The above means that:

    • the visible updates will stop as soon as 10 seconds have passed since the loading started;
    • the updates will probably not be consistent on different conditions or machines due to the unreliable usage of QTimer to load further menus;
    • you can never rely on arbitrary time estimations to "interrupt" a progress: the process is complete when it is complete;

    So, the "simple" solution to your issue is to not stop the timerProcess timer before the loading has finished.

    Not enough

    Unfortunately, this is not the least of your problems, as your implementation has many other important issues.

    The following are strictly related to the issue at hand:

    • as said above, you shouldn't use a QTimer to arbitrary load further items; instead, since the loading is supposed to be synchronous (a menu is loaded only after the previous is completed), you should just call a function that iterates through the menus and loads them;
    • the self.valueMenu addition is unreliable and imprecise, since it's using int(): a more appropriate solution would be to always use floating values for the addition, and set the progress bar value using round();

    Other issues

    These are not that relevant to the specific problem, but are still quite important:

    • Qt already provides a QSplashScreen class, and using a QMainWindow for that purpose is just wrong; either use the proper Qt.SplashScreen window flag, or, if you want the "splash screen" to be "embedded" in the window, set it as a child and outside of any layout, and eventually update its geometry in that window's resizeEvent() (see this related answer);
    • QDialog, similarly to QMainWindow, is intended as a top level widget (aka, a window), and it should never be set as a child of a widget (unless it's done in the proper context, like using MDI areas): there's absolutely NO valid reason in making a dialog as a direct child (and especially as central widget of a QMainWindow), and it is also a pointless, bad practice (unfortunately spread by some irresponsible youtubers); just use a standard QWidget or a widget that is supposed to be a container (like QFrame or QGroupBox);
    • the window flags you're applying only make sense for top level widgets (again, windows), setting them for a widget that is going to be a child is completely useless and has absolutely no effect;
    • setting a fixed size for a lone child widget is rarely a good choice, since it will be a child of a window that could be resized (so, the user will be able to resize the parent anyway, and the widget will still be aligned on its top left corner, and stuck to its limited size);