I'd like to introduce you to the easiest way imaginable to start GUI programming, namely by using Scriptics' TK and Tkinter wrapper. We'll be drawing a lot of parallels with the curses library, which I covered on developerWorks in "Curses programming in Python". Both libraries have a surprisingly similar interface despite the fact that curses targets text consoles and TK implements GUIs. Before using either library, you need a basic understanding of windows and event loops and a reference to the available widgets. (Well, a good reference and a moderate amount of practice.)
Like the article on curses, this article is limited to the features of Tkinter itself. Since Tkinter comes with many Python distributions, you probably won't have to download support libraries or other Python modules. The Resources later in this article point to several collections of higher-level user interface widgets, but you can do a lot with Tkinter itself, including construction of your own high-level widgets. Learning the base Tkinter module will introduce you to the TK way of thinking, which is important even if you go on to use more advanced widget collections.
TK is a widely used graphics library most closely associated with the TCL language, both developed by John Ousterhout. Although TK started out in 1991 as an X11 library, it has since been ported to virtually every popular GUI. (It's as close as Python comes to having a "standard" GUI.) There are TK bindings (the Tkinter modules) for most popular languages now, as well as many of the smaller languages.
Before we begin, I must make a confession: I am no wizened expert at TK programming. In fact, the bulk of my TK programming experience began about three days before I started this article. Those three days were not without their challenges, but by the end I felt like I had a pretty good grasp of Tkinter . The moral here is that both TK and the Tkinter wrapper are extraordinarily well designed, user-friendly, and just about the easiest introduction to GUI programming out there.
As a test application we'll use a wrapper for Txt2Html, a file format conversion program used in many of my previous columns (see Resources). Although you can run Txt2Html in several ways, the wrapper here is based on running Txt2Html from the command line. The application runs as a batch process, with command-line arguments indicating various aspects of the conversion to be performed. (Later it might be nice to offer users the option of an interactive selection screen that leads them through conversion options and provides visual feedback of selected options before performing the actual conversion.)
tk_txt2html is based on a topbar menu with drop-downs and nested submenus. Implementation details aside, it looks a lot like the curses version discussed in "Curses programming in Python". tk_txt2html and curses_txt2html are clearly in the same ballpark, even though TK accomplishes more with less code. In TK, for example, features like menus can rely on built-in Tkinter classes instead of needing to be written from scratch.
Along with setting configuration options, the TK wrapper also includes a scrolling help box built with the TK Text widget (an about box with the Message widget) and a history window that exercises TK's dynamic geometry management. And like most interactive applications, the wrapper accepts some user input with TK's Entry widget.
Let's look at the application in action now before discussing the code any further.
There are really only three things that a Tkinter program has to do:
Minimum possible [Tkinter] program
Tkinter # import the Tkinter module root = Tkinter.Tk() # create a root window root.mainloop() # create an event loop
This is a perfectly legitimate Tkinter program (never mind that it's useless because it doesn't even manage "hello world"). The only thing this program needs to do is create some widgets to populate its root window. Thus enhanced, our program's root
.mainloop() method call will handle all user interaction without further programmer intervention.
Now let's look at the more realistic main() function of tk_txt2html.py. Notice that I prefer to perform John Grayson's
import Tkinter statement rather than the
from Tkinter import (see his book listed in Resources). This is not so much because I'm worried about namespace pollution (the usual caveat for
from ... import statements), but rather because I want to be explicit about using Tkinter classes; I don't want to risk confusing them with my own functions and classes. I recommend you do the same thing, at least at the beginning.
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 note in our simple
- Every widget has a parent. Whenever we create a widget, the first argument to the instance creation is the parent of the new widget.
- If there are any other widget creation arguments, they are passed by name. This Python feature gives us lots of flexibility in specifying options or allowing them to default.
- A number of widget instances (Frame) are global variables. We could make these local by passing them from function to function in order to maintain a theoretical purity of scope, but it would be much more trouble than it's worth. Besides, making these basic UI elements global underlines the fact that they are useful in all of our functions. But be sure to use a good naming convention for your own global variables. (As a forewarning, Pythonists seem to hate Hungarian notation.)
- After we create a widget, we call a geometry manager method to let TK know where to put it. A lot of magic goes into TK's calculation of the details, especially when windows are resized or when widgets are added dynamically. But in any case we need to let TK know which set of incantations to use.
TK provides three geometry managers:
.place(). Only the first two are used by tk_txt2html, although
.place() can be used for fine-grained (in other words, very complicated) control. Most of the time you'll use
You're certainly allowed to call the
.pack() method without arguments. But if you do that, you can count on the widget winding up somewhere on your display and you'll probably want to give
.pack() some hints. The most important of these will then be the
side argument. Possible values are LEFT, RIGHT, TOP, and BOTTOM (note that these 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 (on occasion it shows borders of various types). So it's particularly handy to pack several frames in the desired orientations and then add other widgets within each frame. Frames (and other widgets) are packed in the order their
.pack() methods are called. So if two widgets both ask for
side=TOP, it's first come, first served.
tk_txt2html also plays a bit with
.grid(). The grid geometry manager overlays a parent widget with invisible graph-paper lines. When a widget calls
.grid(row=3, column=4), it's requesting of its parent that it be placed on the third row and on the fourth column. The parent's total rows and columns are computed by looking at the requests made by all its children.
Don't forget to apply a geometry manager to your own widgets, lest you have the rude awakening of not seeing them on your display.
By itself this line might mystify as much as it clarifies. Most of the work that must be done lives in the functions 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
A drop-down menu is a Menubutton widget with a Menu widget as a child. The Menubutton is
.pack()'d to the appropriate location (or
.grid()'d, etc.). And the Menu widget has items added with the
.add_command() method. (Note the odd assignment to the Menubutton's dictionary above. Don't question this, just blindly follow me here and do the same thing in your own code.)
The example we're going to look at now shows how the Label widget displays output (see Resources for the full source for some examples of the Text and Message widgets). The basic widget for field input is Entry. It's simple to use, but the technique might be a bit different from what you might expect if you've used Python's
raw_input() or curses'
.getstr() before. TK's Entry widget does not return a value that can be assigned. It instead populates the field object by taking an argument. The following function, for example, 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()
There are a few things to notice at this point. We've created a new Toplevel widget and a dialog box for this input, and we've specified the input field by creating an Entry widget with a
textvariable argument. But wait, there's more!
source = Tkinter.StringVar() source.set('txt2html.txt')
This creates an object suitable for taking user input and gives it an initial
value. This object 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 in the style of
raw_input(), and not just upon termination
of a read.
To do something with the value entered by the user, we use the
.get() method of our StringVar instance. For example:
source_string = source.get()
The techniques we outlined here, along with the ones we used in the full application source code, should get you started with Tkinter programming. After you play with it a bit you'll find that it's not hard to work with. One nice thing is that the TK library may be accessed by many languages other than Python, so what you learn using Python's Tkinter module is mostly transferable to other languages.
- Read the previous installments of Charming Python.
- Check out python.org's Tkinter Resources for a good starting point.
- Several widget collections may save you time in constructing complex UIs. PMW (Python Mega Widgets) is written 100% in Python and is widely used in the Python community.
- Fredrik Lundh has written a good tutorial for Tkinter that contains much more detail than covered here.
- A few printed books are worth checking out. The first is a good intro to TK itself. The second is specific to Python and has many examples that use the PMW collection:
- TCK/TK in a Nutshell, by Paul Raines and Jeff Tranter (O'Reilly, 1999)
- Python and Tkinter Programming by John E. Grayson (Manning, 2000)
- ActiveState has recently created a very nice Python distribution that includes Tkinter and a variety of other handy packages and modules not contained in most other distributions. (They also have an ActivePerl distribution for those inclined towards that other scripting language.)
- Scriptics (home of the maintainers and creators of TK) has been renamed.
- Read these related articles by David Mertz on developerWorks:
- "Charming Python: Curses programming in Python"
- "Charming Python: My first Web-based filtering proxy"
- Take a look at the files and articles mentioned in this article.