How to run a code whenever a Tkinter widget value changes?

26,424

Solution 1

I think the correct method is to use trace on a tkinter variable that has been assigned to a widget.

For example...

import tkinter

root = tkinter.Tk()
myvar = tkinter.StringVar()
myvar.set('')
mywidget = tkinter.Entry(root,textvariable=myvar,width=10)
mywidget.pack()

def oddblue(a,b,c):
    if len(myvar.get())%2 == 0:
        mywidget.config(bg='red')
    else:
        mywidget.config(bg='blue')
    mywidget.update_idletasks()

myvar.trace('w',oddblue)

root.mainloop()

The w in trace tells tkinter whenever somebody writes (updates) the variable, which would happen every time someone wrote something in the Entry widget, do oddblue. The trace always passes three values to whatever function you've listed, so you'll need to expect them in your function, hence a,b,c. I usually do nothing with them as everything I need is defined locally anyway. From what I can tell a is the variable object, b is blank (not sure why), and c is the trace mode (i.e.w).

For more info on tkinter variables check this out.

Solution 2

How I would solve this in Tcl would be to make sure that the checkbutton, spinbox and radiobutton widgets are all associated 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()

Solution 3

You have three different ways of doing the same:

1) Use the built-in "command" configuration, like the one you use on buttons

import tkinter as tk
from tkinter import messagebox as tk_messagebox

def spinbox1_callback():
    tk_messagebox.showinfo("Spinbox callback", "You changed the spinbox.")

if __name__ == "__main__":
    master = tk.Tk()
    spinbox1 = tk.Spinbox(master, from_=0, to=10, command=spinbox1_callback)
    spinbox1.pack()
    tk.mainloop()

2) Use the event bindings to capture specific events: http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm

import tkinter as tk
from tkinter import messagebox as tk_messagebox

root = tk.Tk()

def callback(event):
    tk_messagebox.showinfo("clicked at", event.x, event.y)

frame = tk.Frame(root, width=100, height=100)
frame.bind("<Button-1>", callback)
frame.pack()

root.mainloop()

3) "trace" changes on a tkinter variable classes, so if your widget uses a StringVar, BooleanVar, IntVar, or DoubleVar in the textvariable parameter, you will get a callback once it gets updated. https://effbot.org/tkinterbook/variable.htm

import tkinter as tk
from tkinter import messagebox as tk_messagebox


if __name__ == "__main__":
    master = tk.Tk()
    widget_contents = tk.StringVar()
    widget_contents.set('')
    some_entry = tk.Entry(master,textvariable=widget_contents,width=10)
    some_entry.pack()

    def entry1_callback(*args):
        tk_messagebox.showinfo("entry callback", "You changed the entry %s" % str(args))
        some_entry.update_idletasks()

    widget_contents.trace('w',entry1_callback)

    tk.mainloop()

Solution 4

It's quite late, but yet, somebody found something that might be useful.

The whole idea comes from @bryan Oakley's post

If I understand well, the main problem is to detech Entry widget's . To detect it in spinbox, Checkbutton and Radiobutton you can use command options when creating widget.

To catch the <onChange> in Entry widget you can use Bryan`s approach using Tcl, which generates this event. As I said, this is not my solution, I've only changed it slightly for this case.

For example:

import tkinter as tk
from tkinter import ttk

def generateOnChange(obj):
        obj.tk.eval('''
            proc widget_proxy {widget widget_command args} {

                # call the real tk widget command with the real args
                set result [uplevel [linsert $args 0 $widget_command]]

                # generate the event for certain types of commands
                if {([lindex $args 0] in {insert replace delete}) ||
                    ([lrange $args 0 2] == {mark set insert}) || 
                    ([lrange $args 0 1] == {xview moveto}) ||
                    ([lrange $args 0 1] == {xview scroll}) ||
                    ([lrange $args 0 1] == {yview moveto}) ||
                    ([lrange $args 0 1] == {yview scroll})} {

                    event generate  $widget <<Change>> -when tail
                }

                # return the result from the real widget command
                return $result
            }
            ''')
        obj.tk.eval('''
            rename {widget} _{widget}
            interp alias {{}} ::{widget} {{}} widget_proxy {widget} _{widget}
        '''.format(widget=str(obj)))

def onEntryChanged(event = None):
    print("Entry changed")

def onCheckChanged(event = None):
    print("Check button changed")

def onSpinboxChanged(event = None):
    print("Spinbox changed")

def onRadioChanged(event = None):
    print("Radio changed")

if __name__ == '__main__':
    root = tk.Tk()

    frame = tk.Frame(root, width=400, height=400)

    entry = tk.Entry(frame, width=30)
    entry.grid(row=0, column=0)
    generateOnChange(entry)
    entry.bind('<<Change>>', onEntryChanged)

    checkbutton = tk.Checkbutton(frame, command=onCheckChanged)
    checkbutton.grid(row=1, column=0)

    spinbox = tk.Spinbox(frame, width=100, from_=1.0, to=100.0, command=onSpinboxChanged)
    spinbox.grid(row=2, column=0)


    phone = tk.StringVar()
    home = ttk.Radiobutton(frame, text='Home', variable=phone, value='home', command=onRadioChanged)
    home.grid(row=3, column=0, sticky=tk.W)
    office = ttk.Radiobutton(frame, text='Office', variable=phone, value='office', command=onRadioChanged)
    office.grid(row=3, column=0, sticky=tk.E)

    frame.pack()    
    root.mainloop()

Of course modify it to create different callback for plenty of instances (as you mentioned in the question) is easy now.

I hope somebody will find it useful.

Share:
26,424
Denilson Sá Maia
Author by

Denilson Sá Maia

Software developer || software engineer || programmer || developer. Whatever job title you want to call me. http://denilson.sa.nom.br/

Updated on April 10, 2020

Comments

  • Denilson Sá Maia
    Denilson Sá Maia about 4 years

    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)