Как запустить код при изменении значения виджета Tkinter?
Я использую Python и Tkinter
, и я хочу эквивалент события onchange
из других наборов инструментов/языков. Я хочу запускать код всякий раз, когда пользователь обновляет состояние некоторых виджетов.
В моем случае у меня есть много Entry
, Checkbutton
, Spinbox
и Radiobutton
виджетов. Когда бы ни было какое-либо из этих изменений, я хочу запустить свой код (в этом случае обновить текстовое поле на другой панели).
(просто помните, что пользователь может взаимодействовать с этими виджетами с помощью мыши или клавиатуры и даже использовать Ctrl + V для вставки текста)
Ответы
Ответ 1
Я думаю, что правильный метод - использовать trace
для переменной tkinter, которая была назначена виджету.
Например...
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()
Функция w
in trace сообщает tkinter всякий раз, когда кто-то записывает (обновляет) переменную, что происходит каждый раз, когда кто-то что-то пишет в виджете Entry, выполняет oddblue
. Трассировка всегда передает три значения любой функции, которую вы перечислили, поэтому вам нужно ожидать их в вашей функции, следовательно, a,b,c
. Я обычно ничего с ними не делаю, так как все, что мне нужно, так или иначе определяется локально. Из того, что я могу сказать, a
- это переменный объект, b
- пустое (не знаю почему), а c
- режим трассировки (т. w
).
Для получения дополнительной информации о переменных tkinter проверьте это.
Ответ 2
Как бы я решил это в Tcl, было бы убедиться, что виджеты checkbutton, spinbox и radiobutton связаны с переменной массива. Затем я поместил бы трассировку на массив, который вызывал бы функцию, которая будет вызываться каждый раз, когда будет записана переменная. Tcl делает это тривиальным.
К сожалению, Tkinter не поддерживает работу с массивами Tcl. К счастью, это довольно легко взломать. Если вы предприимчивы, попробуйте следующий код.
От отдела полного раскрытия. Я бросил это вместе сегодня утром примерно через полчаса. Я не использовал эту технику в любом реальном коде. Однако я не мог устоять перед проблемой, чтобы выяснить, как использовать массивы с 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()
Ответ 3
Уже довольно поздно, но все же кто-то нашел что-то полезное.
Вся идея взята из поста @bryan Oakley
Если я хорошо понимаю, основная проблема заключается в обнаружении виджета Entry. Чтобы обнаружить его в spinbox
, Checkbutton
и Radiobutton
вы можете использовать параметры command
при создании виджета.
Чтобы поймать <onChange>
в виджете Entry
вы можете использовать подход Брайана с использованием Tcl, который генерирует это событие. Как я уже сказал, это не мое решение, я только немного изменил его для этого случая.
Например:
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()
Конечно, изменить его, чтобы создать другой обратный вызов для большого количества экземпляров (как вы упомянули в вопросе), теперь легко.
Я надеюсь, что кто-нибудь найдет это полезным.
Ответ 4
До сих пор я не встречал ничего эквивалентного onChange в Tkinter.
Виджеты могут быть связаны с различными событиями, и я сделал это явно.