Look at the Graphical User Interfaces (GUIs) in Dawson, Chapter 10: There are buttons, text fields, boxes, etc., in them. There are lots of little things --- objects --- assembled together to make a GUI.
In particular, look at the GUI for the program Lazy Buttons:
There are three buttons placed inside a frame.
Each button has its own label and its own position in the frame.
Yet, each button is coded more-or-less the same and was constructed
from the same piece of coding.
We will learn how to construct the three button objects
from the one program piece (called a class).
Next, look at the Pizza-Panic game in Dawson, Chapter 11: When the game operates,
there are multiple pizza objects that appear and move downwards across
the display.
The pizzas look the same, but each has a unique position and velocity.
We will see that the pizza objects are constructed from a pizza class.
Finally, look at the Astrocrash game in Chapter 12: when the game operates, you see multiple asteroids that move across the frame. The asteroids are similar, but each has its own size, position, direction, and velocity. The asteroid objects are constructed from an asteroid class.
When a program must make many copies of a slightly complicated, ''little'' data structure (e.g., a GUI button or a pizza or an asteroid), it is best to make copies from the same piece of program code. A module is a good construction for making a data structure, but remember there can be only one copy of a module per program --- we cannot make (import) multiple copies! This is a fundamental limitation of modules.
To solve this problem, Python uses a construction called a class, which looks like a module one writes to implement a data structure. A class is a kind of ``mini-module.'' But unlike a module, one can ``import'' (construct) multiple copies of the class within the same program. Each copy is called an object. This is how buttons, pizzas, and asteroids are constructed within Dawson's programs.
Our first goal is to become intelligent users of pre-written classes for GUIs and animations, and our second goal is to become a writer of simple classes.
But a module can be imported only once, and the example GUI has three buttons, and there must be three namespaces, one for each button.
The namespaces must be given distinct names,
so Python uses a syntax like this to construct the three button objects
and give each a distinct name.
Say that Button is the name of the class that holds the coding
for making a button:
button1 = Button( ... )
button2 = Button( ... )
button3 = Button( ... )
The three objects (namespaces)
are constructed in heap storage, and the addresses
of the namespaces are assigned to the three variable names ---
this is how we can distinguish the three buttons in our programming
of the GUI --- by their variable names, button1,
button2, and button3.
Now, a module can hold both variables and functions, and the objects
we construct also appear to hold both variables and functions.
We reference
an attribute of an object, like a button object, by mentioning the button's
name and the attribute's name, in module-style ``dot notation,''
like this:
button2.configure( ... )
This calls the configure function in the namespace for
object button2.
This reinforces our intuition that an object is a ``mini module,''
and that we have three disctinct Button mini-modules.
For the moment, pretend that a Python class is coded just like a Python module, that is, as a file holding variable definitions and function definitions. Actually, a class is coded a bit differently than a module is, but we need not worry about the details just yet --- we want to learn how to use classes first and how to write them second.
A good reference to Tkinter is found at http://www.pythonware.com/library/tkinter/introduction/ You will need this reference to build interesting GUIs.
To build a Tkinter GUI, one starts with a window into which one inserts a frame. Into the frame, one inserts buttons, labels, and other items. The items must be arranged within the frame by using some form of layout, typically a ``grid'' layout.
Here is a first example that shows how we construct a window,
then a frame, and within the frame, a label, a textentry, and two
buttons:
Here is the Python program that constructed the window:
FIGURE 1===================================================
# Demo0 shows a window holding a frame holding
# a label, a textentry, and two buttons:
# Tkinter is a huge module that holds lots of classes:
from Tkinter import *
# construct a window object:
window = Tk()
# give it a title:
window.title("Demo0")
# construct a frame and attach it to the window:
frame = Frame(window)
# place the frame within the window using ``grid layout'':
frame.grid()
# construct a label and attach it to the frame:
label = Label(frame, text = "I am a label")
# position it in the frame:
label.grid()
# construct a text entry and attach it to the frame:
textentry = Entry(frame, width = 20)
textentry.grid()
# construct a button and attach it to the frame:
button1 = Button(frame, text = "Press me")
button1.grid()
# again:
button2 = Button(frame, text = "Don't press me")
button2.grid()
# This command starts the window's controller function so that
# the window can respond to the user's interactions with it:
window.mainloop()
ENDFIGURE=====================================================
The program uses the classes,
Tk, Frame, Label, Entry, and
Button to construct objects and assemble them into a GUI.
Notice how we use the title method of the window
to cause the title to appear at the top of the window.
Each object has a grid method, which positions the object
within its ``parent'' object. (The parent of a frame is the window,
the parent of a button is the frame, etc.)
Notice the use of keyword parameters in the construction
of the label, textentry and buttons:
label = Label(frame, text = "I am a label")
. . .
textentry = Entry(frame, width = 20)
. . .
button1 = Button(frame, text = "Press me")
Recall that the keywords label the arguments that will be assigned
to the parameters of the same name within the body of the method.
(In Chapter 6A, we saw that a function like,
def f(x) :
... x ...
can be called in the classic way, like f(3), or with
keyword parameters, like f(x = 3). The latter case
is often preferred because it is easier to read and makes explicit the
assignment of argument to parameter. It will prove useful for
using the GUI classes)
As an exercise, start the Python interpreter and type
the above commands one at a time, like this.:
$python
>>> from Tkinker import *
>>> window = TK()
>>> window.title("Demo0")
>>> frame = Frame(window)
>>> frame.grid()
etc.
You will see an empty window
appear, then see its title appear, then see the window
``collapse'' around an empty frame, then see a label appear in the
frame, and so on.
Objects that paint graphical components on the display are called
widgets.
Here is a drawing of the widgets constructed in heap storage by the
above program:
Each widget (object) remembers the class it is constructed from
as well as its local variables (attributes).
The appearances of the label and buttons in the above
example are not so attractive,
and the objects are positioned rather awkwardly within the frame.
With the use of additional keywords and methods, we can
customize our example window, say, like this:
Here is the program that generated the GUI:
FIGURE 2======================================================
# Demo1 shows a window holding a frame holding
# a label, a textentry and two buttons, laid out in a grid.
# Various fonts and colors are used with the widgets.
from Tkinter import *
window = Tk()
window.title("Demo1")
# set the window's size at 200 pixels width by 80 pixels height:
window.geometry("250x150")
frame = Frame(window)
frame.grid()
# construct the label with foreground (fg) color of red, using Arial bold font:
label = Label(frame, text = "I am a label", fg = "red",
font=("Arial", 12, "bold") )
# place the label in the frame's grid, at position 0,0, left (W) justified:
label.grid(row = 0, column = 0, sticky = W)
textentry = Entry(frame, width = 20)
# place it in the grid at 1,0 extending into 1,1:
textentry.grid(row = 1, column = 0, columnspan = 2)
# construct a button and insert it into the frame:
button1 = Button(frame, text = "Press me", font=("Arial", 14, "bold"))
# ``pad'' the button with 10 extra pixels all around:
button1.grid(row = 2, column = 0, padx = 10, pady = 10)
# again, but make the button's background (bg) white:
button2 = Button(frame, text = "Don't press me", bg="white")
button2.grid(row = 2, column = 1)
window.mainloop()
ENDFIGURE==========================================================
The meanings of most of the new keyword tricks are easy to guess. The row and column keywords used with the grid method let one place objects within the frame as if the frame was a grid whose cells were numbered (0,0), (0,1), ..., (1,0), (1,1), ..., and so on.
A press of a button causes an event, and the associated event handling function is automatically called (by a combination of efforts of the operating system, the Python interpreter, and Tkinter) when the event occurs. Each button should have its own event handling function.
Here is an example. This GUI lets a person type some text into
a textentry:
When the user presses the button, the text is copied to a label
at the bottom of the GUI:
This GUI's button is connected to an event handling function that
does the copying.
Here is the program:
FIGURE 3==================================================
# Demo2 shows a window holding a text entry, a button, and a label.
# When the button is pressed, the contents of the text area is
# copied into the label.
from Tkinter import *
def handleButtonPress() :
"""This is the event handler for the button: It copies
the text typed into the textentry to the label, and it
clears the textentry.
"""
message = textentry.get() # get the text from the textentry
label.configure(text = message) # reset the label's text
textentry.delete(0, END) # clear the textentry
print message # print trace info to the command window
myfont = ("Arial", 14, "bold")
window = Tk()
window.title("Demo1")
window.geometry("220x150")
frame = Frame(window)
frame.grid()
textentry = Entry(frame, width = 15, font = myfont, fg = "red")
textentry.grid()
# IMPORTANT: attach handleButtonPress as the event handler to the button:
button1 = Button(frame, text = "Copy", command = handleButtonPress,
font = myfont, fg = "blue", bg = "yellow")
button1.grid()
label = Label(frame, text = "********", font = myfont)
label.grid()
# This command starts the window's controller function:
window.mainloop()
ENDFIGURE==========================================================
The command that constructs the button,
button1 = Button(frame, text = "Copy", command = handleButtonPress,
font = myfont, fg = "blue", bg = "yellow")
also attaches the event handling function, by means of the
command keyword.
When the button is pressed, the operating system contacts the
Python interpreter, which contacts Tkinter, which
starts the event handling function.
The command,
label.configure(text = message)
uses the configure function with object label
to reset the text displayed by the label.
The configure function can be used to reconfigure any attribute of any widget.
Let's reuse the TelephoneBook module from the previous
chapter and build a GUI for it that lets a person insert, lookup,
and print telephone numbers.
The GUI looks like this:
The use can insert a name and phone number by typing the information
into the text field and pressing Insert:
Later, the number for the person can be retrieved by typing the
person's name and pressing Lookup:
The third button prints the complete contents of the telephone
book in the command window.
The GUI code lives by itself in the ``view module.'' The program uses three event handler functions, one per button, and when an event handler is called, it calls a corresponding function in the TelephoneBook module. The event handler functions are actually the program's ``controller,'' and if they were large and complex, we would move them into their own module, a ``controller module.'' But we won't do that here.
Here is the view module along with the event-handler functions.
FIGURE==========================================================
"""The view module shows the GUI for the TelephoneBook application."""
from Tkinter import *
import TelephoneBook
#### The event handling functions:
def handleInsert():
"""This is the event handler for the Insert button: It inserts
the name and telephone number typed by the user.
"""
data = textentry.get()
items = data.split(":")
name = items[0]
number = int(items[1])
number = TelephoneBook.insert(name, number)
textentry.delete(0, END)
label2.configure(text = "")
def handleLookup() :
"""This is the event handler for the Lookup button: It looks up
the telephone number of the person typed by the user.
"""
name = textentry.get()
number = TelephoneBook.lookup(name)
label2.configure(text = name + " has number, " + str(number))
textentry.delete(0, END)
def handlePrint() :
"""This prints the contents of the telephone book to the
command window.
"""
TelephoneBook.printBook()
########
myfont = ("Arial", 14, "bold")
window = Tk()
window.title("Telephone Directory")
window.geometry("400x170")
frame = Frame(window)
frame.grid()
label1 = Label(frame, text = "Input: ", font = myfont)
label1.grid(row = 0, column = 0)
textentry = Entry(frame, width = 15, font = myfont)
textentry.grid(row = 0, column = 1)
button1 = Button(frame, text = "Lookup",
command = handleLookup,
font = myfont, fg = "blue", bg = "white")
button1.grid(row = 1, column = 0, padx = 10, pady = 10 )
button2 = Button(frame, text = "Insert",
command = handleInsert,
font = myfont, fg = "blue", bg = "white")
button2.grid(row = 1, column = 1, padx = 10, pady = 10 )
button3 = Button(frame, text = "Print",
command = handlePrint,
font = myfont, fg = "blue", bg = "white")
button3.grid(row = 1, column = 2, padx = 10, pady = 10)
label2 = Label(frame, text = " ", font = myfont)
label2.grid(row = 2, column = 0, columnspan = 2)
window.mainloop()
ENDFIGURE=============================================
The coding is routine, because all the
computational effort is embedded in the functions in the
model module, which we reproduce from the previous chapter:
FIGURE=================================================
# TelephoneBook holds the data structure and maintenance functions
# for a telephone book.
# The data structure is a list of (name,number) pairs
# Example: book = [("Mary Smith", 3453), ("Jake Jones", 2458)]
book = [] # the book starts empty
def printBook() :
"""printBook prints the entire contents of the phone book"""
global book
for entry in book :
print entry[0] + ":", entry[1]
def inBook(name) :
"""inBook checks if name is already in the phone book.
parameter: name, a string - a person's name
returns: the index number of where the name is found in the book;
returns -1, if the name is not in the book
"""
global book
answer = -1
index = 0
for entry in book :
if entry[0] == name :
answer = index
break
else :
index = index + 1
return answer
def insert(name, number) :
"""addPerson adds a new name and number to the phone book,
provided that the name is not already in the book.
parameters: name - a string, the person's name
number - an int, the four-digit phone number
"""
global book
if inBook(name) == -1 : # name not in the book?
book = book + [(name,number)]
else :
print "error: " + name + " is already in the book"
def lookup(name) :
"""lookup searches the phone book for name.
parameter: name - a string, the person's name
returns: the phone number, an int, for the person
(if the name isn't in the book, -1 is returned)
"""
global book
number = -1
for entry in book :
if entry[0] == name :
number = entry[1]
break
return number
def delete(name) :
"""delete removes the entry for name in the book.
parameter: name - the name to be removed
"""
global book
where = inBook(name)
if where != -1 :
book = book[:where] + book[where+1:]
ENDFIGURE=========================================================
This module organization is standard for programs that use GUIs:
The code for building the GUI is kept separate from the code
that does the computation. The latter is typically saved in
a model module that has functions which are contacted to
do the computation.
The module that holds the GUI code is called the
View module. When the event-handling functions are
placed in their own module, which is often the case when the
functions are detailed, the third module is called
the controller module, because its functions control
the computation.
The resulting program --- a trio of model, view, and controller modules --- is connected in a Model-View-Controller (MVC) architecture, which we surveyed in the previous chapter.
Here is a second example. Perhaps we have built a module that maintains a dictionary of translations of English words to Pig Latin. (Recall that ``tree'' translates to ``eetray'', ``schedule'' to ``eduleschay,'' ``and'' to ``andshay,'' etc., in Pig Latin --- leading consonants are moved to the end of the word and ``ay'' is appended. If there are no leading consonants, ``shay'' is appended.)
The module can be connected to a GUI.
The user types an English word:
When she presses the Translate button, the translation appears:
The Dump button prints a dictionary of all the
English-to-pig-latin translations accomplished so far.
The GUI code lives by itself in the ``view module.'' The program uses two event handler functions, one per button, and when an event handler is called, it calls a corresponding function in the PigLatin module. The event handler functions are actually the program's ``controller,'' and if they were large and complex, we would move them into their own module, a ``controller module.'' But we won't do that here.
Here is the view module along with the two event handler functions.
FIGURE 5==========================================================
"""The view module shows the GUI for the PigLatin translator."""
from Tkinter import *
import PigLatin
#### The event handling functions:
def handleTranslate() :
"""This is the event handler for the Translate button: It looks up
the PigLatin translation of the word typed by the user.
"""
english_word = textentry.get()
pig_word = PigLatin.lookup(english_word)
label2.configure(text = '"' + english_word + '" translates to "' + pig_word + '"')
textentry.delete(0, END)
def handleDump() :
"""This prints the contents of the PigLatin dictionary to the
command window.
"""
PigLatin.printPig()
########
myfont = ("Arial", 14, "bold")
window = Tk()
window.title("PigLatin Translator")
window.geometry("400x170")
frame = Frame(window)
frame.grid()
label1 = Label(frame, text = "English word: ", font = myfont)
label1.grid(row = 0, column = 0)
textentry = Entry(frame, width = 15, font = myfont)
textentry.grid(row = 0, column = 1)
button1 = Button(frame, text = "Translate",
command = handleTranslate,
font = myfont, fg = "blue", bg = "white")
button1.grid(row = 1, column = 0, padx = 10, pady = 10 )
button2 = Button(frame, text = "Dump",
command = handleDump,
font = myfont, fg = "blue", bg = "white")
button2.grid(row = 1, column = 1, padx = 10, pady = 10)
label2 = Label(frame, text = " ", font = myfont)
label2.grid(row = 2, column = 0, columnspan = 2)
window.mainloop()
ENDFIGURE======================================================
The coding is entirely routine, because all the serious
computational effort is embedded in the functions in the
model module:
FIGURE 4========================================================
"""PigLatin models a dictionary that maps English words to their pig-latin
translations and counts the number of times each English word was
consulted.
"""
# Variable pig holds the dictionary. It uses English words as keys.
# Each key maps to a 2-list, holding the corresponding pig-latin word
# and the count of how many times the word has been looked up:
# pig : { [ word : [ pig_latin_translation, lookup_count ]* }
#
# Example, built from the input, "It is, is it not?"
# pig : { "it":["itshay", 2], "is":["isshay", 2], "not":["otnay", 1] }
pig = { } # starts at empty
def lookup(word) :
"""lookup looks up word in the dictionary to find its pig-latin
equivalent.
If word isn't in the dictionary, the translation is made on the
spot, stored in the dictionary, and returned.
In all cases, the count for the word is increased by one.
parameter: word - the English word to translate
returns: the pig-latin translation of the word.
"""
global pig
if not(word in pig) : # word not there ?
pig[word] = [translate(word), 0] # translate and insert it
translation = pig[word][0]
pig[word][1] = pig[word][1] + 1
return translation
def translate(word) :
"""translate converts an English word into pig-latin.
parameter: word - the English word
returns the word in pig-latin.
"""
vowels = "aeiou"
if word[0] in vowels :
word = word + "sh" # no leading consonants, so attach "sh" to end
else :
# move the leading consonant to the end and add y to vowels:
word = word[1:] + word[0]
vowels = vowels + "y"
# rotate remaining leading consonants to the end of the word:
# IMPORTANT: every English word holds at least one vowel!
while not(word[0] in vowels) :
word = word[1:] + word[0]
word = word + "ay"
return word
def printPig() :
"""printPig prints the contents of the dictionary, words arranged alphabetically.
"""
global pig
keylist = pig.keys() # builds a list of all the keys used in pig
keylist.sort() # reorders the list alphabetically
for k in keylist :
record = pig[k]
print k, ":", record[0], record[1]
ENDFIGURE===========================================================
This module organization is standard for programs that use GUIs:
The code for building the GUI is kept separate from the code
that does the computation. The latter is typically saved in
a model module that has functions which are contacted to
do the computation.
The module that holds the GUI code is called the
View module. When the event-handling functions are
placed in their own module, which is often the case when the
functions are detailed, the third module is called
the controller module, because its functions control
the computation.
The resulting program --- a trio of model, view, and controller modules --- is connected in a Model-View-Controller (MVC) architecture, which we surveyed in the previous chapter.
Say that we have a GUI whose buttons display on their faces the
number of times the buttons are pressed:
When a button is pressed, its count increases:
Each button has its own event handling
function, and
it is not too painful to write the three buttons and their
functions; it looks like this:
=========================================================
"""A GUI with three counter buttons"""
from Tkinter import *
myfont = ("Arial", 16, "bold")
window = Tk()
window.title("Three counters")
window.geometry("300x120")
frame = Frame(window)
frame.grid()
# Construct three buttons:
button0 = Button(frame, font = ("Arial", 14, "bold"), text = "0",
fg = "blue", bg = "white", width = 4, height = 2)
button0.grid(row = 1, column = 0, padx = 10, pady = 10 )
button1 = Button(frame, font = ("Arial", 14, "bold"), text = "0",
fg = "blue", bg = "white", width = 4, height = 2)
button1.grid(row = 1, column = 1, padx = 10, pady = 10 )
button2 = Button(frame, font = ("Arial", 14, "bold"), text = "0",
fg = "blue", bg = "white", width = 4, height = 2)
button2.grid(row = 1, column = 2, padx = 10, pady = 10 )
#Construct three event-handling functions for the three buttons:
count0 = 0
def handle0() :
global count0
count0 = count0 + 1
button0.configure(text = str(count0))
button0.configure(text = str(count0), command = handle0)
count1 = 0
def handle1() :
global count1
count1 = count1 + 1
button1.configure(text = str(count1))
button1.configure(text = str(count1), command = handle1)
count2 = 0
def handle2() :
global count2
count2 = count2 + 1
button2.configure(text = str(count2))
button2.configure(text = str(count2), command = handle2)
window.mainloop()
========================================================
Each event-handling function maintains its own
integer counter variable, which it uses to reconfigure the text
on the button it controls.
This is not a pretty program. Worse yet, what if the
GUI portrayed a ``game board,'' where each button represented a
square on the board? Here is an example of an 8-square GUI:
Would we write 8 distinct yet basically identical
event-handling functions? NO.
We now examine no less than three possible solution strategies for this problem. The intention of each stategy is to ``customize'' one form of event-handling for each button in the GUI:
Let's study functions-that-return-functions with a small example:
def makeNewFunction(num) :
"""constructs an answer function and returns it"""
def printNumber() :
"""this function remembers and prints the value of num
which was active when this function is returned as the answer
"""
print num
return printNumber
f1 = makeNewFunction(2)
f2 = makeNewFunction(4)
f3 = makeNewFunction(5)
f1()
f2()
f3()
Function makeNewFunction returns an answer that is a function,
namely, printNumber; the answer function remembers the
value of the parameter, num, that was active when the
function is returned as the answer.
This means the assignment,
f1 = makeNewFunction(2)
assigns to variable f1 a function that prints 2.
In a similar way, functions that print 4 and 5 are assigned to the names
f2 and f3. Then, the functions can be called
by using the variable names! (Try it.)
This is not crazy, but completely sensible --- if a function can
return an answer that is an int that can be later used in an arithmetic
expression, then a function can just as well return a function
that can be later used in a function call.
Each answer function that is returned is ``customized'' to the values of
the parameters that were used to construct the answer function.
We use this technique to write a function that returns
an event-handling function customized to the button that uses
it. See this example,
which is the eight-button GUI written with this technique:
FIGURE==========================================================
"""Buttons2 shows how to construct a customized event-handling function for
each button in a GUI by using a _function that returns a function as its answer_.
"""
from Tkinter import *
HOW_MANY = 8 # how many buttons we place into the GUI
all_counts = HOW_MANY * [0] # a list of counts for all the buttons
def constructHandler(but, location) :
"""constructs a custom event-handling function for a GUI button.
parameters: but - a button object, the button just constructed
that requires its own event-handling function
location - the position of the button on the GUI and also the
position of the button's count in the all_counts list
"""
def handlePress() :
"""is called when the button, but, is pressed: increases the count for
but and updates the text on but's face
"""
global all_counts
all_counts[location] = all_counts[location]+1 # increment the count
but.configure(text=all_counts[location]) # reset the pressed button's text
return handlePress # return the handlePress function, customized by
# the current values of parameters but and location
myfont = ("Arial", 16, "bold")
window = Tk()
window.title("counters")
width = HOW_MANY * 100
window.geometry(str(width) + "x120")
frame = Frame(window)
frame.grid()
for i in range(HOW_MANY) :
new_button = Button(frame, font = myfont, text = 0,
fg = "blue", bg = "white", width = 4, height = 2)
new_button.grid(row = 1, column = i, padx = 10, pady = 10 )
new_handler_function = constructHandler(new_button,i)
# connect the new handler function to the new button:
new_button.configure(command = new_handler_function)
window.mainloop()
ENDFIGURE======================================================
Notice how constructHandler uses the arguments assigned
to its parameters, but and location, to customize
handlePress and return it as its answer. The returned
answer is used here:
for i in range(HOW_MANY) :
new_button = Button(frame, font = myfont, text = 0, ...)
. . .
new_handler_function = constructHandler(new_button,i)
# connect the new handler function to the new button:
new_button.configure(command = new_handler_function)
Now, when button number i is pressed, the
customized function returned by constructHandler(new_button,i)
is called and updates the count for the i-th button and the text
on the face of the i-th button.
The key technical trick is to attach an event-handling function
to a new button like this:
new_button = Button(frame, font = myfont, text = 0, ...)
new_button.bind("<Button-1>", handlePress)
The second command uses the bind method to bind explicitly
the new button to a function, handlePress, that will be called
when ``Button 1'' (the left mouse button) is pressed on the button's
icon on the display. When the mouse is clicked, Tkinker will
call handlePress with an argument that is the name of
the button pressed..
Here is how we use this trick to rebuild the GUI with its eight
buttons:
FIGURE================================================
"""Buttons3 shows how to use an event-handling function that receives an
_event argument_ that knows the name of the button that was pressed
"""
from Tkinter import *
HOW_MANY = 8 # how many buttons we place into the GUI
all_counts = HOW_MANY * [0] # a list of counts for all the buttons
all_buttons = [] # a list of all the buttons placed into the GUI
def handlePress(event) :
"""This function handles presses of _all_ the buttons on the GUI.
It must determine which button was pressed, and then it updates the
count and text for the button that was pressed
parameter: event - an object that holds the name of the button that was pressed.
This argument is constructed by Tkinter when a button is pressed.
"""
global all_counts, all_buttons
button_pressed = event.widget # get the name of the button pressed
# find the button in all_buttons that was pressed:
location = 0
for but in all_buttons :
if but == button_pressed :
break
else :
location = location + 1
# assert: the button at position location in the GUI was pressed
# (NOTE: the above loop can be replaced by: location = buttons.index(event.widget)
all_counts[location] = all_counts[location]+1
button_pressed.configure(text = all_counts[location])
myfont = ("Arial", 16, "bold")
window = Tk()
window.title("counters")
width = HOW_MANY * 100
window.geometry(str(width) + "x120")
frame = Frame(window)
frame.grid()
for i in range(HOW_MANY) :
new_button = Button(frame, font = myfont, text = 0,
fg = "blue", bg = "white", width = 4, height = 2)
new_button.grid(row = 1, column = i, padx = 10, pady = 10 )
all_buttons = all_buttons + [new_button]
# connect the handle function as the event handler for left-mouse clicks:
new_button.bind("<Button-1>", handlePress)
window.mainloop()
ENDFIGURE========================================================
Because the event argument is customized to hold the name of the
button pressed, the handlePress function can search its
list of all_buttons to learn which button was pressed and
which count value should be increased.
The third solution to the problem of customized event handling for each button is to write a ``mini-module'' that holds one counter variable and one event-handling function for each button constructed. We ``import'' the mini-module each time we construct a new button.
More precisely stated, we write our own class and use it to construct a new controller object (counter variable plus event-handling function) that connects to each new button we construct.
Here is what the 8-button GUI looks like. It uses the class we write
ourselves:
FIGURE===========================================================
"""The view module shows the GUI for the counters."""
from Tkinter import *
import CounterButton # this module holds the class we wrote
myfont = ("Arial", 16, "bold")
HOW_MANY = 8
window = Tk()
window.title("Lots of counters")
width = HOW_MANY * 100
window.geometry(str(width) + "x120")
frame = Frame(window)
frame.grid()
# Construct the buttons and controllers and connect them:
for i in range(HOW_MANY) :
button = Button(frame, font = myfont, text = "0",
fg = "blue", bg = "white", width = 4, height = 2)
button.grid(row = 1, column = i, padx = 10, pady = 10 )
# use class Control, the class we wrote within module CounterButton,
# to construct new a controller object:
controller = CounterButton.Control(button)
# connect the controller object's handle function as the event handler:
button.configure(command = controller.handle)
window.mainloop()
==================================================================
Within another module, CounterButton.py, there is
a class named class Control. This class, a ``mini-module,''
holds its own counter variable and a function named handle.
The command:
controller = CounterButton.Control(button)
``imports'' the class and builds a new object (namespace)
with its own variable and function. The address of the namespace
is assigned to variable controller.
The next command,
button.configure(command = controller.handle)
uses the handle function for the new object as the event-handling
function for the new button. In this way, each button is attached
to a distinct counter variable and event-handling function.
Here is module CounterButton; you will see
it holds class Control. You must look closely
to see where the counter variable is hiding; you will find
it within the start-up commands in function __init__:
FIGURE=========================================================
"""The CounterButton module contains the class that builds a customized
controller for the each button of the GUI.
"""
from Tkinter import *
total_presses = 0 # remembers how many times _all_ buttons were pressed
class Control(object) :
"""Control constructs a customized controller that remembers the
number of times the corresponding button is pressed.
The controller holds the event-handling function.
It also remembers these attributes:
mybutton - the button that this controller is connected to
mycount - a nonnegative int, how many times the button is pressed
"""
def __init__(self, b) :
"""init constructs the controller
parameters: self - the new controller object we are constructing
b - the button controlled by this controller
"""
# create these two variables in the controller's namespace:
self.mybutton = b
self.mycount = 0
def handle(self) :
"""This is the event handler for the button."""
global total_presses
# update mycount in this controller's namespace:
self.mycount = self.mycount + 1
# reconfigure the text on the button controlled by this controller:
self.mybutton.configure(text = str(self.mycount))
# update the total count of button presses and print it:
total_presses = total_presses + 1
print total_presses
======================================================================
Here is what happens when we construct a new
Control object, with the command,
controller = CounterButton.Control(button)
Control.__init__(self = addr1, b = button)is called. That is, the __init__ function inside class Control is called.
The __init__ function contains the start-up commands for building the new object. Its first argument gives the address of its own new namespace, and the second argument gives the address of the button object it will connect to.
self.mybutton = b self.mycount = 0The first command saves the value of parameter, b as variable mybutton in the object's new namespace. (Read this as saying, ``namespace self gets the variable mybutton with value b.'') The second command creates a new variable, mycount, in the namespace and sets it at 0.
The style of the commands is meant to match the style we use to reference variables and functions within a module.
Here is a diagram of the program and its storage after the
buttons and their matching controllers are constructed:
Each button object has a namespace, and the command
attribute (variable) in each namespace is connected
to the namespace and function of the corresponding
control object and event handler.
Say that the second button on the GUI is pressed; its namespace lives at addr5. The operating system contacts the Python interpreter, which contacts Tkinter, which locates the namespace at addr5.
Here is what happens next:
Control.handle(self = addr6)because addr6 is the namespace of the Control object.
global total_presses self.mycount = self.mycount + 1 self.mybutton.configure(text = str(self.mycount)) total_presses = total_presses + 1 print total_pressesFirst, self.mycount = self.mycount + 1 makes the mycount variable in the addr6 namespace increase by one. Next, self.mybutton computes to the value addr6.mybutton, which computes to addr5. This calls the function, configure(self = addr5, text = "1"), which reconfigures the text so that the button displays 1. Next, the global variable, total_presses, which is shared by all the controller objects, is incremented and printed.
Here is a small modification to the above exercise.
What if we change the behavior so that,
when one button is pressed, all the other buttons ``go blank''?
That is, if we start like this
and we press the rightmost button, we see this:
If we next press the leftmost button three times in succession, we see this:
and when we next press the rightmost button, we see this:
The underlying controllers are correctly maintaining
the courts for the buttons, but only the count for the most recently
pressed button is displayed. To implement this behavior,
the controller that reacts to a
button press asks all the other buttons' controllers to erase the text
from the faces of the buttons they control.
We make this behavior come to life by inserting an erase
function in class Control:
def erase(self) :
"""erases the text on the button controlled by this controller"""
self.mybutton.configure(text = "")
Next, we make a master list of all the controllers, so that when
a button is pressed, we systematically ask all controllers to erase
the text from the buttons:
for controller in master_controller_list :
controller.erase()
Here are the pieces, fitted together, in the modified controller module:
FIGURE=============================================================
"""CounterButton module contains the class that builds a customized
controller for the each button of the GUI.
"""
from Tkinter import *
total_presses = 0 # remembers how many times _all_ buttons were pressed
master_controller_list = [] # master list of all controllers we build
class Control(object) :
"""Control constructs a customized controller that remembers the
number of times the corresponding button is pressed.
The controller holds the event-handling function.
It also remembers these attributes:
mybutton - the button that this controller is connected to
mycount - a nonnegative int, how many times the button is pressed
"""
def __init__(self, b) :
"""init constructs the controller
parameters: self - the new controller object we are constructing
b - the button controlled by this controller
"""
global master_controller_list
# create these two variables in the controller's namespace:
self.mybutton = b
self.mycount = 0
master_controller_list.append(self) # add new controller to master list
def handle(self) :
"""This is the event handler for the button."""
global total_presses, master_controller_list
# tell all the controllers to erase each button's text"
for controller in master_controller_list :
controller.erase()
# update mycount and display it on my button:
self.mycount = self.mycount + 1
self.mybutton.configure(text = str(self.mycount))
# update the total count of button presses and print it:
total_presses = total_presses + 1
print total_presses
def erase(self) :
"""erases the text on the button controlled by this controller"""
self.mybutton.configure(text = "")
==================================================================
Each time a new controller object is constructed, its __init__
function appends the new object's address
to the master_controller_list.
In the handle function, each controller in
master_controller_list is called to erase the text from its
button.
This technique, where controllers call one another, is standard in complex GUI building.
If the game board, pieces, and the rules for using the board are complex, it is best to isolate the board, its squares, its playing pieces, and its laws in a separate model module. Then, when a button is pressed, signifying a ``move'' of a playing piece, the corresponding controller's event-handling function asks the model module to move the piece on the corresponding square of the board. In this way, we have an important division of labors in the program:
Here is the previous example rebuilt with an explicit model module,
which remembers the contents of the 8-celled game board.
Its maintenance functions are called when a move is made or an object
wishes to know the contents of the board:
================================================================
"""GameBoard models a simple game board that keeps numbers on
each of its squares.
"""
SIZE = 8
board = 8 * [0] # the game board
def getContentsAt(squarenum) :
"""getContentsAt returns the number resting on the board at
position squarenum.
parameter: squarenum, an int
precondition: 0 <= squarenum < SIZE
returns: the value at board[squarenum]. If squarenum is
out of bounds, returns -1.
"""
if squarenum < 0 or squarenum >= SIZE :
answer = -1
else :
answer = board[squarenum]
return answer
def updateAt(squarenum, howmuch) :
"""updateAt alters the board at position squarenum by howmuch.
parameters: squarenum, howmuch, are ints
precondition: 0 <= squarenum < SIZE
postcondition: board[squarenum]_new = board[squarenum]_old + howmuch
"""
if squarenum >= 0 and squarenum < SIZE :
board[squarenum] = board[squarenum] + howmuch
def toString() :
"""toString returns a string representation of the board
for printing for trace and debugging purposes
"""
answer = ""
for square in board :
answer = answer + " " + str(square)
return answer
====================================================================
The revised controller objects
no longer remember the counts for their respective
buttons. This job has been entrusted to the model module.
Each controller merely triggers computation in the model module
and tells its button to reset its text:
===============================================================
"""Controllers contains the class that builds a customized
controller for the each button of the GUI. Each controller
asks the GameBoard module for the correct number to display.
"""
from Tkinter import *
import GameBoard
controller_list = [] # master list of all controllers we build
class Control(object) :
"""Control constructs a customized controller that remembers the
number of times the corresponding button is pressed.
The controller holds the event-handling function.
It remembers this attribute:
mybutton - the button that this controller is connected to
"""
def __init__(self, b, index) :
"""init constructs the controller
parameters: self - the new controller object we are constructing
b - the button controlled by this controller
index - the gameboard square represented by button b
"""
global controller_list
# create these two variables in the controller's namespace:
self.mybutton = b
self.myindex = index
controller_list.append(self) # add new controller to master list
def handle(self) :
"""This is the event handler for the button."""
global total_presses, cotroller_list
# tell all the controllers to erase all the buttons' text"
for controller in controller_list :
controller.erase()
# Ask the GameBoard to update:
GameBoard.updateAt(self.myindex, howmuch = 1)
# Ask the GameBoard for the new contents of the square:
new_contents = GameBoard.getContentsAt(self.myindex)
self.mybutton.configure(text = str(new_contents))
def erase(self) :
"""erases the text on the button controlled by this controller"""
self.mybutton.configure(text = "")
===================================================================
Finally, the view module remains essentially unchanged:
==================================================================
"""The view module shows the GUI for the simple gameboard."""
from Tkinter import *
import Controllers
import GameBoard
myfont = ("Arial", 16, "bold")
SIZE = GameBoard.SIZE
window = Tk()
window.title("One-row gameboard")
width = SIZE * 100
window.geometry(str(width) + "x120")
frame = Frame(window)
frame.grid()
for i in range(SIZE) :
button = Button(frame, font = myfont, text = "0",
fg = "blue", bg = "white", width = 4, height = 2)
button.grid(row = 1, column = i, padx = 10, pady = 10 )
controller = Controllers.Control(button, i)
button.configure(command = controller.handle)
window.mainloop()
================================================================
A class is a ``mini-module,'' and its start-up commands must be contained in a specially named function, __init__. When a class is used to ``import'' (build a namespace for) an object, the commands in the __init__ function define the variables that will be saved in the namespace. The init function is also called a constructor method.
The class also contains additional functions, which are called by other modules and objects to ``enter'' the class and use its variables. Such functions are called methods.
The syntax format looks like this:
============================
class NAME ( BASE_CLASS ) :
def __init__(self, ... ) :
"""constructs the object (namespace) in heap storage. The
address of the new object is assigned to the parameter, self.
The object's variables are saved in the namespace named by self.
"""
. . .
self.mydata = ... # make a variable, mydata, in the object's namespace
. . .
# the function automatically returns the value of self as its answer.
def f(self, ... ) :
"""method f can lookup and alter the variables in the namespace
named by self.
"""
. . . self.mydata . . .
====================================
For now, the BASE_CLASS is object, but we will see in a later example why this keyword might change.
When the class is used to construct an object, we write this:
x = NAME( ... )
The occurrence of NAME on the right-hand side of the assignment
is a disguised call of the __init__ function. Indeed, the Python
interpreter reformats the above assignment into a ``module'' call in
dot notation:
x = NAME.__init__( self = getNewAddressInHeapForTheNewObject, ... )
The __init__ function returns the address,
getNewAddressInHeapForTheNewObject and it is assigned
to x.
A method within the object is called like this:
x.f(...)
Again, the Python intepreter reformats this call, to look like this:
NAME.f(self = x, ...)
The address held by variable x is
assigned to parameter self in handle, correctly
connecting the object to its function.
Here is a side-by-side comparison of how we write a class and how we
write a module. The class is placed in a module by itself, where
it can use global variables of its own; this is the usual format.
===============================================================
In Module1.py: In Module2.py:
g = ... # global var to be g = ...
# shared by all objects
class C(object) :
def __init__(self,p,...) # these are global vars, too:
self.x = p # define vars in x = ... # no easy way to obtain p
self.y = ...# object's n.s. y = ...
def f(self,...) : def f(...) :
global g global x,y,g
... self.x ... self.y ... g ... ... x ... y ... g ...
===================================================================
When we import a module, we can do it at most once, but we can
``import'' (construct) multiple objects from the same class.
Then we can use the functions in each.
The comparison looks like this:
==================================================================
In Main1.py: In Main2.py:
import Module1 import Module2
# there is only
d1 = Module1.C(arg1,...) # one namespace
# translates internally to
# d1 = Module1.C.__init__(self=newaddr1,arg1,...)
# d1 is assigned newaddr1
d2 = Module1.C(arg2,...)
# translates internally to
# d2 = Module1.C.__init__(self=newaddr2,arg1,...)
# d2 is assigned newaddr2
... d1.f(...) ... ... Module2.f(...) ...
# translates internally to # there is only one
# ... Module1.C.f(self=d1,...) ... # function, f
... d2.f(...) ...
# translates internally to
# ... Module1.C.f(self=d2,...) ...
===================================================================
Of course, if we import a module, M, with the
command, from M import *, then
we rewrite the above examples to read like this:
==================================================================
from Module1 import * from Module2 import *
d1 = C(arg1,...)
d2 = C(arg2,...)
... d1.f(...) ... ... f(...) ...
... d2.f(...) ...
====================================================================
Classes are often used to build simulations and ``virtual reality'' inside the computer, where real-life objects (cars or clouds or genes or ants) are modelled by objects constructed from classes. Often, this has nothing to do with GUIs, but it has everything to do with constructing multiple objects that are ``born,'' ``live,'' ``communicate,'' and ``die'' within computer storage.
We now study an example of this form of class writing:
It is a
class Smiley, which constructs silly biology life forms that
remember their name (a string) and age (an int).
The name and the age are the ``data structure'' held within the object.
FIGURE 1==============================================
class Smiley(object) :
"""Smiley objects know their name and age."""
def __init__(self, new_name) :
"""__init__ constructs a new Smiley object.
It holds two variables: name, a string
age, an integer (initialized to 0).
parameters: self - the new object we are constructing
new_name - the string that we assign to variable, name
"""
self.name = new_name # make the object hold a name
self.age = 0 # make the object hold an age
print "Smiley object named", self.name, "constructed." # print trace info
def talk(self) :
"""talk makes an object say its name and age.
parameter: self - the object that will talk
"""
print "(-: I am", self.name, "and I am", self.age, "days old."
self.age = self.age + 1
def tellSecret(self) :
"""tellSecret makes an object tell a ``secret message''
parameters: self - the object that will tell the secret
returns: a string, the secret
"""
return "I don't know"
ENDFIGURE =================================================
Class Smiley's __init__ function
constructs an object that holds two variables: name and
age. The two assignments,
self.name = new_name # make the object hold a name
self.age = 0 # make the object hold an age
insert the two variables into the new object and give them values.
As stated earlier, the name, self, refers to the object that we are building. The name must be listed as the first argument to __init__.
Next, function talk is called when we want to make an object print its name and age. The function again uses the name, self, to refer to the object that from which one extracts the name and age for printing. The third function, tellSecret, will be improved later.
The class can be typed as shown above, just like it was a big
function, into a program.
Or, it can be placed in a file by itself, like a module.
Say that we place the class into a module and test it interactively:
$ python -i Smile.py
>>>
The Python interpreter reads and saves the definition of class Smiley.
We can try constructing some
objects from the class. Here's how:
s1 = Smiley("larry")
This constructs an object and names it s1. It is a bit
like ``importing'' a ``module'' and naming it s1. Let's
review the
key steps taken:
Smiley.__init__(addr1, "larry")That is, the __init__ function within class Smiley starts, where addr1 is the argument assigned to parameter self. When the function executes, the object (namespace) at address addr1 gets the name, "larry":
self.name = new_name # make the object hold a name self.age = 0 # make the object hold an age
s1 : addr1
addr1 : +------------- | name : "larry" | age : 0 | class : Smiley +------------(The third variable is automatically inserted; it remembers the class from which the object was constructed.)
We use an object's functions just like the object was a little module:
s1.talk()
The function call executes the talk function using the object
(namespace) for s1.
This prints
(-: I am larry and I am 0 days old.
Let's understand the precise semantics of the execution:
print "(-: I am", self.name, "and I am", self.age, "days old."and we see printed
(-: I am larry and I am 0 days old.because self.name refers to the string held in variable name in the object at addr1.
self.age = self.age + 1updates the age variable in the same object.
Now, if we repeat the same command, s1.talk(), this calls
the same function using the same object and prints
(-: I am larry and I am 1 days old.
We can create a second, distinct, new object with this command:
s2 = Smiley("moe")
This command constructs an object, say, at address addr2,
in heap storage, calls function Smiley.__init__(addr2, "moe"),
and assigns addr2 to s2.
When these steps finish, heap storage now holds two objects:
addr1 :
+--------------
| name: "larry"
| age: 2
| class: Smiley
+-------------
addr2 :
+--------------
| name: "moe"
| age: 0
| class: Smiley
+--------------
We can tell the new object to talk:
s2.talk()
and it of course says,
(-: I am moe and I am 0 days old.
Here is the complete program we just developed:
FIGURE 2==================================
"""Test defines class Smiley and builds Smiley objects."""
class Smiley(object) :
def __init__(self, new_name) :
self.name = new_name # make the object hold a name
self.age = 0 # make the object hold an age
print "Smiley object named", self.name, "constructed."
def talk(self) :
print "(-: I am", self.name, "and I am", self.age, "days old."
self.age = self.age + 1
def tellSecret(self) :
return "I don't know"
# Test the class by constructing Smiley objects and asking them to talk:
s1 = Smiley("larry") # Smiley("larry") does these steps:
# (1) it constructs a Smiley object in heap storage,
# say, at address addr1
# (2) it calls function Smiley.__init__(addr1, "larry")
# (3) it returns as its answer, addr1
#
# At address addr1 in heap storage is now an object
# (actually, a namespace) that holds three variables:
# name: "larry"
# age: 0
# class: Smiley
s1.talk() # s1.talk() is shorthand for Smiley.talk(s1)
s1.talk()
s2 = Smiley("moe")
s2.talk() # s2.talk() is shorthand for Smiley.talk(s2)
s1.talk()
s3 = Smiley("curly")
s1.talk()
s2.talk()
s3.talk()
ENDFIGURE=====================================
Within the script, we use the names s1, s2, and s3
for the three distinct objects --- we never see or learn the values
of the objects' addresses in heap storage.
Here is a diagram of computer storage that shows the program,
the three objects, and the execution of the program's very last command,
s3.talk():
To summarize,
Now, here is another program that uses the class:
from TextSmiley import Smiley
s1 = Smiley("larry")
s1.talk()
s1.talk()
s2 = Smiley("moe")
# and so on....
That is, file TextSmiley is a module that holds as its contents
class Smiley. So, we must import the class from its file (module)
to use it to construct objects. This is the style used by Dawson,
and we will follow this style.
But if we wished, we could also say this:
import TextSmiley
s1 = TextSmiley.Smiley("larry")
. . .
Take your pick.
This is what Dawson does in Chapter 10 --- Python has several modules of GUI-classes that you import and use. To do this correctly, you must first learn how to extend an existing, pre-written class with additional functions. This is done by writing another class, a subclass that extends the pre-written class with new functions.
To do this, we code an extension --- a subclass --- of Smiley,
called SmartSmiley, that holds the revised coding of
tellSecret and a new function, learnsSecretOf.
Here is the subclass:
FIGURE 3========================================================
from TextSmiley import Smiley
class SmartSmiley(Smiley) : # this class extends class Smiley
"""SmartSmiley is a Smiley that can also remember and tell secrets."""
def __init__(self, new_name, my_secret) :
"""__init__ constructs a new SmartSmiley object.
It holds the variables from a Smiley -- name and age -- along with
a new variable, secret.
parameters: self - the new object we are constructing
new_name - a string that we assign to name
my_secret - a string that we assign to secret
"""
Smiley.__init__(self, new_name) # call Smiley's __init__ function
self.secret = my_secret # define a new variable, secret
def tellSecret(self) : # this cancels the version of tellSecret in Smiley
"""tellSecret makes an object tell a ``secret message''.
parameters: self - the object that will tell the secret
returns: a string, the secret
"""
return self.secret
def learnsSecretOf(self, anotherSmiley) :
"""learnsSecretOf lets one object learn another object's secret message
parameters: self - the object that will learn a new secret
anotherSmiley - a Smiley object that will surrender its secret
"""
self.secret = self.secret + ", and " + anotherSmiley.tellSecret()
ENDFIGURE=============================================
The header line shows that
class SmartSmiley contains all the coding of class Smiley.
Next, there are three functions:
Smiley.__init__(self, new_name)calls Smiley's __init__ function, so that the name and age variables can be initialized. The next command,
self.secret = my_secretdefines the new variable, secret.
def learnsSecretOf(self, anotherSmiley) : self.secret = self.secret + ", and " + anotherSmiley.tellSecret()
Let's use class SmartSmiley. We might start like this:
s1 = SmartSmiley("larry", "eat your vegetables")
s1.talk()
print s1.tellSecret()
This prints:
Smiley object named larry constructed.
(-: I am larry and I am 0 days old.
eat your vegetables
Inside heap storage, is the object:
addr1 :
+--------------
| name: "larry"
| age: 1
| secret: "eat your vegetables"
| class: SmartSmiley
+--------------
Let's make a second object:
s2 = SmartSmiley("moe", "get enough sleep")
Heap storage looks like this:
addr1 :
+--------------
| name: "larry"
| age: 1
| secret: "eat your vegetables"
| class: SmartSmiley
+--------------
addr2 :
+--------------
| name: "moe"
| age: 0
| secret: "get enough sleep"
| class: SmartSmiley
+--------------
Let's make the object give its secret to the first object:
s1.learnsSecretOf(s2)
print s1.tellSecret()
The second command prints
eat your vegetables, and get enough sleep
showing that the secret held within s2 was appended to the
secret held by s1.
Now, we can still use the orginal class Smiley:
s3 = Smiley("curly")
Heap storage looks like this:
addr1 :
+--------------
| name: "larry"
| age: 1
| secret: "eat your vegetables, and get enough sleep"
| class: SmartSmiley
+--------------
addr2 :
+--------------
| name: "moe"
| age: 0
| secret: "get enough sleep"
| class: SmartSmiley
+--------------
addr3 :
+-------------
| name: "curly"
| age: 0
| class: Smiley
+-------------
Next, when we try
print s3.tellSecret()
the command prints,
I don't know, because the coding of tellSecret
for Smiley objects has no secret.
We can even do this:
s1.learnsSecretOf(s3)
print s1.tellSecret()
and we see that s1 has learned
eat your vegetables, and get enough sleep, and I don't know
But if we try this:
s3.learnsSecretOf(s1)
the Python interpreter shows us this error message:
Traceback (most recent call last):
File "Smiley2.py", line 49, in ?
test()
File "Smiley2.py", line 44, in test
s3.learnsSecretOf(s1)
AttributeError: 'Smiley' object has no attribute 'learnsSecretOf'
The message tells us that s3 does not own a function named
learnsSecretOf (because s3 is a Smiley object
and not a SmartSmiley object).
Here is program that includes the subclass and the tests we tried:
FIGURE 4==================================
"""Test2 contains class SmartSmiley and some tests of SmartSmiley objects."""
from TextSmiley import Smiley
class SmartSmiley(Smiley) : # this class extends class Smiley
def __init__(self, new_name, my_secret) :
Smiley.__init__(self, new_name) # call Smiley's __init__ function
self.secret = my_secret # define a new variable, secret
def tellSecret(self) : # this cancels the version of tellSecret in Smiley
return self.secret
def learnsSecretOf(self, anotherSmiley) :
self.secret = self.secret + ", and " + anotherSmiley.tellSecret()
s1 = SmartSmiley("larry", "eat your vegetables")
s1.talk()
print s1.tellSecret()
s2 = SmartSmiley("moe", "get enough sleep")
s1.learnsSecretOf(s2)
print s1.tellSecret()
s3 = Smiley("curly")
print s3.tellSecret()
s1.learnsSecretOf(s3)
print s1.tellSecret()
s3.learnsSecretOf(s1)
FIGURE 4==================================
Here is a diagram of the program at the point when
s1.learnsSecretOf(s3) executes and it calls the tellSecret
function with object s3:
The GUI is constructed like the:
==================================
"""The view module shows the GUI for the counters."""
from Tkinter import *
import CountController # module that holds class CounterButton
myfont = ("Arial", 16, "bold")
HOW_MANY = 8
window = Tk()
window.title("Lots of counters")
width = HOW_MANY * 100
window.geometry(str(width) + "x120")
frame = Frame(window)
frame.grid()
for i in range(HOW_MANY) :
# construct a button customized with its own counter and event handler:
button = CountController.CounterButton(frame)
button.grid(row = 1, column = i, padx = 10, pady = 10 )
window.mainloop()
==============================================
Class CounterButton
uses the techniques we learned when we experimented
with Smiley objects.
Here is the module and the class:
=============================================
"""The CountController module contains the class that builds a customized
button/controller for the GUI.
"""
from Tkinter import *
total_presses = 0 # how many times _all_ buttons were pressed
class CounterButton(Button) :
"""CounterButton constructs a customized button with its own control
that remembers the number of times the button is pressed.
It holds this attribute:
mycount - a nonnegative int, how many times the button is pressed
"""
def __init__(self, parent) :
"""init constructs the controller
parameters: self - the new controller object we are constructing
parent - the parent widget
"""
Button.__init__(self, parent, text = "0",
font = ("Arial", 14, "bold"), fg = "blue", bg = "white",
width = 4, height = 2)
self.mycount = 0
self.configure(command = self.handle) # see immediately below
def handle(self) :
"""The event handler for the button."""
global total_presses
self.mycount = self.mycount + 1
self.configure(text = str(self.mycount))
total_presses = total_presses + 1
print total_presses
========================================
CounterButton is a subclass of Button.
(See again the class's header line:
class CounterButton(Button).)
Its __init__ method calls the __init__ method of
Button to build the usual, generic button.
The command,
self.configure(command = self.handle)
connects the button to the handle method contained within
this very object, and
self.mycount = count
adds a variable to the button's namespace to remember the count of
button presses for this button.
The handle method is the event handling function. When the button is pressed, the method looks into its own namespace to find self.mycount, which it increments and displays.
A typical example
is Dawson's ``click counter,'' which is
a Frame that holds a single button whose label states how many
times the button is pressed:
Here is Dawson's coding of the frame and its button and the code that
updates the button's label. Notice that Dawson
defines a ``customized frame'' for his GUI. (There is no strong
reason to do this, but try to understand what he is doing.)
FIGURE==============================
# Click Counter
# Demonstrates binding an event with an event handler
# Michael Dawson - 6/6/03
from Tkinter import *
class Application(Frame):
""" GUI application which counts button clicks. """
def __init__(self, master):
""" Initialize the frame. """
Frame.__init__(self, master)
self.grid()
self.bttn_clicks = 0 # the number of button clicks
self.create_widget()
def create_widget(self):
""" Create button which displays number of clicks. """
self.bttn = Button(self)
self.bttn["text"]= "Total Clicks: 0"
self.bttn["command"] = self.update_count
self.bttn.grid()
def update_count(self):
""" Increase click count and display new total. """
self.bttn_clicks += 1
self.bttn["text"] = "Total Clicks: " + str(self.bttn_clicks)
# main
root = Tk()
root.title("Click Counter")
root.geometry("200x50")
app = Application(root)
root.mainloop()
ENDFIGURE==============================
Dawson's customized frame --- a subclass of Frame ---
holds the button's event handling function, update_count.
Dawson also simplifies his __init__ method by calling a helper
method, create_widget, to do the details of building a button.
Finally, note the command,
self.bttn["command"] = self.update_count
which connects the button object to its event handling function,
update_count.
This is exactly the same as saying,
self.bttn.configure(command = self.update_count)
The former works because object-namespaces in Python as saved
as dictionaries in the heap.
A collection of related classes, like Tkinter, that help you build a complex program, like a GUI, is called a framework. When you use a drag-and-drop tool, like Visual Basic, to build programs with GUIs, you are using a framework.
In Chapters 11 and 12, Dawson demonstrates how to use the pygame and livewires modules to build animations. Within the modules are classes that can display images and moving objects within a frame. The key classes are class Screen (builds an animation screen) and class Sprite (buids moving objects).
A sprite is an object that moves within a screen. A sprite object has an appearance, a position, and a velocity.
A position is an x,y integer pair. A screen's pixels are numbered just like the cells in a grid. For example, position (0,0) is the screen's upper left corner and (150, 300) is the pixel that is located 150 pixels to the right and 300 pixels down from the upper left corner.
A velocity is an x,y integer pair that can be added to a position to make an object ``move.'' For example, a velocity of (2, -1) would make an object move rightward 2 pixels and upwards one pixel after one clock tick. For example, if an object is located at position (150, 300), then a velocity of (2, -1) would make the object move to (152, 299).
When an animation starts, the animation controller starts a clock. Each ``clock tick'' is an event, and at each event, the animation controller moves each sprite according to the position and velocity saved within the sprite's namespace. Then the controller calls each sprite's event handling function, named moved, to see if the sprite has other activities to do.
Sprites lie at the heart of computer animations. Consider Dawson's
``bouncing pizza'' animation from Chapter 11, which constructs
a screen and places one pizza sprite within it:
Each clock tick, the pizza moves and its
moved function is called. The latter
checks if the pizza has reached an edge of the screen; if so,
the sprite's velocity is reversed, causing it to ``bounce'':
BEGINFIGURE============================================
# Bouncing Pizza
# Demonstrates dealing with moving sprites and screen boundries
# Michael Dawson 5/11/03
from livewires import games
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480
class Pizza(games.Sprite):
""" A bouncing pizza. """
def __init__(self, screen, x, y, image, dx, dy):
""" Initialize pizza object. """
self.init_sprite(screen = screen, x = x, y = y,
image = image, dx = dx, dy = dy)
def moved(self):
""" Reverse a velocity component if edge of screen reached. """
dx, dy = self.get_velocity()
if self.get_right() > SCREEN_WIDTH or self.get_left() < 0:
self.set_velocity(-dx, dy)
if self.get_bottom() > SCREEN_HEIGHT or self.get_top() < 0:
self.set_velocity(dx, -dy)
# main
my_screen = games.Screen(width = SCREEN_WIDTH, height = SCREEN_HEIGHT)
wall_image = games.load_image("wall.jpg", transparent = False)
my_screen.set_background(wall_image)
pizza_image = games.load_image("pizza.bmp")
Pizza(screen = my_screen, x = SCREEN_WIDTH/2, y = SCREEN_HEIGHT/2,
image = pizza_image, dx = 1, dy =1)
my_screen.mainloop()
ENDFIGURE================================================
The program imports the animation module, livewires (which imports another module, pygame.) Class Pizza is a subclass of Sprite, and the __init__ function merely constructs the sprite. (Notice the keyword parameters, which tell us that a generic sprite must be constructed with the address of its parent screen, its x,y position, its dx,dy velocity, and an image to display.) The real reason for writing class Pizza is to include the moved event handling function, which reads the sprite's velocity and position and resets the former if the latter has hit the screen's edge.
The main program constructs the screen, loads a background image into it, and constructs the moving pizza. Note how bitmap (bmp) and jpeg (jpg) pictures must be made into ``image'' objects before they are inserted into screen and sprite, respectively.
When you run this animation, you will see that the pizza travels as if it were weightless in space. To program the proper physics of a pizza's bounce, influenced by mass and gravity, we must use calculus, like Newton did, to define the velocity (more precisely, to define the change in velocity --- acceleration). We won't do this here; instead, we move the pizzas to outer space.
When a pizza hits the pan, both explode (as portrayed by playing an animation loop of eight jpeg images, taken from Dawson's asteroids game in Chapter 12.)
The animation is constructed from two sprites, Pan
and Pizza. Here is class Pan:
FIGURE================================================
# Program Collide demonstrates how to use the mouse to move a sprite,
# how to detect collisions of sprites, and how to run an animation loop:
# The user moves a pan with the mouse, trying to avoid collision with a pizza.
import random
from livewires import games, color
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480
THE_SCREEN = games.Screen(SCREEN_WIDTH, SCREEN_HEIGHT)
### THE PAN SPRITE:
class Pan(games.Sprite):
"""Class Pan models a pan controlled by the player.
"""
def __init__ (self, x, y):
"""Initialize pan object
Parameters: x,y - ints, the initial position of the pan
"""
self.init_sprite(screen = THE_SCREEN, x = x, y = y,
image = games.load_image("pan.bmp"))
def moved(self):
"""moved is the pan's event handling function, activated after
each clock tick
"""
# move the pan to the current mouse coordinates:
x, y = self.screen.mouse_pos()
self.move_to(x, y)
# don't let the pan escape from the screen:
if self.get_left() < 0:
self.set_left(0)
if self.get_right() > SCREEN_WIDTH:
self.set_right(SCREEN_WIDTH)
self.checkForCollision()
def checkForCollision(self):
"""checkForCollision checks if the pan has collided with any other
sprites. If it collides with a pizza, then the game is over
"""
for ob in self.overlapping_objects():
if isinstance(ob, Pizza) : # is the object a Pizza ?
ob.destroy() # erase the object from heap storage
makeBigExplosion(self.get_xpos(), self.get_ypos()) # see below
self.destroy()
gameOver() # see below
# continued...
ENDFIGURE========================================================
The moved function shows that the pan is continually moved
to the current mouse position on the screen. Like the bouncing
pizza in the earlier example, the pan is not allowed to move past
the edge of the screen.
The last command in moved calls the pan's checkForCollision function, which uses a sprite's overlapping_objects method to see if any other sprite might have collided with the pan. If the sprite is indeed an instance of the Pizza class (isinstance(ob, Pizza)), then the colliding objects are removed from the animation (via a sprite's destroy method), the animation loop is played (makeBigExplosion), and the animation is stopped (game_over) --- see below.
Here is the pizza sprite:
FIGURE (cont.)========================================================
### THE PIZZA SPRITE:
PIZZA_IMAGE = games.load_image("pizza.bmp")
PIZZA_SPEED = 1 # pixels per clock tick
TIME_FOR_ANOTHER_PIZZA = 150 # in clock cycles
class Pizza(games.Sprite):
"""Constructs flying pizzas that hold an internal clock so that the
pizza replicates when the clock says it is TIME_FOR_ANOTHER_PIZZA.
"""
def __init__(self, x, y):
""" Initialize a pizza object.
Parameters: x,y - ints, the initial position of the pizza
"""
self.clock = 0
# randomly calculate a direction and velocity in the range -2..2:
startdx = random.choice([2, -2]) * random.random()
startdy = random.choice([2, -2]) * random.random()
self.init_sprite(screen = THE_SCREEN, x = x, y = y,
dx = startdx, dy = startdy, image = PIZZA_IMAGE)
def moved(self):
"""moved ``bounces'' the pizza when it hits the screen's edge.
It also increments the pizza's clock and spawns a new pizza
when it is TIME_FOR_ANOTHER_PIZZA.
"""
dx, dy = self.get_velocity()
if self.get_right() > SCREEN_WIDTH or self.get_left() < 0:
self.set_velocity(-dx, dy)
if self.get_bottom() > SCREEN_HEIGHT or self.get_top() < 0:
self.set_velocity(dx, -dy)
self.clock = self.clock + 1
if self.clock == TIME_FOR_ANOTHER_PIZZA :
Pizza(self.get_xpos(), self.get_ypos()) # spawn a new pizza
self.clock = 0
# continued...
ENDFIGURE===============================================================
Each pizza is constructed with an internal clock that is incremented
at each clock-tick event. A pizza's velocity is randomly calculated to be
a pair, x,y, where each integer falls in the range of -2 and 2.
A pizza's moved function makes the pizza bounce when the pizza hits the screen's edge. More importantly, when the pizza's internal clock reaches the value, TIME_FOR_ANOTHER_PIZZA, the pizza spawns a copy of itself (Pizza(self.get_xpos(), self.get_ypos())).
The main program constructs the screen and places within it the pan and one
pizza. Two helper functions are called when there is a collision of
the pan with a pizza and the animation must end:
makeBigExplosion builds a list of 9 bitmap
images and uses livewires's Animation class
to construct a ``film loop'' object that plays the images one after another,
and gameOver displays a shutdown message and tells the
screen to terminate after 250 final clock ticks.
To install them, load the CD and open it to
its start_here web page (the one that shows the cover of the text).
Click on I agree and Software.
Click on the link to install Pygame 1.5.6 first. Tell the installer
program to install pygame at the folder,
C:\Python22\Lib\site-packages.
Next,
click on the link to Open the livewires archive.
Within the new window that appears, you will see a folder named
livewires. Copy that folder into
C:\Python22\Lib\site-packages.
(You don't have to execute the setup program --- indeed,
do not --- it does not function correctly.)
If you have difficulties installing either package, speak with the instructor.
A class looks like a module --- it has code for building a data structure
and functions for maintaining the data structure.
The code for building the data structure must be inserted inside a special
function, a constructor function, named __init__.
There are additional functions, such as the event handling function,
handle. The class has this form:
Once the object is constructed, the new address is assigned to variable
x. A method within the object is called like this:
FIGURE (concl.)=====================================================
### MAIN PROGRAM --- HELPER FUNCTIONS:
def gameOver():
""" End the game. """
games.Message(screen = THE_SCREEN,
x = SCREEN_WIDTH/2, y = SCREEN_HEIGHT/2,
text = "Game Over", size = 90, color = color.red,
lifetime = 250, after_death = THE_SCREEN.quit)
def makeBigExplosion(x, y):
"""makeBigExplosion runs twice a loop of nine images, making an explosion.
Parameters: x, y - ints, the coordinates of the explosion
"""
# build loop of images, explosion_i.bmp :
images = []
for i in range(1, 10):
file_name = "explosion" + str(i) + ".bmp"
images.append(file_name)
games.Animation(screen = THE_SCREEN, x = x, y = y,
images = images, n_repeats = 2,
repeat_interval = 4 )
### MAIN PROGRAM:
nebula = games.load_image("nebula.jpg", transparent = False)
THE_SCREEN.set_background(nebula)
# THE_SCREEN.mouse_visible(False)
Pan(x = SCREEN_WIDTH/2, y = 435)
Pizza(x = random.randrange(SCREEN_WIDTH), y = random.randrange(SCREEN_HEIGHT))
THE_SCREEN.mainloop()
ENDFIGURE===========================================================
Dawsons's book shows many more tricks to building animations,
but the key ideas are now in place.
8.8 Installing the support software for animations
The programs in Dawson, Chapters 11 and 12, rely on two support modules,
pygame and livewires. If you use your own computer,
you must install both of these modules; they are found on the disk
that comes with your text.
8.9 Summary
A Python class is a ``mini-module'' that can be ``imported''
(constructed)
multiple times. Each time the class is ``imported,''
a new, permanent namespace is
constructed. Each namespace is called an object.
============================
class NAME ( BASE_CLASS ) :
def __init__(self, ... ) :
"""constructs the object (namespace) in heap storage. The
address of the new object is assigned to the parameter, self.
Parameter self is used to define and save the object's variables
in its namespace.
"""
. . .
self.mydata = ... # make a variable, mydata, in the object
. . .
# the function automatically returns the value of self as its answer.
def handle(self, ... ) :
"""event handling function for the object whose address is self"""
. . . self.mydata . . .
====================================
When the class is used to construct an object, we write this:
x = NAME( ... )
The occurrence of NAME on the right-hand side of the assignment
is a disguised call of the __init__ function. Indeed, the Python
interpreter reformats the above assignment into a ``module'' call in
dot notation:
x = NAME.__init__( getNewAddressInHeapForTheNewObject, ... )
Notice that an extra argument is supplied for free --- the address in the
heap where the new object will be constructed.
x.handle(...)
Again, the Python intepreter reformats this call, to look like this:
C.handle(x, ...)
and now it is clear that the address held by variable x is
assigned to parameter self in handle, correctly
connecting the object to its event handling function.