views:

54

answers:

2

I'm using Python and Tkinter, and I want the equivalent of onchange event from other toolkits/languages. I want to run code whenever the user updates the state of some widgets.

In my case, I have many Entry, Checkbutton, Spinbox and Radiobutton widgets. Whenever any one of these changes, I want to run my code (in this case, update a text box on the other panel).

(just remember that user may interact with those widgets using either mouse or keyboard, and even using Ctrl+V to paste text)

A: 

So far, I have not encountered any thing equivalent of onChange in Tkinter. Widgets can be bound to the various events and I have done that explicitly.

pyfunc
This approach requires careful attention to detail, or you may create bindings that happen *before* the widget actually changes rather than immediately after.
Bryan Oakley
A: 

How I would solve this in Tcl would be to make sure that the checkbutton, spinbox and radiobutton widgets are all associate with an array variable. I would then put a trace on the array which would cause a function to be called each time that variable is written. Tcl makes this trivial.

Unfortunately Tkinter doesn't support working with Tcl arrays. Fortunately, it's fairly easy to hack in. If you're adventurous, try the following code.

From the full disclosure department: I threw this together this morning in about half an hour. I haven't actually used this technique in any real code. I couldn't resist the challenge, though, to figure out how to use arrays with Tkinter.

import Tkinter as tk

class MyApp(tk.Tk):
    '''Example app that uses Tcl arrays'''

    def __init__(self):

        tk.Tk.__init__(self)

        self.arrayvar = ArrayVar()
        self.labelvar = tk.StringVar()

        rb1 = tk.Radiobutton(text="one", variable=self.arrayvar("radiobutton"), value=1)
        rb2 = tk.Radiobutton(text="two", variable=self.arrayvar("radiobutton"), value=2)
        cb = tk.Checkbutton(text="checked?", variable=self.arrayvar("checkbutton"), 
                             onvalue="on", offvalue="off")
        entry = tk.Entry(textvariable=self.arrayvar("entry"))
        label = tk.Label(textvariable=self.labelvar)
        spinbox = tk.Spinbox(from_=1, to=11, textvariable=self.arrayvar("spinbox"))
        button = tk.Button(text="click to print contents of array", command=self.OnDump)

        for widget in (cb, rb1, rb2, spinbox, entry, button, label):
            widget.pack(anchor="w", padx=10)

        self.labelvar.set("Click on a widget to see this message change")
        self.arrayvar["entry"] = "something witty"
        self.arrayvar["radiobutton"] = 2
        self.arrayvar["checkbutton"] = "on"
        self.arrayvar["spinbox"] = 11

        self.arrayvar.trace(mode="w", callback=self.OnTrace)

    def OnDump(self):
        '''Print the contents of the array'''
        print self.arrayvar.get()

    def OnTrace(self, varname, elementname, mode):
        '''Show the new value in a label'''
        self.labelvar.set("%s changed; new value='%s'" % (elementname, self.arrayvar[elementname]))

class ArrayVar(tk.Variable):
    '''A variable that works as a Tcl array variable'''

    _default = {}
    _elementvars = {}

    def __del__(self):
        self._tk.globalunsetvar(self._name)
        for elementvar in self._elementvars:
            del elementvar


    def __setitem__(self, elementname, value):
        if elementname not in self._elementvars:
            v = ArrayElementVar(varname=self._name, elementname=elementname, master=self._master)
            self._elementvars[elementname] = v
        self._elementvars[elementname].set(value)

    def __getitem__(self, name):
        if name in self._elementvars:
            return self._elementvars[name].get()
        return None

    def __call__(self, elementname):
        '''Create a new StringVar as an element in the array'''
        if elementname not in self._elementvars:
            v = ArrayElementVar(varname=self._name, elementname=elementname, master=self._master)
            self._elementvars[elementname] = v
        return self._elementvars[elementname]

    def set(self, dictvalue):
        # this establishes the variable as an array 
        # as far as the Tcl interpreter is concerned
        self._master.eval("array set {%s} {}" % self._name) 

        for (k, v) in dictvalue.iteritems():
            self._tk.call("array","set",self._name, k, v)

    def get(self):
        '''Return a dictionary that represents the Tcl array'''
        value = {}
        for (elementname, elementvar) in self._elementvars.iteritems():
            value[elementname] = elementvar.get()
        return value


class ArrayElementVar(tk.StringVar):
    '''A StringVar that represents an element of an array'''
    _default = ""

    def __init__(self, varname, elementname, master):
        self._master = master
        self._tk = master.tk
        self._name = "%s(%s)" % (varname, elementname)
        self.set(self._default)

    def __del__(self):
        """Unset the variable in Tcl."""
        self._tk.globalunsetvar(self._name)


if __name__ == "__main__":
    app=MyApp()
    app.wm_geometry("400x200")
    app.mainloop()
Bryan Oakley