Python Forum
Making a launcher application for the .exe made using Pyinstaller --onefile
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Making a launcher application for the .exe made using Pyinstaller --onefile
#1
Hello. I have been working on my GUI application based on PySide6. My application simply starts:

if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = MainWindow()
    widget.show()
    sys.exit(app.exec())
After generating the .exe using pyinstaller --onefile option, I have noticed that when I try to run an application, it takes approximately 10-15 seconds until the application actually starts. From what I understand, this is normal because it is extracting all the dependencies.

To solve this issue, I have decided to create a launcher for my application. I simply want to display some image that would say "Application is loading" until my application properly starts. I have tried the following Python script :

import sys
import os
import time
from PySide6.QtWidgets import QApplication, QSplashScreen
from PySide6.QtGui import QPixmap, QFont
from PySide6.QtCore import Qt, QTimer, QProcess

def get_resource_path(relative_path):
    """ Get the absolute path to a resource in the PyInstaller bundle. """
    try:
        # PyInstaller creates a temporary folder and stores the path in _MEIPASS
        base_path = sys._MEIPASS
    except AttributeError:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)


def check_process_state():
    state = process.state()
    remaining_time = min_duration_timer.remainingTime()
    print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Process state: {state}, Remaining time: {remaining_time} ms")
    
    if state == QProcess.Running:
        if remaining_time > 0:
            QTimer.singleShot(remaining_time, lambda: close_splash(splash))
        else:
            close_splash(splash)

def close_splash(splash):
    print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Closing splash screen")
    splash.close()

if __name__ == "__main__":
    app = QApplication(sys.argv)

    print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Application started")

    # Create and display the splash screen
    splash_pix = QPixmap(get_resource_path("splash_image.png"))
    splash = QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint)
    splash.setFont(QFont("Arial", 10))
    splash.show()
    splash.showMessage("Loading application...", Qt.AlignBottom | Qt.AlignCenter, Qt.white)

    print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Splash screen displayed")
    
    # Process events to ensure the splash screen is shown
    app.processEvents()

    # Path to the main executable
    main_executable = get_resource_path("mainwindow.exe")
    print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Main executable path: {main_executable}")

    # Start the main executable
    process = QProcess()
    process.setProgram(main_executable)
    process.start()
    process.stateChanged.connect(check_process_state)

    print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Main executable started")

    # Ensure the splash screen is visible for at least 2 seconds
    min_duration_timer = QTimer()
    min_duration_timer.setSingleShot(True)
    min_duration_timer.start(20000)  # Minimum duration for the splash screen

    print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Minimum duration timer started (20 seconds)")
    
    
    min_duration_timer.timeout.connect(lambda: close_splash(splash))

    sys.exit(app.exec())
The serial logs of my launcher:
2024-07-30 07:42:08 - Application started
2024-07-30 07:42:09 - Splash screen displayed
2024-07-30 07:42:09 - Main executable path: C:\Users\petrikas.lu\Desktop\WORK\PyQT\test2\Test2\mainwindow.exe
2024-07-30 07:42:09 - Main executable started
2024-07-30 07:42:09 - Minimum duration timer started (20 seconds)
2024-07-30 07:42:29 - Closing splash screen
The launcher application is supposed to start mainwindow.exe process and monitor it state. When it is fully started OR after 20 seconds, the splash screen should dissappear.

In reality, the splash screen always remains for 20 seconds regardless of whether my main application started earlier or not.


I assume the issue lies within check_process_state method:
    def check_process_state():
        state = process.state()
        remaining_time = min_duration_timer.remainingTime()
        print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - Process state: {state}, Remaining time: {remaining_time} ms")
        
        if state == QProcess.Running:
            if remaining_time > 0:
                QTimer.singleShot(remaining_time, lambda: close_splash(splash))
            else:
                close_splash(splash)
It does not seem to detect that the application has started. I would appreciate any insights.
Reply
#2
This appears to be the source of the problem you describe.
    if state == QProcess.Running:
        if remaining_time > 0:
            QTimer.singleShot(remaining_time, lambda: close_splash(splash))
        else:
            close_splash(splash)
When state goes to running, you do an additional wait before closing the splash screen.

I think you have another problem. The launcher remains running after the process is finished. I would do it like this:
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Qt


class Launcher(QtWidgets.QApplication):
    def __init__(self, cmd_str, pix_file, message, timeout=None):
        super().__init__()
        # Draw splash screen.
        self.splash = QtWidgets.QSplashScreen(QtGui.QPixmap(pix_file), Qt.WindowStaysOnTopHint)
        self.splash.showMessage(message, Qt.AlignBottom | Qt.AlignCenter, Qt.white)
        self.splash.show()
        self.processEvents()

        # Erase splash screen if timed out waiting to start.
        if timeout is not None:
            self.timer = QtCore.QTimer()
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.close_splash_screen)
            self.timer.start(timeout)
        else:
            self.timer = None

        # Run process as QProcess.  Erase splash screen when process starts.
        # Shut down launcher when process finishes.
        self.process = QtCore.QProcess()
        self.process.started.connect(self.close_splash_screen)
        self.process.finished.connect(self.quit)
        self.process.startCommand(cmd_str)

    def close_splash_screen(self):
        # Close the splash screen and stop the timer.
        if self.splash:
            self.splash.close()
            self.splash = None
        if self.timer:
            self.timer.stop()
            self.timer = None

    def quit(self):
        # Do graceful exit when process finishes.
        self.close_splash_screen
        self.exit()


if __name__ == "__main__":
    app = Launcher("python junk2.py", "test.png", "Loading application...", 5000)
    app.exec()
I can't really test this because I don't have anything that takes a long time to start running.
Reply
#3
(Jul-31-2024, 04:09 AM)deanhystad Wrote: This appears to be the source of the problem you describe.
    if state == QProcess.Running:
        if remaining_time > 0:
            QTimer.singleShot(remaining_time, lambda: close_splash(splash))
        else:
            close_splash(splash)
When state goes to running, you do an additional wait before closing the splash screen.

I think you have another problem. The launcher remains running after the process is finished. I would do it like this:
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Qt


class Launcher(QtWidgets.QApplication):
    def __init__(self, cmd_str, pix_file, message, timeout=None):
        super().__init__()
        # Draw splash screen.
        self.splash = QtWidgets.QSplashScreen(QtGui.QPixmap(pix_file), Qt.WindowStaysOnTopHint)
        self.splash.showMessage(message, Qt.AlignBottom | Qt.AlignCenter, Qt.white)
        self.splash.show()
        self.processEvents()

        # Erase splash screen if timed out waiting to start.
        if timeout is not None:
            self.timer = QtCore.QTimer()
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.close_splash_screen)
            self.timer.start(timeout)
        else:
            self.timer = None

        # Run process as QProcess.  Erase splash screen when process starts.
        # Shut down launcher when process finishes.
        self.process = QtCore.QProcess()
        self.process.started.connect(self.close_splash_screen)
        self.process.finished.connect(self.quit)
        self.process.startCommand(cmd_str)

    def close_splash_screen(self):
        # Close the splash screen and stop the timer.
        if self.splash:
            self.splash.close()
            self.splash = None
        if self.timer:
            self.timer.stop()
            self.timer = None

    def quit(self):
        # Do graceful exit when process finishes.
        self.close_splash_screen
        self.exit()


if __name__ == "__main__":
    app = Launcher("python junk2.py", "test.png", "Loading application...", 5000)
    app.exec()
I can't really test this because I don't have anything that takes a long time to start running.


Thank you very much for your response. I have added additional debugging statements to your code to understand what is happening:

from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Qt
 
class Launcher(QtWidgets.QApplication):
    def __init__(self, cmd_str, pix_file, message, timeout=None):
        print("Initializing Launcher")
        super().__init__()
        # Draw splash screen.
        print("Drawing splash screen")
        self.splash = QtWidgets.QSplashScreen(QtGui.QPixmap(pix_file), Qt.WindowStaysOnTopHint)
        self.splash.showMessage(message, Qt.AlignBottom | Qt.AlignCenter, Qt.white)
        self.splash.show()
        self.processEvents()
        print("Splash screen displayed")
 
        # Erase splash screen if timed out waiting to start.
        if timeout is not None:
            print(f"Setting timer with timeout: {timeout} ms")
            self.timer = QtCore.QTimer()
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.close_splash_screen)
            self.timer.start(timeout)
        else:
            self.timer = None
            print("No timeout set for splash screen")
 
        # Run process as QProcess. Erase splash screen when process starts.
        # Shut down launcher when process finishes.
        print(f"Starting process: {cmd_str}")
        self.process = QtCore.QProcess()
        self.process.started.connect(self.close_splash_screen)
        self.process.finished.connect(self.quit)
        self.process.startCommand(cmd_str)
 
    def close_splash_screen(self):
        # Close the splash screen and stop the timer.
        print("Closing splash screen")
        if self.splash:
            self.splash.close()
            self.splash = None
        if self.timer:
            self.timer.stop()
            self.timer = None
 
    def quit(self):
        # Do graceful exit when process finishes.
        print("Process finished, exiting launcher")
        self.close_splash_screen()
        self.exit()
 
 
if __name__ == "__main__":
    print("Starting application launcher")
    app = Launcher("mainwindow.exe", "splash_image.png", "Loading application...", 10000)
    app.exec()
    print("Application launcher exited")
I have captured a screen video to show you the results. Unfortunately, the splash screen seems to dissapear shortly after the application is ran:
https://www.youtube.com/watch?v=BEi3yKrJ...asPetrikas


Additionally, I have tried method 2 (using subprocess). I try this method because I can ensure the console starts using this method. Since I have generated my .exe using pyinstaller console = True option, I should see a console open before an actual application started. I am hoping this is going to give me some hints what is happening. See full code:

import sys
import subprocess
import threading
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Qt

class Launcher(QtWidgets.QApplication):
    def __init__(self, cmd_str, pix_file, message, timeout=None):
        print("Initializing Launcher")
        super().__init__(sys.argv)
        # Draw splash screen.
        print("Drawing splash screen")
        self.splash = QtWidgets.QSplashScreen(QtGui.QPixmap(pix_file), Qt.WindowStaysOnTopHint)
        self.splash.showMessage(message, Qt.AlignBottom | Qt.AlignCenter, Qt.white)
        self.splash.show()
        self.processEvents()
        print("Splash screen displayed")

        # Start the process using subprocess to ensure console is shown.
        print(f"Starting process: {cmd_str}")
        self.process = subprocess.Popen(cmd_str, creationflags=subprocess.CREATE_NEW_CONSOLE)
        
        # Start a thread to wait for the process to complete
        self.wait_thread = threading.Thread(target=self.wait_for_process)
        self.wait_thread.start()

        # Erase splash screen if timed out waiting to start.
        if timeout is not None:
            print(f"Setting timer with timeout: {timeout} ms")
            self.timer = QtCore.QTimer()
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.close_splash_screen)
            self.timer.start(timeout)
        else:
            self.timer = None
            print("No timeout set for splash screen")

    def wait_for_process(self):
        # Wait for the process to complete
        self.process.wait()
        # Close the splash screen when the process finishes
        QtCore.QMetaObject.invokeMethod(self, "close_splash_screen", QtCore.Qt.QueuedConnection)

    def close_splash_screen(self):
        # Close the splash screen and stop the timer.
        print("Closing splash screen")
        if self.splash:
            self.splash.close()
            self.splash = None
        if self.timer:
            self.timer.stop()
            self.timer = None

    def quit(self):
        # Do graceful exit when process finishes.
        print("Process finished, exiting launcher")
        self.close_splash_screen()
        self.exit()

if __name__ == "__main__":
    print("Starting application launcher")
    app = Launcher("mainwindow.exe", "splash_image.png", "Loading application...", 20000)
    app.exec()
    print("Application launcher exited")
https://www.youtube.com/watch?v=8v2o9ITpzFI&ab_channel=LukasPetrikas

As you can see, I have configured timeout 20 seconds and the splash screen did not close after the application has fully started but it did close after timeout (20 seconds)

I hope this is brings more light into the issue I am dealing with.
Reply
#4
I don’t think process state changing to running reflects the process window appearing. I think it just means the process is running, and extracting dependencies is happening when running, not starting. I don’t think monitoring process state gives you the information you want.

Maybe pywinauto will do what you want. I think there is a command to get a window handle using the window title. It even has a timeout so you can stop waiting for the window to appear after 20 seconds.
Reply
#5
(Jul-31-2024, 12:10 PM)deanhystad Wrote: I don’t think process state changing to running reflects the process window appearing. I think it just means the process is running, and extracting dependencies is happening when running, not starting. I don’t think monitoring process state gives you the information you want.

I dont think that I am using running signal in the 2 methods I have just shown. I have used it in my initial post though.
Reply
#6
The started signal emits when the process state changes to running. I think pywinauto provides a way to wait until you window appears.
Reply
#7
(Jul-31-2024, 12:22 PM)deanhystad Wrote: The started signal emits when the signal state changes to running.

Oh I see. Do you have any other recommendations what can be used to detect that the application has fully loaded?

I will see if I can come up with something else
Reply
#8
I found pygetwindow.getWindowWithTitle(window_title) that claims to be cross platform.
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Qt
import pygetwindow
import subprocess


class Launcher(QtWidgets.QApplication):
    def __init__(self, cmd_str, title, pix_file, message, timeout=5):
        super().__init__()
        # Draw splash screen
        self.splash = QtWidgets.QSplashScreen(QtGui.QPixmap(pix_file), Qt.WindowStaysOnTopHint)
        self.splash.showMessage(message, Qt.AlignBottom | Qt.AlignCenter, Qt.white)
        self.splash.show()
        self.processEvents()
    
        # launch process
        subprocess.Popen(cmd_str, shell=True)

        # start periodic event to look if process window is visible
        self.window_title = title
        self.timeout = timeout
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.check_process_state)
        self.timer.start(100)

    def check_process_state(self):
        """Close splash screen when window appears or timeout expires."""
        self.timeout -= 0.1
        if pygetwindow.getWindowsWithTitle(self.window_title) or self.timeout <= 0:
            self.splash.close()
            self.timer.stop()
            self.exit()


if __name__ == "__main__":
    app = Launcher("python games/minesweeper.py", "Minesweeper", "image.png", "Loading application...", 20)
    app.exec()
Using subprocess.Popen instead of QProcess I can close the launcher without closing the process.
Reply
#9
(Jul-31-2024, 03:23 PM)deanhystad Wrote: I found pygetwindow.getWindowWithTitle(window_title) that claims to be cross platform.
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Qt
import pygetwindow
import subprocess


class Launcher(QtWidgets.QApplication):
    def __init__(self, cmd_str, title, pix_file, message, timeout=5):
        super().__init__()
        # Draw splash screen
        self.splash = QtWidgets.QSplashScreen(QtGui.QPixmap(pix_file), Qt.WindowStaysOnTopHint)
        self.splash.showMessage(message, Qt.AlignBottom | Qt.AlignCenter, Qt.white)
        self.splash.show()
        self.processEvents()
    
        # launch process
        subprocess.Popen(cmd_str, shell=True)

        # start periodic event to look if process window is visible
        self.window_title = title
        self.timeout = timeout
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.check_process_state)
        self.timer.start(100)

    def check_process_state(self):
        """Close splash screen when window appears or timeout expires."""
        self.timeout -= 0.1
        if pygetwindow.getWindowsWithTitle(self.window_title) or self.timeout <= 0:
            self.splash.close()
            self.timer.stop()
            self.exit()


if __name__ == "__main__":
    app = Launcher("python games/minesweeper.py", "Minesweeper", "image.png", "Loading application...", 20)
    app.exec()
Using subprocess.Popen instead of QProcess I can close the launcher without closing the process.



Thanks for suggestion, however the same issue persists, I have set the timeout to 40:

    app = Launcher("mainwindow.exe", "Minesweeper", "splash_image.png", "Loading application...", 40)
and ran the launcher. My application started about 15 seconds later but the launcher image remained for another 25 seconds (for a total of timeout of 40 seconds).

Additionally, there is another issue: Clicking on image while it is loading will cause it to dissappear for some reason. So I can run the launcher and click on my splash image and it will dissappear right away which is not ideal.

For you to be able to test it on your end, I have created a very simple .exe that takes about 5-10 seconds to launch. This .exe has been generated using pyinstaller --onefile option. The reason why it is so large it is because it contains Python virtual environment embedded within an application. This is required for my project.

You can download it via wetransfer link below:
https://we.tl/t-V4WuiN0srf

If you try and launch it using long timeout:
such as 40 as shown above, you will notice the behaviour I have explained.

Appreciate all the help !
Reply
#10
Does your main window.exe window say “Minesweeper” in the title bar? That would be a crazy coincidence. There is no such thing as "fully launched" that can be determined by looking at the process state, so the launcher waits until a window appears that has the same title as specified in the arguments. If the title in your program window is not "Minesweeper", the splash screen is erased by the timeout.

Disappearing when clicked is probably a desirable attribute for a splash screen. You don't really need to use a QSplashScreen. Any QtWidget will do. Here I use a label to make the splash screen.
from PySide6 import QtWidgets, QtGui, QtCore
from pathlib import Path
import pygetwindow
import subprocess


class Launcher(QtWidgets.QLabel):
    """Display splash screen while launching GUI application.  Closes splash screen when application window appears."""
    def __init__(self, cmd_str, title, pixfile=None, message=None, timeout=20):
        """Arguments:
        
        cmd_str: Command str used to run application as a subprocess.
        title: Title in application window.  Used to find when application window appears.
        pixfile: File for image to appear in splash screen.  Defaults to applauncher.png.
        message: Text to display in splash screen.  Defaults to Launching {title}.
        timeout: Maximum time that splash screen is visible.
        """
        if message is None:
            message = f"Launching {title}"
        if pixfile is None:
            pixfile = (Path(__file__).parent / "applauncher.png").absolute()
        app = QtWidgets.QApplication()

        # Draw the splash screen.  Using a label instead of QSplashScreen to have more control.
        super().__init__()
        self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
        self.setText(message)
        self.setFont(QtGui.QFont("Arial", 32))
        self.setMargin(50)
        pixmap = QtGui.QPixmap(pixfile)
        pixmap.scaled(self.size(), QtCore.Qt.KeepAspectRatio)
        palette = QtGui.QPalette()
        palette.setBrush(QtGui.QPalette.Window, pixmap)
        palette.setBrush(QtGui.QPalette.WindowText, QtGui.QColor("white"))
        self.setPalette(palette)
        self.show()
        app.processEvents()

        # launch process
        subprocess.Popen(cmd_str, shell=True)

        # start periodic event to check if process window is visible
        self.window_title = title
        self.timeout = timeout
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.check_process_state)
        self.timer.start(100)

        app.exec()

    def check_process_state(self):
        """Close splash screen when window appears or timeout expires."""
        self.timeout -= 0.1
        if pygetwindow.getWindowsWithTitle(self.window_title) or self.timeout <= 0:
            self.timer.stop()
            self.close()


if __name__ == "__main__":
    Launcher("python games/minesweeper.py", "Minesweeper")
An advantage to using a regular window instead of a splash screen is that closing the window causes the launcher to exit. This allows repackaging the launcher to make it work like a standalone dialog, which makes it much easier to use.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Python Launcher Pops Up When Py-based App Is Running (Mac) radix_optimus 0 1,151 Sep-18-2023, 09:22 AM
Last Post: radix_optimus
  Python Launcher INI File leodavinci1990 1 2,488 Jul-30-2023, 03:38 PM
Last Post: snippsat
  How to send data from a python application to an external application aditya_rajiv 1 3,010 Jul-26-2021, 06:00 AM
Last Post: ndc85430
  pyinstaller --onefile mhzr 6 4,798 Jun-24-2021, 05:56 PM
Last Post: Marbelous
  PyInstaller OneFile felixS_zema 4 8,256 Oct-09-2019, 01:02 PM
Last Post: felixS_zema
  Making an application with a 3d engine in it moo5e 1 2,701 May-27-2019, 03:17 PM
Last Post: heiner55
  Fatal error in launcher: Unable to create process using '"' rsmldmv 0 6,473 May-13-2019, 01:34 AM
Last Post: rsmldmv
  help needed with python launcher fallenlight 3 4,482 Jan-19-2019, 01:06 PM
Last Post: snippsat
  help with making an application Radu97 2 3,233 Sep-19-2018, 03:29 PM
Last Post: Axel_Erfurt
  Can the launcher show 2.7 notebook or terminal option? miner_tom 1 2,818 Aug-24-2018, 07:53 AM
Last Post: perfringo

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020