使用Tkinter控制回调顺序
简短版:有没有办法控制在Tkinter中与不同控件相关的回调函数的处理顺序?
详细版:我在使用IDLE测试和学习Tkinter时,写了以下程序:
import Tkinter
guiRoot = Tkinter.Tk()
hWindow = Tkinter.Frame(guiRoot)
hWindow.grid(); x = 0; y = 0
et1 = Tkinter.Entry(hWindow)
et2 = Tkinter.Entry(hWindow)
def ut(x, y, event):
print "X",x,", Y:",y
print "Args:",
print "Widget:",event.widget.get()
print
def Tkquit():
print "Leaving program..."
et1 = Tkinter.Entry(hWindow)
et2 = Tkinter.Entry(hWindow)
eb = Tkinter.Button(hWindow, command=Tkquit, text="Send")
et1.grid(column=x, row=y)
et1.bind("<FocusOut>", lambda event, x1=x, y1=y:ut(x1, y1, event))
y = y + 1; et2.grid(column=x, row=y)
et2.bind("<FocusOut>", lambda event, x1=x, y1=y:ut(x1, y1, event))
y = y + 1
eb.grid(column=x, row=y)
guiRoot.mainloop()
当我从一个输入框移动到另一个输入框时,ut()这个函数会被调用。当我点击按钮时,我会看到“Leaving program...”的信息(后面会在这个程序里加上退出的代码),但是刚刚失去焦点的文本框却没有任何消息。
这让我有两个疑问:
1) 为什么输入框的回调函数没有被调用?
还有,隐含的意思是,如果我想让那个按钮退出程序,我希望在按钮的回调函数执行之前,其他的回调函数能先完成。所以:
2) 我该如何控制Tkinter中回调函数的调用顺序?
3 个回答
简短版本:
是的,可以。
详细版本:
使用一个统一的<回调处理器>,这个处理器需要在Tkinter的控件事件路由接口上注册(也就是在调用方那边,使用command =
<回调处理器>,或者用<某个Tk控件实例>.bind(
<<_事件类型_>>,
<回调处理器> )
,或者用<某个Tk状态变量>.trace_variable(
{ "w" | "r" | "u" }, <回调处理器> )
)。
另外,设计一个合适的中间层应用逻辑,用来接收所有经过路由的事件,捕捉调用者的上下文,并对接收到的回调请求进行排序和处理,按照你希望的顺序来处理它们。
Tkinter为每个事件提供了很多细节信息,因此你可以对每次调用或每个事件有完全的控制。
编辑#1
2014-08-25 07:00
一个command
设置有很强的能力,可以使用带有可选实例包装的lambda容器:
command = ( lambda deltaMinutes = 1, # sets lambda-container to use a var.
aWidget = self.B1, # sets lambda-container to ref aWidget->B1
anotherVar = 0:
self.DateTimeNOW_MODEL_Add( deltaMinutes )
or # trick to work with a sequence of commands
self.LogPROCESS( "Step back 1x M1", anotherVar )
)
我觉得你可能在解决一个错误的问题。当你使用一个有回调函数的库时,你完全得听这个库的。除非Tkinter给你提供了改变这个行为的选项,否则你无法选择你的函数什么时候被调用(如果有的话),或者它们被调用的顺序。很可能这些函数和Tkinter的内部实现紧密相连,所以不容易改变。
如果你的程序本身依赖于事件按照特定顺序发生,那你就给程序引入了一个脆弱的因素。在一些少见的情况下,这可能是必要的(比如为了效率),但在大多数情况下,这只会让你的程序和库之间的关系变得复杂。如果库后来决定改变事件发生的顺序怎么办?如果用户找到了一种事件组合导致你的逻辑出错怎么办?如果你后来添加了什么东西,结果引入了逻辑错误怎么办?
为了让你的界面和逻辑更稳健,尽量设计你的程序,使得处理函数被调用的顺序对程序的行为没有影响。尽可能保持你的函数是幂等的,这样可以避免程序状态的组合爆炸。
在你的情况下,如果你希望某个动作在按钮被触发之前执行,可以考虑直接调用这个动作,确保它会发生,而不管Tkinter是否发出了FocusOut事件。
你发现按下退出键时,<FocusOut>
事件没有被触发,是因为输入框并没有失去焦点。默认情况下,Tkinter的按钮在被点击时不会获得焦点。不过,如果你使用ttk.Button
,它就会“抢走”焦点,这样你的回调函数就会被调用。个人觉得这是ttk按钮实现上的一个小问题,但这种情况已经存在很多年了。
无论如何,你可以给按钮添加一些设置,这样在点击按钮时就会抢走焦点。这样做会导致之前获得焦点的控件触发<FocusOut>
事件。通常情况下,这并不是我们想要的效果,但Tkinter给了我们这样的灵活性。
比如,你可以在代码中添加以下内容,这样在点击按钮时就会触发<FocusOut>
事件:
# force the button to steal focus when clicked
eb.bind("<1>", lambda event: eb.focus_set())
你无法控制事件处理的顺序,因为事件是按照发生的顺序被处理的。如果一个工具包做其他的事情,那就是不对的。我想你会发现,一旦你对Tkinter的工作原理有了更好的理解,你就不需要以不按顺序处理事件了。