CHAPTER 9

Graphical User Interfaces

9.1 Basics of tkinter GUI Development

9.2 Event-Based tkinter Widgets

9.3 Designing GUIs

9.4 OOP for GUIs

9.5 Case Study: Developing a Calculator

Chapter Summary

Solutions to Practice Problems

Exercises

Problems

THIS CHAPTER INTRODUCES graphical user interface (GUI) development.

When you use a computer application—whether it is a web browser, an email client, a computer game, or your Python integrated development environment (IDE)—you typically do so through a GUI, using a mouse and a keyboard. There are two reasons for using a GUI: A GUI gives a better overview of what an application does, and it makes it easier to use the application.

In order to develop GUIs, a developer will require a GUI application programming interface (API) that provides the necessary GUI toolkit. There are several GUI APIs for Python; in this text we use tkinter, a module that is part of Python's Standard Library.

Beyond the development of GUIs using tkinter, this chapter also covers fundamental software development techniques that are naturally used in GUI development. We introduce event-driven programming, an approach for developing applications in which tasks are executed in response to events (such a button clicks). We also learn that GUIs are ideally developed as user-defined classes, and we take the opportunity to once again showcase the benefit of object-oriented programming (OOP).

9.1 Basics of tkinter GUI Development

A graphical user interface (GUI) consists of basic visual building blocks such as buttons, labels, text entry forms, menus, check boxes, and scroll bars, among others, all packed inside a standard window. The building blocks are commonly referred to as widgets. In order to develop GUIs, a developer will require a module that makes such widgets available. We will use the module tkinter that is included in the Standard Library.

In this section, we explain the basics of GUI development using tkinter: how to create a window, how to add text or images to it, and how to manipulate the look and location of widgets.

Widget Tk: The GUI Window

In our first GUI example, we build a bare-bones GUI that consists of a window and nothing else. To do this we import the class Tk from module tkinter and instantiate an object of type Tk:

>>> from tkinter import Tk
>>> root = Tk()

A Tk object is a GUI widget that represents the GUI window; it is created without arguments.

If you execute the preceding code, you will notice that creating a Tk() widget did not get you a window on the screen. To get the window to appear, the Tk method mainloop() needs to be invoked on the widget:

>>> root.mainloop()

You should now see a window like the one in Figure 9.1.

image

Figure 9.1 A tkinter GUI window. The window can be minimized and closed, and looks and feels like any other window in the underlying operating system.

This GUI window is just that: a window and nothing else. To display text or pictures inside this window, we need to use the tkinter widget Label.

Widget Label for Displaying Text

The widget Label can be used to display text inside a window. Let's illustrate its usage by developing a GUI version of the classic “Hello World!” application. To get started, we need to import the class Label in addition to class Tk from tkinter:

>>> from tkinter import Tk, Label
>>> root = Tk()

We then create a Label object that displays the text “Hello GUI world!”:

>>> hello = Label(master = root, text = ‘Hello GUI world!’)

The first argument in this Label constructor, named master, specifies that the Label widget will live inside widget root. A GUI typically contains many widgets organized in a hierarchical fashion. When a widget X is defined to live inside widget Y, widget Y is said to be the master of widget X.

The second argument, named text, refers to the text displayed by the Label widget. The text argument is one of about two dozen optional constructor arguments that specify the look of a Label widget (and of other tkinter widgets as well). We list some of those optional arguments in Table 9.1 and show their usage in this section.

While the Label constructor specifies that the label widget lives inside widget root, it does not specify where in the widget root the label should be placed. There are several ways to specify the geometry of the GUI (i.e., the placement of the widgets inside their master); we discuss them in more detail later in this section. One simple way to specify the placement of a widget inside its master is to invoke method pack() on the widget. The method pack() can take arguments that specify the desired position of the widget inside its master; without any arguments, it will use the default position, which is to place the widget centered and against the top boundary of its master:

>>> hello.pack() # hello is placed against top boundary of master
>>> root.mainloop()

Just as in our first example, the mainloop() method will get the GUI shown in Figure 9.2 started:

image

Figure 9.2 A text label. The Label widget created with the text argument will display a text label. Note that the label is packed against the top boundary of its master, the window itself.

As Table 9.1 illustrates, the text argument is only one of a number of optional widget constructor arguments that define the look of a widget. We showcase some of the other options in the next three GUI examples.

Table 9.1 tkinter widget options. Shown are some of the tkinter widget options that can be used to specify the look of the widget. The values for the options are passed as input arguments to the widget constructor. The options can be used to specify the look of all tkinter widgets, not just widget Label. The usage of the options in this table is illustrated throughout this section.

Option Description
text Text to display
image Image to display
width Width of widget in pixels (for images) or characters (for text); if omitted, size is calculated based on content
height Height of widget in pixels (for images) or characters (for text); if omitted, size is calculated based on content
relief Border style; possibilities are FLAT (default), GROOVE, RAISED, RIDGE, and SUNKEN, all defined in tkinter
borderwidth Width of border, default is 0 (no border)
background Background color name (as a string)
foreground Foreground color name (as a string)
font Font descriptor (as a tuple with font family name, font size, and—optionally—a font style)
padx,pady Padding added to the widget along the x- or y-axis

Displaying Images

A Label widget can be used to display more than just text. To display an image, an argument named image should be used in the Label constructor instead of a text argument. The next example program places a GIF image inside a GUI window. (The example uses file peace.gif, which should be in the same folder as module peace.py.)

File: peace.gif
Module: peace.py
 1  from tkinter import Tk, Label, PhotoImage
 2  root = Tk()                  # the window
 3  # transform GIF image to a format tkinter can display
 4  photo = PhotoImage(file=‘peace.gif’)
 5
 6  peace = Label(master=root,
 7                image=photo,
 8                width=300,     # width of label, in pixels
 9                height=180)    # height of label, in pixels
10  peace.pack()
11  root.mainloop()

The resulting GUI is shown in Figure 9.3. The constructor argument image must refer to an image in a format that tkinter can display. The PhotoImage class, defined in the module tkinter, is used to transform a GIF image into an object with such a format. Arguments width and height specify the width and height of the label in pixels.

image

Figure 9.3 An image label. With the image argument, a Label widget displays an image. Options width and height specify the width and height of the label, in pixels. If the image is smaller than the label, white padding is added around it.

image GIF and Other Image Formats

GIF is just one among many image file formats that have been defined. You are probably familiar with the Joint Photographic Experts Group (JPEG) format used primarily for photographs. Other commonly used image formats include Bitmap Image File (BMP), Portable Document Format (PDF), and Tagged Image File Format (TIFF).

In order to display images in formats other than GIF, the Python Imaging Library (PIL) can be used. It contains classes that load images in one of 30+ formats and convert them to tkinter-compatible image object. The PIL also contains tools for processing images. For more information, go to

www.pythonware.com/products/pil/

Note: At the time of writing, the PIL was not updated to support Python 3.

Packing Widgets

The tkinter geometry manager is responsible for the placement of widgets within their master. If multiple widgets must be laid out, the placement will be computed by the geometry manager using sophisticated layout algorithms (that attempt to ensure that the layout looks good) and using directives given by the programmer. The size of a master widget containing one or more widgets is based on their size and placement. Furthermore, the size and layout will be dynamically adjusted as the GUI window is resized by the user.

The method pack() is one of the three methods that can be used to provide directives to the geometry manager. (We will see another one, method grid(), later in this section.) The directives specify the relative position of widgets within their master.

To illustrate how to use the directives and also to show additional widget constructor options, we develop a GUI with with two image labels and a text label, shown in Figure 9.4:

image

Figure 9.4 Multiple widgets GUI. Three Label widgets are packed inside the GUI window; the peace image is pushed left, the smiley face is pushed right, and the text is pushed down.

The optional argument side of method pack() is used to direct the tkinter geometry manager to push a widget against a particular border of its master. The value of side can be TOP, BOTTOM, LEFT, or RIGHT, which are constants defined in module tkinter; the default value for side is TOP. In the implementation of the preceding GUI, we use the side option to appropriately pack the three widgets:

File: peace.gif,smiley.gif
Module: smileyPeace.py
 1  from tkinter import Tk,Label,PhotoImage,BOTTOM,LEFT,RIGHT,RIDGE
 2  # GUI illustrates widget constructor options and method pack()
 3  root = Tk()
 4
 5  # label with text “Peace begins with a smile.
 6  text = Label(root,
 7              font = (‘Helvetica’, 16, ‘bold italic’),
 8              foreground=‘white’,   # letter color
 9              background=‘black’,   # background color
10              padx=25, # widen label 25 pixels left and right
11              pady=10, # widen label 10 pixels up and down
12              text=‘Peace begins with a smile.’)
13 text.pack(side=BOTTOM)               # push label down
14
15 # label with peace symbol image
16 peace = PhotoImage(file=‘peace.gif’)
17 peaceLabel = Label(root,
18                    borderwidth=3, # label border width
19                    relief=RIDGE,  # label border style
20                    image=peace)
21 peaceLabel.pack(side=LEFT)          # push label left
22 # label with smiley face image
23 smiley = PhotoImage(file=‘smiley.gif’)
24 smileyLabel = Label(root,
25                     image=smiley)
26 smileyLabel.pack(side=RIGHT)        # push label right
27
28 root.mainloop()

Table 9.2 lists two other options for method pack(). The option expand, which can be set to True or False, specifies whether the widget should be allowed to expand to fill any extra space inside the master. If option expand is set to True, option fill can be used to specify whether the expansion should be along the x-axis, the y-axis, or both.

Table 9.2 Some packing options. In addition to option side, method pack() can take options fill and expand.

Option Description
side Specifies the side (using constants TOP, BOTTOM, LEFT, or RIGHT defined in tkinter) the widget will be pushed against; the default is TOP
fill Specifies whether the widget should fill the width or height of the space given to it by the master; options include ‘both’, ‘x’, ‘y’, and ‘none’ (the default)
expand Specifies whether the widget should expand to fill the space given to it; the default is False, no expansion

The GUI program smileyPeace.py also showcases a few widget constructor options we have not seen yet. A RIDGE-style border of width 3 around the peace symbol is specified using options borderwith and relief. Also, the text label (a quote by Mother Theresa) is constructed with options that specify white lettering (option foreground) on a black background (option background) with extra padding of 10 pixels up and down (option pady) and of 25 pixels left and right (option padx). The font option specifies that the text font should be a bold, italic, Helvetica font of size 16 points.

Practice Problem 9.1

Write a program peaceandlove.py that creates this GUI:

File: peace.gif

image

The “Peace & Love” text label should be pushed to the left and have a black background of size to fit 5 rows of 20 characters. If the user expands the window, the label should remain right next to the left border of the window. The peace symbol image label should be pushed to the right. However, when the user expands the window, white padding should fill the space created. The picture shows the GUI after the user manually expanded it.

Forgetting the Geometry Specification image

It's a common mistake to forget to specify the placement of the widgets. A widget appears in a GUI window only after it has been packed in its master. This is achieved by invoking, on the widget, the method pack(), the method grid(), which we discuss shortly, or the method place(), which we do not go over.

Arranging Widgets in a Grid

We now consider a GUI that has more than just a couple of labels. How would you go about developing the phone dial GUI shown in Figure 9.5?

image

Figure 9.5 Phone dial GUI. This GUI's labels are stored in a 4 × 3 grid. Method grid() is more suitable than pack() for placing widgets in a grid. Rows (resp. columns) are indexed top to bottom (resp. left to right) starting from index 0.

We already know how to create each individual phone dial “button” using a Label widget. What is not clear at all is how to get all 12 of them arranged in a grid.

If we need to place several widgets in a gridlike fashion, method grid() is more appropriate than method pack(). When using method grid(), the master widget is split into rows and columns, and each cell of the resulting grid can store a widget. To place a widget in row r and column c, method grid() is invoked on the widget with the row r and column c as input arguments, as shown in this implementation of the phone dial GUI:

Module: phone.py
 1 from tkinter import Tk, Label, RAISED
 2 root = Tk()
 3 labels = [[‘1’, ‘2’, ‘3’],     # phone dial label texts
 4          [‘4’, ‘5’, ‘6’],      # organized in a grid
 5          [‘7’, ‘8’, ‘9’],
 6          [‘*’, ‘0’, ‘#’]]
 7
 8 for r in range(4):      # for every row r = 0, 1, 2, 3
 9     for c in range(3):      # for every row c = 0, 1, 2
10         # create label for row r and column c
11         label = Label(root,
12                       relief=RAISED,       # raised border
13                       padx=10,             # make label wide
14                       text=labels[r][c])   # label text
15    # place label in row r and column c
16    label.grid(row=r, column=c)
17
18 root.mainloop()

In lines 5 through 8, we define a two-dimensional list that stores in row r and column c the text that will be put on the label in row r and column c of the phone dial. Doing this facilitates the creation and proper placement of the labels in the nested for loop in lines 10 through 19. Note the use of the method grid() with row and column input arguments.

Table 9.3 shows some options that can be used with the grid() method.

Table 9.3 Some grid() method options. The columnspan (i.e., rowspan) option is used to place a widget across multiple columns (i.e., rows).

Option Description
column Specifies the column for the widget; default is column 0
columnspan Specifies how many columns the widgets should occupy
row Specifies the row for the widget; default is row 0
rowspan Specifies how many rows the widgets should occupy

image Mixing pack() and grid()

The methods pack() and grid() use different methods to compute the layout of the widgets. Those methods do not work well together, and each will try to optimize the layout in its own way, trying to undo the other algorithm's choices. The result is that the program may never complete execution.

The short story is this: You must use one or the other for all widgets with the same master.

Practice Problem 9.2

Implement function cal() that takes as input a year and a month (a number between 1 and 12) and starts up a GUI that shows the corresponding calendar. For example, the calendar shown is obtained using:

>>> cal(2012, 2)

image

To do this, you will need to compute (1) the day of the week (Monday, Tuesday,…) on which the first day of the month falls and (2) the number of days in the month (taking into account leap years). The function monthrange() defined in the module calendar returns exactly those two values:

>>> from calendar import monthrange
>>> monthrange(2012, 2) # year 2012, month 2 (February)
(2, 29)

The returned value is a tuple. The first value in the tuple, 2, corresponds to Wednesday (Monday is 0, Tuesday is 1, etc.). The second value, 29, is the number of days in February of year 2012, a leap year.

Do You Want to Learn More? image

This chapter is only an introduction to GUI development using tkinter. A comprehensive overview of GUI development and tkinter would fill a whole textbook. If you want to learn more, start with the Python documentation at

http://docs.python.org/py3k/library/tkinter.html

There are also other free, online resources that you can use to learn more. The “official” list of these resources is at

http://wiki.python.org/moin/TkInter

Two particularly useful resources (although they use Python 2) are at

http://www.pythonware.com/library/tkinter/introduction/
http://infohost.nmt.edu/tcc/help/pubs/tkinter/

9.2 Event-Based tkinter Widgets

We now explore the different types of widgets available in tkinter. In particular, we study those widgets that respond to mouse clicks and keyboard inputs by the user. Such widgets have an interactive behavior that needs to be programmed using a style of programming called event-driven programming. In addition to GUI development, event-driven programming is also used in the development of computer games and distributed client/server applications, among others.

Button Widget and Event Handlers

Let's start with the classic button widget. The class Button from module tkinter represents GUI buttons. To illustrate its usage, we develop a simple GUI application, shown in Figure 9.6, that contains just one button.

image

Figure 9.6 GUI with one Button widget. The text “Click it” is displayed on top of the button. When the button is clicked, the day and time information is printed.

The application works in this way: When you press the button “Click it”, the day and time of the button click is printed in the interpreter shell:

>>>
Day: 07 Jul 2011
Time: 23:42:47 PM

You can click the button again (and again) if you like:

>>>
Day: 07 Jul 2011
Time: 23:42:47 PM

Day: 07 Jul 2011
Time: 23:42:50 PM

Let's implement this GUI. To construct a button widget, we use the Button constructor. Just as for the Label constructor, the first argument of the Button constructor must refer to the button's master. To specify the text that will be displayed on top of the button, the text argument is used, again just as for a Label widget. In fact, all the options for customizing widgets shown in Table 9.1 can be used for Button widgets as well.

The one difference between a button and a label is that a button is an interactive widget. Every time a button is clicked, an action is performed. This “action” is actually implemented as a function, which gets called every time the button is clicked. We can specify the name of this function using a command option in the Button constructor. Here is how we would create the button widget for the GUI just shown:

root = Tk()
button = Button(root, text=‘Click it’, command=clicked)

When the button is clicked, the function clicked() will be executed. Now we need to implement this function. When called, the function should print the current day and time information. We use the module time, covered in Chapter 4, to obtain and print the local time. The complete GUI program is then:

Module: clickit.py
 1  from tkinter import Tk, Button
 2  from time import strftime, localtime
 3
 4 def clicked():
 5     ‘prints day and time info’
 6     time = strftime(‘Day: %d %b %Y
Time: %H:%M:%S %p
’,
 7                     localtime())
 8     print(time)
 9
10 root = Tk()
11
12 # create button labeled ‘Click it’ and event handler clicked()
13 button = Button(root,
14                 text=‘Click it’,   # text on top of button
15                 command=clicked)   # button click event handler
16 button.pack()
17 root.mainloop()

The function clicked() is said to be an event handler; what it handles is the event of the button “Click it” being clicked.

In the first implementation of clicked(), the day and time information is printed in the shell. Suppose we prefer to print the message in its own little GUI window, as shown in Figure 9.7, instead of the shell.

image

Figure 9.7 Window showinfo(). The function showinfo() from module tkinter.messagebox displays a message in a separate window. Clicking the “OK” button makes the window disappear.

In module tkinter.messagebox, there is a function named showinfo that prints a string in a separate window. So, we can just replace the original function clicked() with:

Module: clickit.py
1  from tkinter.messagebox import showinfo
2
3  def clicked():
4      ‘prints day and time info’
5      time = strftime(‘Day: %d %b %Y
Time: %H:%M:%S %p
’,
6                      localtime())
7      showinfo(message=time)

Practice Problem 9.3

Implement a GUI app that contains two buttons labeled “Local time” and “Greenwich time”. When the first button is pressed, the local time should be printed in the shell. When the second button is pressed, the Greenwich Mean Time should be printed.

>>>
Local time
Day: 08 Jul 2011
Time: 13:19:43 PM

Greenwich time
Day: 08 Jul 2011
Time: 18:19:46 PM

You can obtain the current Greenwich Mean Time using the function gmtime() from module time.

Events, Event Handlers, and mainloop()

Having seen the workings of the interactive Button widget, it is now a good time to explain how a GUI processes user-generated events, such as button clicks. When a GUI is started with the mainloop() method call, Python starts an infinite loop called an event loop. The event loop is best described using pseudocode:

while True:
   wait for a an event to occur
   run the associated event handler function

In other words, at any point in time, the GUI is waiting for an event. When an event such as a button click occurs, the GUI executes the function that is specified to handle the event. When the handler terminates, the GUI goes back to waiting for the next event.

A button click is just one type of event that can occur in a GUI. Movements of the mouse and pressing keys on the keyboard in an entry field also generate events the can be handled by the GUI. We see examples of this later in this section.

image Short History of GUIs

The first computer system with a GUI was the Xerox Alto computer developed in 1973 by researchers at Xerox PARC (Palo Alto Research Center) in Palo Alto, California. Founded in 1970 as a research and development division of Xerox Corporation, Xerox PARC was responsible for developing many now-common computer technologies, such as laser printing, Ethernet, and the modern personal computer, in addition to GUIs.

The Xerox Alto GUI was inspired by the text-based hyperlinks clickable with a mouse in the On-Line System developed by researchers at Stanford Research Institute International in Menlo Park, California, led by Douglas Engelbart. The Xerox Alto GUI included graphical elements such as windows, menus, radio buttons, check boxes, and icons, all manipulated using a mouse and a keyboard.

In 1979, Apple Computer's cofounder Steve Jobs visited Xerox PARC, where he learned of the mouse-controlled GUI of the Xerox Alto. He promptly integrated it, first into the Apple Lisa in 1983 and then in the Macintosh in 1984. Since then, all major operating systems have supported GUIs.

The Entry Widget

In our next GUI example, we introduce the Entry widget class. It represents the classic, single-line text box you would find in a form. The GUI app we want to build asks the user to enter a date and then computes the weekday corresponding to it. The GUI should look as shown in Figure 9.8:

image

Figure 9.8 Weekday application. The app requests the user to type a data in the format MMM DD, YYY, as in “Jan 21, 1967”.

After the user types “Jan 21, 1967” in the entry box and clicks the button “Enter”, a new window, shown in Figure 9.9, should pop up:

image

Figure 9.9 Pop-up window of the weekday app. When the user enters the date and presses button “Enter”, the weekday corresponding to the date is shown in the pop-up window.

It is clear that the GUI should have a Label and a Button widget. For a text entry box, we need to use the Entry widget defined in tkinter. The Entry widget is appropriate for entering (and displaying) a single line of text. The user can enter text inside the widget using the keyboard. We can now start the implementation of the GUI:

Module: day.py
 1  # import statements and
 2  # event handler compute() that computes and displays the weekday
 3
 4  root = Tk()
 5
 6  # label
 7  label = Label(root, text=‘Enter date’)
 8  label.grid(row=0, column=0)
 9
10  # entry
11  dateEnt = Entry(root)
12  dateEnt.grid(row=0, column=1)
13
14  # button
15  button = Button(root, text=‘Enter’, command=compute)
16  button.grid(row=1, column=0, columnspan=2)
17
18  root.mainloop()

In line 13, we create an Entry widget. Note that we are using method grid() to place the three widgets. The only thing left to do is to implement the event-handling function compute(). Let's first describe what this function needs to do:

  1. Read the date from the entry dateEnt.
  2. Compute the weekday corresponding to the date.
  3. Display the weekday message in a pop-up window.
  4. Erase the date from entry dateEnt.

The last step is a nice touch: We delete the date just typed in to make it easier to enter a new date.

To read the string that is inside an Entry widget, we can use the Entry method get(). It returns the string that is inside the entry. To delete the string inside an Entry widget, we need to use the Entry method delete(). In general, it is used to delete a substring of the string inside the Entry widget. Therefore, it takes two indexes first and last and deletes the substring starting at index first and ending before index last. Indexes 0 and END (a constant defined in tkinter) are used to delete the whole string inside an entry. Table 9.4 shows the usage of these and other Entry methods.

Table 9.4 Some Entry methods. Listed are three core methods of class Entry. The constant END is defined in tkinter and refers to the index past the last character in the entry.

Method Description
e.get() Returns the string inside the entry e
e.insert(index, text) Inserts text into entry e at the given index; if index is END, it appends the string
e.delete(from, to) Deletes the substring in entry e from index from up to and not including index to; delete(0, END) deletes all the text in the entry

Armed with the method of the Entry widget class, we can now implement the event-handling function compute():

Module: day.py
 1  from tkinter import Tk, Button, Entry, Label, END
 2  from time import strptime, strftime
 3  from tkinter.messagebox import showinfo
 4
 5  def compute():
 6      ‘‘‘display day of the week corresponding to date in dateEnt;
 7         date must have format MMM DD, YYY (e.g., Jan 21, 1967)’’’
 8
 9      global dateEnt # dateEnt is a global variable
10
11      # read date from entry dateEnt
12      date = dateEnt.get()
13
14      # compute weekday corresponding to date
15      weekday = strftime(‘%A’, strptime(date, ‘%b %d, %Y’))
16
17      # display the weekday in a pop-up window
18      showinfo(message = ‘{} was a {}’.format(date, weekday))
19
20      # delete date from entry dateEnt
21      dateEnt.delete(0, END)
22
23  # rest of program

In line 9, we specify that dateEnt is a global variable. While that is not strictly necessary (we are not assigning to dateEnt inside function compute()), it is a warning so the programmer maintaining the code is aware that dateEnt is not a local variable.

In line 15, we use two functions from module time to compute the weekday corresponding to a date. Function strptime() takes as input a string containing a date (date) and a format string (‘%b %d, %Y’), which uses directives from Table 4.6. The function returns the date in an object of type time.struct_time. Recall from the case study in Chapter 4 that function strftime() takes such an object and a format string (‘%A’) and returns the date formatted according to the format string. Since the format string contains only the directive %A that specifies the date weekday, only the weekday is returned.

Practice Problem 9.4

Implement a variation of GUI program day.py called day2.py. Instead of displaying the weekday message in a separate pop-up window, insert it in front of the date in the entry box, as shown. Also add a button labeled “Clear” that erases the entry box.

image

Text Widget and Binding Events

We introduce next the Text widget, which is used to interactively enter multiple lines of text in a way similar to entering text in a text editor. The Text widget class supports the same methods get(), insert(), and delete() that class Entry does, albeit in a different format (see Table 9.5).

Table 9.5 Some Text methods. Unlike indexes used for Entry methods, indexes used in Text methods are of the form row.column (e.g., index 2.3 refers to the fourth character in the third row).

Method Description
t.insert(index, text) Insert text into Text widget t before index
t.get(from, to) Return the substring in Text widget t from index from up to but not including index to
t.delete(from, to) Delete the substring in Text widget t between index from up to but not including index to

We use a Text widget to develop an application that looks like a text editor, but “secretly” records and prints every keystroke the user types in the Text widget. For example, suppose you were to type the sentence shown in Figure 9.10:

image

Figure 9.10 Key logger application. The key logger GUI consists of a Text widget. When the user types text inside the text box, the keystrokes are recorded and printed in the shell.

This would be printed in the shell:

>>>
char = Shift_L
char = T
char = o
char = p
char = space
char = s
char = e
char = c
char = r
char = e
char = t
…

(We omit the rest of the characters.) This application is often referred to as a keylogger.

We now develop this GUI app. To create a Text widget big enough to contain five rows of 20 characters, we use the width and height widget constructor options:

from tkinter import Text
t = Text(root, width=20, height=5)

In order to record every keystroke when we type inside the Text widget text, we need to somehow associate an event-handling function with keystrokes. We achieve this with the bind() method, whose purpose is to “bind” (or associate) an event type to an event handler. For example, the statement

text.bind(‘<KeyPress >’, record)

binds a keystroke, an event type described with string ‘<KeyPress>’, to the event handler record().

In order to complete the keylogger application, we need to learn a bit more about event patterns and the tkinter Event class.

Event Patterns and the tkinter Class Event

In general, the first argument of the bind() method is the type of event we want to bind. The type of event is described by a string that is the concatenation of one or more event patterns. An event pattern has the form

<modifier-modifier-type-detail>

Table 9.6 shows some possible values for the modifier, type, and detail. For our keylogger application, the event pattern will consist of just a type, KeyPress. Here are some other examples of event patterns and associated target events:

  • <Control-Button-1>: Hitting image and the left mouse button simultaneously
  • <Button-1><Button-3>: Clicking the left mouse button and then the right one
  • <KeyPress-D><Return>: Hitting the keyboard key image and then image
  • <Buttons1-Motion>: Mouse motion while holding left mouse button

The second argument to method bind() is the event-handling function. This function must be defined by the developer to take exactly one argument, an object of type Event. The class Event is defined in tkinter. When an event (like a key press) occurs, the Python interpreter will create an object of type Event associated with the event and call the event-handling function with the Event object passed as the single argument.

An Event object has many attributes that store information about the event that caused its instantiation. For a key press event, for example, the Python interpreter will create an Event object and assign the pressed key symbol and (Unicode) number to attributes keysym and keysum_num.

Table 9.6 Some event pattern modifiers, types, and details. An event pattern is a string, delimited by symbols < and > consisting of up to two modifiers, one type, and up to one detail, in that order.

Modifier Description
Control image key
Button1 Left mouse button
Button3 Right mouse button
Shift image key
Type
Button Mouse button
Return image key
KeyPress Press of a keyboard key
KeyRelease Release of a keyboard key
Motion Mouse motion
Detail
<button number> 1, 2, or 3 for left, middle, and right button, respectively
<key symbol> Key letter symbol

Therefore, in our keyLogger application, the event-handling function record() should take this Event object as input, read the key symbol and number information stored in it, and display them in the shell. This will achieve the desired behavior of continuously displaying the keystrokes made by the GUI user.

Module: keyLogger.py
 1  from tkinter import Tk, Text, BOTH
 2
 3  def record(event):
 4      ‘‘‘event handling function for key press event;
 5         input event is of type tkinter.Event’’’
 6      print(‘char = {}’.format(event.keysym)) # print key symbol
 7
 8  root = Tk()
 9
10  text = Text(root,
11              width=20, # set width to 20 characters
12              height=5) # set height to 5 rows of characters
13
14  # Bind a key press event with the event handling function record()
15  text.bind(‘<KeyPress >’, record)
16
17  # widget expands if the master does
18  text.pack(expand = True, fill = BOTH)
19
20  root.mainloop()

Other Event object attributes are set by the Python interpreter, depending on the type of event. Table 9.7 shows some of the attributes. The table also shows, for each attribute, the type of event that will cause it to be defined. For example, the num attribute will be defined by a ButtonPress event, but not by a KeyPress or KeyRelease event.

Table 9.7 Some Event attributes. Afewof attributes of class Event are shown. The type of event that causes the attribute to be defined is also shown. All event types will set the time attribute, for example.

image

Practice Problem 9.5

In the original day.py program, the user has to click button “Enter” after typing a date in the entry box. Requiring the user to use the mouse right after typing his name using the keyboard is an inconvenience. Modify the program day.py to allow the user just to press the image keyboard key instead of clicking the button “Enter”.

image Event-Handling Functions

There are two distinct types of event-handling functions in tkinter. A function buttonHandler() that handles clicks on a Button widget is one type:

Button(root, text=‘example’, command=buttonHandler)

Function buttonhandler() must be defined to take no input arguments.

A function eventHandler() that handles an event type is:

widget.bind(‘<event type>’, eventHandler)

Function eventHandler() must be defined to take exactly one input argument that is of type Event.

9.3 Designing GUIs

In this section, we continue to introduce new types of interactive widgets. We discuss how to design GUIs that keep track of some values that are read or modified by event handlers. We also illustrate how to design GUIs that contain multiple widgets in a hierarchical fashion.

Widget Canvas

The Canvas widget is a fun widget that can display drawings consisting of lines and geometrical objects. You can think of it as a primitive version of turtle graphics. (In fact, turtle graphics is essentially a tkinter GUI.)

We illustrate the Canvas widget by building a very simple pen drawing application. The application consists of an initially empty canvas. The user can draw curves inside the canvas using the mouse. Pressing the left mouse button starts the drawing of the curve. Mouse motion while pressing the button moves the pen and draws the curve. The curve is complete when the button is released. A scribble done using this application is shown in Figure 9.11.

image

Figure 9.11 Pen drawing app. This GUI implements a pen drawing application. A left mouse button press starts the curve. You then draw the curve by moving the mouse while pressing the left mouse button. The drawing stops when the button is released.

We get started by first creating a Canvas widget of size 100 × 100 pixels. Since the drawing of the curve is to be started by pressing the left mouse button, we will need to bind the event type <Button-1> to an event-handling function. Furthermore, since mouse motion while holding down the left mouse button draws the curve, we will also need to bind the event type <Button1-Motion> to another event-handling function.

This is what we have so far:

Module: draw.py
 1 from tkinter import Tk, Canvas
 2
 3  # event handlers begin and draw here
 4
 5  root = Tk()
 6
 7  oldx, oldy = 0, 0 # mouse coordinates (global variables)
 8
 9  # canvas
10  canvas = Canvas(root, height=100, width=150)
11
12  # bind left mouse button click event to function begin()
13  canvas.bind(”<Button - 1>”, begin)
14
15  # bind mouse motion while pressing left button event
16  canvas.bind(”<Button1 - Motion>”, draw)
17
18  canvas.pack()
19  root.mainloop()

We now need to implement the handlers begin() and draw() that will actually draw the curve. Let's discuss the implementation of draw() first. Every time the mouse is moved while pressing the left mouse button, the handler draw() is called with an input argument that is an Event object storing the new mouse position. To continue drawing the curve, all we need to do is connect this new mouse position to the previous one with a straight line. The curve that is displayed will effectively be a sequence of very short straight line segments connecting successive mouse positions.

The Canvas method create_line() can be used to draw a straight line between points. In its general form, it takes as input a sequence of (x, y) coordinates (x1, y1, x2, y2,…, xn, yn) and draws a line segment from point (x1, y1) to point (x2, y2), another one from point (x2, y2) to point (x3, y3), and so on. So, to connect the old mouse position at coordinates (oldx, oldy) to the new one at coordinates (newx, newy), we just need to execute:

canvas.create_line(oldx, oldy, newx, newy)

The curve is thus drawn by repeatedly connecting the new mouse position to the old (previous) mouse position. This means that there must be an “initial” old mouse position (i.e., the start of the curve). This position is set by the event handler begin() called when the left mouse button is pressed:

Module: draw.py
1  def begin(event):
2      ‘initializes the start of the curve to mouse position’
3
4      global oldx, oldy
5      oldx, oldy = event.x, event.y

In handler begin(), the variables oldx and oldy receive the coordinates of the mouse when the left mouse button is pressed. These global variables will be constantly updated inside handler draw() to keep track of the last recorded mouse position as the curve is drawn. We can now implement event handler draw():

Module: draw.py
1 def draw(event):
2     ‘draws a line segment from old mouse position to new one’
3     global oldx, oldy, canvas      # x and y will be modified
4     newx, newy = event.x, event.y  # new mouse position
5
6     # connect previous mouse position to current one
7     canvas.create_line(oldx, oldy, newx, newy)
8
9     oldx, oldy = newx, newy        # new position becomes previous

Before we move on, we list in Table 9.8 some methods supported by widget Canvas.

Table 9.8 Some Canvas methods. Only a few methods of tkinter widget class Canvas are listed. Every object drawn in the canvas has a unique ID (which happens to be an integer).

Method Description
create_line(x1, y1, x2, y2,…) Creates line segments connecting points (x1, y1), (x2, y2),…; returns the ID of the item constructed
create_rectangle(x1, y1, x2, y2) Creates a rectangle with vertexes at (x1, y1) and (x2, y2); returns the ID of the item constructed
create_oval(x1, y1, x2, y2) Creates an oval that is bounded by a rectangle with vertexes at (x1, y1) and (x2, y2); returns the ID of the item constructed
delete(ID) Deletes item identified with ID
move(item, dx, dy) Moves item right dx units and down dy units

image Storing State in a Global Variable

In program draw.py, the variables oldx and oldy store the coordinates of the mouse's last position. These variables are initially set by function begin() and then updated by function draw(). Therefore the variables oldx, oldy cannot be local variables to either function and have to be defined as global variables.

The use of global variables is problematic because the scope of global variables is the whole module. The larger the module and the more names it contains, the more likely it is that we inadvertently define a name twice in the module. This is even more likely when variables, functions, and classes are imported from another module. If a name is defined multiple times, all but one definition will be discarded, which then typically results in very strange bugs.

In the next section, we learn how to develop GUIs as new widget classes using OOP techniques. One of the benefits is that we will be able to store the GUI state in instance variables rather than in global variables.

Practice Problem 9.6

Implement program draw2.py, a modification of draw.py that supports deletion of the last curve drawn on the canvas by pressing image and the left mouse button simultaneously. In order to do this, you will need to delete all the short line segments created by create_line() that make up the last curve. This in turn means that you must store all the segments forming the last curve in some type of container.

Widget Frame as an Organizing Widget

We now introduce the Frame widget, an important widget whose primary purpose is to serve as the master of other widgets and facilitate the specification of the geometry of a GUI. We make use of it in another graphics GUI we call plotter shown in Figure 9.12. The plotter GUI allows the user to draw by moving a pen horizontally or vertically using the buttons to the right of the canvas. A button click should move the pen 10 pixels in the direction indicated on the button.

image

Figure 9.12 Plotter App. This GUI presents a canvas and four buttons controlling the pen moves. Each button will move the pen 10 units in the indicated direction.

It is clear that the plotter GUI consists of a Canvas widget and four Button widgets. What is less clear is how to specify the geometry of the widgets inside their master (i.e., the window itself). Neither the pack() method nor the grid() method can be used to pack the canvas and button widgets directly in the window so that they are displayed as shown in Figure 9.12.

To simplify the geometry specification, we can use a Frame widget whose sole purpose is to be the master of the four button widgets. The hierarchical packing of the widgets is then achieved in two steps. The first step is to pack the four button widgets into their Frame master using method grid(). Then we simply pack the Canvas and the Frame widgets next to each other.

Module: plotter.py
 1 from tkinter import Tk, Canvas, Frame, Button, SUNKEN, LEFT, RIGHT
 2
 3 # event handlers up(), down(), left(), and right()
 4
 5 root = Tk()
 6
 7  # canvas with border of size 100 x 150
 8 canvas = Canvas(root, height=100, width=150,
 9                 relief=SUNKEN, borderwidth=3)
10 canvas.pack(side=LEFT)
11

12 # frame to hold the 4 buttons
13 box = Frame(root)
14 box.pack(side=RIGHT)
15
16 # the 4 button widgets have Frame widget box as their master
17 button = Button(box, text=‘up’, command=up)
18 button.grid(row=0, column=0, columnspan=2)
19 button = Button(box, text=‘left’,command=left)
20 button.grid(row=1, column=0)
21 button = Button(box, text=‘right’, command=right)
22 button.grid(row=1, column=1)
23 button = Button(box, text=‘down’, command=down)
24 button.grid(row=2, column=0, columnspan=2)
25
26 x, y = 50, 75 # pen position, initially in the middle
27
28 root.mainloop()

The four button event handlers are supposed to move the pen in the appropriate direction. We only show the handler for the up button, leaving the implementation of the remaining three handlers as an exercise:

Module: plotter.py
1 def up():
2     ‘move pen up 10 pixels’
3     global y, canvas                  # y is modified
4     canvas.create_line(x, y, x, y-10)
5     y -= 10

image Why Does the y Coordinate Decrease When Moving Up?

The function up() is supposed to move the pen at position (x, y) up by 10 units. In a typical coordinate system, that means that y should be increased by 10 units. Instead, the value of y is decreased by 10 units.

The reason for this is that coordinate system in a canvas is not quite the same as the coordinate system we are used to. The origin, that is, the position at coordinates (0,0), is at the top left corner of the canvas. The x coordinates increase to the right and the y coordinates increase to the bottom of the canvas. Therefore, moving up means decreasing the y coordinate, which is what we do in function up().

While peculiar, the Canvas coordinate system follows the screen coordinate system. Every pixel on your screen has coordinates defined with respect to the upper left corner of the screen, which has coordinates (0,0). Why does the screen coordinate system use such a system?

It has to do with the order in which pixels are refreshed in a television set, the precursor of the computer monitor. The top line of pixels is refreshed first from left to right, and then the second, third, and so on.

Practice Problem 9.7

Complete the implementation of functions down(), left(), and right() in program plotter.py.

9.4 OOP for GUIs

So far in this chapter, the focus of our presentation has been on understanding how to use tkinter widgets. We developed GUI applications to illustrate the usage of the widgets. To keep matters simple, we have not concerned ourselves about whether our GUI apps can easily be reused.

To make a GUI app or any program reusable, it should be developed as a component (a function or a class) that encapsulates all the implementation details and all the references to data (and widgets) defined in the program. In this section, we introduce the OOP approach to designing GUIs. This approach will make our GUI applications far easier to reuse.

GUI OOP Basics

In order to illustrate the OOP approach to GUI development, we reimplement the application clickit.py. This application presents a GUI with a single button; when clicked, a window pops up and displays the current time. Here is our original code (with the import statements and comments removed so we can focus on the program structure):

Module: clickit.py
 1 def clicked():
 2     ‘prints day and time info’
 3     time = strftime(‘Day:  %d %b %Y
Time: %H:%M:%S %p
’,
 4                     localtime())
 5     showinfo(message=time)
 6
 7 root = Tk()
 8 button = Button(root,
 9                 text=‘Click it’,
10                 command=clicked)  # button click event handler
11 button.pack()
12 root.mainloop()

This program has a few undesirable properties. The names button and clicked have global scope. (We ignore the window widget root as it is really “outside of the application,” as we will see soon.) Also, the program is not encapsulated into a single named component (function or class) that can be cleanly referred to and incorporated into a larger GUI.

The key idea of the OOP approach to GUI development is to develop the GUI app as a new, user-defined widget class. Widgets are complicated beasts, and it would be an overwhelming task to implement a widget class from scratch. To the rescue comes OOP inheritance. We can ensure that our new class is a widget class simply by having it inherit attributes from an existing widget class. Because our new class has to contain another widget (the button), it should inherit from a widget class that can contain other widgets (i.e., the Frame class).

The reimplementation of the GUI clickit.py therefore consists of defining a new class, say ClickIt, that is a subclass of Frame. A ClickIt widget should contain inside of it just one button widget. Since the button must be part of the GUI from the GUI start-up, it will need to be created and packed at the time the ClickIt widget is instantiated. This means the the button widget must be created and packed in the ClickIt constructor.

Now, what will be the master of the button? Since the button should be contained in the instantiated ClickIt widget, its master is the widget itself (self).

Finally, recall that we have always specified a master when creating a widget. We also should be able to specify the master of a ClickIt widget, so we can create the GUI in this way:

>>> root = Tk()
>>> clickit = Clickit(root)  # create ClickIt widget inside root
>>> clickit.pack()
>>> root.mainloop()

Therefore, the ClickIt constructor should be defined to take one argument, its master widget. (By the way, this code shows why we chose not to encapsulate the window widget root inside the class ClickIt.)

With all the insights we have just made, we can start our implementation of the ClickIt widget class, in particular its constructor:

Module: ch9.py
 1 from tkinter import Button, Frame
 2 from tkinter.messagebox import showinfo
 3 from time import strftime, localtime
 4
 5 class ClickIt(Frame):
 6     ‘GUI that shows current time’
 7
 8     def _ _init_ _(self, master):
 9         ‘constructor’
10         Frame._ _init_ _(self, master)
11         self.pack()
12         button = Button(self, button.pack()
13                         text=‘Click it’,
14                         command=self.clicked)
15         button.pack()
16
17  # event handling function clicked()

There are three things to note about the constructor _ _init_ _(). First note in line 10 that the ClickIt _ _init_ _() constructor extends the Frame _ _init_ _() constructor. There are two reasons why we are doing that:

  1. We want the ClickIt widget to get initialized just like a Frame widget so it is a full-fledged Frame widget.
  2. We want the ClickIt widget to be assigned a master the same way any Frame widget is assigned a master; we thus pass the master input argument of the ClickIt constructor to the Frame constructor.

The next thing to note is that button is not a global variable, as it was in the original program clickit.py. It is simply a local variable, and it cannot affect names defined in the program that uses class ClickIt. Finally note that we defined the button event handler to be self.clicked, which means that clicked() is a method of class ClickIt. Here is its implementation:

Module: ch9.py
1       def clicked(self):
2           ‘prints day and time info’
3           time = strftime(‘Day: %d %b %Y
Time: %H:%M:%S %p
’,
4                           localtime())
5           showinfo(message=time)

Because it is a class method, the name clicked is not global, as it was in the original program clickit.py.

The class ClickIt therefore encapsulates the code and the names clicked and button. This means that neither of these names is visible to a program that uses a ClickIt widget, which relieves the developer from worrying about whether names in the program will clash with them. Furthermore, the developer will find it extremely easy to use and incorporate a ClickIt widget in a larger GUI. For example, the next code incorporates the ClickIt widget in a window and starts the GUI:

>>> root = Tk()
>>> app = Clickit(root)
>>> app.pack()
>>> root.mainloop()

Shared Widgets Are Assigned to Instance Variables

In our next example, we reimplement the GUI application day.py as a class. We use it to illustrate when to give widgets instance variable names. The original program day.py (again without import statements or comments) is:

Module: day.py
 1 def compute():
 2     global dateEnt    # dateEnt is a global variable
 3
 4     date = dateEnt.get()
 5     weekday = strftime(‘%A’, strptime(date, ‘%b %d, %Y’))
 6     showinfo(message = ‘{} was a {}’.format(date, weekday))
 7     dateEnt.delete(0, END)
 8
 9 root = Tk()
10
11 label = Label(root, text=‘Enter date’)
12 label.grid(row=0, column=0)
13
14 dateEnt = Entry(root)
15 dateEnt.grid(row=0, column=1)
16
17 button = Button(root, text=‘Enter’, command=compute)
18 button.grid(row=1, column=0, columnspan=2)
19
20 root.mainloop()

In this implementation, names compute, label, dateEnt, and button have global scope. We reimplement the application as a class called Day that will encapsulate those names and the code.

The Day constructor should be responsible for creating the label, entry, and button widgets, just as the ClickIt constructor was responsible for creating the button widget. There is one difference, though: The entry dateEnt is referred to in the event handler compute(). Because of that, dateEnt cannot just be a local variable of the Day constructor. Instead, we make it an instance variable that can be referred from the event handler:

Module: ch9.py
1 from tkinter import Tk, Button, Entry, Label, END
2 from time import strptime, strftime
3 from tkinter.messagebox import showinfo
4
5 class Day(Frame):
6     ‘an application that computes weekday corresponding to a date’
7
8     def _ _init_ _(self, master):
9         Frame._ _init_ _(self, master)
10        self.pack()
11
12        label = Label(self, text=‘Enter date’)
13        label.grid(row=0, column=0)
14
15        self.dateEnt = Entry(self)              # instance variable
16        self.dateEnt.grid(row=0, column=1)
17
18        button = Button(self, text=‘Enter’,
19                        command=self.compute)
20        button.grid(row=1, column=0, columnspan=2)
21
22 def compute(self):
23     ‘‘‘display weekday corresponding to date in dateEnt; date
24        must have format MMM DD, YYY (e.g., Jan 21, 1967)’’’
25     date = self.dateEnt.get()
26     weekday = strftime(‘%A’, strptime(date, ‘%b %d, %Y’))
27     showinfo(message = ‘{} was a {}’.format(date, weekday))
28     self.dateEnt.delete(0, END)

The Label and Button widgets do not need to be assigned to instance variables because they are never referenced by the event handler. They are simply given names that are local to the constructor. The event handler compute() is a class method just like clicked() in ClickIt. In fact, event handlers should always be class methods in a user-defined widget class.

The class Day therefore encapsulates the four names that were global in program day.py. Just as for the ClickIt class, it becomes very easy to incorporate a Day widget into a GUI. To make our point, let's run is a GUI that incorporates both:

>>> root = Tk()
>>> day = Day(root)
>>> day.pack()

image

Figure 9.13 Two user-defined widgets in a GUI. A user-defined widget class can be used just like a built-in widget class.

>>> clickit = ClickIt(root)
>>> clickit.pack()
>>> root.mainloop()

Figure 9.13 shows the resulting GUI, with a Day widget above a ClickIt widget.

Practice Problem 9.8

Reimplement the GUI application keylogger.py as a new, user-defined widget class. You will need to decide whether it is necessary to assign the Text widget contained in this GUI to an instance variable or not.

Shared Data Are Assigned to Instance Variables

To further showcase the encapsulation benefit of implementing a GUI as a user-defined widget class, we reimplement the GUI application draw.py. Recall that this application provides a canvas that the user can draw on using the mouse. The original implementation is this:

Module: draw.py
 1 from tkinter import Tk, Canvas
 2
 3 def begin(event):
 4     ‘initializes the start of the curve to mouse position’
 5     global oldx, oldy
 6     oldx, oldy = event.x, event.y
 7
 8 def draw(event):
 9     ‘draws a line segment from old mouse position to new one’
10     global oldx, oldy, canvas      # x and y will be modified
11     newx, newy = event.x, event.y  # new mouse position
12     canvas.create_line(oldx, oldy, newx, newy)
13     oldx, oldy = newx, newy    # new position becomes previous
14
15 root = Tk()
16
17 oldx, oldy = 0, 0   # mouse coordinates (global variables)
18
19 canvas = Canvas(root, height=100, width=150)
20 canvas.bind(”<Button - 1>”, begin)
21 canvas.bind(”<Button1 - Motion>”, draw)
22 canvas.pack()
23
24 root.mainloop()

In the original implementation draw.py, we needed to use global variables oldx and oldy to keep track of the mouse position. This was because event handlers begin() and draw() referred to them. In the reimplementation as a new widget class, we can store the mouse coordinates in instance variables instead.

Similarly, because canvas is referred to by event handler draw(), we must make it an instance variable as well:

Module: ch9.py
 1 from tkinter import Canvas, Frame, BOTH
 2 class Draw(Frame):
 3     ‘a basic drawing application’
 4
 5     def _ _init_ _(self, parent):
 6         Frame._ _init_ _(self, parent)
 7         self.pack()
 8
 9         # mouse coordinates are instance variables
10         self.oldx, self.oldy = 0, 0
11
12         # create canvas and bind mouse events to handlers
13         self.canvas = Canvas(self, height=100, width=150)
14         self.canvas.bind(”<Button - 1>”, self.begin)
15         self.canvas.bind(”<Button1 - Motion>”, self.draw)
16         self.canvas.pack(expand=True, fill=BOTH)
17
18    def begin(self,event):
19        ‘handles left button click by recording mouse position’
20        self.oldx, self.oldy = event.x, event.y
21
22    def draw(self, event):
23        ‘‘‘handles mouse motion, while pressing left button, by
24           connecting previous mouse position to the new one’’’
25        newx, newy = event.x, event.y
26        self.canvas.create_line(self.oldx, self.oldy, newx, newy)
27        self.oldx, self.oldy = newx, newy

Practice Problem 9.9

Reimplement the plotter GUI application as a user-defined widget class that encapsulates the state of the plotter (i.e., the pen position). Think carefully about which widgets need to be assigned to instance variables.

9.5 Case Study: Developing a Calculator

In this chapter's case study, we implement a basic calculator GUI, shown in Figure 9.14. We use OOP techniques to implement it as a user-defined widget class, from scratch. In the process, we explain how to write a single event-handling function that handles many different buttons.

image

Figure 9.14 GUI Calc. A calculator application with the usual four operators, a square root and a square function, and a memory capability.

The Calculator Buttons and Passing Arguments to Handlers

Let's get our hands dirty right away and tackle the code that creates the 24 buttons of the calculator. We can use the approach based on a two-dimensional list of button labels and a nested loop that we used in program phone.py from Section 9.1. Let's get started.

Module: calc.py
 1 # calculator button labels in a 2D list
 2 buttons = [[‘MC’,     ‘M+’,      ‘M-’, ‘MR’],
 3            [‘C’ , ‘u221a’, ‘xu00b2’, ‘+’ ],
 4            [‘7’ ,     ‘8’ ,      ‘9’ , ‘-’ ],
 5            [‘4’ ,     ‘5’ ,      ‘6’ , ‘*’ ],
 6            [‘1’ ,     ‘2’ ,      ‘3’ , ‘/’ ],
 7            [‘0’ ,     ‘.’ ,      ‘+-’, ‘=’ ]]
 8
 9  # create and place buttons in appropriate row and column
10 for r in range(6):
11     for c in range(4):
12         b = Button(self,        # button for symbol buttons[r][c]
13                    text=buttons[r][c],
14                    width=3,
15                    relief=RAISED,
16                    command=???)            # method ??? to be done
17         b.grid(row = r+1, column = c)      # entry is in row 0

(We use Unicode characters u221a and u00b2 for the square root and the superscript in x2.)

What's missing in this code is the name of each event-handling function (note the question marks ??? in line 16). With 24 different buttons, we need to have 24 different event handlers. Writing 24 different handlers would not only be very painful, but it would also be quite repetitive since many of them are essentially the same. For example, the 10 handlers for the 10 “digit” buttons should all do essentially the same thing: append the appropriate digit to the string in the entry field.

Wouldn't it be nicer if we could write just one event handler called click() for all 24 buttons? This handler would take one input argument, the label of the clicked button, and then handle the button click depending on what the label is.

The problem is that a button event handler cannot take an input argument. In other words, the command option in the Button constructor must refer to a function that can and will be called without arguments. So are we out of luck?

There is actually a solution to the problem, and it uses the fact that Python functions can be defined so that when called without an input value, the input argument receives a default value. Instead of having function click() be the official handler, we define, inside the nested for loop, the handler to be a function cmd() that takes one input argument x—which defaults to the label buttons[r][c]—and calls self.click(x). The next module includes this approach (and the code that creates the Entry widget):

Module: calc.py
 1 # use Entry widget for display
 2 self.entry = Entry(self, relief=RIDGE, borderwidth=3,
 3                    width=20, bg=‘gray’,
 4                    font=(‘Helvetica’, 18))
 5 self.entry.grid(row=0, column=0, columnspan=5)
 6
 7 # create and place buttons in appropriate row and column
 8 for r in range(6):
 9     for c in range(4):
10
11         # function cmd() is defined so that when it is
12         # called without an input argument, it executes
13         # self.click(buttons[r][c])
14         def cmd(x=buttons[r][c]):
15             self.click(x)
16
17         b = Button(self,        # button for symbol buttons[r][c]
18                    text=buttons[r][c],
19                    width=3,
20                    relief=RAISED,
21                    command=cmd)            # cmd() is the handler
22         b.grid(row=r+1, column=c)          # entry is in row 0

In every iteration of the innermost for loop, a new function cmd is defined. It is defined so that when called without an input value, it executes self.clicked(buttons[r][c]). The label buttons[r][c] is the label of the button being created in the same iteration. The button constructor will set cmd() to be the button's event handler.

In summary, when the calculator button with label key is clicked, the Python interpreter will execute self.click(key). To complete the calculator, we need only to implement the “unofficial” event handler click().

Implementing the “Unofficial” Event Handler click()

The function click() actually handles every button click. It takes the text label key of the clicked button as input and, depending on what the button label is, does one of several things. If key is one of the digits 0 through 9 or the dot, then key should simply be appended to the digits already in the Entry widget:

self.entry.insert(END, key)

(We will see in a moment that this is not quite enough.)

If key is one of the operators +, -, *, or /, it means that we just finished typing an operand, which is displayed in the entry widget, and are about to start typing the next operand. To handle this, we use an instance variable self.expr that will store the expression typed so far, as a string. This means that we need to append the operand currently displayed in the entry box and also the operator key:

self.expr += self.entry.get()
self.expr += key

In addition, we need to somehow indicate that the next digit typed is the start of the next operand and should not be appended to the current value in the Entry widget. We do this by setting a flag:

self.startOfNextOperand = True

This means that we need to rethink what needs to be done when key is one of the digits 0 through 9. If startOfNextOperand is True, we need to first delete the operand currently displayed in the entry and reset the flag to False:

if self.startOfNextOperand:
   self.entry.delete(0, END)
   self.startOfNextOperand = False
self.entry.insert(END, key)

What should be done if key is =? The expression typed so far should be evaluated and displayed in the entry. The expression consists of everything stored in self.expr and the operand currently in the entry. Before displaying the result of the evaluation, the operand currently in the entry should be deleted. Because the user may have typed an illegal expression, we need to do all this inside a try block; the exception handler will display an error message if an exception is raised while evaluating the expression.

We can now implement a part of the click() function:

Module: calc.py
 1 def click(self, key):
 2     ‘handler for event of pressing button labeled key’
 3
 4     if key == ‘=’:
 5         # evaluate the expression, including the value
 6         # displayed in entry and display result
 7         try:
 8             result = eval(self.expr + self.entry.get())
 9             self.entry.delete(0, END)
10             self.entry.insert(END, result)
11             self.expr = ‘’
12         except:
13             self.entry.delete(0, END)
14             self.entry.insert(END, ‘Error’)
15
16     elif key in ‘+*-/’:
17        # add operand displayed in entry and operator key
18        # to expression and prepare for next operand
19        self.expr += self.entry.get()
20        self.expr += key
21        self.startOfNextOperand = True

22    # the cases when key is ‘u221a’, ‘xu00b2’, ‘C’,
23    # ‘M+’, ‘M-’, ‘MR’, ‘MC’ are left as an exercise
24
25    elif key == ‘+-’:
26        # switch entry from positive to negative or vice versa
27        # if there is no value in entry, do nothing
28        try:
29            if self.entry.get()[0] == ‘-’:
30                self.entry.delete(0)
31            else:
32                self.entry.insert(0, ‘-’)
33       except IndexError:
34           pass
35
36   else:
37       # insert digit at end of entry, or as the first
38       # figit if start of next operand
39       if self.startOfNextOperand:
40           self.entry.delete(0, END)
41           self.startOfNextOperand = False
42       self.entry.insert(END, key)

Note that the case when the user types the +- button is also shown. Each press of this button should either insert a - operator in front of the operand in the entry if it positive, or remove the - operator if it is negative. We leave the implementation of some of the other cases as a practice problem.

Lastly, we implement the constructor. We have already written the code that creates the entry and the buttons. Instance variables self.expr and self.startOfNextOperand should also be initialized there. In addition, we should initialize an instance variable that will represent the calculator's memory.

Module: calc.py
1 def _ _init_ _(self, parent=None):
2     ‘calculator constructor’
3     Frame._ _init_ _(self, parent)
4     self.pack()
5
6     self.memory = ‘’               # memory
7     self.expr = ‘’                 # current expression
8     self.startOfNextOperand = True # start of new operand
9
10    # entry and buttons code

Practice Problem 9.10

Complete the implementation of the Calc class. You will need to implement the code that handles buttons C, MC, M+, M-, and MR as well as the square root and square buttons.

Use the instance variable self.memory in the code handling the four memory buttons. Implement the square root and the square button so that the appropriate operation is applied to the value in the entry and the result is displayed in the entry.

Chapter Summary

In this chapter, we introduce the development of GUIs in Python.

The specific Python GUI API we use is the Standard Library module tkinter. This module defines widgets that correspond to the typical components of a GUI, such as buttons, labels, text entry forms, and so on. In this chapter, we explicitly cover widget classes Tk, Label, Button, Text, Entry, Canvas, and Frame. To learn about other tkinter widget classes, we give pointers to online tkinter documentation.

There are several techniques for specifying the geometry (i.e., the placement) of widgets in a GUI. We introduce the widget class methods pack() and grid(). We also illustrate how to facilitate the geometry specification of more complex GUIs by organizing the widgets in a hierarchical fashion.

GUIs are interactive programs that react to user-generated events such as mouse button clicks, mouse motion, or keyboard key presses. We describe how to define the handlers that are executed in response to these events. Developing event handlers (i.e., functions that respond to events) is a style of programming called event-driven programming. We encounter it again when we discuss the parsing of HTML files in Chapter 11.

Finally, and perhaps most important, we use the context of GUI development to showcase the benefits of OOP. We describe how to develop GUI applications as new widget classes that can be easily incorporated into larger GUIs. In the process, we apply OOP concepts such class inheritance, modularity, abstraction, and encapsulation.

Solutions to Practice Problems

9.1 The width and height options can be used to specify the width and height of the text label. (Note that a width of 20 means that 20 characters can fit inside the label.) To allow padding to fill the available space around the peace symbol widget, the method pack() is called with options expand = True and fill = BOTH.

Module: peaceandlove.py
 1 from tkinter import Tk, Label, PhotoImage, BOTH, RIGHT, LEFT
 2 root = Tk()
 3
 4 label1 = Label(root, text=”Peace & Love”, background=‘black’,
 5                width=20, height=5, foreground=‘white’,
 6                font=(‘Helvetica’, 18, ‘italic’))
 7 label1.pack(side=LEFT)
 8
 9 photo = PhotoImage(file=‘peace.gif’)
10
11 label2 = Label(root, image=photo)
12 label2.pack(side=RIGHT, expand=True, fill=BOTH)
13
14 root.mainloop()

9.2 Using iteration makes the creation of all the labels manageable. The first row of “days of the week” labels can be best done by creating the list of days of the week, iterating over this list, creating a label widget for each, and placing it in the appropriate column of row 0. The relevant code fragment is shown next.

Module: ch9.py
1     days = [‘Mon’, ‘Tue’, ‘Wed’, ‘Thu’, ‘Fri’, ‘Sat’, ‘Sun’]
2     # create and place weekday labels
3     for i in range(7):
4         label = Label(root, text=days[i])
5         label.grid(row=0,column=i)

Iteration is also used to create and place the number labels. Variables week and weekday keep track of the row and column, respectively.

Module: ch9.py
 1     # obtain the day of the week for first of the month and
 2     # the number of days in the month
 3     weekday, numDays = monthrange(year, month)
 4     # create calendar starting at week (row) 1 and day (column) 1
 5     week = 1
 6     for i in range(1, numDays+1): # for i = 1, 2, …, numDays
 7         # create label i and place it in row week, column weekday
 8         label = Label(root, text=str(i))
 9         label.grid(row=week, column=weekday)
10
11         # update weekday (column) and week (row)
12         weekday += 1
13         if weekday > 6:
14             week += 1
15             weekday = 0

9.3 Two buttons should be created instead of one. The next code fragment shows the separate event-handling functions for each button.

Module: twotimes.py
 1 def greenwich():
 2     ‘prints Greenwich day and time info’
 3     time = strftime(‘Day: %d %b %Y
Time: %H:%M:%S %p
’,
 4                     gmtime())
 5     print(‘Greenwich time
’ + time)
 6
 7 def local():
 8     ‘prints local day and time info’
 9     time = strftime(‘Day: %d %b %Y
Time: %H:%M:%S %p
’,
10                    localtime())
11    print(‘Local time
’ + time)
12
13 # Local time button
14 buttonl = Button(root, text=‘Local time’, command=local)
15 buttonl.pack(side=LEFT)
16
17 # Greenwich mean time button
18 buttong = Button(root,text=‘Greenwich time’, command=greenwich)
19 buttong.pack(side=RIGHT)

9.4 We only describe the changes from program day.py. The event-handling function compute() for button “Enter” should be modified to:

def compute():
    global dateEnt   # warning that dateEnt is a global variable
    # read date from entry dateEnt
    date = dateEnt.get()
    # compute weekday corresponding to date
    weekday = strftime(‘%A’, strptime(date, ‘%b %d, %Y’))
    # display the weekday in a pop-up window
    dateEnt.insert(0, weekday+‘ ’)

The event-handling function for button “Clear” should be:

def clear():
    ‘clears entry datEnt’
    global dateEnt
    dateEnt.delete(0, END)

Finally, the buttons should be defined as shown:

# Enter button
button = Button(root, text=‘Enter’, command=compute)
button.grid(row=1, column=0)

# Clear button
button = Button(root, text=‘Clear’, command=clear)
button.grid(row=1, column=1)

9.5 We need to bind the image key press to an event-handling function that takes an Event object as input. All this function really has to do is call the handler compute(). So we only need to add to day.py:

def compute2(event):
    compute()

dateEnt.bind(‘<Return>’, compute2)

9.6 The key is to store the items returned by canvas.create_line(x,y,newX,newY) in some container, say list curve. This container should be initialized to an empty list every time we start drawing:

Module: draw2.py
1 def begin(event):
2     ‘initializes the start of the curve to mouse position’
3     global oldx, oldy, curve
4     oldx, oldy = event.x, event.y
5     curve = []

As we move the mouse, the IDs of line segments created by Canvas method create_line() need to be appended to list curve. This is shown in the reimplementation of event-handling function draw(), shown next.

Module: draw2.py
 1 def draw(event):
 2     ‘draws a line segment from old mouse position to new one’
 3     global oldx, oldy, canvas, curve # x and y will be modified
 4     newx, newy = event.x, event.y    # new mouse position
 5     # connect previous mouse position to current one
 6     curve.append(canvas.create_line(oldx, oldy, newx, newy))
 7     oldx, oldy = newx, newy        # new position becomes previous
 8 def delete(event):
 9     ‘delete last curve drawn’
10     global curve
11     for segment in curve:
12         canvas.delete(segment)
13  # bind Ctrl-Left button mouse click to delete()
14 canvas.bind(‘<Control - Button - 1>’, delete)

The event handler for the <Control-Button-1> event type, function delete(), should iterate over the line segment ID in curve and call canvas.delete() on each.

9.7 The implementations are similar to function up():

Module: plotter.py
 1 def down():
 2     ‘move pen down 10 pixels’
 3     global y, canvas                 # y is modified
 4     canvas.create_line(x, y, x, y+10)
 5     y += 10
 6 def left():
 7     ‘move pen left 10 pixels’
 8     global x, canvas                 # x is modified
 9     canvas.create_line(x, y, x-10, y)
10     x -= 10
11 def right():
12     ‘move pen right 10 pixels’
13     global x, canvas                 # x is modified
14     canvas.create_line(x, y, x+10, y)
15     x += 10

9.8 Because the Text widget is not used by the event handler, it is not necessary to assign it to an instance variable.

Module: ch9.py
 1 from tkinter import Text, Frame, BOTH
 2 class KeyLogger(Frame):
 3     ‘a basic editor that logs keystrokes’
 4     def _ _init_ _(self, master=None):
 5         Frame._ _init_ _(self, master)
 6         self.pack()
 7         text = Text(width=20, height=5)
 8         text.bind(‘<KeyPress>’, self.record)
 9         text.pack(expand=True, fill=BOTH)
10 def record(self, event):
11     ‘‘‘handles keystroke events by printing character
12        associated with key’’’
13     print(‘char={}’.format(event.keysym))

9.9 Only the Canvas widget is referenced by the function move() that handles button clicks, so it is the only widget that needs to be assigned to an instance variable, self.canvas. The coordinates (i.e., state) of the pen will also need to be stored in instance variables self.x and self.y. The solutions is in module ch9.py. Next is the constructor code fragment that creates the button “up” and its handler; the remaining buttons are similar.

Module: ch9.py
1          # create up button
2          b = Button(buttons, text=‘up’, command=self.up)
3          b.grid(row=0, column=0, columnspan=2)
4
5     def up(self):
6         ‘move pen up 10 pixels’
7         self.canvas.create_line(self.x, self.y, self.x, self.y-10)
8         self.y -= 10

9.10 Here is the code fragment that is missing:

Module: calc.py
 1 elif key == ‘u221a’:
 2     # compute and display square root of entry
 3     result = sqrt(eval(self.entry.get()))
 4     self.entry.delete(0, END)
 5     self.entry.insert(END, result)
 6
 7 elif key == ‘xu00b2’:
 8     # compute and display the square of entry
 9     result = eval(self.entry.get())**2
10     self.entry.delete(0, END)
11     self.entry.insert(END, result)
12
13 elif key == ‘C’:            # clear entry
14     self.entry.delete(0, END)
15
16 elif key in {‘M+’, ‘M-’}:
17     # add or subtract entry value from memory
18     self.memory = str(eval(self.memory+key[1]+self.entry.get()))
19
20 elif key == ‘MR’:
21     # replace value in entry with value stored in memory
22     self.entry.delete(0, END)
23     self.entry.insert(END, self.memory)
24
25 elif key == ‘MC’:           # clear memory
26     self.memory = ‘’

Exercises

9.11 Develop a program that displays a GUI window with your picture on the left side and your first name, last name, and place and date of birth on the right. The picture has to be in the GIF format. If you do not have one, find a free online converter tool online and a JPEG picture to the GIF format.

9.12 Modify the solution to Practice Problem 9.3 so that the times are displayed in a separate pop-up window.

9.13 Modify the phone dial GUI from Section 9.1 so it has buttons instead of digits. When the user dials a number, the digits of the number should be printed in the interactive shell.

9.14 In program plotter.py, the user has to click one of the four buttons to move the pen. Modify the program to allow the user to use the arrow keys on the keyboard instead.

9.15 In the implementation of widget class Plotter, there are four very similar button event handlers: up(), down(), left(), and right(). Reimplement the class using just one function move() that takes two input arguments dx and dy and moves the pen from position (x, y) to (x+dx, y+dx).

9.16 Add two more buttons to the Plotter widget. One, labeled “clear”, should clear the canvas. The other, labeled “delete”, should erase the last pen move

9.17 Augment calculator widget Calc so that the user can type keyboard keys instead of clicking buttons corresponding to the 10 digits, the dot ., and the operators +, -, *, and /. Also allow the user to type the image key instead of clicking button labeled =.

Problems

9.18 Implement a GUI application that allows users to compute their body mass index (BMI), which we defined Practice Problem 5.1. Your GUI should look as shown below.

image

After entering the weight and height and then clicking the button, a new window should pop up with the computed BMI. Make sure your GUI is user friendly by deleting the entered weight and height so the user can enter new inputs without having to erase the old ones.

9.19 Develop a GUI application whose purpose is to compute the monthly mortgage payment given a loan amount (in $), the interest rate (in %), and the loan term (i.e., the number of months that it will take to repay the loan). The GUI should have three labels and three entry boxes for users to enter this information. It should also have a button labeled “Compute mortgage” that, when clicked, should compute and display the monthly mortgage in a fourth entry box.

The monthly mortgage m is computed from the loan amount a, interest rate r, and loan terms t as:

image

where c = r/1200.

9.20 Develop a widget class Finances that incorporates a calculator and a tool to compute the monthly mortgage. In your implementation, you should use the Calc class developed in the case study and a Mortgage widget from Problem 9.19.

9.21 Develop a GUI that contains just one Frame widget of size 480 × 640 that has this behavior: Every time the user clicks at some location in the frame, the location coordinates are printed in the interactive shell.

>>>
you clicked at (55, 227)
you clicked at (426, 600)
you clicked at (416, 208)

9.22 Modify the phone dial GUI from Section 9.1 so it has buttons instead of digits and an entry box on top. When the user dials a number, the number should be displayed in the traditional U.S. phone number format. For example, if the user enters 1234567890, the entry box should display 123-456-7890.

9.23 Develop new widget Game that implements a number guessing game. When started, a secret random number between 0 and 9 is chosen. The user is then requested to enter number guesses. Your GUI should have an Entry widget for the user to type the number guess and a Button widget to enter the guess:

image

If the guess is correct, a separate window should inform the user of that. The user should be able to enter guesses until he makes the correct guess.

9.24 In Problem 9.23, pressing the image key on your keyboard after entering a guess in the entry is ignored. Modify the the Game GUI so that pressing the key is equivalent to pressing the button.

9.25 Modify the widget Game from Problem 9.24 so that a new game starts automatically when the user has guessed the number. The window informing the user that she made the correct guess should say something like “Let's do this again…” Note that a new random number would have to be chosen at the start of each game.

9.26 Implement GUI widget Craps that simulates the gambling game craps. The GUI should include a button that starts a new game by simulating the initial roll of a pair of dice. The result of the initial roll is then shown in an Entry widget, as shown.

image

If the initial roll is not a win or a loss, the user will have to click the button “Roll for point”, and keep clicking it until she wins.

9.27 Develop an application with a text box that measures how fast you type. It should record the time when you type the first character. Then, every time you press the blank character, it should print (1) the time you took to type the preceding word and (2) an estimate of your typing speed in words per minute by averaging the time taken for typing the words so far and normalizing over 1 minute. So, if the average time per word is 2 seconds, the normalized measure is 30 words per minute.

9.28 Most calculators clear to 0 and not an empty display. Modify the calculator Calc implementation so the default display is 0.

9.29 Develop new GUI widget class Ed that can be used to teach first-graders addition and subtraction. The GUI should contain two Entry widgets and a Button widget labeled “Enter”.

At start-up, your program should generate (1) two single-digit pseudorandom numbers a and b and (2) an operation o, which could be addition or subtraction—with equal likelihood—using the randrange() function in the random module. The expression a o b will then be displayed in the first Entry widget (unless a is less than b and the operation o is subtraction, in which case b o a is displayed, so the result is never negative). Expressions displayed could be, for example, 3+2, 4+7, 5-2, 3-3 but could not be 2-6.

The user will have to enter, in the second Entry widget, the result of evaluating the expression shown in the first Entry widget and click the “Enter” button (just the image key on the keyboard. If the correct result is entered, a new window should say “You got it!”.

9.30 Augment the GUI you developed in Problem 9.29 so that a new problem gets generated after the user answers a problem correctly. In addition, your app should keep track of the number of tries for each problem and include that information in the message displayed when the user gets the problem right.

9.31 Enhance the widget Ed from Problem 9.30 so it does not repeat a problem given recently. More precisely, ensure that a new problem is always different from the previous 10 problems.

9.32 Develop widget class Calendar that implements a GUI-based calendar application. The Calendar constructor should take as input three arguments: the master widget, a year, and a month (using numbers 1 through 12). For example, Calendar(root, 2012, 2) should create a Calendar widget within the master widget root. The Calendar widget should display the calendar page for the given month and year, with a button for every day:

image

Then, when you click on a day, a dialog will appear:

image

This dialog gives you an entry field to enter an appointment. When you click button “OK”, the dialog window will disappear. However, when you click the same day button in the main calendar window again, the dialog window should reappear together with the appointment information.

You may use the askstring function from module tkinter.simpledialog for the dialog window. It takes the window title and label as input and returns whatever the user typed. For example, the last dialog window was created with the function call

askstring(‘example’, ‘Enter text’)

When the user clicks OK, the string typed in the entry box is returned by this function call.

The function can also take an optional argument initialvalue that takes a string and puts it in the entry field:

askstring(‘example’, ‘ Enter text’, initialvalue=‘appt with John’)

9.33 Modify class Calendar from Problem 9.32 so that it can be used for any month in any year. When started, it should display the calendar for the current month. It should also have two additional buttons labeled “previous” and “next” that, when clicked, switch the calendar to the previous or next month.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset