你如何在 Tkinter 的事件循环中运行自己的代码?

How do you run your own code alongside Tkinter's event loop?

提问人:Allan S 提问时间:1/20/2009 最后编辑:Bill the LizardAllan S 更新时间:4/2/2019 访问量:220500

问:

我的弟弟刚刚开始编程,在他的科学博览会项目中,他正在模拟天空中的一群鸟。他已经编写了大部分代码,而且运行良好,但鸟儿需要每时每刻移动。

然而,Tkinter 为自己的事件循环占用了时间,因此他的代码不会运行。执行运行、运行和保持运行,它运行的唯一内容是事件处理程序。root.mainloop()

有没有办法让他的代码与主循环一起运行(没有多线程,这很混乱,这应该保持简单),如果是这样,它是什么?

现在,他想出了一个丑陋的技巧,将他的功能与 绑定在一起,这样只要他按住按钮并摆动鼠标,它就可以工作。但必须有更好的方法。move()<b1-motion>

Python 事件 tkinter

评论

0赞 metatoaster 2/8/2023
对此进行更彻底、更深入的讨论:了解 mainloop

答:

185赞 Dave Ray 1/20/2009 #1

对对象使用以下方法:afterTk

from tkinter import *

root = Tk()

def task():
    print("hello")
    root.after(2000, task)  # reschedule event in 2 seconds

root.after(2000, task)
root.mainloop()

下面是该方法的声明和文档:after

def after(self, ms, func=None, *args):
    """Call function once after given time.

    MS specifies the time in milliseconds. FUNC gives the
    function which shall be called. Additional parameters
    are given as parameters to the function call.  Return
    identifier to cancel scheduling with after_cancel."""

评论

44赞 Nathan 9/9/2009
如果将超时指定为 0,则 Task 将在完成后立即将自身放回事件循环。这不会阻止其他事件,同时仍会尽可能频繁地运行代码。
2赞 JxAxMxIxN 10/16/2016
在拉扯我的头发几个小时后,试图让 opencv 和 tkinter 在单击 [X] 按钮时正确协同工作并干净利落地关闭,这与 win32gui 一起。FindWindow(None, 'window title') 成功了!我真是个菜鸟;-)
2赞 Anonymous 4/27/2019
这不是最好的选择;虽然它在这种情况下有效,但它对大多数脚本来说并不好(它每 2 秒运行一次),并且根据 @Nathan 发布的建议将超时设置为 0,因为它仅在 tkinter 不忙时运行(这可能会导致一些复杂程序出现问题)。最好坚持使用该模块。threading
0赞 Jason Waltz 8/22/2020
哇,我现在花了几个小时来调试为什么我的 gui 一直冻结。我觉得自己很傻,谢谢一百万!
1赞 weeix 9/26/2021
如果你是CPU密集型的,可能需要线程解决方案(例如KevinBjorn发布)。我最初用于我的 opencv 任务,因为它看起来很简单,导致 GUI 非常慢---仅调整窗口大小就需要大约 2-3 秒。task()after()
8赞 Bjorn 2/12/2009 #2

另一种选择是让 tkinter 在单独的线程上执行。一种方法是这样的:

import Tkinter
import threading

class MyTkApp(threading.Thread):
    def __init__(self):
        self.root=Tkinter.Tk()
        self.s = Tkinter.StringVar()
        self.s.set('Foo')
        l = Tkinter.Label(self.root,textvariable=self.s)
        l.pack()
        threading.Thread.__init__(self)

    def run(self):
        self.root.mainloop()


app = MyTkApp()
app.start()

# Now the app should be running and the value shown on the label
# can be changed by changing the member variable s.
# Like this:
# app.s.set('Bar')

但要小心,多线程编程很难,而且很容易搬起石头砸自己的脚。例如,在更改上述示例类的成员变量时必须小心,以免中断 Tkinter 的事件循环。

评论

3赞 jldupont 8/12/2011
不确定这是否有效。刚刚尝试了类似的东西,我得到“RuntimeError:主线程不在主循环中”。
6赞 mgiuca 8/12/2012
jldupont:我收到“RuntimeError:从不同的公寓调用 Tcl”(可能在不同版本中出现相同的错误)。修复方法是在 run() 中初始化 Tk,而不是在 __init__() 中初始化。这意味着您在调用 mainloop() 的同一线程中初始化 Tk。
75赞 Kevin 12/3/2009 #3

Bjorn 发布的解决方案导致我的计算机上出现“RuntimeError:从不同的公寓调用 Tcl”消息(RedHat Enterprise 5,python 2.6.1)。Bjorn 可能没有收到此消息,因为根据我检查的一个地方,使用 Tkinter 错误处理线程是不可预测的,并且依赖于平台。

问题似乎在于算作对 Tk 的引用,因为应用程序包含 Tk 元素。我通过用内部替换来解决这个问题。我还使所有 Tk 引用要么位于调用 mainloop() 的函数内部,要么位于调用函数调用的函数内部(这显然对于避免“不同公寓”错误至关重要)。app.start()app.start()self.start()__init__mainloop()

最后,我添加了一个带有回调的协议处理程序,因为如果没有回调,当用户关闭 Tk 窗口时,程序会退出并出现错误。

修订后的守则如下:

# Run tkinter code in another thread

import tkinter as tk
import threading

class App(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)
        self.start()

    def callback(self):
        self.root.quit()

    def run(self):
        self.root = tk.Tk()
        self.root.protocol("WM_DELETE_WINDOW", self.callback)

        label = tk.Label(self.root, text="Hello World")
        label.pack()

        self.root.mainloop()


app = App()
print('Now we can continue running code while mainloop runs!')

for i in range(100000):
    print(i)

评论

6赞 Andre Holzner 10/3/2015
通常,您会将参数传递给 ,将它们存储在 并在__init__(..)selfrun(..)
3赞 Bob Bobster 8/11/2019
根根本没有显示,并发出警告:“警告:NSWindow 拖动区域应仅在主线程上失效!这将在将来抛出异常
0赞 m3nda 12/23/2019
这是救命稻草。如果您不想在退出 GUI 后退出 python 脚本,则 GUI 外部的代码应该检查 tkinter 线程是否处于活动状态。类似的东西while app.is_alive(): etc
0赞 FotisK 12/12/2020
您将如何实现以便能够在需要时从 for 循环或脚本的其余部分更新标签文本?
0赞 John Sohn 9/13/2021
我试图使用在线程代码外部定义的窗口来使它工作,以便其他代码可以访问它。
28赞 jma 1/29/2011 #4

在编写自己的循环时,就像在模拟中一样(我假设),您需要调用函数来执行该操作:使用您的更改更新窗口,但您可以在循环中执行此操作。updatemainloop

def task():
   # do something
   root.update()

while 1:
   task()  

评论

14赞 Bryan Oakley 2/2/2011
你必须非常小心地进行这种编程。如果任何事件导致被调用,你最终会得到嵌套的事件循环,这很糟糕。除非您完全了解事件循环的工作原理,否则应不惜一切代价避免调用。taskupdate
0赞 jldupont 8/12/2011
我曾经使用过这种技术 - 工作正常,但根据你的操作方式,你可能会在UI中出现一些错开。
0赞 Green05 8/7/2020
@Bryan Oakley 那么更新是一个循环吗?这怎么会有问题?
4赞 Micheal Morrow 10/25/2016 #5

这是GPS阅读器和数据呈现器的第一个工作版本。tkinter 是一个非常脆弱的东西,错误消息太少了。它不会把东西放上去,很多时候也不会说出原因。来自一个好的所见即所得表单开发人员非常困难。无论如何,它每秒运行一个小例程 10 次,并在表单上显示信息。花了一段时间才实现它。当我尝试将计时器值设置为 0 时,表单从未出现过。我的头现在很痛!每秒 10 次或更多次对我来说已经足够了。我希望它能帮助到其他人。迈克·莫罗

import tkinter as tk
import time

def GetDateTime():
  # Get current date and time in ISO8601
  # https://en.wikipedia.org/wiki/ISO_8601 
  # https://xkcd.com/1179/
  return (time.strftime("%Y%m%d", time.gmtime()),
          time.strftime("%H%M%S", time.gmtime()),
          time.strftime("%Y%m%d", time.localtime()),
          time.strftime("%H%M%S", time.localtime()))

class Application(tk.Frame):

  def __init__(self, master):

    fontsize = 12
    textwidth = 9

    tk.Frame.__init__(self, master)
    self.pack()

    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             text='Local Time').grid(row=0, column=0)
    self.LocalDate = tk.StringVar()
    self.LocalDate.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             textvariable=self.LocalDate).grid(row=0, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             text='Local Date').grid(row=1, column=0)
    self.LocalTime = tk.StringVar()
    self.LocalTime.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#be004e', fg = 'white', width = textwidth,
             textvariable=self.LocalTime).grid(row=1, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             text='GMT Time').grid(row=2, column=0)
    self.nowGdate = tk.StringVar()
    self.nowGdate.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             textvariable=self.nowGdate).grid(row=2, column=1)

    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             text='GMT Date').grid(row=3, column=0)
    self.nowGtime = tk.StringVar()
    self.nowGtime.set('waiting...')
    tk.Label(self, font=('Helvetica', fontsize), bg = '#40CCC0', fg = 'white', width = textwidth,
             textvariable=self.nowGtime).grid(row=3, column=1)

    tk.Button(self, text='Exit', width = 10, bg = '#FF8080', command=root.destroy).grid(row=4, columnspan=2)

    self.gettime()
  pass

  def gettime(self):
    gdt, gtm, ldt, ltm = GetDateTime()
    gdt = gdt[0:4] + '/' + gdt[4:6] + '/' + gdt[6:8]
    gtm = gtm[0:2] + ':' + gtm[2:4] + ':' + gtm[4:6] + ' Z'  
    ldt = ldt[0:4] + '/' + ldt[4:6] + '/' + ldt[6:8]
    ltm = ltm[0:2] + ':' + ltm[2:4] + ':' + ltm[4:6]  
    self.nowGtime.set(gdt)
    self.nowGdate.set(gtm)
    self.LocalTime.set(ldt)
    self.LocalDate.set(ltm)

    self.after(100, self.gettime)
   #print (ltm)  # Prove it is running this and the external code, too.
  pass

root = tk.Tk()
root.wm_title('Temp Converter')
app = Application(master=root)

w = 200 # width for the Tk root
h = 125 # height for the Tk root

# get display screen width and height
ws = root.winfo_screenwidth()  # width of the screen
hs = root.winfo_screenheight() # height of the screen

# calculate x and y coordinates for positioning the Tk root window

#centered
#x = (ws/2) - (w/2)
#y = (hs/2) - (h/2)

#right bottom corner (misfires in Win10 putting it too low. OK in Ubuntu)
x = ws - w
y = hs - h - 35  # -35 fixes it, more or less, for Win10

#set the dimensions of the screen and where it is placed
root.geometry('%dx%d+%d+%d' % (w, h, x, y))

root.mainloop()