Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tkinter splash screen & multiprocessing outside of mainloop

I have implemented a splash screen that is shown while my application loads the database from remote cloud storage on startup. The splash screen is kept alive (there's a progressbar on it) with calls to .update() and is destroyed once the separate loading process ends. After this, the mainloop is started and the app runs normally.

The code below used to work fine on my Mac with python 3.6 and tcl/tk 8.5.9. However, after the update to Sierra I was forced to update tk to ActiveTcl 8.5.18. Now, the splash screen is not displayed until the separate process finishes, but then appears and stays on screen together with the root window (even though its .destroy() method is called).

import tkinter as tk
import tkinter.ttk as ttk
import multiprocessing
import time


class SplashScreen(tk.Toplevel):
    def __init__(self, root):
        tk.Toplevel.__init__(self, root)
        self.geometry('375x375')
        self.overrideredirect(True)

        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        self.label = ttk.Label(self, text='My Splashscreen', anchor='center')
        self.label.grid(column=0, row=0, sticky='nswe')

        self.center_splash_screen()
        print('initialized splash')

    def center_splash_screen(self):
        w = self.winfo_screenwidth()
        h = self.winfo_screenheight()
        x = w / 2 - 375 / 2
        y = h / 2 - 375 / 2
        self.geometry("%dx%d+%d+%d" % ((375, 375) + (x, y)))

    def destroy_splash_screen(self):
        self.destroy()
        print('destroyed splash')


class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)

        self.start_up_app()

        self.title("MyApp")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        self.application_frame = ttk.Label(self, text='Rest of my app here', anchor='center')
        self.application_frame.grid(column=0, row=0, sticky='nswe')

        self.mainloop()

    def start_up_app(self):
        self.show_splash_screen()

        # load db in separate process
        process_startup = multiprocessing.Process(target=App.startup_process)
        process_startup.start()

        while process_startup.is_alive():
            # print('updating')
            self.splash.update()

        self.remove_splash_screen()

    def show_splash_screen(self):
        self.withdraw()
        self.splash = SplashScreen(self)

    @staticmethod
    def startup_process():
        # simulate delay while implementation is loading db
        time.sleep(5)

    def remove_splash_screen(self):
        self.splash.destroy_splash_screen()
        del self.splash
        self.deiconify()

if __name__ == '__main__':
    App()

I do not understand why this is happening and how to solve it. Can anybody help? Thanks!

Update:

The splash screen is displayed correctly if you outcomment the line self.overrideredirect(True). However, I don't want window decorations and it still stays on screen at the end of the script. It is being destroyed internally though, any further method calls on self.splash (e.g. .winfo_...-methods) result in _tkinter.TclError: bad window path name ".!splashscreen".

Also, this code works fine under windows and tcl/tk 8.6. Is this a bug/problem with window management of tcl/tk 8.5.18 on Mac?

like image 802
Sam Avatar asked Nov 18 '22 16:11

Sam


1 Answers

I came across this while looking for an example on how to make a tkinter splash screen that wasn't time dependent (as most other examples are). Sam's version worked for me as is. I decided to make it an extensible stand-alone class that handles all the logic so it can just be dropped into an existing program:

# Original Stackoverflow thread:
# https://stackoverflow.com/questions/44802456/tkinter-splash-screen-multiprocessing-outside-of-mainloop
import multiprocessing
import tkinter as tk
import functools

class SplashScreen(tk.Toplevel):
    def __init__(self, root, **kwargs):
        tk.Toplevel.__init__(self, root, **kwargs)
        self.root = root
        self.elements = {}
        root.withdraw()
        self.overrideredirect(True)

        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        # Placeholder Vars that can be updated externally to change the status message
        self.init_str = tk.StringVar()
        self.init_str.set('Loading...')

        self.init_int = tk.IntVar()
        self.init_float = tk.DoubleVar()
        self.init_bool = tk.BooleanVar()

    def _position(self, x=.5,y=.5):
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        splash_w = self.winfo_reqwidth()
        splash_h = self.winfo_reqheight()
        x_loc = (screen_w*x) - (splash_w/2)
        y_loc = (screen_h*y) - (splash_h/2)
        self.geometry("%dx%d+%d+%d" % ((splash_w, splash_h) + (x_loc, y_loc)))

    def update(self, thread_queue=None):
        super().update()
        if thread_queue and not thread_queue.empty():
            new_item = thread_queue.get_nowait()
            if new_item and new_item != self.init_str.get():
                self.init_str.set(new_item)

    def _set_frame(self, frame_funct, slocx=.5, sloxy=.5, ):
        """

        Args:
            frame_funct: The function that generates the frame
            slocx: loction on the screen of the Splash popup
            sloxy:
            init_status_var: The variable that is connected to the initialization function that can be updated with statuses etc

        Returns:

        """
        self._position(x=slocx,y=sloxy)
        self.frame = frame_funct(self)
        self.frame.grid(column=0, row=0, sticky='nswe')

    def _start(self):
        for e in self.elements:
            if hasattr(self.elements[e],'start'):
                self.elements[e].start()

    @staticmethod
    def show(root, frame_funct, function, callback=None, position=None, **kwargs):
        """

        Args:
            root: The main class that created this SplashScreen
            frame_funct: The function used to define the elements in the SplashScreen
            function: The function when returns, causes the SplashScreen to self-destruct
            callback: (optional) A function that can be called after the SplashScreen self-destructs
            position: (optional) The position on the screen as defined by percent of screen coordinates
                (.5,.5) = Center of the screen (50%,50%) This is the default if not provided
            **kwargs: (optional) options as defined here: https://www.tutorialspoint.com/python/tk_toplevel.htm

        Returns:
            If there is a callback function, it returns the result of that. Otherwise None

        """
        manager = multiprocessing.Manager()
        thread_queue = manager.Queue()

        process_startup = multiprocessing.Process(target=functools.partial(function,thread_queue=thread_queue))
        process_startup.start()
        splash = SplashScreen(root=root, **kwargs)
        splash._set_frame(frame_funct=frame_funct)
        splash._start()

        while process_startup.is_alive():
            splash.update(thread_queue)


        process_startup.terminate()

        SplashScreen.remove_splash_screen(splash, root)
        if callback: return callback()
        return None

    @staticmethod
    def remove_splash_screen(splash, root):
        splash.destroy()
        del splash
        root.deiconify()

    class Screen(tk.Frame):
        # Options screen constructor class
        def __init__(self, parent):
            tk.Frame.__init__(self, master=parent)
            self.grid(column=0, row=0, sticky='nsew')
            self.columnconfigure(0, weight=1)
            self.rowconfigure(0, weight=1)


### Demo ###

import time

def splash_window_constructor(parent):
    """
        Function that takes a parent and returns a frame
    """
    screen = SplashScreen.Screen(parent)
    label = tk.Label(screen, text='My Splashscreen', anchor='center')
    label.grid(column=0, row=0, sticky='nswe')
    # Connects to the tk.StringVar so we can updated while the startup process is running
    label = tk.Label(screen, textvariable=parent.init_str, anchor='center')
    label.grid(column=0, row=1, sticky='nswe')
    return screen


def startup_process(thread_queue):
    # Just a fun method to simulate loading processes
    startup_messages = ["Reticulating Splines","Calculating Llama Trajectory","Setting Universal Physical Constants","Updating [Redacted]","Perturbing Matrices","Gathering Particle Sources"]
    r = 10
    for n in range(r):
        time.sleep(.2)
        thread_queue.put_nowait(f"Loading database.{'.'*n}".ljust(27))
    time.sleep(1)
    for n in startup_messages:
        thread_queue.put_nowait(n)
        time.sleep(.2)
    for n in range(r):
        time.sleep(.2)
        thread_queue.put_nowait(f"Almost Done.{'.'*n}".ljust(27))
    for n in range(r):
        time.sleep(.5)
        thread_queue.put_nowait("Almost Done..........".ljust(27))
        time.sleep(.5)
        thread_queue.put_nowait("Almost Done......... ".ljust(27))



def callback(text):
    # To be run after the splash screen completes
    print(text)


class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)

        self.callback_return = SplashScreen.show(root=self,
                                   frame_funct=splash_window_constructor,
                                   function=startup_process,
                                   callback=functools.partial(callback,"Callback Done"))

        self.title("MyApp")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        self.application_frame = tk.Label(self, text='Rest of my app here', anchor='center')
        self.application_frame.grid(column=0, row=0, sticky='nswe')

        self.mainloop()



if __name__ == "__main__":
    App()
like image 141
asielen Avatar answered Dec 15 '22 00:12

asielen