关于python:如何在Tkinter的事件循环中运行自己的代码?

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

我的小弟弟刚刚开始学习编程,为了他的科学展项目,他正在模拟天空中的一群鸟。他写了大部分代码,而且工作得很好,但是鸟儿需要随时移动。

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

有没有一种方法可以让他的代码与主循环一起运行(不需要多线程,这很混乱,而且应该保持简单),如果有,是什么?

现在,他想出了一个丑陋的黑客,把他的move()功能与绑定在一起,这样只要他按住按钮并晃动鼠标,它就会工作。但必须有更好的方法。


Tk对象上使用after方法:

1
2
3
4
5
6
7
8
9
10
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方法的声明和文件:

1
2
3
4
5
6
7
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."""


bjorn发布的解决方案在我的计算机上产生了一条"runtimeerror:calling tcl from different appartment"消息(Redhat Enterprise 5,python 2.6.1)。bjorn可能没有收到这个消息,因为根据我检查过的一个地方,用tkinter错误处理线程是不可预测的,并且依赖于平台。

问题似乎是app.start()作为对tk的引用,因为app包含tk元素。我用__init__内的self.start()替换app.start()来修复这个问题。我也这样做了,所有tk引用要么在调用mainloop()的函数内部,要么在调用mainloop()的函数调用的函数内部(这显然是避免"不同单元"错误的关键)。

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

修订后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 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)


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

1
2
3
4
5
6
def task():
   # do something
   root.update()

while 1:
   task()


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的事件循环。


这是GPS阅读器和数据演示器的第一个工作版本。tkinter是一个非常脆弱的东西,错误信息太少。它不会把东西放在上面,也不会告诉你为什么会有很多时间。很难从一个好的Wysiwyg表单开发人员那里获得。无论如何,这会每秒运行10次小程序,并在表单上显示信息。花了一段时间才实现。当我尝试计时器值为0时,窗体就没有出现。我的头现在疼了!每秒10次以上对我来说已经足够了。我希望它能帮助别人。迈克莫罗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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()