Tkinter 理解 mainloop

Posted

技术标签:

【中文标题】Tkinter 理解 mainloop【英文标题】:Tkinter understanding mainloop 【发布时间】:2015-05-23 08:55:30 【问题描述】:

直到现在,我过去常常用:tk.mainloop() 结束我的 Tkinter 程序,否则什么都不会出现!见例子:

from Tkinter import *
import random
import time

tk = Tk()
tk.title = "Game"
tk.resizable(0,0)
tk.wm_attributes("-topmost", 1)

canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()

class Ball:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)
    def draw(self):
        pass

ball = Ball(canvas, "red")

tk.mainloop()

但是,当尝试该程序的下一步(使球按时间移动)时,正在阅读的书说要执行以下操作。所以我把draw函数改成:

def draw(self):
    self.canvas.move(self.id, 0, -1)

并将以下代码添加到我的程序中:

while 1:
    ball.draw()
    tk.update_idletasks()
    tk.update()
    time.sleep(0.01)

但我注意到添加这段代码后,tk.mainloop() 的使用毫无用处,因为即使没有它,一切都会显示出来!!!

此时我应该提一下,我的书从不谈论tk.mainloop()(可能是因为它使用了 Python 3),但我通过网络搜索了解了它,因为我的程序无法通过复制书的代码来运行!

所以我尝试了以下行不通的操作!!!

while 1:
    ball.draw()
    tk.mainloop()
    time.sleep(0.01)

发生了什么事? tk.mainloop() 是什么? tk.update_idletasks()tk.update() 有什么作用,它们与 tk.mainloop() 有何不同?我应该使用上面的循环吗?tk.mainloop()?还是两者都在我的程序中?

【问题讨论】:

关于主循环:***.com/questions/8683217/… 这些问题以某种方式联系在一起......我不认为在 3 个主题中提出 3 个问题几乎谈论同一件事......:| 阅读它...但是,我可以修改我的程序,这样我就不需要新的循环了吗? 正在为孩子们阅读 Python 我读过它们...帮助我意识到循环中不需要 tk.update() 和 tk.update_idletasks() ! tk.update() 完成了这项工作!但是 tk.mainloop 呢?它可以以某种方式取代 tk.update 吗?我需要两者吗? 【参考方案1】:

tk.mainloop() 。这意味着您的 Python 命令的执行将在那里停止。你可以通过写来看到:

while 1:
    ball.draw()
    tk.mainloop()
    print("hello")   #NEW CODE
    time.sleep(0.01)

您永远不会看到 print 语句的输出。因为没有循环,所以球不会移动。

另一方面,update_idletasks()update() 方法在这里:

while True:
    ball.draw()
    tk.update_idletasks()
    tk.update()

...不要阻止;在这些方法完成后,将继续执行,因此while 循环将一遍又一遍地执行,从而使球移动。

包含方法调用update_idletasks()update() 的无限循环可以替代调用tk.mainloop()。请注意,整个 while 循环可以说是 block 就像 tk.mainloop() 一样,因为 while 循环之后什么都不会执行。

但是,tk.mainloop() 不能仅替代这些行:

tk.update_idletasks()
tk.update()

相反,tk.mainloop() 是整个 while 循环的替代品:

while True:
    tk.update_idletasks()
    tk.update()

对评论的回应:

tcl docs 是这样说的:

更新空闲任务

update 的这个子命令会刷新所有当前计划的空闲事件 来自 Tcl 的事件队列。空闲事件用于推迟处理 直到“没有别的事可做”,典型的用例是 它们是 Tk 的重绘和几何重新计算。通过推迟 这些直到 Tk 空闲,昂贵的重绘操作直到 来自一组事件的所有内容(例如,按钮释放、更改 当前窗口等)在脚本级别进行处理。这使得 Tk 看起来快得多,但如果你正在做一些很长的时间 运行处理,也可能意味着没有空闲事件被处理 许久。通过调用update idletasks,由于内部重绘 立即处理状态更改。 (因系统重绘 事件,例如,被用户取消图标化,需要完全更新 已处理。)

APN 如更新中所述,认为有害,使用更新来处理 update idletasks 未处理的重绘有很多问题。乔英语 在 comp.lang.tcl 帖子中描述了一种替代方法:

所以update_idletasks() 导致处理update() 导致处理的某些事件子集。

来自update docs:

更新 ?idletasks?

update 命令用于通过以下方式使应用程序“更新” 重复进入 Tcl 事件循环,直到所有未决事件 (包括空闲回调)已被处理。

如果将 idletasks 关键字指定为命令的参数, 然后不会处理新的事件或错误;只有空闲的回调是 调用。这会导致通常延迟的操作,例如 显示更新和窗口布局计算,待执行 马上。

KBK(2000 年 2 月 12 日)——我个人认为 [更新] 命令不是最佳实践之一,程序员很好 建议避免。我很少看到使用 [update] 通常无法通过其他方式更有效地编程 适当使用事件回调。顺便说一句,这个警告适用 对所有 Tcl 命令(vwait 和 tkwait 是其他常见的 罪魁祸首)递归地进入事件循环,除了 在全局级别使用单个 [vwait] 在内部启动事件循环 一个不会自动启动的 shell。

我见过 [update] 推荐的最常见用途是:

    在进行一些长时间运行的计算时保持 GUI 活动 执行。有关替代方案,请参阅倒计时程序。 2)在做类似的事情之前等待一个窗口被配置 几何管理就可以了。另一种方法是绑定事件,例如 因为它通知窗口几何的过程。看 将窗口居中作为替代方案。

更新有什么问题?有几个答案。首先,它倾向于 使周围 GUI 的代码复杂化。如果你工作 倒计时程序中的练习,你会感觉到有多少 当每个事件都在自己的回调中处理时,它会更容易。 其次,它是潜在错误的来源。一般的问题是 执行 [update] 具有几乎不受约束的副作用;返回时 从 [update] 开始,脚本可以很容易地发现地毯已经 从它下面拉出来。对此有进一步的讨论 更新时出现的现象被认为是有害的。

.....

我有没有机会让我的程序在没有 while 循环的情况下工作?

是的,但事情变得有点棘手。您可能会认为以下内容会起作用:

class Ball:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)

    def draw(self):
        while True:
           self.canvas.move(self.id, 0, -1)

ball = Ball(canvas, "red")
ball.draw()
tk.mainloop()

问题是 ball.draw() 会导致执行进入 draw() 方法中的无限循环,因此 tk.mainloop() 将永远不会执行,并且您的小部件将永远不会显示。在 gui 编程中,必须不惜一切代价避免无限循环,以保持小部件对用户输入的响应,例如鼠标点击。

所以,问题是:你如何一遍又一遍地执行某事而不实际创建无限循环? Tkinter 对这个问题有一个答案:一个小部件的after() 方法:

from Tkinter import *
import random
import time

tk = Tk()
tk.title = "Game"
tk.resizable(0,0)
tk.wm_attributes("-topmost", 1)

canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()

class Ball:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)

    def draw(self):
        self.canvas.move(self.id, 0, -1)
        self.canvas.after(1, self.draw)  #(time_delay, method_to_execute)


       

ball = Ball(canvas, "red")
ball.draw()  #Changed per Bryan Oakley's comment
tk.mainloop()

after() 方法不会阻塞(它实际上会创建另一个执行线程),因此在调用 after() 之后,python 程序中的执行将继续,这意味着 tk.mainloop( ) 接下来执行,因此您的小部件得到配置和显示。 after() 方法还允许您的小部件保持对其他用户输入的响应。尝试运行以下程序,然后在画布上的不同位置单击鼠标:

from Tkinter import *
import random
import time

root = Tk()
root.title = "Game"
root.resizable(0,0)
root.wm_attributes("-topmost", 1)

canvas = Canvas(root, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()

class Ball:
    def __init__(self, canvas, color):
        self.canvas = canvas
        self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
        self.canvas.move(self.id, 245, 100)

        self.canvas.bind("<Button-1>", self.canvas_onclick)
        self.text_id = self.canvas.create_text(300, 200, anchor='se')
        self.canvas.itemconfig(self.text_id, text='hello')

    def canvas_onclick(self, event):
        self.canvas.itemconfig(
            self.text_id, 
            text="You clicked at (, )".format(event.x, event.y)
        )

    def draw(self):
        self.canvas.move(self.id, 0, -1)
        self.canvas.after(50, self.draw)


       

ball = Ball(canvas, "red")
ball.draw()  #Changed per Bryan Oakley's comment.
root.mainloop()

【讨论】:

是否有机会让我的程序在没有 while 循环的情况下工作? 关于我刚刚发布的另一个question,你为什么要在update之前使用update_idletasks,因为后者完成了前者所做的一切? 没有理由同时调用update_idletasksupdateupdate 做所有update_idletasks 做的事情,还有更多。 在程序的最后,您以canvas.after(0, ball.draw) 开始动画。你不需要在这里使用after——你可以直接调用ball.draw来开始。 此语句不正确:“after() 方法不会阻塞(它实际上会创建另一个执行线程)” - after 不会不 创建另一个执行线程。 Tkinter 是单线程的。 after 只是将一个函数添加到队列中。【参考方案2】:
while 1:
    root.update()

...(非常!)大致类似于:

root.mainloop()

不同之处在于,mainloop 是正确的编码方式,而无限循环则有点不正确。不过,我怀疑在绝大多数情况下,两者都行得通。只是mainloop 是一个更清洁的解决方案。毕竟,调用mainloop 本质上是在幕后:

while the_window_has_not_been_destroyed():
    wait_until_the_event_queue_is_not_empty()
    event = event_queue.pop()
    event.handle()

... 如您所见,这与您自己的 while 循环没有太大区别。那么,既然 tkinter 已经有一个可以使用的无限循环,为什么还要创建自己的无限循环呢?

尽可能用最简单的术语表示:始终将mainloop 作为程序中最后的逻辑代码行调用。这就是 Tkinter 的设计用途。

【讨论】:

在我的例子中,什么是(对你来说)正确的方法来让我的球在不使用新循环但继续使用主循环的情况下移动?【参考方案3】:

我使用的是 MVC / MVA 设计模式,具有多种类型的“视图”。一种类型是“GuiView”,它是一个 Tk 窗口。我将视图引用传递给我的窗口对象,该对象执行诸如将按钮链接回视图函数(适配器/控制器类也调用)之类的操作。

为此,需要在创建窗口对象之前完成视图对象构造函数。创建并显示窗口后,我想自动使用视图执行一些初始任务。起初我尝试在 mainloop() 后执行它们,但这不起作用,因为 mainloop() 阻塞了!

因此,我创建了窗口对象并使用 tk.update() 来绘制它。然后,我开始了我最初的任务,最后开始了主循环。

import Tkinter as tk

class Window(tk.Frame):
    def __init__(self, master=None, view=None ):
        tk.Frame.__init__( self, master )
        self.view_ = view       
        """ Setup window linking it to the view... """

class GuiView( MyViewSuperClass ):

    def open( self ):
        self.tkRoot_ = tk.Tk()
        self.window_ = Window( master=None, view=self )
        self.window_.pack()
        self.refresh()
        self.onOpen()
        self.tkRoot_.mainloop()         

    def onOpen( self ):        
        """ Do some initial tasks... """

    def refresh( self ):        
        self.tkRoot_.update()

【讨论】:

以上是关于Tkinter 理解 mainloop的主要内容,如果未能解决你的问题,请参考以下文章

Tkinter - 无法理解为什么在两个输入框中同时输入数据。

使用 tkinter 的简单动画

Tkinter pyimage 不存在

在Python中触发来自不同类的Tkinter事件

tkinter Text() 小部件上的 4096 个单行字符限制?

Matplotlib在tkinter中起作用