如何与 Tkinter 的事件循环一起运行您自己的代码?
- 2024-11-25 08:49:00
- admin 原创
- 158
问题描述:
我弟弟刚刚开始学习编程,他正在为他的科学展项目做一个模拟天空中一群鸟儿的项目。他已经写好了大部分代码,而且运行良好,但鸟儿需要时刻移动。
然而,Tkinter 占用了它自己的事件循环的时间,所以他的代码不会运行。Doingroot.mainloop()
运行、运行、继续运行,它唯一运行的就是事件处理程序。
有没有办法让他的代码与主循环一起运行(不使用多线程,这会令人困惑,而且应该保持简单),如果可以,那么它是什么?
现在,他想出了一个丑陋的办法,将他的move()
功能绑定到<b1-motion>
,这样只要他按住按钮并摆动鼠标,它就可以工作。但一定有更好的方法。
解决方案 1:
在对象上使用该after
方法Tk
:
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."""
解决方案 2:
Bjorn 发布的解决方法导致我的计算机 (RedHat Enterprise 5,python 2.6.1) 上出现“RuntimeError: 从不同单元调用 Tcl”消息。Bjorn 可能没有收到此消息,因为根据我检查过的一个地方,使用 Tkinter 错误处理线程是不可预测的,并且依赖于平台。
问题似乎是,app.start()
由于 app 包含 Tk 元素,因此将其视为对 Tk 的引用。我通过将其替换app.start()
为self.start()
inside来修复此问题__init__
。我还使所有 Tk 引用要么位于调用函数mainloop()
内部,要么位于调用函数所调用的函数内部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)
解决方案 3:
当编写自己的循环时,就像在模拟中一样(我假设),您需要调用update
执行其功能的函数mainloop
:使用您的更改更新窗口,但您在循环中执行此操作。
def task():
# do something
root.update()
while 1:
task()
解决方案 4:
另一个选项是让 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 的事件循环。
解决方案 5:
这是 GPS 阅读器和数据呈现器的第一个工作版本。tkinter 非常脆弱,错误消息太少。它不会显示内容,而且大多数时候也不会说明原因。对于一个优秀的所见即所得表单开发人员来说,这非常困难。无论如何,它每秒运行 10 次小程序,并在表单上显示信息。花了一段时间才实现。当我尝试将计时器值设为 0 时,表单从未出现。我的头现在很痛!每秒 10 次或更多对我来说已经足够了。我希望它能帮助别人。Mike Morrow
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()
解决方案 6:
对于首次使用 tkinter 的程序员和新手 Python 程序员来说,这是一个非常常见的问题,他们要么在这里卡住并放弃,要么以“有效”但错误的方式实现他们的代码并导致问题。我知道,因为我是一个新手,刚刚克服了这个障碍。
首先,阅读一些关于 mainloop() 在 tkinter 中实际如何工作的优秀文档,这也解释了为什么将 root.update() 放在代码循环中是一个糟糕的主意,特别是对于长时间运行的脚本(想想 24x7 监控工具等)并且会导致它们最终中断。
接下来,阅读有关线程和类的所有内容,然后尝试实现线程,结果才意识到线程并不是 tkinter 真正想要的架构,而且无论文档怎么说,tkinter 都不是线程安全的。
因此,作为只需要一个具体示例即可入门的新手,请参阅以下最基本的代码(无类、无多余内容,剪切并粘贴到您的 Python IDE 中并按“Go”按钮):
import tkinter as tk
def main(args):
# Build the UI for the user
root = tk.Tk()
buttonShutDown = tk.Button(root, text="PUSH ME!")#, command=sys.exit(0))
buttonShutDown.pack()
# Go into the method that looks at inputs and outputs. Must be run BEFORE you get to root.mainloop()
monitorInputsAndActionOutputs(root)
# The main tkinter loop. This loop is blocking - ie once you call it your python script stays here forever, hence why you can't do other stuff in your while(blah) loop that you also want to run
root.mainloop()
def monitorInputsAndActionOutputs(root):
# This is where your code does something that you want to do regularly and not have blocked by the root.mainloop(). You could make this big or small as you desire.
print ("checked the room temperature")
print ("adjusted the airconditioning to make comfy")
# This line causes this method to add a "future event" to the root.mainloop() queue in a manner of speaking. This means that the root.mainloop() method in which your script will be stuck forever will in approx 100 miliseconds time execute this method again for you.
# Because this method contains this "put myself back in the queue to be run again" trigger, this method will run every 100 miliseconds until you quit your GUI with root.destroy() or sys.exit() or something similar.
root.after(100, monitorInputsAndActionOutputs, root)
return None
# Don't worry about this bit it's not related to your GUI / loop
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))
这是一个非常基本的空调监控脚本示例,其 GUI 上只有一个按钮 - 该按钮可以启用或禁用 AC 监控系统或其他功能。
无论如何,你明白了......这段代码是实现 tkinter 的架构上适当的方式,带有某种额外的运行循环,不需要用户与 GUI 交互并按下按钮或类似操作。
这个例子
没有实现线程
是线程安全的(假设您没有在代码中的其他地方自己实现线程)
是非阻塞的(如果你把你的东西放在那个 monitorInputsAndActionsOutputs() 方法中
将导致你的脚本在一段随机的时间后不会崩溃
对新手程序员是否友好(没有课程,没有难以理解的自我架构)
可扩展 - 你可以添加 100 个额外的方法,使用相同的 after() 样式回调,只要你的处理器有足够的马力,它就会工作 - 也许不要在其中放入 1k 个带有 after() 的方法,然后尝试在 5 年前的 Raspberry Pi 上运行它,同时观看高清视频
此代码不是什么:
最佳实现(见上面的链接)
易于大规模管理
但是,它可能足够有用,可以让新手走上正确的道路,并帮助他们避免陷入我所犯的 root.update() 陷阱。