Tkinter splash screen & multiprocessing outside of mainloop
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 tkclass 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.
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 multiprocessingimport tkinter as tkimport functoolsclass 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 timedef 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 screendef 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()