tkinter - tkinter:绑定事件中奇怪的操作顺序,如何更改?
问题描述
这与使用 Tkinter 控制回调顺序有关,但并不完全相同
我有一个条目字段(tk.Entry)和一个复选框字段(tk.Checkbutton)。我绑定到Entry 的FocusOut,并且复选框被赋予一个command= 以在它被选中时运行。这俩都被解雇了,但是...
在许多情况下,用户输入文本,并且知道我处理 FocusOut 作为提交文本更改的一种方式,他不会费心点击 Return。他只是去点击相关的复选框,确保文本更改将在复选框更改状态之前提交。这很重要;文本更改和复选标记状态都会在发生时发送到服务器,服务器将根据文本确定复选标记状态是否合法。
问题是“当它们发生时”。无论出于何种原因,tkinter 决定首先触发复选标记更改,然后才开始在文本字段上触发 FocusOut。我在 ubuntu 上,但我(和我的用户)已经习惯了 Windows SDK 应用程序,这不会发生。结果是,当文本(据被告知)处于错误状态、采用错误路径、发生邪恶事件、用户咬牙切齿、我受到牙医账单的威胁等时,服务器会看到已设置的复选框。
这对我来说没有意义。复选框应该必须获得焦点才能处理点击;为了获得焦点,Entry widgit 应该不得不放弃它。但 tkinter 显然不同意。
我尝试在复选标记的命令处理程序中将焦点设置为复选标记,但显然为时已晚。tkinter 不会在那个时候停下来打电话给 FocusOut。理论上我可以让复选标记命令处理程序到达文本框并首先发送该内容,但由于复杂的原因,复选标记的代码甚至不知道文本字段的存在,并且更改它会非常困难。我大概可以让复选标记命令在 50 毫秒后排队一个“之后”操作以发送复选标记状态,但是我正在与快速手指的用户竞争。
在我看来,这是一个 tkinter 错误 - 单击时 checkbutton 接收焦点,处理空间作为切换的方式,就像其他任何可以获取焦点的东西一样。在我看来,好像 tkinter 通过以错误的顺序报告焦点来短暂断言焦点属于 2 个小工具。但是假设 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()
解决方案
在我看来,这是一个 tkinter 错误 - 单击时检查按钮会获得焦点,
不,这不是 tkinter 错误。Tkinter 正在按设计执行。单击时,标准检查按钮不会获得焦点。但是,ttk 复选按钮可以。
在我看来,好像 tkinter 通过以错误的顺序报告焦点来短暂断言焦点属于 2 个小工具。
这是一个不正确的评估。一次不可能将焦点集中在一个以上的小部件上。
问题是您明确地将焦点设置为检查按钮,但您没有让 tkinter 事件循环有机会<FocusOut>
在继续之前处理事件。因此,即使您认为焦点在复选按钮上,但事实并非如此。在 tkinter 有机会处理更改焦点的请求之前,焦点无法更改。
一个快速、hacky 的解决方案是update
在更改焦点后调用,这让 tkinter 有机会处理所有未决事件。但是,update
如果存在可能导致再次调用处理程序的未决事件,则调用可能会很棘手。我个人的经验法则是update
除非绝对必要,否则永远不要打电话。
如果您的主要目标是将焦点更改为检查按钮(从而强制在<FocusOut>
单击检查按钮之前处理条目小部件绑定),您可以在小部件类 ( 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 复选按钮
update
您可以使用 ttk 复选按钮,而不是调用。它具有在单击时将焦点设置到复选按钮的内置行为。
首先,修改处理程序以删除管理焦点的代码:
def handle_checkbutton(self):
self.text.insert("end", "received checkbutton command\n")
self.text.see("end")
然后,使用ttk.Checkbutton
instad tk.Checkbutton
:
self.cb = ttk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
这里的优点是您不必编写代码来管理焦点。
解决方案 3:将焦点处理添加到 tk Checkbutton 类
第三种解决方案是向 checkbutton 类添加一个绑定,以模仿 ttk checkbutton 的焦点窃取行为。这里的优点是您只需设置一次绑定,它将应用于 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__
. 请注意,检查按钮的内置行为也是通过类绑定完成的。为了不删除该行为,我们需要将其包含add=True
到bind
命令中,该命令会添加新行为而不是替换内置行为。
self.root.bind_class("Checkbutton", "<ButtonPress-1>", self.set_cb_focus)
最后,将代码切换回使用 atk.Checkbutton
而不是ttk.Checkbutton
:
self.cb = tk.Checkbutton(self.root, command=self.handle_checkbutton, text="Click me")
推荐阅读
- email - Flask-Mail - 任何方式来请求阅读回执?
- java - 怎么
::新的Runnable? - azure - 视频索引允许在 1 小时内上传 24 小时的视频
- vb.net - 为什么 continueWith 使用 action(of task) 作为参数?
- c++ - 使用来自 boost::math 的 Gauss-Kronrod 求积对复杂函数进行积分
- python - 如果字典中不存在键,我如何跳到下一个条目
- android-studio - 为什么AS要在flutter更新后编译项目
- mongodb - 基于 Range 查询 MongoDB 中嵌入文档的数组
- flutter - flutter bottomNavigationBar和自定义字体问题
- css - 如何用周围的白色更改复选框背景颜色