You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何关闭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。

具体修复步骤

  1. 改用Tkinter原生after()方法:替换死循环和sleep,这是Tkinter处理定时任务的标准方式,不会阻塞主循环,能实时响应窗口状态变化。
  2. 每个窗口使用独立的Tk实例:避免多个窗口共享同一个主窗口,关闭单个窗口时不影响其他窗口,同时触发新窗口创建。
  3. 绑定窗口关闭事件:通过protocol("WM_DELETE_WINDOW", callback)监听关闭操作,在回调中创建新窗口,再销毁当前窗口。
  4. 捕获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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 15:54:55