Posts: 48
Threads: 20
Joined: Sep 2020
Jul-30-2024, 04:47 AM
(This post was last modified: Jul-30-2024, 04:47 AM by zazas321.)
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.
Posts: 6,799
Threads: 20
Joined: Feb 2020
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.
Posts: 48
Threads: 20
Joined: Sep 2020
Jul-31-2024, 05:12 AM
(This post was last modified: Jul-31-2024, 05:12 AM by zazas321.)
(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.
Posts: 6,799
Threads: 20
Joined: Feb 2020
Jul-31-2024, 12:10 PM
(This post was last modified: Jul-31-2024, 12:19 PM by deanhystad.)
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.
Posts: 48
Threads: 20
Joined: Sep 2020
Jul-31-2024, 12:15 PM
(This post was last modified: Jul-31-2024, 12:16 PM by zazas321.)
(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.
Posts: 6,799
Threads: 20
Joined: Feb 2020
Jul-31-2024, 12:22 PM
(This post was last modified: Jul-31-2024, 12:32 PM by deanhystad.)
The started signal emits when the process state changes to running. I think pywinauto provides a way to wait until you window appears.
Posts: 48
Threads: 20
Joined: Sep 2020
(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
Posts: 6,799
Threads: 20
Joined: Feb 2020
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.
Posts: 48
Threads: 20
Joined: Sep 2020
Aug-01-2024, 04:40 AM
(This post was last modified: Aug-01-2024, 04:40 AM by zazas321.)
(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 !
Posts: 6,799
Threads: 20
Joined: Feb 2020
Aug-02-2024, 03:15 AM
(This post was last modified: Aug-02-2024, 03:15 AM by deanhystad.)
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.
|