Part 6: Graphical user interfaces

GUI programming and Tk

GUI objects

All modern GUI toolkits, including the Tk package used in Python, are based on an object-oriented model of the user interface. Typical object types are windows, entry fields, buttons, text fields, graphics fields, menus, etc. Constructing a GUI means creating all the necessary objects, putting them together correctly, and specifying the actions associated with them.

Event-driven computing

In traditional programs, the program flow is determined by the code. With a GUI, it is the user who determines what happens by activating menus, buttons, etc. This requires a different program structure: a main loop checks for user actions and calls appropriate subroutines that do the work. The main loop is terminated only when the user decides to quit the program.

User interface design

This course covers only the technical aspects of creating a GUI. An equally important question is the design of a user interface, i.e. deciding what to put where and how. There is an extensive literature on this subject, but the most important recommendation is to study well-written programs and try to imitate their behaviour. The most common beginner's mistake is trying to be too innovative. The goal is not to create a GUI that packs the most features into a single window, using tricks specific to the application, but to design a GUI that users can understand quickly, based on their experience with other programs.

Tk and Python

Python does not have a user interface package of its own, but there are Python interfaces to several such packages. The most popular one is Tk, which was originally developed for Tcl, a very simple scripting language. Tk has several advantages: it is easy to use, complete, and available for all major operating systems. However, it is rather closely tied to Tcl, and although the Python interface tries to hide this as much as possible, there remain some features that don't seem to make sense with Python. An interface toolkit designed for Python from the start would probably look different.

Tk's world view

Tk knows about two types of objects: windows and widgets (from "window" and "gadget"). Widgets are the user interface objects that populate windows: buttons, text fields, etc. Widgets are arranged in a hierarchical order; every widget has a master (another widget or, for the outermost widget, a window), its position on the screen and many of its properties depend on its master. Windows are part of a hierarchy as well, but of a different kind: a window's position and content is completely independent of that of its master, but it disappears when its master is closed. The top of the window hierarchy is the root window, which has no master. When the root window is closed, the Tk event main loop is terminated.

A typical Tk application starts by constructing the widget that will be attached to the root window and running its main loop. Widgets specify functions to be called in reaction to events (user actions), and these functions are called by the Tk main loop when the events occur. These functions can do anything at all; there are no restrictions. They can open and close windows and even modify the widgets in existing windows. They can also ask Tk to terminate its main loop.

After a widget has been created, its creator must decide where to put it within its master. Tk provides two strategies for placing widgets: the placer provides a very flexible, but also very complicated, strategy, whereas the packer is simpler to use and does most of the work by itself, at the price of some loss of flexibility. Most Tk programs use only the packer, as will all examples in this course.

A very simple example

The following program is about as simple as a Tk application can get. It opens a window containing only a button. When the button is pressed, a message is printed to the screen. The program is terminated when the window is closed.

from Tkinter import *

def report():
    print "The button has been pressed"

object = Button(None, text = 'A trivial example', command = report)
object.pack()
object.mainloop()

The first statement imports all names from the module Tkinter. Due to the large number of definitions in this module, an individual import is rather inconvenient. Then the event action function is defined, which can be any Python function. The next line creates the button widget. Its first parameter is the widget's master; None indicates the root window. The remaining parameters specify the details of the button: the text it displays, and the command to be executed when the button is pressed. Then the widget's pack() method is called, which indicates that the packer is to be used for arranging the widget. There are no indications on how to arrange the objects, because there is just one widget and thus no choice. Finally, the main loop is started.

Arranging widgets

A Tk window can contain only one widget. Of course one-widget windows are not very useful, so there is a way to construct more complicated ones: the use of frames. A frame is a widget that acts like a subwindow. It is normally invisible (although a visible border can be added), but it contains other widgets that usually aren't. A frame can contain any number of widgets of any kind.

If a frame contains several widgets, there must be some specification of how to arrange them. This information is supplied by optional keyword arguments to the method pack. The simplest specification is side=some_side, with the choice of TOP, BOTTOM, LEFT and RIGHT. The packer then tries to place the widget as close as possible to the indicated side of the frame, within the constraints imposed by other widgets. If, for example, you specify side=TOP for all widgets in a frame, they will be stacked up vertically and centered horizontally, with the first widget for which pack was called at the top.

More complicated constructions can be made by using frames within frames. The packer specifications for any widget affect its arrangement within the surrounding frame, which then has its own packer options for placing it within another frame.

The following example shows the use of frames to construct a window containing four buttons in two centered lines:

from Tkinter import *

def report():
    print "A button has been pressed"

outer_frame = Frame(None)
outer_frame.pack()

first_line = Frame(outer_frame)
first_line.pack(side=TOP)
button1 = Button(first_line, text='Button 1', command=report)
button1.pack(side=LEFT)
button2 = Button(first_line, text='Button 2', command=report)
button2.pack(side=LEFT)

second_line = Frame(outer_frame)
second_line.pack(side=TOP)
button3 = Button(second_line, text='Button 3', command=report)
button3.pack(side=LEFT)
button4 = Button(second_line, text='Button 4', command=report)
button4.pack(side=LEFT)

outer_frame.mainloop()

Note that the first frame has master None, meaning the root window. The inner frames, representing the lines, have the outer frame as master, and the buttons are attached to the inner frames.

Creating your own widgets

In principle, any GUI can be built up like the last example, i.e. by constructing object by object and providing the packer options. However, such a program quickly becomes large and unreadable, and modifying it will be next to impossible. The GUI objects have to be structured in some way, and the usual way is the definition of application-specific higher-level widgets. The example above, for example, would benefit from a "line of buttons" widget.

Widgets are defined by subclassing existing widgets, which can be either standard Tk Widgets or other application-defined widgets. Almost all higher-level widgets are subclasses of Frame. Their initialization method creates all the widgets within the frame and specifies their arrangement. Such widgets can then be used as if they were standard widgets.

With a "button line" widget, the four-button example can be written as follows:

from Tkinter import *

def report():
    print "A button has been pressed"

class ButtonLine(Frame):

    def __init__(self, master, button_data):
	Frame.__init__(self, master)
	for text, command in button_data:
	    Button(self, text=text, command=command).pack(side=LEFT)

outer_frame = Frame(None)
outer_frame.pack()

first_line = ButtonLine(outer_frame, [('Button 1', report),
				      ('Button 2', report)])
first_line.pack(side=TOP)

second_line = ButtonLine(outer_frame, [('Button 3', report),
				       ('Button 4', report)])
second_line.pack(side=TOP)

outer_frame.mainloop()

Tk widgets: an overview

It is impossible to describe all Tk widgets here in detail, but an overview of the widgets and their most common application is a good start for further exploration in the Tk manual.

There are also various image objects to display drawings, photos, etc.

For more information about these widgets, see the Tkinter documentation and the Tk man pages.

A not-so-simple example

The following program displays a small window where the user can type a filename and then either display the text in a window or print it. Instead of typing the filename, a file browser can be used. Here's a screen snapshot showing the main window, a text window, and the file browser:

[screen snapshot]

The program uses three widgets that are not standard Tk widgets, but part of the Python standard library: a "dialog box" for displaying error messages, a file browser, and a text widget with scroll bars. The program also defines two widgets of its own: a "filename entry" widget, consisting of a label, an entry field for the filename, and a button to run the file browser, and a "button bar" that contains a group of left-aligned buttons and a group of right-aligned buttons. It also defines the two full-window widgets by classes, one for the text display window, and one for the main window.

The program uses one packer option that has not been mentioned yet. Normally all widgets have a fixed size, depending on their contents. For some widgets, however, there is no obvious correct size, e.g. for entry fields. Entry fields get an arbitrary size assigned by the packer. The option fill=X tells the packer to extend the widget in the x direction to fill whatever space is available in the widget's master.

Note that the actions linked to the buttons are defines as methods of the widgets. This is necessary to give the actions access to the attributes of the widget, which they often need. In a real Tk program, almost all actions are defined by widget methods.

from Tkinter import *
from Dialog import Dialog
from FileDialog import LoadFileDialog
from ScrolledText import ScrolledText

class FilenameEntry(Frame):

    def __init__(self, master, text):
	Frame.__init__(self, master)
	Label(self, text=text).pack(side=LEFT)
	self.filename = StringVar()
	Entry(self, textvariable=self.filename).pack(side=LEFT, fill=X)
	Button(self, text="Browse...", command=self.browse).pack(side=RIGHT)

    def browse(self):
	file = LoadFileDialog(self).go(pattern='*')
	if file:
	    self.filename.set(file)

    def get(self):
	return self.filename.get()


class ButtonBar(Frame):

    def __init__(self, master, left_button_list, right_button_list):
	Frame.__init__(self, master, bd=2, relief=SUNKEN)
	for button, action in left_button_list:
	    Button(self, text=button, command=action).pack(side=LEFT)
	for button, action in right_button_list:
	    Button(self, text=button, command=action).pack(side=RIGHT)


class FileNotFoundMessage(Dialog):

    def __init__(self, master, filename):
	Dialog.__init__(self, master, title = 'File not found',
			text = 'File ' + filename + ' does not exist',
			bitmap = 'warning', default = 0,
			strings = ('Cancel',))


class TextWindow(Frame):

    def __init__(self, master, text):
	Frame.__init__(self, master)
	text_field = ScrolledText(self)
	text_field.insert(At(0,0), text)
	text_field.pack(side=TOP)
	text_field.config(state=DISABLED)
	ButtonBar(self, [],
		  [('Close', self.master.destroy)]).pack(side=BOTTOM, fill=X)


class MainWindow(Frame):

    def __init__(self, master):
	Frame.__init__(self, master)
	Label(self, text="Enter a filename and " +
                         "select an action").pack(side=TOP)
	self.filename_field = FilenameEntry(self, "Filename: ")
	self.filename_field.pack(side=TOP, fill=X)
	ButtonBar(self, [('Show', self.show), ('Print', self.lpr)],
		  [('Quit', self.quit)]).pack(side=BOTTOM, fill=X)

    def show(self):
	filename = self.filename_field.get()
	try:
	    text = open(filename).read()
	except IOError:
	    FileNotFoundMessage(self, filename)
	else:
	    new_window = Toplevel()
	    new_window.title(filename)
	    TextWindow(new_window, text).pack()

    def lpr(self):
	filename = self.filename_field.get()
	import os
	os.system('lpr ' + filename)


mw = MainWindow(None)
mw.pack()
mw.mainloop()

Learning more about Tk

Tkinter documentation has been insufficient for a long time, and there is still no fully complete documentation available. However, the documentation provided by Pythonware is almost complete and sufficient for most people's needs. Start by reading the introduction and then use the reference as you develop your programs.


Exercises


Table of Contents