[Tutor] Tkinter callback question [and the Command class]

Daniel Yoo dyoo@hkn.eecs.berkeley.edu
Tue, 3 Apr 2001 23:56:07 -0700 (PDT)


On Tue, 3 Apr 2001, Ryan Booz wrote:

> How do I get a widget (let's say a button) to execute a command that
> changes another widget (say a label).  This seems like it should be

[warning --- slightly long message]



One way to change the contents of a label is to use StringVar()'s.  Here's
an example of a StringVar:

    s = StringVar()

StringVars allow us to change the contents of a label, as long as we 
created the Label to listen to it.  That is, when we make the label:

    l = Label(textvariable=s)

we can later call the set() or get() methods of our StringVar.  Whenever
we do something to our StringVar, something will happen to the label.  For
example:

###
from Tkinter import *
root = Tk()
s = StringVar()
l = Label(root, textvariable=s)
s.set("Hello World!")
###

is a small script that uses StringVars.



I think the real question that you're asking about is how to use Tkinter
callbacks, and how to pass arguments off to functions that we haven't
called yet.  It won't do if we try to do something like:

    b = Button(text='Goodbye', command=s.set('goodbye!'))

simply because s.set() is being called too early: we want to somehow
"delay" the call to the function, but still pass that particular argument
when the time is right.


Here's something that will do what you want: take a look below at the
sample code:


###
from Tkinter import *

class Command:
    """ A class we can use to avoid using the tricky "Lambda" expression.
    "Python and Tkinter Programming" by John Grayson, introduces this
    idiom."""

    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs

    def __call__(self):
        apply(self.func, self.args, self.kwargs)


if __name__ == '__main__':
    root = Tk()
    contents = StringVar()
    contents.set("Default message")
    l = Label(root, textvariable=contents)
    l.pack()
    
    b1 = Button(root, text="Goodbye",
                command=Command(contents.set, "goodbye!"))
    b2 = Button(root, text="Hello",
                command=Command(contents.set, "greetings!"))
    b1.pack()
    b2.pack()
    mainloop()
###

The idea is to use a "Command" class: it takes in a function's name and
the arguments that you want to pass to that function.  Rather than calling
the function immediately, it acts as the delayer.  For example:

###
def sayHello(name):
    print "Hello", name

hiRyan = Command(sayHello, 'Ryan')      # a delayed sayHello
hiRyan()                 # Here's where we actually call the function.
###

In this case, "hiRyan" takes on the value of a function, and only after we
call it does sayHello() fire off.  Likewise,i n the Tkinter example, we're
giving our buttons a function that knows exactly what arguments to pass to
contents.set(), without actually doing it immediately.  This avoids the
problem of having the functions fire off too quickly, and also lets us
pass in our arguments.


(Side note: the more traditional way to do the above is to use lambda's,
but after being exposed to this Command idiom, I'm really leaning toward
using the Command class: it's easier to read.)


If you want to show this to your students, it's probably a good idea to
just give them the Command class to play with: tell them how to use it,
and point them to us when they want to know how it actually works.  *grin*



> If I'm not making any sense, let me know and I'll try to add some
> examples of what I've done and what I want, but at the moment I have
> to go teach.  Thanks for any help and for your understanding of asking
> what I'm sure is a straightforward answer that should be obvious.  
> IT's a perspective issue probably.

You're making sense.  Still, it'd be cool to see what you've shown your
students.  Can you show us some examples?


Good luck to you!