tkinter06_事件循环
原文:https://tkdocs.com/tutorial/eventloop.html
事件循环
在上一章的最后,我们解释了如何使用进度条向用户提供有关长时间运行的操作的反馈。进度条本身很简单:调用它的start
方法,执行您的操作,然后调用它的stop
方法。不幸的是,您了解到如果您尝试这样做,您的应用程序很可能会完全冻结。
要理解为什么,我们需要重新讨论我们在 Tk 概念一章中对事件处理的讨论。正如我们所见,在我们构建应用程序的初始用户界面后,它进入了 Tk 事件循环。在事件循环中,它不断地处理从系统事件队列中拉出的事件,通常每秒几十次。它监视鼠标或键盘事件,根据需要调用命令回调和事件绑定。
不太明显,所有屏幕更新仅在事件循环中处理。例如,您可以更改标签小部件的文本。但是,这种变化不会立即出现在屏幕上。相反,小部件通知 Tk 它需要重绘。稍后,在处理其他事件之间,Tk 的事件循环将要求小部件重绘自身。所有绘制仅发生在事件循环中。更改似乎立即发生,因为对小部件进行更改和事件循环中的实际重绘之间的时间非常短。
显示应用程序回调和屏幕更新的事件循环。
阻塞事件循环
遇到问题的地方是事件循环在很长一段时间内无法处理事件。您的应用程序不会重绘或响应事件,并且会出现冻结状态。事件循环被称为被阻塞。这怎么会发生?
让我们首先将事件循环可视化为执行时间线。在正常情况下,每次与事件循环(回调、屏幕更新)的偏差在将控制权返回给事件循环之前只需要几分之一秒。
行为良好的事件循环的执行时间表。
在我们的场景中,整个过程可能是从用户按下按钮等事件开始的。所以事件循环调用我们的应用程序代码来处理事件。我们的代码创建进度条,执行(冗长的)操作,并停止进度条。只有这样,我们的代码才会将控制权返回给事件循环。在此期间没有处理任何事件。没有发生屏幕重绘。它们只是堆积在事件队列中。
冗长的回调阻塞了事件循环。
为了防止阻塞事件循环,事件处理程序必须快速执行并将控制权返回给事件循环。
如果您确实有一个长时间运行的操作要执行,或者诸如网络 I/O 之类的可能需要很长时间的操作,您可以采用几种不同的方法。
对于技术性更强的人,Tk 使用单线程、事件驱动的编程模型。所有 GUI 代码、事件循环和您的应用程序都在同一个线程中运行。因此,强烈建议不要进行任何阻塞事件处理程序的调用或计算。其他一些 GUI 工具包使用不同的模型,这些模型允许阻塞代码、在与应用程序代码不同的线程中运行 GUI 和事件处理程序等。尝试将这些模型硬塞到 Tk 中可能会令人沮丧,并导致代码脆弱和 hacky。如果你尊重 Tk 的模型而不是试图与之抗争,你就不会遇到问题。
一步一步来
如果可能,您能做的最好的事情就是将您的操作分解为非常小的步骤,每个步骤都可以非常快地执行。当下一步发生时,您让事件循环负责。这样,事件循环继续运行,处理常规事件,更新屏幕,并在所有这些之间调用您的代码以执行下一步操作。
为此,我们使用了计时器事件。我们的程序可以要求事件循环在未来的某个时间生成这些事件之一。作为其常规工作的一部分,当事件循环到达该时间时,它会回调我们的代码来处理事件。我们的代码将执行操作的下一步。然后它为下一步的操作安排另一个计时器事件,并立即将控制权返回给事件循环。
将大型操作分解为与计时器事件联系在一起的小步骤。
Tk 的after
命令可用于生成定时器事件。您提供等待事件触发的毫秒数。如果 Tk 忙于处理其他事件但不会在此之前发生,则它可能会晚于发生。您也可以要求idle
生成一个事件;当队列中没有其他事件需要处理时,它将触发。(Tk 的屏幕更新和重绘发生在空闲事件的上下文中。)您可以after
在参考手册中找到更多详细信息。
在下面的示例中,我们将执行一个分解为 20 个小步骤的长操作。在执行此操作时,我们将更新进度条,并允许用户中断操作。
def start():b.configure(text='Stop', command=stop)l['text'] = 'Working...'global interrupt; interrupt = Falseroot.after(1, step)def stop():global interrupt; interrupt = Truedef step(count=0):p['value'] = countif interrupt:result(None)returnroot.after(100) # next step in our operation; don't take too long!if count == 20: # done!result(42)returnroot.after(1, lambda: step(count+1))def result(answer):p['value'] = 0b.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)
为了中断进程,我们刚刚设置了一个全局变量,下次触发计时器事件时会检查该变量。另一种选择是取消挂起的计时器事件。当我们创建计时器事件时,它会返回一个 id 号来唯一标识挂起的计时器。要取消它,我们可以调用after_cancel
方法,将唯一的 id 传递给它。
您还会注意到我们使用了一种阻塞形式after
来模拟执行我们的操作。调用阻塞,在返回之前等待给定的时间,而不是安排事件在未来发生。它的工作原理与sleep
系统调用相同。
异步输入/输出
计时器事件负责分解长时间运行的计算,您知道每个步骤都可以保证快速完成,以便您的处理程序将返回到事件循环。如果您的操作可能无法快速完成怎么办?当您对操作系统进行各种调用时,就会发生这种情况。最常见的是当我们进行某种 I/O 时,无论是写入文件、与数据库通信还是从远程 Web 服务器检索数据。
大多数 I/O 调用都是阻塞的。在操作完成(或失败)之前,它们不会返回。我们想要使用的是非阻塞 或异步I/O 调用。当您进行异步 I/O 调用时,它会在操作完成之前立即返回。您的代码可以继续运行,或者在这种情况下,返回到事件循环。稍后,当 I/O 操作完成时,您的程序会收到通知并可以处理 I/O 操作的结果。
如果这听起来像是将 I/O 视为另一种类型的事件,那么您是完全正确的。事实上,它也被称为事件驱动的 I/O。
在 Python 中,异步 I/O 是通过asyncio
模块提供的,以及位于它之上的许多其他模块。
所有 asyncio 应用程序都严重依赖事件循环。多么方便,Tkinter 有一个很棒的事件循环!不幸的是,asyncio 事件循环和 Tkinter 事件循环并不相同。您不能真正让它们同时运行,至少在同一个线程中运行(好吧,您可以将一个调用重复到另一个中,但它非常笨拙和脆弱)。
我的建议:将 Tkinter 保留在主线程中,并在另一个线程中分离您的 asyncio 事件循环。
在主线程中运行的应用程序代码可能需要与在另一个线程中运行的 asyncio 事件循环进行协调。您可以使用 asynciocall_soon_threadsafe
方法调用在 asyncio 事件循环线程中运行的函数(甚至来自 Tkinter 事件循环,例如在小部件回调中)。要从 asyncio 事件循环调用 Tkinter,请继续阅读。
线程或进程
有时,将长时间运行的计算分解为每个都快速运行的离散部分是不可能或不切实际的。或者您可能正在使用不支持异步操作的库。或者,像 Python 的asyncio
,它不能很好地与 Tk 的事件循环配合使用。在这种情况下,为了让您的 Tk GUI 保持响应,您需要将那些耗时的操作或库调用移出您的事件处理程序并在其他地方运行它们。线程,甚至其他进程,可以帮助解决这个问题。
在线程中运行任务、与它们通信等超出了本教程的范围。但是,您应该注意将 Tk 与线程一起使用的一些限制。主要规则是您只能从加载 Tk 的线程中进行 Tk 调用。
Tkinter 在内部进行了大量工作,因此您可以通过将多个线程路由到主线程(创建 Tk 实例的线程)来从多个线程进行 Tkinter 调用。它主要有效,但并非总是如此。尽管它尝试做所有事情,但我强烈建议您从单个线程进行所有 Tkinter 调用。
如果您需要从另一个线程到运行 Tkinter 的线程进行通信,请使其尽可能简单。用于event_generate
将虚拟事件发布到 Tkinter 事件队列,然后发布bind
到代码中的该事件。
root.event_generate("<<MyOwnEvent>>")
它可能更复杂。Tcl/Tk 库可以在有或没有线程支持的情况下构建。如果您的应用程序中有多个线程,请确保您在线程构建中运行。如果您不确定,请检查 Tcl 变量tcl_platform(threaded)
;应该是1
,不是0
。
>>> tkinter.Tcl().eval('set tcl_platform(threaded)')
嵌套事件处理
前三种方法是处理长时间运行的操作同时仍然保持 Tk GUI 响应的正确方法。它们的共同点是一个单独的事件循环,可以连续处理各种事件。该事件循环将在您的应用程序代码中调用事件处理程序,它们执行它们的操作并快速返回。
还有另一种方式。在长时间运行的操作中,您可以调用事件循环来处理一堆事件。您可以使用单个命令执行此操作,update
. 没有搞乱定时器事件或异步 I/O。相反,您只需update
在整个操作过程中添加一些调用。如果您只想保持屏幕重绘而不处理其他事件,甚至还有一个选项 ( update_idletasks
)。
这种方法非常容易。如果你很幸运,它可能会奏效。至少有一段时间。但迟早,你会在尝试以这种方式做事时遇到严重的困难。某些东西不会更新,事件处理程序没有得到应有的调用,事件丢失或被无序触发,或者更糟。您将彻底改变程序的逻辑,并试图使其再次运行。
当你使用 update
,您不会将控制权返回给正在运行的事件循环。您正在有效地启动一个嵌套在现有事件循环中的新事件循环。请记住,事件循环遵循单个执行线程:没有线程,没有协程。如果你不小心,你最终会在从调用的事件循环中调用事件循环…好吧,你明白了。如果你甚至意识到你正在这样做,展开事件循环(每个循环可能有不同的条件来终止它)将是一个有趣的练习。现实与您的简单事件循环的心理模型不匹配,一次一个事件,独立于其他事件。这是一个对抗 Tk 模型的经典例子。在非常特殊的情况下,可以让它发挥作用。在实践中,你 重新自找麻烦。不要说你没有被警告…
嵌套事件循环…这样疯狂就行了。
tkinter06_事件循环相关推荐
- 【转载】浏览器事件循环机制(event loop)
首先,本文转自https://juejin.im/post/5afbc62151882542af04112d 当我看完菲利普·罗伯茨的 javascript event loop的演讲的时候,就对于事 ...
- 原生js循环展示dom_【前端面试】用一道题讲 js 的事件循环队列
昨天去面了滴滴,一口气面了三面,考了 promise 和事件循环.之前的猿辅导也考察了这些,几乎所有的大厂中厂都一定会考原生 js 的事件循环队列. 今天,我把昨天考察的原题拿出来分析一下. setT ...
- boost log 能不能循环覆盖_前端基础进阶(十四):深入核心,详解事件循环机制...
Event Loop JavaScript的学习零散而庞杂,很多时候我们学到了一些东西,但是却没办法感受到进步!甚至过了不久,就把学到的东西给忘了.为了解决自己的这个困扰,在学习的过程中,我一直在试图 ...
- JS 总结之事件循环
众所周知,JavaScript 为了避免复杂,被设计成了单线程. ⛅️ 任务 单线程意味着所有任务都需要按顺序执行,如果某个任务执行非常耗时,线程就会被阻断,后面的任务需要等上一个任务执行完毕才会进行 ...
- 【译】理解Javascript函数执行—调用栈、事件循环、任务等
原文作者:Gaurav Pandvia 原文链接:medium.com/@gaurav.pan- 文中部分链接可能需要梯子. 欢迎批评指正. 现如今,web开发者(我们更喜欢被叫做前端工程师)用一门脚 ...
- 从一道题浅说 JavaScript 的事件循环
阮老师在其推特上放了一道题: new Promise(resolve => {resolve(1);Promise.resolve().then(() => console.log(2)) ...
- js中如何得到循环中的点击的这个id_Js篇面试题9请说一下Js中的事件循环机制
虽互不曾谋面,但希望能和您成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 标星公众号(ID:itclanCoder) 如果不知道如 ...
- Linux事件循环阻塞,深入浅析Node.js 事件循环、定时器和process.nextTick()
什么是事件循环 尽管JavaScript是单线程的,但通过尽可能将操作放到系统内核执行,事件循环允许Node.js执行非阻塞I/O操作. 由于现代大多数内核都是多线程的,因此它们可以处理在后台执行的多 ...
- Node - 异步IO和事件循环
前言 学习Node就绕不开异步IO, 异步IO又与事件循环息息相关, 而关于这一块一直没有仔细去了解整理过, 刚好最近在做项目的时候, 有了一些思考就记录了下来, 希望能尽量将这一块的知识整理清楚, ...
最新文章
- 开源创新、软件定义网络和网络功能虚拟化特性
- jcmd:JDK14中的调试神器
- C#获取txt记事本内容,防止乱码情况
- (pytorch-深度学习)通过时间反向传播
- 计算机基础简介、编程语言、翻译器、数据储存
- java授权失败_自定义Spring Security的身份验证失败处理方法
- 无用小知识-递归的使用
- reverse() ; sort() ; sorted()
- visual studio 2012 密钥记录
- 与计算机相关的创意网名,过目不忘创意好听网名
- QT从下载到安装的具体教程
- 2D制作动画软件:Cartoon Animato 支持win/mac 中文激活版
- CTFSHOW web入门 命令执行+文件包含+PHP特性
- DCB value for SVN 77 not found on dcb.dat
- PB Send()函数应用有关数据整理
- e光的matlab,【e光嫩肤的效果怎么样】_功效_作用-大众养生网
- 【板栗糖GIS】arcmap如何进行拓扑检查并输出结果
- 中式红木装修展现传统文化底蕴
- 小米3 日历 同步google日历
- Springboot+Redis接入腾讯云短信服务实现验证码发送
热门文章
- ubuntu自带中文拼音输入法
- 袋鼠跳河问题——回溯法解决
- 全志XR819 wifi芯片规格书/Datasheet完整资料
- 三星android翻盖机,韩国首款安卓翻盖 三星GALAXY GOLDEN赏
- 一文详解wait与notify
- 135. 分发糖果(困难)-贪心
- java万人同屏手游_在手游里实现百人同屏竞技?网易这款手游已吊打很多端游!...
- 我学会了用计算机作文,我学会了用智慧解决问题作文400字
- css中怎么让图片居中?css图片居中的方法总结
- Redmibook 14 EE 电脑 Hackintosh 黑苹果efi引导文件