In the simple button examples in the preceding section, the callback handler was simply an existing function that killed the GUI program. It’s not much more work to register callback handlers that do something a bit more useful. Example 8-12 defines a callback handler of its own in Python.
Example 8-12. PP3EGuiIntrogui3.py
from Tkinter import * def quit( ): # a custom callback handler print 'Hello, I must be going...' # kill windows and process import sys; sys.exit( ) widget = Button(None, text='Hello event world', command=quit) widget.pack( ) widget.mainloop( )
The window created by this script is shown in Figure 8-13. This script and its
GUI are almost identical to the last example. But here, the command
option specifies a function we’ve
defined locally. When the button is pressed, Tkinter calls the
quit
function in this file to
handle the event, passing it zero arguments. Inside quit
, the print
statement types a message on the
program’s stdout
stream, and the
GUI process exits as before.
As usual, stdout
is normally
the window that the program was started from unless it’s been
redirected to a file. It’s a pop-up DOS console if you run this
program by clicking it on Windows; add a raw_input
call before sys.exit
if you have trouble seeing the
message before the pop up disappears. Here’s what the printed output
looks like back in standard stream world when the button is pressed;
it is generated by a Python function called automatically by
Tkinter:
C:...PP3EGuiIntro>python gui3.py
Hello, I must be going...
C:...PP3EGuiIntro>
Normally, such messages would be displayed in another window, but we haven’t gotten far enough to know how just yet. Callback functions usually do more, of course (and may even pop up new windows altogether), but this example illustrates the basics.
In general, callback handlers can be any callable object:
functions, anonymous functions generated with lambda expressions, bound methods of class or type instances,
or class instances that inherit a _ _call_
_
operator overload method. For Button
press callbacks, callback handlers
always receive no arguments (other than a self
, for bound methods); any state
information required by the callback handler must be provided in other
ways—as global variables, class instance attributes, extra arguments
provided by an indirection layer, and so on.
To make the last paragraph a bit more concrete, let’s take a quick look at some other ways to code the callback handler in this example. Recall that the Python lambda expression generates a new, unnamed function object when run. If we need extra data passed in to the handler function, we can register lambda expressions to defer the call to the real handler function, and specify the extra data it needs.
Later in this part of the book, we’ll see how this can be
useful, but to illustrate the basic idea, Example 8-13 shows what this
example looks like when recoded to use a lambda instead of a
def
.
Example 8-13. PP3EGuiIntrogui3b.py
from Tkinter import * from sys import stdout, exit # lambda generates a function widget = Button(None, # but contains just an expression text='Hello event world', command=(lambda: stdout.write('Hello lambda world ') or exit( )) ) widget.pack( ) widget.mainloop( )
This code is a bit tricky because lambdas can contain only an
expression; to emulate the original script, this version uses an
or
operator to force two
expressions to be run, and writes to stdout
to mimic a print
. More typically, lambdas are used to
provide an indirection layer that passes along extra data to a
callback handler:
def handler(A, B): # would normallly be called with no args
use A and B...
X = 42
Button(text='ni',command=(lambda: handler(X, 'spam'))
)
mainloop( )
Although Tkinter invokes command
callbacks with no arguments, such
a lambda can be used to provide an indirect anonymous function that
wraps the real handler call and passes along information that
existed when the GUI was first constructed. The call to the real
handler is, in effect, deferred, so we can add the extra arguments
it requires. Here, the value of global variable X
and string 'spam'
will be passed to arguments
A
and B
, even though Tkinter itself runs
callbacks with no arguments. The net effect is that the lambda
serves to map a no-argument function call to one with arguments
supplied by the lambda.
If lambda syntax confuses you, remember that a lambda
expression such as the one in the preceding code can usually be
coded as a simple def
statement
instead, nested or otherwise. In the following code, the second
function does exactly the same work as the prior lambda:
def handler(A, B): # would normally be called with no args use A and B....
X = 42def func( ):
# indirection layer to add argumentshandler(X, 'spam')
Button(text='ni', command=func) mainloop( )
Notice that the handler function in this code could refer to
X
directly, because it is a
global variable (and would exist by the time the code inside the
handler is run). Because of that, we make the handler a one-argument
function and pass in just the string 'spam'
in the lambda:
def handler(A): # X is in my global scope, implicitly
use X and A...
X = 42
Button(text='ni',command=(lambda: handler('spam'))
)
mainloop( )
Arguments are generally preferred to globals, though, because they make external dependencies more explicit, and so make code easier to understand and change. In general, using a lambda to pass extra data with an inline function definition:
def handler(name): print name Button(command=(lambda: handler('spam')))
is always equivalent to the longer, and arguably less convenient, double-function form:
def handler(name): print name def temp( ): handler('spam') Button(command=temp)
To make that more obvious, notice what happens if you code the handler call in the button call without the lambda—it runs immediately when the button is created, not when it is later clicked. That’s why we need to wrap the call in an intermediate function:
def handler(name): print name Button(command=handler('spam')) # runs the callback now!
Although lambda-based callbacks defer calls and allow extra
data to be passed in, they also imply some scoping issues that may
seem subtle at first glance. Notice that if the button in the
example we’ve been discussing was constructed inside a
function rather than at the top level of the
file, name X
would no longer be
global but would be in the enclosing function’s local scope; it
would disappear after the function exits and before the callback
event occurs and runs the lambda’s code.
Luckily, default argument values can be used to remember the
values of variables in the enclosing local scope, even after the
enclosing function returns. In the following code, for instance,
the default argument name X
(on
the left side of the X=X
default) will remember object 42
, because the variable name X
(on the right side of the X=X
) is evaluated in the enclosing
scope, and the generated function is later called without any
arguments:
def handler(A, B): # older Pythons: defaults save state
use A and B...
def makegui( ):
X = 42
Button(text='ni',command=(lambda X=X: handler(X, 'spam'))
)
makegui( ) # lambda function is created here
mainloop( ) # event happens after makegui returns
Since default arguments are evaluated and saved when the
lambda runs (not when the function it creates is later called),
they are a way to explicitly remember objects that must be
accessed again later, during event processing. And because Tkinter
calls the lambda with no arguments, all its defaults are used.
This was not an issue in the original version of this example
because name X
lived in the
global scope, and the code of the lambda will find it there when
it is run. When nested within a function, though, X
may have disappeared after the
enclosing function exits.
Things are a bit simpler today, however. In more recent Python releases that support automatic nested scope lookup (added in release 2.2), defaults are less commonly needed to retain state this way. Rather, lambdas simply defer the call to the actual handler and provide extra handler arguments. Variables from the enclosing scope used by the lambda are automatically retained, even after the enclosing function exits.
For instance, the prior code listing can today normally be
coded as follows; name X
in the
handler will be automatically mapped to X
in the enclosing scope, and so
effectively remember what X
was
when the button was made:
def handler(A, B): # enclosing scope X automatically retained use A and B... def makegui( ): X = 42.
Button(text='ni',command=(lambda: handler(X, 'spam'))
) makegui( ) mainloop( )
We’ll see this technique put to more concrete use later.
When using classes to build your GUI, for instance, the self
argument is a local variable in
methods and is thus available in the bodies of lambda functions
today without passing it in explicitly with defaults:
class Gui: def handler(self, A, B): use self, A and B... def makegui(self): X = 42.
Button(text='ni',command=(lambda: self.handler(X, 'spam'))
) Gui().makegui( ) mainloop( )
When using classes, though, instance attributes provide an alternative way to provide extra state for use in callback handlers. We’ll see how in a moment. First, though, we need to take a quick diversion onto Python’s scope rules to understand why default arguments are still sometimes necessary to pass values into nested lambda functions.
As we saw in the prior section, enclosing scope references can simplify callback handler code in recent Python releases. In fact, it seems as though the new nested scope lookup rules in Python automate and replace the previously manual task of passing in enclosing scope values with defaults.
Well, almost. There is a catch. It turns out that within a
lambda (or def
), references to
names in the enclosing scope are actually resolved when the
generated function is called, not when it is
created. Because of this, when the function is later called, such
name references will reflect the latest or final assignments made
to the names anywhere in the enclosing scope, which are not
necessarily the values they held when the function was made. This
holds true even when the callback function is nested only in a
module’s global scope, not in an enclosing function; in either
case, all enclosing scope references are resolved at function call
time, not at creation time.
This is subtly different from default argument values, which are evaluated once when the function is created, not when it is later called. Because of that, they can be used to remember the values of enclosing scope variables as they were when you made the function. Unlike enclosing scope name references, defaults will not have a different value if the variable later changes in the enclosing scope. (In fact, this is why mutable defaults retain their state between calls—they are created only once, when the function is made.)
This is normally a nonissue, because most enclosing scope
references name a variable that is assigned just once in the
enclosing scope (the self
argument in class methods, for example). But this can lead to
coding mistakes if not understood, especially if you create
functions within a loop; if those functions reference the loop
variable, it will evaluate to the value it was given on the
last loop iteration in
all the functions generated. By contrast, if
you use defaults instead, each function will remember the
current value of the loop variable, not the
last.
Because of this difference, nested scope references are not always sufficient to remember enclosing scope values, and defaults are sometimes still required today. Let’s see what this means in terms of code. Consider the following nested function:
def simple( ): spam = 'ni' def action( ): print spam # name maps to enclosing function return action act = simple( ) # make and return nested function act( ) # then call it: prints 'ni'
This is the simple case for enclosing scope references, and
it works the same way whether the nested function is generated
with a def
or a lambda. But
notice that this still works if we assign the enclosing scope’s
spam
variable
after the nested function is created:
def normal( ): def action( ): return spam # really, looked up when used spam = 'ni' return action act = normal( ) print act( ) # also prints 'ni'
As this implies, the enclosing scope name isn’t resolved when the nested function is made—in fact, the name hasn’t even been assigned yet in this example. The name is resolved when the nested function is called. The same holds true for lambdas:
def weird( ): spam = 42 return (lambda: spam * 2) # remembers spam in enclosing scope act = weird( ) print act( ) # prints 84
So far so good. The spam
inside this nested lambda function remembers the value that this
variable had in the enclosing scope, even after the enclosing
scope exits. This pattern corresponds to a registered GUI callback
handler run later on events. But once again, the nested scope
reference really isn’t being resolved when the lambda is run to
create the function; it’s being resolved when the generated
function is later called. To make that more
apparent, look at this code:
def weird( ): tmp = (lambda: spam * 2) # remembers spam spam = 42 # even though not set till here return tmp act = weird( ) print act( ) # prints 84
Here again, the nested function refers to a variable that hasn’t even been assigned yet when that function is made. Really, enclosing scope references yield the latest setting made in the enclosing scope, whenever the function is called. Watch what happens in the following code:
def weird( ): spam = 42 handler = (lambda: spam * 2) # func doesn't save 42 now spam = 50 print handler( ) # prints 100: spam looked up now spam = 60 print handler( ) # prints 120: spam looked up again now weird( )
Now, the reference to spam
inside the lambda is different each
time the generated function is called! In fact, it refers to what
the variable was set to last in the enclosing
scope at the time the nested function is called, because it is
resolved at function call time, not at function creation time. In
terms of GUIs, this becomes significant most often when you
generate callback handlers within loops and try to use enclosing
scope references to remember extra data created within the loops.
If you’re going to make functions within a loop, you have to apply
the last example’s behavior to the loop variable:
def odd( ): funcs = [] for c in 'abcdefg': funcs.append((lambda: c)) # c will be looked up later return funcs # does not remember current c for func in odd( ): print func( ), # print 7 g's, not a,b,c,... !
Here, the func
list
simulates registered GUI callback handlers associated with
widgets. This doesn’t work the way most people expect it to. The
variable c
within the nested
function will always be g
here,
the value that the variable was set to on the final iteration of
the loop in the enclosing scope. The net effect is that all seven
generated lambda functions wind up with the same extra state
information when they are later called.
Analogous GUI code that adds information to lambda callback handlers will have similar problems—all buttons created in a loop, for instance, may wind up doing the same thing when clicked! To make this work, we still have to pass values into the nested function with defaults in order to save the current value of the loop variable (not its future value):
def odd( ): funcs = [] for c in 'abcdefg': funcs.append((lambda c=c: c)) # force to remember c now return funcs # defaults eval now for func in odd( ): print func( ), # OK: now prints a,b,c,...
This works now only because the default, unlike an external scope reference, is evaluated at function creation time, not at function call time. It remembers the value that a name in the enclosing scope had when the function was made, not the last assignment made to that name anywhere in the enclosing scope. The same is true even if the function’s enclosing scope is a module, not another function; if we don’t use the default argument in the following code, the loop variable will resolve to the same value in all seven functions:
funcs = [] # enclosing scope is module for c in 'abcdefg': # force to remember c now funcs.append((lambda c=c: c)) # else prints 7 g's again for func in funcs: print func( ), # OK: prints a,b,c,...
The moral of this story is that enclosing scope name references are a replacement for passing values in with defaults, but only as long as the name in the enclosing scope will not change to a value you don’t expect after the nested function is created. You cannot generally reference enclosing scope loop variables within a nested function, for example, because they will change as the loop progresses. In most other cases, though, enclosing scope variables will take on only one value in their scope and so can be used freely.
We’ll see this phenomenon at work in later examples. For now, remember that enclosing scopes are not a complete replacement for defaults; defaults are still required in some contexts to pass values into callback functions. Also keep in mind that classes are often a better and simpler way to retain extra state for use in callback handlers than are nested functions. Because state is explicit in classes, these scope issues do not apply. The next two sections cover this in detail.
Class bound methods work particularly well as callback handlers: they record both an instance to send the event to and an associated method to call. For instance, Example 8-14 shows Example 8-12 rewritten to register a bound class method rather than a function or lambda result.
Example 8-14. PP3EGuiIntrogui3c.py
from Tkinter import * class HelloClass: def _ _init_ _(self): widget = Button(None, text='Hello event world', command=self.quit) widget.pack( ) def quit(self): print 'Hello class method world' # self.quit is a bound method import sys; sys.exit( ) # retains the self+quit pair HelloClass( ) mainloop( )
On a button press, Tkinter calls this class’s quit
method with no arguments, as usual.
But really, it does receive one argument—the original self
object—even though Tkinter doesn’t
pass it explicitly. Because the self.quit
bound method retains both
self
and quit
, it’s compatible with a simple
function call; Python automatically passes the self
argument along to the method
function. Conversely, registering an unbound method such as HelloClass.quit
won’t work, because there
is no self
object to pass along
when the event later occurs.
Later, we’ll see that class callback handler coding schemes
provide a natural place to remember information for use on events;
simply assign the information to self
instance attributes:
class someGuiClass:
def _ _init_ _(self):
self.X = 42
self.Y = 'spam'
Button(text='Hi',command=self.handler
)
def handler(self):
use self.X, self.Y ...
Because the event will be dispatched to this class’s method
with a reference to the original instance object, self
gives access to attributes that
retain original data. In effect, the instance’s attributes retain
state information to be used when events occur.
Because Python class instance objects can also be
called if they inherit a _ _call_
_
method to intercept the operation, we can pass one of
these to serve as a callback handler. Example 8-15 shows a class that
provides the required function-like interface.
Example 8-15. PP3EGuiIntrogui3d.py
from Tkinter import * class HelloCallable: def _ _init_ _(self): # _ _init_ _ run on object creation self.msg = 'Hello _ _call_ _ world' def _ _call_ _(self): print self.msg # _ _call_ _ run later when called import sys; sys.exit( ) # class object looks like a function widget = Button(None, text='Hello event world', command=HelloCallable( )) widget.pack( ) widget.mainloop( )
Here, the HelloCallable
instance registered with command
can be called like a normal function; Python invokes its _ _call_ _
method to handle the call
operation made in Tkinter on the button press. Notice that self.msg
is used to retain information for
use on events here; self
is the
original instance when the special _ _call_
_
method is automatically invoked.
All four gui3
variants
create the same GUI window but print different messages to stdout
when their button is
pressed:
C:...PP3EGuiIntro>python gui3.py
Hello, I must be going... C:...PP3EGuiIntro>python gui3b.py
Hello lambda world C:...PP3EGuiIntro>python gui3c.py
Hello class method world C:...PP3EGuiIntro>python gui3d.py
Hello _ _call_ _ world
There are good reasons for each callback coding scheme (function, lambda, class method, callable class), but we need to move on to larger examples in order to uncover them in less theoretical terms.
For future reference, also keep in mind that using
command
options to intercept
user-generated button press events is just one way to register
callbacks in Tkinter. In fact, there are a variety of ways for
Tkinter scripts to catch events:
As we’ve just seen, button press events are intercepted
by providing a callable object in widget command
options. This is true of
other kinds of button-like widgets we’ll meet in Chapter 9 (e.g., radio and
check buttons, and scales).
In the upcoming Tkinter tour chapters, we’ll also find
that a command
option is
used to specify callback handlers for menu selections.
Scroll bar widgets register handlers with command
options too, but they have a
unique event protocol that allows them to be cross-linked with
the widget they are meant to scroll (e.g., listboxes, text
displays, and canvases): moving the scroll bar automatically
moves the widget, and vice versa.
bind
methodsA more general Tkinter event bind
method mechanism can be used to
register callback handlers for lower-level interface
events—key presses, mouse movement and clicks, and so on.
Unlike command
callbacks,
bind
callbacks receive an
event object argument (an instance of the Tkinter Event
class) that gives context
about the event—subject widget, screen coordinates, and so
on.
In addition, scripts can also intercept window manager
events (e.g., window close requests) by tapping into the
window manager protocol
method mechanism available on top-level window objects.
Setting a handler for WM_DELETE_WINDOW
, for instance,
takes over window close buttons.
Finally, Tkinter scripts can also register callback handlers to be run in special contexts, such as timer expirations, input data arrival, and event-loop idle states. Scripts can also pause for state-change events related to windows and special variables. We’ll meet these event interfaces in more detail near the end of Chapter 10.
Of all the options listed in the prior section,
bind
is the most general, but
also perhaps the most complex. We’ll study it in more detail later,
but to let you sample its flavor now, Example 8-16 uses bind
, not the command
keyword, to catch button
presses.
Example 8-16. PP3EGuiIntrogui3e.py
from Tkinter import * def hello(event): print 'Press twice to exit' # on single-left click def quit(event): # on double-left click print 'Hello, I must be going...' # event gives widget, x/y, etc. import sys; sys.exit( ) widget = Button(None, text='Hello event world') widget.pack( ) widget.bind('<Button-1>', hello) # bind left mouse clicks widget.bind('<Double-1>', quit) # bind double-left clicks widget.mainloop( )
In fact, this version doesn’t specify a command
option for the button at all.
Instead, it binds lower-level callback handlers for both left mouse
clicks (<Button-1>
) and
double-left mouse clicks (<Double-1>
) within the button’s
display area. The bind
method
accepts a large set of such event identifiers in a variety of
formats, which we’ll meet in Chapter
9.
When run, this script makes the same window as before (see Figure 8-13). Clicking on the button once prints a message but doesn’t exit; you need to double-click on the button now to exit as before. Here is the output after clicking twice and double-clicking once (a double-click fires the single-click callback first):
C:...PP3EGuiIntro>python gui3e.py
Press twice to exit
Press twice to exit
Press twice to exit
Hello, I must be going...
Although this script intercepts button clicks manually, the
end result is roughly the same; widget-specific protocols such as
button command
options are really
just higher-level interfaces to events you can also catch with
bind
.
We’ll meet bind
and all of
the other Tkinter event callback handler hooks again in more detail
later in this book. First, though, let’s focus on building GUIs that
are larger than a single button and on other ways to use classes in
GUI work.
18.226.34.197