如何关闭Tkinter主循环窗口避免_!canvas TclError错误?
Tkinter模仿“You Are An Idiot”程序的窗口关闭异常问题
问题描述
我看完YouTube上的Python教程后,想用Tkinter做一个模仿“You Are An Idiot”的程序(无冒犯之意)。因为Tkinter中不适合直接使用多线程,所以改用计时tick来处理时间逻辑。
当前核心问题:我希望关闭主窗口时生成更多新窗口,但关闭主窗口后,原有代码中的部件操作仍在运行,会触发_!canvas TclError。尝试过用winfo_exists()检测窗口状态,反而引发错误;考虑过异常处理,但需要利用异常触发新窗口的创建,目前找不到可行的解决办法,想知道是否有方案可以解决,还是应该先转做其他项目,之后再用其他语言尝试。
原代码
''' Ver 1.1 - Updated variable names, ''' from tkinter import * import time import pygame import threading test = True mw = Tk() def common_properties(self, window): if mw.winfo_exists(): icon = PhotoImage(file="smile.png") self.canvas.pack() mw.iconphoto(True, icon) mw.title("YOU ARE AN IDIOT") while mw.winfo_exists(): move(self) change_colour(self) mw.mainloop() def move(self): if mw.winfo_exists(): bounds = self.canvas.bbox(self.text) text_height = bounds[3] - bounds[1] if bounds[2]>= self.width or bounds[0]<0: self.xVelocity = -self.xVelocity if bounds[1]>= self.height - text_height or bounds[1]<0: self.yVelocity = -self.yVelocity self.canvas.move(self.text, self.xVelocity, self.yVelocity) mw.update() time.sleep(0.001) def change_colour(self): if mw.winfo_exists(): if self.is_colourinversed > 0 and self.is_colourinversed <= 1: self.is_colourinversed = round(self.is_colourinversed -0.005, 3) if self.is_colourinversed == 0: self.is_colourinversed = -1 elif self.is_colourinversed < 0 and self.is_colourinversed >= -1: self.is_colourinversed = round(self.is_colourinversed +0.005, 3) if self.is_colourinversed == -0.005: self.is_colourinversed = 1 time.sleep(0.001) if self.is_colourinversed < 2 and self.is_colourinversed > 0: self.canvas.config(bg="black") self.canvas.itemconfig(self.text, fill="white") else: self.canvas.config(bg="white") self.canvas.itemconfig(self.text, fill="black") def play(): if mw.winfo_exists(): pygame.mixer.music.load("y2mate.com - sans.mp3") pygame.mixer.music.play(loops=0) class MainWindow(): def __init__(self, xVelocity, yVelocity, height, width, font_size) -> None: self.is_colourinversed = -1 self.xVelocity = xVelocity self.yVelocity = yVelocity self.height = height self.width = width self.font_size = font_size self.canvas = Canvas(mw, height=self.height, width=self.width) self.text = self.canvas.create_text(1,0, text="you are an idiot\n ☺ ☺ ☺", font=("Times New Roman", self.font_size), anchor=NW) pygame.mixer.init() play() common_properties(self, mw) if __name__=="__main__": while True: test = True while test: MainWindow(1.4, 1.4, 400, 400, 30) test = False
解决方案
核心问题分析
你的代码用死循环+time.sleep()+mw.update()模拟动画,会阻塞Tkinter主事件循环,导致窗口状态检测失效;同时所有组件绑定到同一个主窗口mw,窗口销毁后,循环仍在尝试操作已失效的组件,引发TclError。
具体修复步骤
- 改用Tkinter原生
after()方法:替换死循环和sleep,这是Tkinter处理定时任务的标准方式,不会阻塞主循环,能实时响应窗口状态变化。 - 每个窗口使用独立的
Tk实例:避免多个窗口共享同一个主窗口,关闭单个窗口时不影响其他窗口,同时触发新窗口创建。 - 绑定窗口关闭事件:通过
protocol("WM_DELETE_WINDOW", callback)监听关闭操作,在回调中创建新窗口,再销毁当前窗口。 - 捕获
TclError异常:在组件操作逻辑中捕获异常,当窗口已销毁时终止当前任务,避免报错。
修改后的示例代码
from tkinter import * import pygame pygame.mixer.init() class IdiotWindow: def __init__(self, x_vel=1.4, y_vel=1.4, height=400, width=400, font_size=30): self.root = Tk() self.root.title("YOU ARE AN IDIOT") try: icon = PhotoImage(file="smile.png") self.root.iconphoto(True, icon) except: pass # 图标文件不存在时跳过 # 绑定关闭事件 self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.x_vel = x_vel self.y_vel = y_vel self.height = height self.width = width self.font_size = font_size self.is_colour_inversed = -1 # 创建画布和文本 self.canvas = Canvas(self.root, height=self.height, width=self.width, bg="white") self.canvas.pack() self.text = self.canvas.create_text(1, 0, text="you are an idiot\n ☺ ☺ ☺", font=("Times New Roman", self.font_size), anchor=NW, fill="black") # 播放音频 self.play_audio() # 启动动画循环 self.animate() # 启动窗口主循环 self.root.mainloop() def play_audio(self): try: pygame.mixer.music.load("y2mate.com - sans.mp3") pygame.mixer.music.play(loops=-1) # 循环播放 except: pass # 音频文件不存在时跳过 def animate(self): try: # 移动文本 bounds = self.canvas.bbox(self.text) text_height = bounds[3] - bounds[1] if bounds[2] >= self.width or bounds[0] < 0: self.x_vel = -self.x_vel if bounds[1] >= self.height - text_height or bounds[1] < 0: self.y_vel = -self.y_vel self.canvas.move(self.text, self.x_vel, self.y_vel) # 切换颜色 if 0 < self.is_colour_inversed <= 1: self.is_colour_inversed = round(self.is_colour_inversed - 0.005, 3) if self.is_colour_inversed <= 0: self.is_colour_inversed = -1 elif -1 <= self.is_colour_inversed < 0: self.is_colour_inversed = round(self.is_colour_inversed + 0.005, 3) if self.is_colour_inversed >= 0: self.is_colour_inversed = 1 if 0 < self.is_colour_inversed < 2: self.canvas.config(bg="black") self.canvas.itemconfig(self.text, fill="white") else: self.canvas.config(bg="white") self.canvas.itemconfig(self.text, fill="black") # 1毫秒后继续动画 self.root.after(1, self.animate) except TclError: # 窗口已销毁,终止动画 pass def on_close(self): # 创建新窗口 IdiotWindow() # 销毁当前窗口 self.root.destroy() if __name__ == "__main__": # 启动第一个窗口 IdiotWindow()
说明
- 每个窗口都是独立的
Tk实例,关闭时会自动创建新窗口,不会影响其他窗口运行。 - 用
after()实现动画循环,避免阻塞主事件循环,响应更流畅。 - 异常捕获处理窗口销毁后的组件操作,避免报错。
- 音频和图标文件不存在时会跳过,避免程序崩溃。
内容的提问来源于stack exchange,提问作者Aldis Nuts
相关产品推荐
相关产品推荐

