当您更新进度条小部件的值(或者,通常是任何小部件的值或配置)时,Tk 会创建一个内部空闲*事件来进行重绘;这个想法是,如果您连续更新多个事物以响应一系列事件(这是一件非常常见的事情!),那么您只会重绘一个小部件。这在大多数情况下都非常有效,但在您执行某种繁忙的动画循环时就不行了。执行 anupdate idletasks
会使任何预定的空闲事件立即发生;此时会处理内部生成的重绘。
但这还不是全部。还有来自外部世界(即主机窗口系统)通过真实事件的外部生成的重绘:在 Windows 上WM_PAINT
,在 X11 上Expose
,在 OSX 上,......,在 OSX 上很复杂。(为了争论,忽略那个平台。)因为这些外部事件来自外部世界,它们只有在事件循环的完整运行中才会被注意到——低级事件处理系统不知道它们是请求重绘,直到收到它们 - 这意味着使用主事件循环,或由 开始的辅助事件循环,vwait
完整的update
,或tkwait
. (实际上,处理这些外部重绘事件的方式是在空闲事件中安排重绘,因为可能还有其他事件,例如小部件调整大小或焦点更改,也需要同时处理,并且也需要重绘。)
处理此问题的推荐方法是将执行长时间运行处理的代码拆分,以便您可以定期返回事件循环并允许它处理任何未决的外部事件。在 8.5 及之前的版本中,您通过after $aFewMilliseconds doTheNextBit
不时使用来做到这一点;这需要对代码进行结构化,使其以连续传递方式工作,这可能会有点痛苦。另一方面,在 8.6 中,您可以将长时间运行的代码放入协程中,并在after $aFewMilliseconds [info coroutine];yield
不显着中断代码流的情况下进行停顿。
不太推荐的是洒在update
. 这样做的问题是它启动了一个辅助事件循环,如果您尝试重新进入任何长时间运行的处理,这可能会导致堆栈耗尽问题。可以通过适当的联锁来完成这项工作(例如,禁用在您进行长时间处理时可能会导致问题的按钮),但这确实更棘手。您还可以考虑将处理转移到一个单独的非 GUI线程**,该线程不时将消息发送回主 GUI 线程以导致进度条发生变化。(顺便说一句,Tcl 将线程间消息视为事件。)在您的情况下,多线程可能有意义,也可能没有意义。
* 您可以制作自己的空闲事件,after idle
但通常不需要它们。
** 真正的多线程 GUI 非常疯狂,而且 Tk 无论如何也不能那样工作。每个带有 Tk 的线程都有自己完整的副本。