Skip to content Skip to sidebar Skip to footer

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 progressba

Solution 1:

Apparently this is due to a problem with the window stacking order when windows are not decorated by the window manager after calling overrideredirect(True). It seems to have occurred on other platforms as well.

Running the following code on macOS 10.12.5 with Python 3.6.1 and tcl/tk 8.5.18, toplevel windows do not appear after the button 'open' is clicked:

import tkinter as tk

class TL(tk.Toplevel):
    def __init__(self):
        tk.Toplevel.__init__(self)
        self.overrideredirect(True)
        # self.after_idle(self.lift)
        tl_label = tk.Label(self, text='this is a undecorated\ntoplevel window')
        tl_label.grid(row=0)
        b_close = tk.Button(self, text='close', command=self.close)
        b_close.grid(row=1)

    def close(self):
        self.destroy()

def open():
    TL()

root = tk.Tk()
label = tk.Label(root, text='This is the root')
label.grid(row=0)
b_open = tk.Button(root, text='open', command=open)
b_open.grid(row=1)
root.mainloop()

Uncommenting the line self.after_idle(self.lift) fixes the problem (simply calling self.lift() does too. But using after_idle()prevents the window from flashing up for a fraction of a second before it is moved to its position and resized, which is another problem I have experienced repeatedly with tkinter and keeps me wondering whether I should move on to learn PyQT or PySide2...).

As to the problem with closing an undecorated window in my original question: calling after_idle(window.destroy()) instead of window.destroy() seems to fix that too. I do not understand why.

In case other people reproduce this and somebody hints me towards where to report this as a bug, I am happy to do so.

Solution 2:

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-mainloopimport multiprocessing
import tkinter as tk
import functools

classSplashScreen(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)))

    defupdate(self, thread_queue=None):
        super().update()
        if thread_queue andnot 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:
            ifhasattr(self.elements[e],'start'):
                self.elements[e].start()

    @staticmethoddefshow(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()
        returnNone    @staticmethoddefremove_splash_screen(splash, root):
        splash.destroy()
        del splash
        root.deiconify()

    classScreen(tk.Frame):
        # Options screen constructor classdef__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

defsplash_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


defstartup_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 = 10for n inrange(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 inrange(r):
        time.sleep(.2)
        thread_queue.put_nowait(f"Almost Done.{'.'*n}".ljust(27))
    for n inrange(r):
        time.sleep(.5)
        thread_queue.put_nowait("Almost Done..........".ljust(27))
        time.sleep(.5)
        thread_queue.put_nowait("Almost Done......... ".ljust(27))



defcallback(text):
    # To be run after the splash screen completesprint(text)


classApp(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()

Post a Comment for "Tkinter Splash Screen & Multiprocessing Outside Of Mainloop"