Interactive Python Apps using tk GUI (tkinter)

Posted on Thu 27 February 2020 in Python

What is tcl-tk?

Tcl (Tool Command Language) is a simple open-source interpreted programming language that provides multiple common facilities such as variables, procedures, and control structures as well as many useful features that are not found in any other major language. Tcl runs on almost all modern operating systems such as Unix, Macintosh, and Windows (including Windows Mobile).

Tk (Tool Kit) is a free open-source, cross platform widget toolkit that together with tcl, allows users to program interfaces. Tk comes installed for pretty much all macOS and Linux machines (as of this post current version is 8.6). While tk provides all the snippets like buttons, boxes and windows, tcl defines what to do with them.

If you are using Windows please jump to Test tkinter for Windows Users.

Enable tkinter Python package for iOS

We will assume that you are using Python 3. One clean way to install components to macOS is via homebrew, if you are using it as your package manager first you need to install tcl-tk:

Via homebrew

Depending on the version you are in, this can be a tricky installation. The process will be shown in steps that hopefully would cover most of the known issues/scenarios.

1. Install tcl-tk cask

brew update  
brew install tcl-tk

Now you would be able to see in cellar /usr/local/Cellar/, in our case we installed tcl-tk 8.6.10.

2. Install pyenv (if you have consider 2.b)

The pyenv 1.2.18 was used for this post.

brew install pyenv

Then you will need to do modify your bash and zsh profiles to facilitate the use of pyenv and the global python and pip in use.

a. Create or modify ~/.bash_profile and include:

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
eval "$(pyenv init -)"
fi

b. Create or modify ~/.zshrc and include:

export PATH="/usr/local/opt/tcl-tk/bin:$PATH"
export LDFLAGS="-L/usr/local/opt/tcl-tk/lib"
export CPPFLAGS="-I/usr/local/opt/tcl-tk/include"
export PKG_CONFIG_PATH="/usr/local/opt/tcl-tk/lib/pkgconfig"
if command -v pyenv 1>/dev/null 2>&1; then
eval "$(pyenv init -)"
fi

Make sure the above paths match the ones in your system (if you didn't customise anything on your homebrew installation the paths should match).

Restart the terminal and verify your path echo $PATH, you should be able to see .pyenv and tcl-tk directories there.

3. Install python

If you have python you will have to reinstall it to add tcl-tk configuration.

a. it --with-tcl-tk works. Try (in our case we are installing python 3.8.2, but it can be any other python 3 version):

pyenv install python 3.8.2 --with-tcl-tk

If it doesn't work don't try to install python via brew, since this will mess up your environment. Rather proceed with option 3.b.

b. Hardcoded installation.
Modify the pyenv python-build script. You will find it in /usr/local/opt/pyenv/plugins/python-build/bin. Open python-build and find the line that contains the Configuration options:

$CONFIGURE_OPTS ${!PACKAGE_CONFIGURE_OPTS} "${!PACKAGE_CONFIGURE_OPTS_ARRAY}" || return 1

and replace if with:

$CONFIGURE_OPTS --with-tcltk-includes='-I/usr/local/opt/tcl-tk/include' --with-tcltk-libs='-L/usr/local/opt/tcl-tk/lib -ltcl8.6 -ltk8.6' ${!PACKAGE_CONFIGURE_OPTS} "${!PACKAGE_CONFIGURE_OPTS_ARRAY}" || return 1

Save changes and now you will be able to install pyhton

pyenv install python 3.8.2

4. Change global python and test it

Check the versions of python installed

pyenv versions

Depending on what was your global python option before, you will see an '*' in front of one of the options. You will now select the newly installed python to be used as the global one.

pyenv global 3.8.2

Next step is to verify that indeed the new python is your default python by running:

pyhton -V

If everything went well you will see your python version displayed (in our case 3.8.2) if not yu may want to take a look to pyenv known issues and documentation.

Finally, its time to try out tkinter. Run:

python -m tkinter -c 'tkinter._test()'

A small window will pop up to your screen, click QUIT to exit.

Via python.org

All current installers for macOS downloadable from python.org supply their own private copies of Tcl/Tk. So no need to do something else. You can also install ActiveTcl via their website.

Test tkinter for Windows Users

To test tkinter you can simply run see offcial documentation:

python -m tkinter -c 'tkinter._test()'

A small window will pop up to your screen, click QUIT to exit.

Create a Simple GUI

I will cover a general way to create a script for a GUI and a simple way to wrap it in a function for re-usability.

We will create a user interface that request credentials to "log in".

1. Create Window

Initialize a tkinter.Tk object, that is a root window.

Every tkinter application must have a root window.

import tkinter as tk
window = tk.Tk()

2. Modify Window Attributes

It is possible to modify aspects fo the window, like size, position, title among others. These attributes are not mandatory if you are not sure of the size of window you can avoid modifying its geometry and the final outcome will have the size that contains all contents.

For our "Log in" example, we will put a title and a fix size of 500 x 300 pp. A good practice is to make the windows appear on top of any other open, so that the user don't miss it.

# Modify window title
window.title('Log in')
# Define window size
window.geometry("500X300")
# Display window on top of all open windows
window.attributes("-topmost", True)

3. Declare User Input Variables

To request input from a user is needed to initialize variables that can collect it. Possible types are linked to basic variable types like String, Boolean, Double or Int.

For our example we require 2 string variables; one for email and another one for the password

user_mail = tk.StringVar(window)
user_pass = tk.StringVar(window)

4. Create Labels for User Input and Arrange them

It is important to let the user know what input do we require from them in each field. This can be achieved by defining Labels and Entries. There are two ways to specify the way the labels and entries will be shown in the window: - By pack. This will organize the objects and show them sequentially. - By grid. This provides flexibility to specify the desired location using rows and columns.

We will use the grid method in our example. We declare labels and entries in pairs, the user label requesting "User email" will be shown in row 0, column 0, its entry in the same row, but column 1. A similar thing is defined for password, however for the password entry we set the parameter show to '*' so that the password is masked.

# Request user email
tk.Label(window, text="email").grid(row=0)
tk.Entry(window, textvariable=user_mail).grid(row=0, column=1)
# Request password (masked)
tk.Label(window, text="password").grid(row=1)
tk.Entry(window, textvariable=user_pass, show='*').grid(row=1, column=1)

5. Add Buttons

For a better user interaction it is possible to define and include buttons so that the user can submit, run, accept, cancel,... the execution. Each button requires its own definition and command that specifies what to do when clicked.

We will include a submit and a cancel buttons that will help us either to terminate the process or to continue with the credentials provided

tk.Button(winroot, text="Submit", command=lambda: user_click('submit')).\
        grid(row=6, column=2)
tk.Button(winroot, text="Cancel", command=lambda: user_click('cancel')).\
        grid(row=6, column=1)

5.1 Buttons command

The command parameter in the Button class, allows you to pass a function to define what process to execute once the button is clicked.

We create a function that changes a global variable that let us know if the button option was cancel or not. We destroy the window after the interaction.

u_selection = 'submit'
def user_click(button_id):
    global u_selection
    if button_id == 'cancel':
        u_selection = 'cancel'
    window.destroy()

6. Display the window

To display the window, it's needed to block any other python process so that it waits for window's result.

You can call the method mainloop direclty from the tkinter library or to the window object created. In our case we will call directly the method to avoid issues when trying to quit the window.

tk.mainloop()

GUI Function

All the components describe above can be wrapped in a function that can be re-usable or if preferred a class that can be extended (npt covered here).

An example of a function that retrieves Log in credentials from user is:

def get_log_in(win_title: str ="Log in", 
               win_geometry: str=None) -> (str, str, str):
    """
    Log in Credentials
    Retreive user credientials

    Parameters
    ----------
    win_title: str
        Window title. Default value is "Log in"
    win_geometry: str
        Window geometry with format "<number>x<number>". Default value is None 
        which will provide the minimum size.

    Returns
    -------
    user selection: sumbit or cancel, user email and user password. 
    """
    def user_click(button_id):
        nonlocal u_selection
        nonlocal window
        if button_id == 'cancel':
            u_selection = 'cancel'
        window.destroy()

    window = tk.Tk()
    u_selection = 'submit'

    # Modify Geometry
    if win_geometry is not None: 
        window.geometry(win_geometry)
    # Title
    window.title(win_title)
    window.attributes("-topmost", True)

    # Request user email
    user_mail = tk.StringVar(window)
    tk.Label(window, text="email").grid(row=0)
    tk.Entry(window, textvariable=user_mail).grid(row=0, column=1)
    # Request password (masked)
    user_pass = tk.StringVar(window)
    tk.Label(window, text="password").grid(row=1)
    tk.Entry(window, textvariable=user_pass, show='*').grid(row=1, column=1)

    # Display Buttons
    tk.Button(window, text="Submit", command=lambda: user_click('submit')).\
        grid(row=6, column=2)
    tk.Button(window, text="Cancel", command=lambda: user_click('cancel')).\
        grid(row=6, column=1)

    tk.mainloop()

    return u_selection, user_mail.get(), user_pass.get()