5

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?

Sam
  • 191
  • 1
  • 7
  • Your code works as intended on my end. I am using the python 3.6.1 released on 2017-03-21. – Mike - SMT Jun 28 '17 at 13:04
  • BTW you should not be using `sleep()` in tkinter. Use `after()` instead – Mike - SMT Jun 28 '17 at 13:32
  • @SierraMountainTech Thanks, I am currently on python 3.6.0 and will try to update. after()/sleep(): I am aware of that, this was for simulation purposes in a seperate process only. – Sam Jun 28 '17 at 13:40
  • Let me know if the update fixes your problem. I will take a look at 3.6.0 when I get a chance. – Mike - SMT Jun 28 '17 at 13:44
  • Updating to python 3.6.1 does not change anything, the problem persists. Further hints on what might be wrong very welcome! – Sam Jun 28 '17 at 14:57
  • @SierraMountainTech Since you say that my code works for you, could you please indicate your platform and version of tk/tcl? – Sam Jun 29 '17 at 10:14
  • I am using both windows 7 and 10. I am using the default Tkinter library that comes with windows versions of python. – Mike - SMT Jun 29 '17 at 12:42
  • I note that `master.update_idletasks()` is not used during initialization. In order to initialize tkinter `Tk` or `Toplevel` windows it is necessary to use `window.update_idletasks()` after window has been fully defined (geometry manager) This will give a flicker free display every time. – Derek Jun 30 '21 at 01:22

2 Answers2

1

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()
asielen
  • 11
  • 2
  • Hi @asielen, thanks a lot for sharing this nice piece of code. It helps me a lot! However, the width and height of the splash screen determined by the _position function using winfo_reqwidth() and winfo_reqheight() obviously doesn't get the "really" required size. I packed an image (350x475) into a ttk.Label instead of the Label with the text "My Splashscreen" you are using, and this is not fully shown - only 200x200 pixels of its top left corner. When I use fixed, pre-determined values for splash_w and splash_h, all is fine, but maybe you may want to fix this more elegantly. I couldn't... – petro4213 Feb 12 '22 at 13:15
  • @asilien: One more comment: Could you explain, what you intend to do with the self.elements member variable, which obviously prepares for something, that is not used in your code example? Thanks a lot! – petro4213 Feb 12 '22 at 13:21
  • @petro4213 Thanks for the comments, do you suggest having the splash screen dynamically change size with the content? I suppose that could be done, although I tend think of splash screens as fixed sizes. self.elements is a hold over of other tkinter extensions I wrote for myself. I just store all the elements of a frame in a dict so I can easily reference them later. Not relevant for this mini-demo. – asielen Feb 13 '22 at 22:42
  • @asilien No, I want it to fit the size of my image that I use instead of the text "My Splashscreen" just when the splash window is positioned, i.e. when the function _position gets called. – petro4213 Feb 14 '22 at 11:37
  • @asilien But actually I'm having a more severe problem: Anything that I create in the "startup_process" is not available to the main application, after the splash window gets destroyed. I tried to hand over a member variable of the main application to store things in there, but it's gone afterwards. I must admit, that I'm not familiar with multiprocessing... – petro4213 Feb 14 '22 at 11:53
0

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.

Sam
  • 191
  • 1
  • 7