一、说明
事件和动作有关;所有的界面都与运动有关,本篇将对事件、事件触发、绑定回调函数等,其实是一系列部件配合的复杂的过程,这些过程牵扯到系统如何设计,线程、消息队列循环等。本篇将详细介绍各种因素的关系。
二、事件循环
在上一章的末尾,我们解释了如何使用进度条向用户提供有关长时间运行的反馈 操作。进度条本身很简单:调用其方法,执行操作,然后 调用其方法。不幸的是,您了解到,如果您尝试此操作,您的应用程序将最 可能看起来完全冻结。start``stop
为了理解原因,我们需要在Tk概念一章中重新审视我们对事件处理的讨论。 正如我们所看到的,在我们构造应用程序的初始用户界面之后,它会进入 Tk 事件循环。事件循环 持续处理从系统事件队列中提取的事件,通常每秒数十次。它监视鼠标或键盘事件,根据需要调用命令回调和事件绑定。
不太明显的是,所有屏幕更新仅在事件循环中处理。例如,您可以更改标注微件的文本。 但是,该更改不会立即显示在屏幕上。相反,小部件会通知 Tk 需要重新绘制。稍后,在处理之间 其他事件,Tk 的事件循环将要求小部件重新绘制自身。所有绘制仅在事件循环中发生。 更改似乎发生了 因为更改小部件和事件循环中实际重绘之间的时间太短了。
显示应用程序回调和屏幕更新的事件循环。
三、阻塞事件循环
当事件循环长时间无法处理事件时,您会遇到问题。您的应用程序不会重绘 或响应事件,并且看起来被冻结。事件循环被称为阻塞。怎么会这样?
让我们首先将事件循环可视化为执行时间线。在正常情况下,每次偏离事件循环 (回调、屏幕更新)只需几分之一秒即可将控制权返回到事件循环。
行为良好的事件循环的执行时间线。
在我们的场景中,整个事情可能从一个事件开始,比如用户按下 一个按钮。因此,事件循环调用我们的应用程序代码来处理事件。我们的代码创建进度条,执行(冗长的)操作, 并停止进度条。只有这样,我们的代码才会将控制权返回到事件循环。在此期间未处理任何事件,并且未发生屏幕重绘。事件在事件队列中堆积如山。
冗长的回调阻止事件循环。
为了防止阻塞事件循环,事件处理程序必须快速执行并将控制权返回到事件循环。
如果要执行长时间运行的操作或可能需要很长时间的网络 I/O 等操作,则有一些操作 您可以采取不同的方法。
对于技术上更倾向于的人,Tk使用单线程,事件驱动的编程模型。所有 GUI 代码、事件循环和您的 应用程序在同一线程中运行。因此,强烈建议不要进行任何阻止事件处理程序的调用或计算。 某些 GUI 工具包使用不同的模型,这些模型允许阻塞代码,在单独的线程中运行 GUI 和事件处理程序 应用程序代码等试图将这些模型硬塞进 Tk 可能是一个秘诀 挫败感并导致脆弱和黑客的代码。如果你尊重Tk的模式而不是与之抗争,你就不会遇到问题。
四、一步一个脚印
如果可能的话,你能做的最好的事情就是把你的操作分成几个小步骤,每个步骤都可以非常快速地执行。你让 事件循环负责下一步何时发生。这样,事件循环将继续运行,处理常规事件, 更新屏幕,并在所有这些之间调用代码以执行操作的下一步。
为此,我们使用计时器事件 。我们的程序可以要求事件循环在以后生成其中一个事件。 作为其正常工作的一部分,当事件循环到达该时间时,它将回调我们的代码来处理事件。我们的代码 将执行操作的下一步。然后,它为操作的下一步安排另一个计时器事件,并立即 将控制权返回到事件循环。
将大型操作分解为与计时器事件绑定在一起的小步骤。
Tk 的命令可用于生成计时器事件。提供在应触发事件之前等待的毫秒数。 如果 Tk 忙于处理其他事件,则可能会晚发生,但在此之前不会发生。 您还可以要求生成事件;当队列中没有其他事件需要处理时,它将触发。 (Tk 的屏幕更新和重绘发生在空闲事件的上下文中。您可以在 中找到更多详细信息 参考手册。after``idle``after
在以下示例中,我们将执行一个分为 20 个小步骤的冗长操作。在执行此操作时, 我们将更新进度条并允许用户中断操作。
def` `start`():
b.configure(text=`'Stop'`, command=stop)
l[`'text'`] = `'Working...'`
`global interrupt; interrupt = False`
`root.after(1, step)`
`def` `stop`():
`global interrupt; interrupt = True`
`def` `step`(count=`0`):
p[`'value'`] = count
`if interrupt:`
`result(None)`
`return`
`root.after(100)` `# next step in our operation; don't take too long!`
`if count == 20:` `# done!`
`result(42)`
`return`
`root.after(1, lambda: step(count+1))`
`def` `result`(answer):
p[`'value'`] = `0`
b.configure(text=`'Start!'`, command=start)
l[`'text'`] = `"Answer: "` + `str`(answer) `if` answer `else` `"No Answer"`
f = ttk.Frame(root); f.grid()
b = ttk.Button(f, text=`"Start!"`, command=start); b.grid(column=`1`, row=`0`, padx=`5`, pady=`5`)
l = ttk.Label(f, text=`"No Answer"`); l.grid(column=`0`, row=`0`, padx=`5`, pady=`5`)
p = ttk.Progressbar(f, orient=`"horizontal"`, mode=`"determinate"`, maximum=`20`);
p.grid(column=`0`, row=`1`, padx=`5`, pady=`5`)`
Ruby/Tk 提供该类作为 Tk 命令的前端。第一个参数 构造函数是计时器事件的毫秒数,秒是计时器应重复的次数。 Tk 命令的阻塞变体没有一个很好的界面,所以我们改用。
为了中断该过程,我们设置一个全局变量,并在每次计时器事件触发时检查它。另一种选择是取消待处理的计时器事件。当我们创建计时器事件时,它会返回一个 ID 号来唯一标识待处理的计时器。要取消它,我们可以调用 after_cancel 方法,并向其传递唯一的 id。
您还会注意到,我们使用了 after 的阻塞形式来模拟执行我们的操作。在这种形式中,调用会阻塞,在返回之前等待给定时间,而不是调度事件。它的工作原理与睡眠系统调用相同。
五、异步 I/O
计时器事件负责分解长时间运行的计算,您知道可以保证每个步骤快速完成,以便 处理程序将返回到事件循环。如果您的操作可能无法快速完成怎么办?当您制作各种品种时,可能会发生这种情况 对操作系统的调用。最常见的是当我们执行某种 I/O 时,无论是编写文件、与数据库通信还是 从远程 Web 服务器检索数据。
大多数 I/O 调用都在阻塞 ,因此在操作完成(或失败)之前它们不会返回。我们想要使用的是非阻塞 或异步 I/O 调用。当您进行异步 I/O 调用时,它会在操作完成之前立即返回。您的代码 可以继续运行,或者在这种情况下,返回到事件循环。稍后,当 I/O 操作完成时,您的程序会收到通知,并且可以 处理 I/O 操作的结果。
如果这听起来像是将 I/O 视为另一种类型的事件,那么您完全正确。事实上,它也被称为事件驱动的 I/O。
在 Tcl 中,异步 I/O 通过命令进行管理,该命令使用与 Tk、计时器等相同的事件循环。 参考手册中有详细说明。它广泛用于Tcl的其他部分,例如 HTTP 包,以及第三方包。fileeevent
在 Python 中,异步 I/O 由模块和其他模块提供 层层叠叠。asyncio
所有异步应用程序都严重依赖于事件循环。多么方便;特金特有一个很棒的事件循环!不幸 异步事件循环和 Tkinter 事件循环是不一样的。您不能同时运行两者 时间,至少不在同一个线程中(好吧,你可以让一个重复调用另一个,但它非常笨拙和脆弱)。
我的建议:将 Tkinter 保留在主线程中,并在另一个线程中分拆您的 asyncio 事件循环。
在主线程中运行的应用程序代码可能需要与在其他线程中运行的 asyncio 事件循环进行协调。 您可以调用在 asyncio 事件循环线程中运行的函数(甚至可以从 Tkinter 事件循环中调用,例如,在小部件回调中) 使用 asyncio 方法。要从 asyncio 事件循环调用 Tkinter,请继续阅读。call_soon_threadsafe
六、线程或进程
有时,将长时间运行的计算分解为每个快速运行的离散部分是不可能的或不切实际的。或者你可能 使用不支持异步操作的库。或者,像Python一样,它不能很好地与Tk的事件循环配合使用。 在这种情况下,为了保持Tk GUI的响应,您需要移动它们 从事件处理程序中取出耗时的操作或库调用,并在其他位置运行它们。线程,甚至其他进程,都可以对此有所帮助。asyncio
在线程中运行任务、与它们通信等超出了本教程的范围。 但是,您应该注意将 Tk 与线程一起使用有一些限制。 主要规则是,您只能从加载 Tk 的线程进行 Tk 调用。
Tkinter 在内部竭尽全力,因此您可以通过将它们路由到 主线程(创建 Tk 实例的线程)。它大多有效,但并非总是如此。尽管它尽力做,但我强烈推荐 您可以从单个线程进行所有 Tkinter 调用。
如果您需要从另一个线程到运行 Tkinter 的线程进行通信,请使其尽可能简单。用于将虚拟事件发布到 Tkinter 事件队列,然后在代码中发布到该事件。event_generate``bind
root`.event_generate(`"<<MyOwnEvent>>"`)`
它可能更加复杂。Tcl/Tk 库可以在有或没有线程支持的情况下构建。 如果应用程序中有多个线程,请确保在线程生成中运行。如果您不确定,请检查 Tcl 变量 ;它应该是 ,而不是 。tcl_platform(threaded)``1``0
>>> tkinter.Tcl().eval('set tcl_platform(threaded)')
`% `set tcl_platform(threaded)
>>` `TclTkIp.new._eval('set tcl_platform(threaded)')
`Perl> `Tkx::eval("set tcl_platform(threaded)")
大多数人都应该运行线程构建。在Tcl/Tk中创建非线程构建的能力将来可能会消失。 如果您使用的是带有线程代码的非线程化构建,请将此视为应用程序中的错误,而不是使其工作的挑战。
七、嵌套事件处理
前三种方法是处理长时间运行的操作,同时保持Tk GUI响应的正确方法。他们有什么 Common 是连续处理各种事件的单个事件循环。该事件循环将调用应用程序代码中的事件处理程序, 做他们的事并迅速返回。
还有另一种方式。在长时间运行的操作中,可以调用事件循环来处理一堆事件。你可以用一个 命令。不会弄乱计时器事件或异步 I/O。相反,你只是洒一些电话 在整个操作过程中。如果您只想保留屏幕重绘而不处理其他事件,甚至还有一个选项()。update``update``update_idletasks
这种方法非常简单。如果你幸运的话,它可能会起作用。至少在一段时间内。但迟早,你会遇到 试图以这种方式做事的严重困难。某些内容不会更新,事件处理程序没有被调用,事件正在发生 失踪或被解雇,甚至更糟。你会把程序的逻辑翻过来,然后扯掉你的头发,试图让它再次工作。
使用 时,不会将控制权返回给正在运行的事件循环。您正在有效地启动嵌套的新事件循环 在现有的一个中。请记住,事件循环遵循单个执行线程:没有线程,没有协程。如果你不小心,你就会去 最终从事件循环中调用的事件循环...好吧,你明白了。如果你甚至意识到你正在这样做,放松一下 事件循环(每个循环可能有不同的条件来终止它)将是一个有趣的练习。现实与你的心智模型不符 一个简单的事件循环,一次调度一个事件,独立于所有其他事件。这是与Tk模式作斗争的典型例子。 在非常特殊的情况下,可以使其工作。实际上,你是在自找麻烦。不要说你没有被警告...update
嵌套事件循环...这样疯狂就说谎了。