tkinter:绑定事件中奇怪的操作顺序,如何更改?
tkinter: strange order of operations in bound events, how to change?
这与 Controlling Order of Callbacks with Tkinter 有关,但并不完全相同
我有一个输入字段 (tk.Entry) 和一个复选框字段 (tk.Checkbutton)。我绑定到 Entry 的 FocusOut,复选框在被选中时被赋予一个 command= to 运行。他们都被解雇了,但是...
在许多情况下,用户输入文本,并且知道我将 FocusOut 处理为提交文本更改的一种方式,他不会费心点击 Return。他只是去点击相关的复选框,确信文本更改当然会在复选框更改状态之前提交。这很重要;文本更改和复选标记状态都会在发生时发送到服务器,服务器将根据文本确定复选标记状态是否合法。
问题是"as they happen"。无论出于何种原因,tkinter 决定首先触发复选标记更改,然后才开始在文本字段上触发 FocusOut。我在 ubuntu,但我(和我的用户)习惯了 Windows SDK 应用程序,而这不会发生。结果是,当文本(据告知)处于错误状态、错误路径被采用、邪恶的事情发生、用户咬牙切齿、我受到牙医账单威胁等时,服务器会看到复选框被设置。
这对我来说没有意义。复选框必须获得焦点才能处理点击;为了获得焦点,Entry widgit 应该放弃它。但是tkinter显然不同意。
我尝试在复选标记的命令处理程序中将焦点设置到复选标记,但显然为时已晚。 tkinter 不会在此时停止并调用 FocusOut。理论上我可以让复选标记命令处理程序到达文本框并首先发送该内容,但由于复杂的原因,复选标记的代码甚至不知道文本字段存在,并且更改它会非常困难。我大概可以在 50 毫秒后使用复选标记命令 queue 和 "after" 操作来发送复选标记状态,但是我正在与手指较快的用户赛跑。
在我看来,这是一个 tkinter 错误 - 选中按钮在单击时接收焦点,处理 space 作为一种切换方式,就像其他任何可以获取焦点的东西一样。在我看来,好像 tkinter 通过以错误的顺序报告它来短暂地断言焦点属于 2 个 widgits。但是根据 tkinter 不会改变的猜测,并且在某些知识中用户不会在编辑每个文本字段后停下来并点击 Return,是否有一些聪明的解决方法?
按要求,代码。对长度感到抱歉,但我想保留所有绑定以防万一。按照显示的说明进行操作,结果显示在标题栏(和标准输出)中。
"""See line 130 to enable the hackAround
Python 3.6.7 linux mint
"""
import tkinter as tk
from tkinter import ttk
from tkinter import font
myFont = None
zerowidth = 10
tkinterHandle = None
firstThingsFirst = False
#infrastructure, ignore
class About:
def __init__(self, isA, info):
self.typeIs = isA
#infrastructure, ignore
class Cmd:
def __init__(self, name, about, text):
self.about = about
self.name = name
self.text = text
#holds a widget, base class of specific widget types
class WidgetHolder:
def __init__(self, name, about):
self.widgit = None
self.name = name
self.about = about
self.parentW = self.about.widgit
def canTakeFocus(self):
try: #apparently not everything has a 'state'.
if self.widgit['state'] == "disabled":
return False
except: #sigh
pass
return isinstance(self.widgit, tk.Entry) or isinstance(self.widgit, ttk.Button)\
or isinstance(self.widgit, ttk.Combobox) or isinstance(self.widgit, tk.Checkbutton)\
or isinstance(self.widgit, ScrolledText)
def set(self, t):
self.apply(t)
def doEnable(self, b):
pass
def setErrorColor(self):
pass
#Holds an Entry
class SingleLineInputOnAPage(WidgetHolder):
def __init__(self, c):
WidgetHolder.__init__(self, c.name, c.about)
self.txt = tk.StringVar()
vcmd = (self.parentW.register(self.onValidate), '%P')
#key, not mouse or something?
self.widgit = tk.Entry(self.parentW, validate="key",
validatecommand=vcmd, textvariable=self.txt, font=myFont, width=0)
self.savedText = c.text
self.apply(c.text)
self.widgit.bind("<Return>", (lambda event: self.timeToSend()))
self.widgit.bind("<Key-ISO_Left_Tab>", (lambda event: self.doNot()))
self.widgit.bind("<Tab>", (lambda event: self.doNot()))
self.widgit.bind("<Button>", (lambda event: self.click(event)))
self.widgit.bind("<FocusIn>", (lambda event: self.save()))
self.widgit.bind("<FocusOut>", (lambda event: self.timeToSendMaybe()))
self.widgit.bind("<Escape>", (lambda event: self.gotEsc()))
self.widgit.bind("<Control-s>", (lambda event: self.timeToSend()))
self.widgit.bind("<Control-a>", (lambda event: self.selectAll()))
def gotEsc(self):
pass
def doNot(self):
print("Reproduing the problem does not involve tab!")
def save(self):
self.savedText = self.txt.get()
def timeToSendMaybe(self):
self.timeToSend()
def click(self, event):
pass
def setErrorColor(self):
self.widgit.config(background="#ff5000")
def selectAll(self):
self.widgit.select_range(0, 'end')
return 'break'
def timeToSend(self):
self.savedText = self.txt.get()
global firstThingsFirst
firstThingsFirst = True
print("Ideally this happens first. Entry text would be sent now:", self.savedText)
return 'break'
def apply(self, t):
self.savedText = t
self.txt.set(t)
def onValidate(self, P):
return True
#Holds a checkbox
class CheckboxOnAPage(WidgetHolder):
def __init__(self, c, cmd):
WidgetHolder.__init__(self, c.name, c.about)
if cmd == None:
cmd = self.fire
self.value = tk.IntVar()
self.widgit = tk.Checkbutton(self.parentW, text="", variable=self.value, command=self.fire)
self.apply(c.text)
#self.widgit.bind("<Key-ISO_Left_Tab>", (lambda event: self.timeToSendMaybeAndPrev()))
#self.widgit.bind("<Tab>", (lambda event: self.timeToSendMaybeAndNext()))
def apply(self, t):
pass
def fire(self):
self.widgit.focus()
v = "X"[0:self.value.get()]
#to get things in a better order, set to True
if False:
global tkinterHandle #clumsy, is there a way to get self.widgit from the tk instance?
tkinterHandle.after(30, lambda sendthis=v: self.push(sendthis))
else:
self.push(v)
def push(self, v):
print("Checkmark ", v, " would be sent now. Ideally this does not happen first")
global firstThingsFirst
if firstThingsFirst:
tkinterHandle.title("In order")
else:
tkinterHandle.title("Out of order!")
class CharSheet(tk.Tk):
def __init__(self, argThing, *args, **kwargs):
global myFont
global zerowidth
global tkinterHandle
tk.Tk.__init__(self, *args, **kwargs)
self.geometry("360x80")
self.title("Out of order?")
myFont = tk.font.Font(family="Courier", size=10)
zerowidth=myFont.measure("0")
tkinterHandle = self
container = tk.Frame(self)
container.pack(side="top", fill="both", expand = True)
abt2 = About("C", "")
abt2.widgit = container
cmd2 = Cmd("b", abt2, "give me focus; then click checkbox")
w2 = SingleLineInputOnAPage(cmd2)
w2.widgit.pack()
abt = About("C", "")
abt.widgit = container
cmd = Cmd("a", abt, "")
w1 = CheckboxOnAPage(cmd, None)
w1.widgit.pack()
CharSheet(None).mainloop()
To my mind this is a tkinter bug - checkbutton receives focus when clicked,
不,这不是 tkinter 错误。 Tkinter 正在按设计执行。单击时标准复选按钮不会获得焦点。但是,ttk 复选按钮可以。
It looks to me as if tkinter is briefly asserting focus belongs to 2 widgits by reporting about it in the wrong order.
这是一个错误的评估。不可能一次将焦点放在一个以上的小部件上。
问题是您显式地将焦点设置到复选按钮,但您没有给 tkinter 事件循环机会在继续之前处理 <FocusOut>
事件。因此,即使您认为焦点在复选按钮上,但事实并非如此。在 tkinter 有机会处理焦点更改请求之前,焦点不能更改。
一个快速、hacky 的解决方案是在更改焦点后调用 update
,这使 tkinter 有机会处理所有未决事件。但是,如果存在可能导致再次调用处理程序的未决事件,调用 update
可能会很棘手。我个人的经验是,除非绝对必要,否则永远不要打电话给 update
。
如果您的主要目标是将焦点更改为复选按钮(从而强制在单击复选按钮之前处理条目小部件 <FocusOut>
绑定),您可以在小部件上创建绑定 class (Checkbutton
),在按钮处理程序之前处理。那,或者使用自动更改焦点的 ttk 复选按钮。
一个适当的最小可重现示例
我不会向您展示如何更改您的代码,因为它太复杂了,无法作为一个好的最小示例。相反,这是一个说明问题的程序。
从这个基本代码开始,它作为您观察到的问题的最小可重现示例。请注意,当您单击条目然后单击复选按钮时,文本小部件中显示的顺序是先单击复选按钮,然后单击焦点。
import tkinter as tk
from tkinter import ttk
class Example():
def __init__(self):
self.root = tk.Tk()
self.entry = tk.Entry(self.root)
self.cb = tk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
self.text = tk.Text(self.root, height=8, width=80, background="bisque")
self.vsb = tk.Scrollbar(self.root, command=self.text.yview)
self.text.configure(yscrollcommand=self.vsb.set)
self.entry.pack(side="top", fill="x")
self.cb.pack(side="top", anchor="w")
self.vsb.pack(side="right", fill="y")
self.text.pack(side="bottom", fill="both", expand=True)
self.entry.insert(0, "click here, then click on the checkbutton")
self.entry.bind("<FocusOut>", self.handle_focus_out)
self.entry.focus_set()
def handle_focus_out(self, event):
self.text.insert("end", "received entry <FocusOut>\n")
self.text.see("end")
def handle_checkbutton(self):
self.cb.focus_set()
self.text.insert("end", "received checkbutton command\n")
self.text.see("end")
e = Example()
tk.mainloop()
方案一:调用update强制tkinter处理焦点变化
在此解决方案中,在更改焦点后立即添加对 self.root.update()
的调用。这将使 tkinter 有机会在继续处理其余代码之前处理焦点更改。
def handle_checkbutton(self):
self.cb.focus_set()
self.root.update()
self.text.insert("end", "received checkbutton command\n")
self.text.see("end")
解决方案 2:使用 ttk 复选按钮
您可以使用 ttk checkbutton 而不是调用 update
。它具有 built-in 单击时将焦点设置到复选按钮的行为。
首先,修改处理程序以删除管理焦点的代码:
def handle_checkbutton(self):
self.text.insert("end", "received checkbutton command\n")
self.text.see("end")
然后,使用 ttk.Checkbutton
代替 tk.Checkbutton
:
self.cb = ttk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
这里的优点是您不必编写代码来管理焦点。
解决方案 3:将焦点处理添加到 tk Checkbutton class
第三种解决方案是向复选按钮添加绑定 class 以模仿 ttk 复选按钮的焦点窃取行为。这里的优点是您只需设置一次绑定,它将应用于 UI.
中的每个复选按钮
首先,添加以下处理程序:
def set_cb_focus(self, event):
self.text.insert("end", "setting focus via class binding\n")
self.text.see("end")
event.widget.focus_set()
接下来,在 __init__
中添加一个 class 绑定。请注意,复选按钮的 built-in 行为也是通过 class 绑定完成的。为了不删除该行为,我们需要将 add=True
包含到 bind
命令中,这会添加新行为而不是替换 built-in 行为。
self.root.bind_class("Checkbutton", "<ButtonPress-1>", self.set_cb_focus)
最后,将代码切换回使用 tk.Checkbutton
而不是 ttk.Checkbutton
:
self.cb = tk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
这与 Controlling Order of Callbacks with Tkinter 有关,但并不完全相同
我有一个输入字段 (tk.Entry) 和一个复选框字段 (tk.Checkbutton)。我绑定到 Entry 的 FocusOut,复选框在被选中时被赋予一个 command= to 运行。他们都被解雇了,但是...
在许多情况下,用户输入文本,并且知道我将 FocusOut 处理为提交文本更改的一种方式,他不会费心点击 Return。他只是去点击相关的复选框,确信文本更改当然会在复选框更改状态之前提交。这很重要;文本更改和复选标记状态都会在发生时发送到服务器,服务器将根据文本确定复选标记状态是否合法。
问题是"as they happen"。无论出于何种原因,tkinter 决定首先触发复选标记更改,然后才开始在文本字段上触发 FocusOut。我在 ubuntu,但我(和我的用户)习惯了 Windows SDK 应用程序,而这不会发生。结果是,当文本(据告知)处于错误状态、错误路径被采用、邪恶的事情发生、用户咬牙切齿、我受到牙医账单威胁等时,服务器会看到复选框被设置。
这对我来说没有意义。复选框必须获得焦点才能处理点击;为了获得焦点,Entry widgit 应该放弃它。但是tkinter显然不同意。
我尝试在复选标记的命令处理程序中将焦点设置到复选标记,但显然为时已晚。 tkinter 不会在此时停止并调用 FocusOut。理论上我可以让复选标记命令处理程序到达文本框并首先发送该内容,但由于复杂的原因,复选标记的代码甚至不知道文本字段存在,并且更改它会非常困难。我大概可以在 50 毫秒后使用复选标记命令 queue 和 "after" 操作来发送复选标记状态,但是我正在与手指较快的用户赛跑。
在我看来,这是一个 tkinter 错误 - 选中按钮在单击时接收焦点,处理 space 作为一种切换方式,就像其他任何可以获取焦点的东西一样。在我看来,好像 tkinter 通过以错误的顺序报告它来短暂地断言焦点属于 2 个 widgits。但是根据 tkinter 不会改变的猜测,并且在某些知识中用户不会在编辑每个文本字段后停下来并点击 Return,是否有一些聪明的解决方法?
按要求,代码。对长度感到抱歉,但我想保留所有绑定以防万一。按照显示的说明进行操作,结果显示在标题栏(和标准输出)中。
"""See line 130 to enable the hackAround
Python 3.6.7 linux mint
"""
import tkinter as tk
from tkinter import ttk
from tkinter import font
myFont = None
zerowidth = 10
tkinterHandle = None
firstThingsFirst = False
#infrastructure, ignore
class About:
def __init__(self, isA, info):
self.typeIs = isA
#infrastructure, ignore
class Cmd:
def __init__(self, name, about, text):
self.about = about
self.name = name
self.text = text
#holds a widget, base class of specific widget types
class WidgetHolder:
def __init__(self, name, about):
self.widgit = None
self.name = name
self.about = about
self.parentW = self.about.widgit
def canTakeFocus(self):
try: #apparently not everything has a 'state'.
if self.widgit['state'] == "disabled":
return False
except: #sigh
pass
return isinstance(self.widgit, tk.Entry) or isinstance(self.widgit, ttk.Button)\
or isinstance(self.widgit, ttk.Combobox) or isinstance(self.widgit, tk.Checkbutton)\
or isinstance(self.widgit, ScrolledText)
def set(self, t):
self.apply(t)
def doEnable(self, b):
pass
def setErrorColor(self):
pass
#Holds an Entry
class SingleLineInputOnAPage(WidgetHolder):
def __init__(self, c):
WidgetHolder.__init__(self, c.name, c.about)
self.txt = tk.StringVar()
vcmd = (self.parentW.register(self.onValidate), '%P')
#key, not mouse or something?
self.widgit = tk.Entry(self.parentW, validate="key",
validatecommand=vcmd, textvariable=self.txt, font=myFont, width=0)
self.savedText = c.text
self.apply(c.text)
self.widgit.bind("<Return>", (lambda event: self.timeToSend()))
self.widgit.bind("<Key-ISO_Left_Tab>", (lambda event: self.doNot()))
self.widgit.bind("<Tab>", (lambda event: self.doNot()))
self.widgit.bind("<Button>", (lambda event: self.click(event)))
self.widgit.bind("<FocusIn>", (lambda event: self.save()))
self.widgit.bind("<FocusOut>", (lambda event: self.timeToSendMaybe()))
self.widgit.bind("<Escape>", (lambda event: self.gotEsc()))
self.widgit.bind("<Control-s>", (lambda event: self.timeToSend()))
self.widgit.bind("<Control-a>", (lambda event: self.selectAll()))
def gotEsc(self):
pass
def doNot(self):
print("Reproduing the problem does not involve tab!")
def save(self):
self.savedText = self.txt.get()
def timeToSendMaybe(self):
self.timeToSend()
def click(self, event):
pass
def setErrorColor(self):
self.widgit.config(background="#ff5000")
def selectAll(self):
self.widgit.select_range(0, 'end')
return 'break'
def timeToSend(self):
self.savedText = self.txt.get()
global firstThingsFirst
firstThingsFirst = True
print("Ideally this happens first. Entry text would be sent now:", self.savedText)
return 'break'
def apply(self, t):
self.savedText = t
self.txt.set(t)
def onValidate(self, P):
return True
#Holds a checkbox
class CheckboxOnAPage(WidgetHolder):
def __init__(self, c, cmd):
WidgetHolder.__init__(self, c.name, c.about)
if cmd == None:
cmd = self.fire
self.value = tk.IntVar()
self.widgit = tk.Checkbutton(self.parentW, text="", variable=self.value, command=self.fire)
self.apply(c.text)
#self.widgit.bind("<Key-ISO_Left_Tab>", (lambda event: self.timeToSendMaybeAndPrev()))
#self.widgit.bind("<Tab>", (lambda event: self.timeToSendMaybeAndNext()))
def apply(self, t):
pass
def fire(self):
self.widgit.focus()
v = "X"[0:self.value.get()]
#to get things in a better order, set to True
if False:
global tkinterHandle #clumsy, is there a way to get self.widgit from the tk instance?
tkinterHandle.after(30, lambda sendthis=v: self.push(sendthis))
else:
self.push(v)
def push(self, v):
print("Checkmark ", v, " would be sent now. Ideally this does not happen first")
global firstThingsFirst
if firstThingsFirst:
tkinterHandle.title("In order")
else:
tkinterHandle.title("Out of order!")
class CharSheet(tk.Tk):
def __init__(self, argThing, *args, **kwargs):
global myFont
global zerowidth
global tkinterHandle
tk.Tk.__init__(self, *args, **kwargs)
self.geometry("360x80")
self.title("Out of order?")
myFont = tk.font.Font(family="Courier", size=10)
zerowidth=myFont.measure("0")
tkinterHandle = self
container = tk.Frame(self)
container.pack(side="top", fill="both", expand = True)
abt2 = About("C", "")
abt2.widgit = container
cmd2 = Cmd("b", abt2, "give me focus; then click checkbox")
w2 = SingleLineInputOnAPage(cmd2)
w2.widgit.pack()
abt = About("C", "")
abt.widgit = container
cmd = Cmd("a", abt, "")
w1 = CheckboxOnAPage(cmd, None)
w1.widgit.pack()
CharSheet(None).mainloop()
To my mind this is a tkinter bug - checkbutton receives focus when clicked,
不,这不是 tkinter 错误。 Tkinter 正在按设计执行。单击时标准复选按钮不会获得焦点。但是,ttk 复选按钮可以。
It looks to me as if tkinter is briefly asserting focus belongs to 2 widgits by reporting about it in the wrong order.
这是一个错误的评估。不可能一次将焦点放在一个以上的小部件上。
问题是您显式地将焦点设置到复选按钮,但您没有给 tkinter 事件循环机会在继续之前处理 <FocusOut>
事件。因此,即使您认为焦点在复选按钮上,但事实并非如此。在 tkinter 有机会处理焦点更改请求之前,焦点不能更改。
一个快速、hacky 的解决方案是在更改焦点后调用 update
,这使 tkinter 有机会处理所有未决事件。但是,如果存在可能导致再次调用处理程序的未决事件,调用 update
可能会很棘手。我个人的经验是,除非绝对必要,否则永远不要打电话给 update
。
如果您的主要目标是将焦点更改为复选按钮(从而强制在单击复选按钮之前处理条目小部件 <FocusOut>
绑定),您可以在小部件上创建绑定 class (Checkbutton
),在按钮处理程序之前处理。那,或者使用自动更改焦点的 ttk 复选按钮。
一个适当的最小可重现示例
我不会向您展示如何更改您的代码,因为它太复杂了,无法作为一个好的最小示例。相反,这是一个说明问题的程序。
从这个基本代码开始,它作为您观察到的问题的最小可重现示例。请注意,当您单击条目然后单击复选按钮时,文本小部件中显示的顺序是先单击复选按钮,然后单击焦点。
import tkinter as tk
from tkinter import ttk
class Example():
def __init__(self):
self.root = tk.Tk()
self.entry = tk.Entry(self.root)
self.cb = tk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
self.text = tk.Text(self.root, height=8, width=80, background="bisque")
self.vsb = tk.Scrollbar(self.root, command=self.text.yview)
self.text.configure(yscrollcommand=self.vsb.set)
self.entry.pack(side="top", fill="x")
self.cb.pack(side="top", anchor="w")
self.vsb.pack(side="right", fill="y")
self.text.pack(side="bottom", fill="both", expand=True)
self.entry.insert(0, "click here, then click on the checkbutton")
self.entry.bind("<FocusOut>", self.handle_focus_out)
self.entry.focus_set()
def handle_focus_out(self, event):
self.text.insert("end", "received entry <FocusOut>\n")
self.text.see("end")
def handle_checkbutton(self):
self.cb.focus_set()
self.text.insert("end", "received checkbutton command\n")
self.text.see("end")
e = Example()
tk.mainloop()
方案一:调用update强制tkinter处理焦点变化
在此解决方案中,在更改焦点后立即添加对 self.root.update()
的调用。这将使 tkinter 有机会在继续处理其余代码之前处理焦点更改。
def handle_checkbutton(self):
self.cb.focus_set()
self.root.update()
self.text.insert("end", "received checkbutton command\n")
self.text.see("end")
解决方案 2:使用 ttk 复选按钮
您可以使用 ttk checkbutton 而不是调用 update
。它具有 built-in 单击时将焦点设置到复选按钮的行为。
首先,修改处理程序以删除管理焦点的代码:
def handle_checkbutton(self):
self.text.insert("end", "received checkbutton command\n")
self.text.see("end")
然后,使用 ttk.Checkbutton
代替 tk.Checkbutton
:
self.cb = ttk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
这里的优点是您不必编写代码来管理焦点。
解决方案 3:将焦点处理添加到 tk Checkbutton class
第三种解决方案是向复选按钮添加绑定 class 以模仿 ttk 复选按钮的焦点窃取行为。这里的优点是您只需设置一次绑定,它将应用于 UI.
中的每个复选按钮首先,添加以下处理程序:
def set_cb_focus(self, event):
self.text.insert("end", "setting focus via class binding\n")
self.text.see("end")
event.widget.focus_set()
接下来,在 __init__
中添加一个 class 绑定。请注意,复选按钮的 built-in 行为也是通过 class 绑定完成的。为了不删除该行为,我们需要将 add=True
包含到 bind
命令中,这会添加新行为而不是替换 built-in 行为。
self.root.bind_class("Checkbutton", "<ButtonPress-1>", self.set_cb_focus)
最后,将代码切换回使用 tk.Checkbutton
而不是 ttk.Checkbutton
:
self.cb = tk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")