Tkinter - 预加载 window?

Tkinter - Preload window?

我开始构建一个 python tkinter gui,问题是,在向 gui 添加许多功能之后,加载开始看起来 非常难看 。启动主循环时,空白 window 在加载小部件之前会出现几分之一秒,其他 Toplevel windows 也会出现同样的情况(除了那些不需要更新的静态元素很少).

我的问题是

有没有办法'preload' window 调用的时候能顺利启动?

例如

root = MyTkRoot()
root.build_stuff_before_calling()
root.mainloop() # Smooth without widget loading lag

我的部分代码

from root_gui import Root
import threading
from time import sleep
from data_handler import DataHandler


def handle_conn():
    DataHandler.try_connect()
    smtp_client.refresh()


def conn_manager(): # Working pretty well
    while 'smtp_client' in locals():
        sleep(3)
        handle_conn()


smtp_client = Root()
handle_conn()


MyConnManager = threading.Thread(target=conn_manager)
MyConnManager.start() # Thanks to the second thread, the tk window doesn't have to wait for 3 seconds before loading the widgets.
smtp_client.mainloop()

git

上的完整项目

这里放的代码太多...

https://github.com/cernyd/smtp_client

简短的回答是肯定的。您可以将 window 设置为看起来好像您看不到它一样。执行任何需要时间的代码,然后显示 window.

方法如下:

class MyTkRoot(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.attributes('-alpha', 0.0)  # make window transparent
        # build gui...
        self.after(0, self.attributes, "-alpha", 1.0)  # back to normal
root = MyTkRoot()
root.mainloop()

Here's 对带有 Toplevel 示例的代码的引用。但是,它也适用于 root。

在您的操作系统上,tkinter 似乎在加载小部件之前显示基础 Tk window,导致几毫秒的空白 window。要使其与所有已加载的小部件一起显示,您需要在开始时隐藏 window 并且 .after 已加载并再次显示它。

有很多方法可以显示和隐藏 window,我个人会使用 .withdraw() 从 window 管理器中删除 window(比如它从来没有出现过)然后 .deiconify()(基本上 "unminimize")再次显示 window。

class TEST(tk.Tk):
    def __init__(self,*args,**kw):
        tk.Tk.__init__(self,*args,**kw)
        self.withdraw() #hide the window
        self.after(0,self.deiconify) #as soon as possible (after app starts) show again
        #setup app...

完全取消 window 的另一种方法是使用 .iconify() 将其最小化,这样它将出现在 taskbar/dock 上,但不会打开 window,直到它已完全加载。

hide/show window 的另一种方法是将 -alpha 属性 更改为 but I would not recommend that in production code because the window is technically still there, it (and the close button etc.) can be clicked on /interacted with for a brief moment before showing which could be undesired, as well it's behaviour can be ambiguous amongst operating systems, from http://wiki.tcl.tk/9457:

Macintosh Attributes

-alpha double controls opacity (from 0.0 to 1.0)
...

Unix/X11 Attributes

-alpha double controls opacity (from 0.0 to 1.0).
This requires a compositing window manager to have any effect. [compiz] is one such, and xfwm4 (for the XFCE desktop) is another.
...

Windows Attributes

-alpha double how opaque the overall window is; note that changing between 1.0 and any other value may cause the window to flash (Tk changes the class of window used to implement the toplevel).

所以在某些 unix 机器上 -alpha 可能没有效果,而在 windows 上它可能会导致 window 闪烁(可能在打开之前就不是问题,但仍然值得注意)

据我所知,withdrawdeiconify 在所有平台上的工作方式相同。

在查看了您存储库中的项目后,我修改了代码并将其全部转储到一个文件中。以下是将您的模块和文件重构为完全独立的程序的结果。它缺少的一件事是处理所有外部依赖项的 Resource class。

您可以在 PasteBin.com 上看到 complete program

#! /usr/bin/env python3
import base64
import contextlib
import email.mime.text
import enum
import functools
import pathlib
import pickle
import pickletools
import re
import smtplib
import socket
import sys
import threading
import tkinter.messagebox
import traceback
import zlib


# Dump all of tkinter's constants into the local namespace.
from tkinter.constants import *


# Import custom modules in a way that an IDE will recognize them.
with Resource.load('affinity.py'), \
     Resource.load('threadbox.py'), \
     Resource.load('safetkinter.py'):
    try:
        from .safetkinter import *
    except SystemError:
        from safetkinter import *
    try:
        from . import threadbox
    except SystemError:
        import threadbox


# Patch the messagebox module to use a thread-safe version of Message.
tkinter.messagebox.Message = Message


# Create an enumeration to represents various program states.
Status = enum.Enum('Status', 'mail_label login_button connection_error')


def event_handler(method):
    """Allow command/event handlers to be marked and written more easily."""
    @functools.wraps(method)
    def wrapper(self, event=None):
        nonlocal none_counter
        none_counter += event is None
        method(self)
        # print('Commands handled:', none_counter)
    none_counter = 0
    return wrapper


class SMTPClient(Frame):

    """Widget for sending emails through a GUI program in Python."""

    TITLE = 'SMTP Mail Sender'

    AT_ICON = 'at.ico'
    HELP_ICON = 'help.ico'
    HELP_TEXT = 'get_help.txt'

    IS_TARGET = r'\A(\w|\.)+@\w+\.\w+\Z'
    IS_SUBJECT = r'\S+'
    IS_MESSAGE = r'\S+'

    @classmethod
    def main(cls):
        """Create an application containing a single SMTPClient widget."""
        root = cls.create_application_root()
        cls.create_windows_bindings(root)
        cls.attach_window_icon(root)
        cls.setup_class_instance(root)
        root.mainloop()

    @classmethod
    def create_application_root(cls):
        """Create and configure the main application window."""
        root = Tk()
        root.title(cls.TITLE)
        # root.resizable(False, False)
        root.minsize(275, 200)
        root.option_add('*tearOff', FALSE)
        return root

    @classmethod
    def create_windows_bindings(cls, root):
        """Change some global bindings to work like Microsoft products."""
        root.bind_all('<Control-Key-a>', cls.handle_control_a)
        root.bind_all('<Control-Key-A>', cls.handle_control_a)
        root.bind_class('Text', '<Control-Key-/>', lambda event: 'break')

    @staticmethod
    def handle_control_a(event):
        """Treat Control-A as would be expected on a Windows system."""
        widget = event.widget
        if isinstance(widget, Entry):
            widget.selection_range(0, END)
            return 'break'
        if isinstance(widget, Text):
            widget.tag_add(SEL, 1.0, END + '-1c')
            return 'break'

    @classmethod
    def attach_window_icon(cls, root):
        """Generate and use the icon in the window's corner."""
        with Resource.load(cls.AT_ICON) as handle:
            root.iconbitmap(handle)

    @classmethod
    def setup_class_instance(cls, root):
        """Build a SMTPClient instance that expects to be have size changes."""
        widget = cls(root)
        widget.grid(row=0, column=0, sticky=NSEW)
        root.grid_rowconfigure(0, weight=1)
        root.grid_columnconfigure(0, weight=1)

    def __init__(self, master=None, **kw):
        """Initialize the SMTPClient instance and configure for operation."""
        super().__init__(master, **kw)
        self.__tk = self.capture_root()
        self.create_bindings()
        self.__to_entry = self.__subject_entry = self.__message_text = \
            self.__quit_button = self.__send_button = self.__from_label = \
            self.__to_label = self.__subject_label = self.__login_button = \
            self.__grip = None
        self.create_widgets()
        self.configure_grid()
        self.__data_handler = DataHandler()
        self.__connector = self.after_idle(self.try_connect)

    def destroy(self):
        """Cancel the connection system before closing."""
        self.after_cancel(self.__connector)
        super().destroy()

    def capture_root(self):
        """Capture the rook (Tk instance) of this application."""
        widget = self.master
        while not isinstance(widget, Tk):
            widget = widget.master
        return widget

    def create_bindings(self):
        """Bind the frame to any events that it will need to handle."""
        self.__tk.bind('<Control-Key-h>', self.handle_help)
        self.__tk.bind('<Control-Key-H>', self.handle_help)
        self.__tk.bind('<Control-Key-l>', self.handle_login)
        self.__tk.bind('<Control-Key-L>', self.handle_login)
        self.__tk.bind('<Key>', self.handle_update)
        self.__tk.bind('<Return>', self.handle_send)

    def create_widgets(self):
        """Create all the widgets that will be placed in this frame."""
        self.__to_entry = Entry(self)
        self.__subject_entry = Entry(self)
        self.__message_text = Text(self)
        self.__quit_button = Button(
            self,
            text='Quit',
            command=self.__tk.destroy
        )
        self.__send_button = Button(
            self,
            text='Send',
            command=self.do_send,
            state=DISABLED
        )
        self.__from_label = Label(self, text='From:')
        self.__to_label = Label(self, text='To:')
        self.__subject_label = Label(self, text='Subject:')
        self.__login_button = Button(
            self,
            text='Login Before Sending',
            command=self.do_login
        )
        self.__grip = Sizegrip(self)

    def configure_grid(self):
        """Place all widgets on the grid in their respective locations."""
        pad = dict(padx=5, pady=5)
        self.__from_label.grid(row=0, column=0, **pad)
        self.__login_button.grid(
            row=0,
            column=1,
            columnspan=4,
            sticky=EW,
            **pad
        )
        self.__to_label.grid(row=1, column=0, **pad)
        self.__to_entry.grid(row=1, column=1, columnspan=4, sticky=EW, **pad)
        self.__subject_label.grid(row=2, column=0, **pad)
        self.__subject_entry.grid(
            row=2,
            column=1,
            columnspan=4,
            sticky=EW,
            **pad
        )
        self.__message_text.grid(
            row=3,
            column=0,
            columnspan=5,
            sticky=NSEW,
            **pad
        )
        self.__quit_button.grid(row=4, column=2, **pad)
        self.__send_button.grid(row=4, column=3, **pad)
        self.__grip.grid(row=4, column=4, sticky=SE, **pad)
        self.grid_rowconfigure(3, weight=1)
        self.grid_columnconfigure(1, weight=1)

    @event_handler
    def handle_help(self):
        """Open the help window and show a message for the user."""
        with Resource.load(self.HELP_TEXT) as text, \
                Resource.load(self.HELP_ICON) as icon:
            TextViewer.load(self.__tk, text, icon)

    @event_handler
    def handle_login(self):
        """Decide if the user should be able to login to the server."""
        if self.current_status == Status.login_button:
            self.do_login()

    @event_handler
    def handle_update(self):
        """Decide if it should be possible to send an e-mail or not."""
        to = self.__to_entry.get()
        subject = self.__subject_entry.get()
        message = self.__message_text.get(0.0, END)
        self.__send_button['state'] = ACTIVE if self.is_target(to) \
            and self.is_subject(subject) \
            and self.is_message(message) \
            and self.__data_handler.is_connected else DISABLED

    @classmethod
    def is_target(cls, text):
        """Determine if this is an acceptable e-mail address."""
        return bool(re.search(cls.IS_TARGET, text))

    @classmethod
    def is_subject(cls, text):
        """Determine if this is an acceptable subject line."""
        return bool(re.search(cls.IS_SUBJECT, text))

    @classmethod
    def is_message(cls, text):
        """Determine if this is an acceptable message to send."""
        return bool(re.search(cls.IS_MESSAGE, text))

    @event_handler
    def handle_send(self):
        """Send only if the application is ready to do so."""
        if self.__send_button['state'] == ACTIVE:
            self.do_send()

    def do_send(self):
        """Start a thread to send an e-mail in an asynchronous method."""
        threading.Thread(target=self.send_thread).start()

    @threadbox.MetaBox.thread
    def send_thread(self):
        """Try to send an e-mail and display the results of the attempt."""
        destination = self.__to_entry.get()
        try:
            self.__data_handler.send(
                destination,
                self.__subject_entry.get(),
                self.__message_text.get(0.0, END)
            )
        except:
            tkinter.messagebox.showerror(
                'Error',
                'An exception has occurred.\n'
                'Continue for more details.',
                master=self
            )
            TextViewer(self.__tk, 'Traceback', traceback.format_exc())
        else:
            tkinter.messagebox.showinfo(
                'Success',
                'The message was sent successfully to {}.'.format(destination),
                master=self
            )

    @property
    def current_status(self):
        """Find out what status the program currently is in."""
        return (Status.connection_error
                if not self.__data_handler.is_connected else
                Status.login_button
                if not self.__data_handler.is_logged_in else
                Status.mail_label)

    def do_login(self):
        """Open the login window and also the user to supply credentials."""
        with Resource.load('login.ico') as icon:
            LoginWindow(self.__tk, icon, self.__data_handler)
        self.__login_button['text'] = self.__data_handler.username or ''
        self.refresh()

    def try_connect(self):
        """Repeatedly try to connect to the server every 0.6 seconds."""
        if not self.__data_handler.is_connected:
            self.__data_handler.try_connect(self.refresh)
        self.__connector = self.after(600, self.try_connect)

    def refresh(self, is_connected=False):
        """Let the user know if there is a connection to the server."""
        if is_connected:
            tkinter.messagebox.showinfo(
                'Server',
                'Your connection is live!',
                master=self
            )


class TextViewer(Toplevel):

    """Widget designed to show text in a window."""

    BACKGROUND = '#FFFFFF'
    FOREGROUND = '#000000'
    WIDTH = 800
    HEIGHT = 600
    X_OFFSET = 10
    Y_OFFSET = 10

    @classmethod
    def load(cls, parent, text_handle, icon_handle):
        """Open a TextViewer with information loaded from a file."""
        with text_handle.open('rt') as file:
            title, *text = map(str.strip, file)
        cls(parent, title, '\n'.join(text), icon_handle)

    def __init__(self, parent, title, text, icon_handle=None):
        """Initializes the window for the reader to see its contents."""
        super().__init__(parent, borderwidth=5)
        if icon_handle is not None:
            self.iconbitmap(icon_handle)
        self.geometry('={}x{}+{}+{}'.format(
            self.WIDTH,
            self.HEIGHT,
            parent.winfo_rootx() + self.X_OFFSET,
            parent.winfo_rooty() + self.Y_OFFSET
        ))
        self.__frame_text = self.__frame_buttons = self.__okay_button = \
            self.__scrollbar_view = self.__text_view = None
        self.create_widgets()
        self.configure_widgets()
        self.title(title)
        self.transient(parent)
        self.grab_set()
        self.protocol('WM_DELETE_WINDOW', self.okay)
        self.__text_view.focus_set()
        self.create_bindings()
        self.__text_view.insert(0.0, text)
        self.__text_view['state'] = DISABLED
        self.wait_window()

    def create_widgets(self):
        """Populates the window with the widgets that will be needed."""
        self.__frame_text = Frame(self, relief=SUNKEN, height=700)
        self.__frame_buttons = Frame(self)
        self.__okay_button = Button(
            self.__frame_buttons,
            text='Close',
            command=self.okay,
            takefocus=FALSE
        )
        self.__scrollbar_view = Scrollbar(
            self.__frame_text,
            orient=VERTICAL,
            takefocus=FALSE
        )
        self.__text_view = Text(
            self.__frame_text,
            wrap=WORD,
            fg=self.FOREGROUND,
            bg=self.BACKGROUND,
            highlightthickness=0
        )

    def configure_widgets(self):
        """Put them in their proper places throughout the layout."""
        self.__scrollbar_view['command'] = self.__text_view.yview
        self.__text_view['yscrollcommand'] = self.__scrollbar_view.set
        self.__okay_button.pack()
        self.__scrollbar_view.pack(side=RIGHT, fill=Y)
        self.__text_view.pack(side=LEFT, expand=TRUE, fill=BOTH)
        self.__frame_buttons.pack(side=BOTTOM, fill=X)
        self.__frame_text.pack(side=TOP, expand=TRUE, fill=BOTH)

    @event_handler
    def okay(self):
        """Close the window."""
        self.destroy()

    def create_bindings(self):
        """Allow the window to respond to certain events."""
        self.bind('<Return>', self.okay)
        self.bind('<Escape>', self.okay)


class DataHandler:

    """Handler for communications with a SMTP server."""

    HOST = 'smtp.gmail.com'
    PORT = 587

    __slots__ = (
        '__is_logged_in',
        '__is_connected',
        '__server',
        '__username',
        '__password'
    )

    def __init__(self):
        """Initializes the DataHandler instance's various flags."""
        self.__is_logged_in = False
        self.__is_connected = self.__server = self.__username = \
            self.__password = None
        self.__cancel_connection()

    @property
    def is_logged_in(self):
        """Checks whether or not the instance believes it is logged in."""
        return self.__is_logged_in

    @property
    def is_connected(self):
        """Checks whether or not the instance believes it is connected."""
        return self.__is_connected

    @staticmethod
    def __check_string(value, name):
        """Verify that the string has a value type and value."""
        if not isinstance(value, str):
            raise TypeError('{} must be of type str'.format(name))
        if not value:
            raise ValueError('{} must not be an empty string'.format(name))

    def __get_username(self):
        """Retrieves the value of the username."""
        return self.__username

    def __set_username(self, text):
        """Validates the username and sets its value."""
        self.__check_string(text, 'username')
        self.__username = text

    username = property(
        __get_username,
        __set_username,
        None,
        'Sets value of username.'
    )

    def __set_password(self, text):
        """Validates the password and sets its value."""
        self.__check_string(text, 'password')
        self.__password = text

    password = property(fset=__set_password, doc='Sets value of password.')

    def try_connect(self, callback=lambda status: None):
        """Attempt to connect to the pre-configured server address."""
        threading.Thread(target=self.__connect, args=(callback,)).start()

    def __connect(self, callback):
        """Connect to the server in an asynchronous fashion."""
        try:
            self.__server = smtplib.SMTP(self.HOST, self.PORT, None, 1)
        except (smtplib.SMTPException, socket.gaierror, socket.timeout):
            self.__cancel_connection()
        else:
            self.__is_connected = True
        callback(self.__is_connected)

    def send(self, destination, subject, message):
        """Try to send an e-mail with the provided information."""
        if self.__server is None:
            raise RuntimeError('cannot send without a valid connection')
        packet = email.mime.text.MIMEText(message)
        packet['From'] = self.__username
        packet['To'] = destination
        packet['Subject'] = subject
        self.__server.starttls()
        self.__server.login(self.__username, self.__password)
        self.__server.send_message(packet)
        self.__finish()

    def __cancel_connection(self):
        """Reset the is_connected and server attributes to default values."""
        self.__is_connected = False
        self.__server = None

    def validate_credentials(self):
        """Verify if the saved credentials are correct or not."""
        if self.__server is None:
            raise RuntimeError('cannot validate without a working connection')
        try:
            self.__server.starttls()
            self.__server.login(self.__username, self.__password)
        except smtplib.SMTPException:
            self.__is_logged_in = False
        else:
            self.__is_logged_in = True
        finally:
            self.__finish()
        return self.__is_logged_in

    def __finish(self):
        """Finish the conversation taking place with the server."""
        self.__server.close()
        self.__cancel_connection()


class Dialog(Toplevel):

    """Generic widget that should be used as a base class."""

    X_OFFSET = 50
    Y_OFFSET = 50

    def __init__(self, parent, title=None, icon_handle=None):
        """Initialize a Dialog window that takes focus away from the parent."""
        super().__init__(parent)
        self.withdraw()
        if icon_handle is not None:
            self.iconbitmap(icon_handle)
        if parent.winfo_viewable():
            self.transient(parent)
        if title:
            self.title(title)
        self.parent = parent
        self.result = None
        body = Frame(self)
        self.initial_focus = self.body(body)
        body.grid(sticky=NSEW, padx=5, pady=5)
        self.okay_button = self.cancel_button = None
        self.button_box()
        if not self.initial_focus:
            self.initial_focus = self
        self.protocol('WM_DELETE_WINDOW', self.cancel)
        parent = self.parent
        if parent is not None:
            self.geometry('+{}+{}'.format(
                parent.winfo_rootx() + self.X_OFFSET,
                parent.winfo_rooty() + self.Y_OFFSET
            ))
        self.deiconify()
        self.initial_focus.focus_set()
        try:
            self.wait_visibility()
        except tkinter.TclError:
            pass
        else:
            self.grab_set()
            self.wait_window(self)

    def destroy(self):
        """Destruct the Dialog window."""
        self.initial_focus = None
        super().destroy()

    def body(self, master):
        """Create the body of this Dialog window."""
        pass

    def button_box(self):
        """Create the standard buttons and Dialog bindings."""
        box = Frame(self)
        self.okay_button = Button(
            box,
            text='Okay',
            width=10,
            command=self.okay,
            default=ACTIVE
        )
        self.okay_button.grid(row=0, column=0, padx=5, pady=5)
        self.cancel_button = Button(
            box,
            text='Cancel',
            width=10,
            command=self.cancel
        )
        self.cancel_button.grid(row=0, column=1, padx=5, pady=5)
        self.bind('<Return>', self.okay)
        self.bind('<Escape>', self.cancel)
        box.grid()

    @event_handler
    def okay(self):
        """Validate and apply the changes made by this Dialog."""
        if self.validate():
            self.withdraw()
            self.update_idletasks()
            try:
                self.apply()
            finally:
                self.cancel()
        else:
            self.initial_focus.focus_set()

    @event_handler
    def cancel(self):
        """Close the Dialog window and return to its parent."""
        if self.parent is not None:
            self.parent.focus_set()
        self.destroy()

    def validate(self):
        """Verify that the Dialog is in a valid state."""
        return True

    @staticmethod
    def apply():
        """Make any changes the Dialog wishes to accomplish."""
        pass


class LoginWindow(Dialog):

    """Widget to allow easy process for supplying credentials."""

    TITLE = 'E-mail Login'
    WIDTH = 45
    MASK = '*'

    def __init__(self, parent, icon_handle, data_handler):
        """Initialize the dialog with the necessary information."""
        self.__data_handler = data_handler
        super().__init__(parent, self.TITLE, icon_handle)
        self.__username_label = self.__password_label = self.__username = \
            self.__password = None

    def body(self, master):
        """Create all the different widgets needed for the body."""
        self.__username_label = Label(master, text='Gmail Address:')
        self.__password_label = Label(master, text='Password:')
        self.__username = Entry(master, width=self.WIDTH)
        self.__password = Entry(master, width=self.WIDTH, show=self.MASK)
        self.__username_label.grid(row=0, column=0, padx=5, pady=5, sticky=E)
        self.__username.grid(row=0, column=1, padx=5, pady=5, sticky=EW)
        self.__password_label.grid(row=1, column=0, padx=5, pady=5, sticky=E)
        self.__password.grid(row=1, column=1, padx=5, pady=5, sticky=EW)
        self.bind('<Key>', self.refresh)
        return self.__username

    def button_box(self):
        """Create the button box and change okay button's options."""
        super().button_box()
        self.okay_button.configure(state=DISABLED, text='Login')

    @event_handler
    def refresh(self):
        """Perform a soft validation for the username and password."""
        username = self.__username.get()
        password = self.__password.get()
        valid = re.search(r'\A(\w|\.)+@gmail\.com\Z', username) is not None \
            and len(password) > 3
        self.okay_button['state'] = ACTIVE if valid else DISABLED

    def validate(self):
        """Attempt to validate username and password with the server."""
        self.__data_handler.username = self.__username.get()
        self.__data_handler.password = self.__password.get()
        valid = self.__data_handler.validate_credentials()
        if valid:
            tkinter.messagebox.showinfo(
                'Login Success',
                'The credentials were accepted.\n'
                'You are now logged in!',
                master=self
            )
        else:
            self.__password.delete(0, END)
            tkinter.messagebox.showerror(
                'Login Error',
                'This username/password combination was not accepted.\n'
                'Please try again.',
                master=self
            )
        return valid


if __name__ == '__main__':
    SMTPClient.main()

这里有两个关于在向用户显示任何内容之前预加载 window 的想法。具体来说,在加载完成之前不要执行应用程序的mainloop。主要缺点是该程序可能需要一段时间才能出现。 main 函数和 main 方法都在显示根 window 之前完成框架设置。示例底部的代码随机选择 运行 的实现,但两个版本为用户完成相同的事情。

#! /usr/bin/env python3
import contextlib
import random
import time
import tkinter


def main():
    root = tkinter.Tk()
    frame = Application(root)
    frame.setup_frame('Hello, world!', ' ' * 50)
    frame.grid()
    root.mainloop()


class Application(tkinter.Frame):

    @classmethod
    def main(cls):
        with cls.setup_root('Tkinter Program') as root:
            root.resizable(False, False)
            frame = cls(root)
            frame.setup_frame('Hello, world!', ' ' * 50)
            frame.grid()

    @staticmethod
    @contextlib.contextmanager
    def setup_root(title):
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        root.withdraw()
        root.title(title)
        yield root
        root.after_idle(root.deiconify)
        root.mainloop()

    def setup_frame(self, *args):
        self.after_idle(self.__setup, *args)

    def __setup(self, *args):
        time.sleep(5)   # simulate taking a long time
        self.label = tkinter.Label(self, text='{1}{0}{1}'.format(*args))
        self.label.grid()
        self.button = tkinter.Button(
            self,
            text='Raise a KeyboardInterrupt exception.',
            command=lambda: self.throw(KeyboardInterrupt)
        )
        self.button.grid()

    @staticmethod
    def throw(kind):
        raise kind()


if __name__ == '__main__':
    random.choice((main, Application.main))()