Charming Python #9 (20000052)

TK programming in Python: Tips for Beginners


David Mertz, Ph.D.
Super model, Gnosis Software, Inc.
October 2000

Built into most distributions of Python you will find the TK GUI library developed by Scriptics for use with TCL. TK is available for a large number of computer platforms, and its Python interface, TKinter, is available almost equally widely. This column introduces a programmer to TKinter by means of source code samples and usage explanations. The project used as an example is a port of the Txt2Html front-end discussed in an earlier article to a GUI environment.

What Is Python? What Is Tk?

Python is a freely available, very-high-level, interpreted language developed by Guido van Rossum. It combines a clear syntax with powerful (but optional) object-oriented semantics. Python is available for almost every computer platform you might find yourself working on, and has strong portability between platforms.

TK is a widely used graphics library developed by John Ousterhout, and most closely associated with the TCL language also developed by Ousterhout. TK started out--in 1991--as an X11 library, but since that time it has been ported to virtually every popular GUI. Bindings for TK have been written for many popular languages (and for many small languages too), including the Tkinter module for Python. TK is as close as Python comes to having a "standard" GUI.

Introduction

This column has a lot of parallels with my earlier "Curses programming in Python" column. Both curses and TK are widely used user interface libraries. And despite the fact that curses targets text consoles, and TK GUIs, working with both libraries is surprisingly similar. Understanding the basic notions of windows and event loops is the first step to programming with either library. Once you have got those concepts down, all you really need is a reference to the widgets available. Well, a good reference, and a moderate amount of practice.

In this column--much as with the curses one--we'll be limiting ourselves to the features of Tkinter itself. Since the Tkinter module is part of many Python distributions, you have a good chance of having it available without requiring users to download support libraries or other Python modules. The Resources section gives pointers to several collections of higher-level widgets for various UI purposes. But you can do a lot with Tkinter itself, even construct your own new high-level widgets. Getting used to the base Tkinter module is a good way to familiarize yourself with the TK way of thinking, even if you go on to use extra widget collections.

The author should also make a confession to readers--but a useful one, perhaps. I am no wizened expert at TK programming. In fact, my TK programming experience stretches back about three days (with a few glances at some of the references in the Resources prior to that). Maybe it was not an entirely painless three days, but at the end of them, I feel like I have a pretty good grasp of working with Tkinter. The moral here is that TK itself, and the Python Tkinter wrapper are both extraordinarily well-designed and friendly libraries that are about the easiest way to start GUI programming I can think of.

The Application

As a test application for this article, the author will discuss a wrapper he has written for the Txt2Html program introduced in "Charming Python #3", whose techniques were discussed further in subsequent columns. Txt2Html works in several ways, but for purposes of this article, we are interested in Txt2Html as a command-line format conversion program. One way to operate Txt2Html is to feed it a bunch of command-line arguments indicating various aspects of the conversion to be performed, and let the application run as a batch process. For occassional usage, it might be friendlier for users to be presented with an interactive selection screen that leads users through conversion options, and provides visual feedback of options selected, before performing the actual conversion.

The application tk_txt2html is structured in terms of a familiar topbar menu with drop-downs and nested submenus. Even beyond the similarities of TK and curses event loops, this application looks a lot like the curses version discussed in "Charming Python #6" at least in terms of the basic parts of the screens and UI techniques. TK gives us a bit more starting material than curses did, so things like the menus can rely on inherent Tkinter classes, rather than being built "from scratch." tk_txt2html has somewhat fewer lines of code than does curses_txt2html, while simultaneously doing a bit more. But they are in the same ballpark. Beyond the capability for selecting each configuration option, a scrolling help box is created with the TK Text widget, an about box used the Message widget, and a series of history lines help flex 'TK's dynamic geometry management a little bit. Of course, as with most interactive applications, some user input is caught with 'TK's Entry widget.

Probably it is worth looking at the application in action before going on with the explanation of its code:

Screenshot of tk_txt2html.py

The Basics

There are really exactly three things that a Tkinter program has to do:

Minimum possible [Tkinter] program

import Tkinter        # import the Tkinter module
root = Tkinter.Tk()   # create a root window
root.mainloop()       # create an event loop

The sample program is a perfectly legitimate Tkinter program (maybe not a perfectly good on, since it doesn't even manage "hello world"). But the only thing actually missing from our first sample program is some widgets to populate the root window we have created. Once we enhance our program with widgets, this same root .mainloop() method call will handle all the interaction with our widgets without further programmer intervention.

Let's take a look at the more realistic main() function of tk_txt2html.py. Notice that I prefer to perform an import Tkinter statement to the from Tkinter import that John Grayson uses throughout his book (see Resources). It is not so much that I am worried about namespace pollution (the usual caveat for from ... import statements); rather, I want to make it explicit when I am using Tkinter classes, and not risk confusion with my own functions and clases. I recommend you do this, at least as you begin working with Tkinter:

tk_txt2html main() function

def main():
    global root, history_frame, info_line
    root = Tkinter.Tk()
    root.title('Txt2Html TK Shell')
    init_vars()

    #-- Create the menu frame, and menus to the menu frame
    menu_frame = Tkinter.Frame(root)
    menu_frame.pack(fill=Tkinter.X, side=Tkinter.TOP)
    menu_frame.tk_menuBar(file_menu(), action_menu(), help_menu())

    #-- Create the history frame (to be filled in during runtime)
    history_frame = Tkinter.Frame(root)
    history_frame.pack(fill=Tkinter.X, side=Tkinter.BOTTOM, pady=2)

    #-- Create the info frame and fill with initial contents
    info_frame = Tkinter.Frame(root)
    info_frame.pack(fill=Tkinter.X, side=Tkinter.BOTTOM)

    # first put the column labels in a sub-frame
    LEFT, Label = Tkinter.LEFT, Tkinter.Label   # shortcut names
    label_line = Tkinter.Frame(info_frame, relief=Tkinter.RAISED, borderwidth=1)
    label_line.pack(side=Tkinter.TOP, padx=2, pady=1)
    Label(label_line, text="Run #", width=5).pack(side=LEFT)
    Label(label_line, text="Source:", width=20).pack(side=LEFT)
    Label(label_line, text="Target:", width=20).pack(side=LEFT)
    Label(label_line, text="Type:", width=20).pack(side=LEFT)
    Label(label_line, text="Proxy Mode:", width=20).pack(side=LEFT)

    # then put the "next run" information in a sub-frame
    info_line = Tkinter.Frame(info_frame)
    info_line.pack(side=Tkinter.TOP, padx=2, pady=1)
    update_specs()

    #-- Finally, let's actually do all that stuff created above
    root.mainloop()

There are a number of things to notice about our simple main() function.

- Every widget has a parent. Whenever we create a widget, the first argument to the instance creation is the parent of that new widget.
- When other arguments are used besides a parent, they are passed in Python's pass-by-name style. This gives us lots of flexibility about what options we want to override the defaults on, and which we are happy to leave be.
A number of (Frame) widget instances are global variables. It would be possible to pass these around from function to function, and maintain a theoretical purity about scoping. Doing that is much more trouble than it is worth. The basic UI elements of our application are perfectly appropriate for any function to play with; making them global just makes this explicit. Of course, you should use a good naming convention when you use global variables (Pythonistas seem to hate Hungarian notation, so don't use that ^)).
- After a widget is created, it needs to call a geometry manager method to let TK know where to put the widget. A lot of magic goes into TK 's calculation of the details, especially when windows are resized or widgets are added dynamically. But you need to let TK know which set of incantations to use, for your part.

Geometry Managers

TK (and therefore Tkinter) has three geometry managers to choose from: .pack(), .grid() and .place(). Only the first two are used by tk_txt2html, although .place() can be used for the most fine-grained (and therefore complicated) control. Most of the time, you will use .pack().

You are certainly allowed to call the .pack() method of a widget with no arguments. If you do that, you can certainly count on the widget winding up somewhere in your application. But you probably want to provide some slight hints by way of named argument. The most important such hint is the side argument. Options are LEFT, RIGHT, TOP, and BOTTOM (note that those words are variables in the Tkinter namespace).

A lot of the magic of .pack() comes from the fact that widgets can be nested. In particular, the Frame widget does little more than act as a container for other widgets (maybe show borders of various types). So a particularly handy way of organizing things is to pack together several frames in the orientations you want them, then later add other widgets within each frame. Frames (or any other widgets) get packed in the order their .pack() methods are called. So if two widgets both want to have side=TOP, it is first-come-first-serve.

.grid() is also used a bit in tk_txt2html (mostly just to play with it though). The idea of the grid geometry manager is that a parent widget is divided into invisible graph-paper lines. When a widget calls .grid(row=3, column=4) it is requesting (of its parent) that if be placed on the third row and fourth column. The total number of rows and columns is just a matter of looking at the requests made by all the siblings.

Don't forget to apply a geometry manager to your widgets, or else you'll be surprised not to see them in your application.

Menus

Tkinter makes creating menus quite painless. If you want, you can even have different fonts, pictures, checkboxes, and all sorts of fancy child widgets populate your menus. Our application is simpler than that though. The menus for tk_txt2html were all created with the line we saw above:

menu_frame.tk_menuBar(file_menu(), action_menu(), help_menu())

But this line might mystify as much as it clarifies, by itself. Most of the work (but still a small amount) lives in the functions I have called *_menu(). Let's look at the simplest one:

Creating a drop-down menu

def help_menu():
    help_btn = Tkinter.Menubutton(menu_frame, text='Help', underline=0)
    help_btn.pack(side=Tkinter.LEFT, padx="2m")
    help_btn.menu = Tkinter.Menu(help_btn)
    help_btn.menu.add_command(label="How To", underline=0, command=HowTo)
    help_btn.menu.add_command(label="About", underline=0, command=About)
    help_btn['menu'] = help_btn.menu
    return help_btn

Basically, a drop-down menu consists of a Menubutton widget that has a Menu widget as a child. The Menubutton needs to be .pack()'d to the appropriate location (or .grid()'d, etc.), but the Menu widget instead has items added with the .add_command() method. There is an odd little assignment to the Menubutton's dictionary in the above: just do the same in your own code.

Getting User Input

We have seen to display output (the Label widget was used above, see the full source for some use of the Text widget and the Message widget also). And we have also seen how to create menus. Probably the most significant remaining UI issue is getting user field input (and the last UI issue for this introduction).

The basic widget for field input is Entry. Using this is simple, but might be a little bit different than you would expect from Python's raw_input() or curses' .getstr(). That is, 'TK's Entry widget does not return a value for an assignment context, but rather takes an argument for the field object to be populated. For example, this is the function that allows the user to specify an input file:

Receiving user field input

def GetSource():
    get_window = Tkinter.Toplevel(root)
    get_window.title('Source File?')
    Tkinter.Entry(get_window, width=30,
                  textvariable=source).pack()
    Tkinter.Button(get_window, text="Change",
                   command=lambda: update_specs()).pack()

Again, there are a few things to look at here. We have created a new Toplevel widget for this input. That is, input occurs in its own dialog box in this example. The input field is created by creating an Entry widget, and specifying a textvariable argument. But there is a bit more to this still.

The textvariable argument does not specify a simple string variable, but is instead a StringVar object. In our case, the init_vars() function that was called from main() contained these lines:

source = Tkinter.StringVar()
source.set('txt2html.txt')

What this did was create an object suitable for taking user input, and then give it an initial value. Once this object exists, it is modified immediately every time a change is made within an Entry widget that links to it. The change occurs for every keystroke within the Entry widget, not just upon termination of a read, in the style of raw_input().

Once we want to do something with the value a user entered, we use the .get() method of our StringVar instance, for example:

source_string = source.get()


Finally

The techniques outlined here--and especially those additional ones used in the full application source code should get you started with Tkinter programming. Play with it a bit, it is not hard to work with. One nice thing is that the TK library may be accessed by many languages other than Python also, so what you learn using Python's Tkinter module is mostly transferrable elsewhere.

Resources

A good online starting point for Tkinter information (and downloads) is:

http://python.org/topics/tkinter/

Several extra widget collections are available to save you some time in constructing complex UIs. PMW (Python Mega Widgets) is one written 100% in Python, and widely used in the Python community. Several widget collections can be found at:

http://python.org/topics/tkinter/widgets.html

Fredrik Lundh has written a good tutorial for Tkinter that contains much more detail than this article:

http://www.pythonware.com/library/tkinter/introduction/index.htm

A couple printed books are worth checking out also. The first is a good intro to TK itself. The second is specific to Python, with a lot of use of the PMW collection in its examples:

TCK/TK in a Nutshell, Paul Raines & Jeff Tranter, O'Reilly, 1999. ISBN 1-56592-433-9
Python and Tkinter Programming, John E. Grayson, Manning, 2000. ISBN 1-884777-81-3

A very nice distribution of Python has been created recently by ActiveState. This distribution includes TKinter and a variety of other nice packages and modules not contained in most other distributions. (They even have an ActivePerl distribution for those inclined towards that other scripting language). Find it at:

http://activestate.com/Products/ActivePython/Download.html

Scriptics (the maintainers and creators of TK) has been renamed as Ajuba Solutions. It can still be found at:

http://www.scriptics.com/home.html

For many comparisons, take a look at "Curses programming in Python:"

http://gnosis.cx/publish/programming/charming_python_6.html

Files used and mentioned in this article:

http://gnosis.cx/download/charming_python_9.zip

About The Author

Picture of Author David Mertz writes many apocopetic articles. David may be reached at mertz@gnosis.cx; his life pored over at http://gnosis.cx/publish/. Suggestions and recommendations on this, past, or future, columns are welcomed.