Section 8.8. Adding User-Defined Callback Handlers


8.8. Adding User-Defined Callback Handlers

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. PP3E\Gui\Intro\gui3.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.

Figure 8-13. A button that runs a Python function


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:\...\PP3E\Gui\Intro>python gui3.py Hello, I must be going... C:\...\PP3E\Gui\Intro> 

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 waysas global variables, class instance attributes, extra arguments provided by an indirection layer, and so on.

8.8.1. Lambda Callback Handlers

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. PP3E\Gui\Intro\gui3b.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\n') 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 = 42 def func( ):                      # indirection layer to add arguments     handler(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 lambdait 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! 

8.8.1.1. Passing in values with default arguments

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.

8.8.1.2. Passing in values with enclosing scope references

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.

8.8.1.3. Enclosing scopes versus defaults

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 callsthey 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 madein 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 problemsall 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.

8.8.2. Bound Method Callback Handlers

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. PP3E\Gui\Intro\gui3c.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 argumentthe original self objecteven 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.

8.8.3. Callable Class Object Callback Handlers

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. PP3E\Gui\Intro\gui3d.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:\...\PP3E\Gui\Intro>python gui3.py Hello, I must be going... C:\...\PP3E\Gui\Intro>python gui3b.py Hello lambda world C:\...\PP3E\Gui\Intro>python gui3c.py Hello class method world C:\...\PP3E\Gui\Intro>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.

8.8.4. Other Tkinter Callback Protocols

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:


Button command options

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).


Menu command options

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 protocols

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.


General widget bind methods

A more general Tkinter event bind method mechanism can be used to register callback handlers for lower-level interface eventskey 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 eventsubject widget, screen coordinates, and so on.


Window manager protocols

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.


Scheduled event callbacks

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.

8.8.5. Binding Events

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. PP3E\Gui\Intro\gui3e.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:\...\PP3E\Gui\Intro>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.




Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net