Skip to content Skip to sidebar Skip to footer

Tkinter - Preload Window?

I started building a python tkinter gui, the problem is, after adding many features to the gui, the loading started looking really ugly. When starting the mainloop, a blank window

Solution 1:

On your operating system tkinter seems to show the base Tk window before loading it's widgets causing that few milliseconds of blank window. To make it appear with all the widgets already loaded you will need to hide the window when starting and .after it has loaded show it again.

There are a number of ways to show and hide a window, personally I'd use .withdraw() to remove the window from the window manager (like it was never there) then .deiconify() (basically "unminimize") to show the window again.

classTEST(tk.Tk):def__init__(self,*args,**kw):
        tk.Tk.__init__(self,*args,**kw)
        self.withdraw() #hide the windowself.after(0,self.deiconify) #as soon as possible (after app starts) show again#setup app...

An alternative to withdrawing the window completely is to start it minimized with .iconify() so it will appear on the taskbar/dock but won't open the window until it is completely loaded.

Another way to hide/show the window is by changing the -alpha property as @double_j has done 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).

So on some unix machines -alpha may have no effect and on windows it may cause the window to flash (probably not an issue before it is even opened but still worth noting)

Where as withdraw and deiconify works identically amongst all platforms as far as I am aware.

Solution 2:

The short answer is yes. You can set the window to appear as though you can't see it. Perform any code that takes time, and then display the window.

Here's how:

classMyTkRoot(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 a reference to the code with the example of a Toplevel. It will also work with the root however.

Solution 3:

After looking at your the project in your repository, I revised the code and dumped it all into one file. The following is the results from refactoring your modules and files into a fully self-contained program. The one thing that it is missing is the Resource class that handles all of the external dependencies.

You can see the complete program on PasteBin.com.

#! /usr/bin/env python3import 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')


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


classSMTPClient(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+'    @classmethoddefmain(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()

    @classmethoddefcreate_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

    @classmethoddefcreate_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')

    @staticmethoddefhandle_control_a(event):
        """Treat Control-A as would be expected on a Windows system."""
        widget = event.widget
        ifisinstance(widget, Entry):
            widget.selection_range(0, END)
            return'break'ifisinstance(widget, Text):
            widget.tag_add(SEL, 1.0, END + '-1c')
            return'break'    @classmethoddefattach_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)

    @classmethoddefsetup_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)

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

    defcapture_root(self):
        """Capture the rook (Tk instance) of this application."""
        widget = self.master
        whilenotisinstance(widget, Tk):
            widget = widget.master
        return widget

    defcreate_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)

    defcreate_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)

    defconfigure_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_handlerdefhandle_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_handlerdefhandle_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_handlerdefhandle_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

    @classmethoddefis_target(cls, text):
        """Determine if this is an acceptable e-mail address."""returnbool(re.search(cls.IS_TARGET, text))

    @classmethoddefis_subject(cls, text):
        """Determine if this is an acceptable subject line."""returnbool(re.search(cls.IS_SUBJECT, text))

    @classmethoddefis_message(cls, text):
        """Determine if this is an acceptable message to send."""returnbool(re.search(cls.IS_MESSAGE, text))

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

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

    @threadbox.MetaBox.threaddefsend_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
            )

    @propertydefcurrent_status(self):
        """Find out what status the program currently is in."""return (Status.connection_error
                ifnot self.__data_handler.is_connected else
                Status.login_button
                ifnot self.__data_handler.is_logged_in else
                Status.mail_label)

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

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

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


classTextViewer(Toplevel):

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

    BACKGROUND = '#FFFFFF'
    FOREGROUND = '#000000'
    WIDTH = 800
    HEIGHT = 600
    X_OFFSET = 10
    Y_OFFSET = 10    @classmethoddefload(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 isnotNone:
            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()

    defcreate_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
        )

    defconfigure_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_handlerdefokay(self):
        """Close the window."""
        self.destroy()

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


classDataHandler:

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

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

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

    @staticmethoddef__check_string(value, name):
        """Verify that the string has a value type and value."""ifnotisinstance(value, str):
            raise TypeError('{} must be of type str'.format(name))
        ifnot 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.')

    deftry_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)

    defsend(self, destination, subject, message):
        """Try to send an e-mail with the provided information."""if self.__server isNone:
            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 = Nonedefvalidate_credentials(self):
        """Verify if the saved credentials are correct or not."""if self.__server isNone:
            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 = Falseelse:
            self.__is_logged_in = Truefinally:
            self.__finish()
        return self.__is_logged_in

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


classDialog(Toplevel):

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

    X_OFFSET = 50
    Y_OFFSET = 50def__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 isnotNone:
            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()
        ifnot self.initial_focus:
            self.initial_focus = self
        self.protocol('WM_DELETE_WINDOW', self.cancel)
        parent = self.parent
        if parent isnotNone:
            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:
            passelse:
            self.grab_set()
            self.wait_window(self)

    defdestroy(self):
        """Destruct the Dialog window."""
        self.initial_focus = Nonesuper().destroy()

    defbody(self, master):
        """Create the body of this Dialog window."""passdefbutton_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_handlerdefokay(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_handlerdefcancel(self):
        """Close the Dialog window and return to its parent."""if self.parent isnotNone:
            self.parent.focus_set()
        self.destroy()

    defvalidate(self):
        """Verify that the Dialog is in a valid state."""returnTrue    @staticmethoddefapply():
        """Make any changes the Dialog wishes to accomplish."""passclassLoginWindow(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 = Nonedefbody(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

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

    @event_handlerdefrefresh(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) isnotNone \
            andlen(password) > 3
        self.okay_button['state'] = ACTIVE if valid else DISABLED

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

Solution 4:

Here are two more ideas for preloading a window before displaying anything to the user. Specifically, do not execute the mainloop of the application until loading is complete. The main disadvantage is that the program might a take a while to appear. Both the main function and the main method finish setting up the frame before the root window is ever displayed. The code at the bottom of the example randomly picks an implementation to run, but both version accomplish the same thing for the user.

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


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


classApplication(tkinter.Frame):

    @classmethoddefmain(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.contextmanagerdefsetup_root(title):
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        root.withdraw()
        root.title(title)
        yield root
        root.after_idle(root.deiconify)
        root.mainloop()

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

    @staticmethoddefthrow(kind):
        raise kind()


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

Post a Comment for "Tkinter - Preload Window?"