"tkinter.TclError: invalid command name" error after calling root.destroy()

"tkinter.TclError: invalid command name" error after calling root.destroy()

我正在 Python 3.X 上学习 tkinter。我正在编写一个简单的程序,它将使一个或多个球(tkinter 椭圆形)在矩形球场上弹跳(tkinter root window,上面有 canvas 和矩形)。

我希望能够通过按 q 键干净地终止程序,并设法将键绑定到根并在按下键时触发回调函数,然后调用 root.destroy().

但是,当我这样做时,仍然 收到形式为 _tkinter.TclError: invalid command name ".140625086752360" 的错误。这真让我抓狂。我做错了什么?

from tkinter import *
import time
import numpy

class Ball:

    def bates():
        """
        Generator for the sequential index number used in order to 
        identify the various balls.
        """
        k = 0
        while True:
            yield k
            k += 1

    index = bates()

    def __init__(self, parent, x, y, v=0.0, angle=0.0, accel=0.0, radius=10, border=2):
        self.parent   = parent           # The parent Canvas widget
        self.index    = next(Ball.index) # Fortunately, I have all my feathers individually numbered, for just such an eventuality
        self.x        = x                # X-coordinate (-1.0 .. 1.0)
        self.y        = y                # Y-coordinate (-1.0 .. 1.0)
        self.radius   = radius           # Radius (0.0 .. 1.0)
        self.v        = v                # Velocity
        self.theta    = angle            # Angle
        self.accel    = accel            # Acceleration per tick
        self.border   = border           # Border thickness (integer)

        self.widget   = self.parent.canvas.create_oval(
                    self.px() - self.pr(), self.py() - self.pr(), 
                    self.px() + self.pr(), self.py() + self.pr(),
                    fill = "red", width=self.border, outline="black")

    def __repr__(self):
        return "[{}] x={:.4f} y={:.4f} v={:.4f} a={:.4f} r={:.4f} t={}, px={} py={} pr={}".format(
                self.index, self.x, self.y, self.v, self.theta, 
                self.radius, self.border, self.px(), self.py(), self.pr())

    def pr(self):
        """
        Converts a radius from the range 0.0 .. 1.0 to window coordinates
        based on the width and height of the window
        """
        assert self.radius > 0.0 and self.radius <= 1.0
        return int(min(self.parent.height, self.parent.width)*self.radius/2.0)

    def px(self):
        """
        Converts an X-coordinate in the range -1.0 .. +1.0 to a position
        within the window based on its width
        """
        assert self.x >= -1.0 and self.x <= 1.0
        return int((1.0 + self.x) * self.parent.width / 2.0 + self.parent.border)

    def py(self):
        """
        Converts a Y-coordinate in the range -1.0 .. +1.0 to a position
        within the window based on its height
        """
        assert self.y >= -1.0 and self.y <= 1.0
        return int((1.0 - self.y) * self.parent.height / 2.0 + self.parent.border)

    def Move(self, x, y):
        """
        Moves ball to absolute position (x, y) where x and y are both -1.0 .. 1.0
        """
        oldx = self.px()
        oldy = self.py()
        self.x = x
        self.y = y
        deltax = self.px() - oldx
        deltay = self.py() - oldy
        if oldx != 0 or oldy != 0:
            self.parent.canvas.move(self.widget, deltax, deltay)

    def HandleWallCollision(self):
        """
        Detects if a ball collides with the wall of the rectangular
        Court.
        """
        pass

class Court:
    """
    A 2D rectangular enclosure containing a centred, rectagular
    grid of balls (instances of the Ball class).
    """    

    def __init__(self, 
                 width=1000,      # Width of the canvas in pixels
                 height=750,      # Height of the canvas in pixels
                 border=5,        # Width of the border around the canvas in pixels
                 rows=1,          # Number of rows of balls
                 cols=1,          # Number of columns of balls
                 radius=0.05,     # Ball radius
                 ballborder=1,    # Width of the border around the balls in pixels
                 cycles=1000,     # Number of animation cycles
                 tick=0.01):      # Animation tick length (sec)
        self.root = Tk()
        self.height = height
        self.width  = width
        self.border = border
        self.cycles = cycles
        self.tick   = tick
        self.canvas = Canvas(self.root, width=width+2*border, height=height+2*border)
        self.rectangle = self.canvas.create_rectangle(border, border, width+border, height+border,                                      outline="black", fill="white", width=border)
        self.root.bind('<Key>', self.key)
        self.CreateGrid(rows, cols, radius, ballborder)
        self.canvas.pack()
        self.afterid = self.root.after(0, self.Animate)
        self.root.mainloop()

    def __repr__(self):
        s = "width={} height={} border={} balls={}\n".format(self.width, 
                self.height, 
                self.border, 
                len(self.balls))
        for b in self.balls:
            s += "> {}\n".format(b)
        return s

    def key(self, event):
        print("Got key '{}'".format(event.char))
        if event.char == 'q':
            print("Bye!")
            self.root.after_cancel(self.afterid)
            self.root.destroy()

    def CreateGrid(self, rows, cols, radius, border):
        """
        Creates a rectangular rows x cols grid of balls of
        the specified radius and border thickness
        """
        self.balls = []
        for r in range(1, rows+1):
            y = 1.0-2.0*r/(rows+1)
            for c in range(1, cols+1):
                x = 2.0*c/(cols+1) - 1.0
                self.balls.append(Ball(self, x, y, 0.001, 
                                       numpy.pi/6.0, 0.0, radius, border))

    def Animate(self):
        """
        Animates the movement of the various balls
        """
        for c in range(self.cycles):
            for b in self.balls:
                b.v += b.accel
                b.Move(b.x + b.v * numpy.cos(b.theta), 
                       b.y + b.v * numpy.sin(b.theta))
            self.canvas.update()
            time.sleep(self.tick)
        self.root.destroy()

为了完整起见,我包含了完整列表,但我相当确定问题出在 Court class 中。我认为这是某种回调或类似的触发,但我似乎正在用头撞墙试图修复它。

你实际上得到了两个主循环。在您的 Court.__init__ 方法中,您使用 after 启动 Animate 方法,然后启动 Tk 主循环,它将处理事件,直到您销毁主 Tk window.

然而,Animate 方法基本上通过调用 update 来处理事件然后 time.sleep 来重复此主循环来浪费一些时间并重复此操作。当您处理按键并终止您的 window 时,Animate 方法仍然是 运行 并尝试更新不再存在的 canvas。

处理此问题的正确方法是重写 Animate 方法以执行单轮移动球,然后使用 after 安排另一次调用 Animate 并提供必要的延迟作为 after 参数。这样,事件系统将以正确的时间间隔调用您的动画函数,同时仍然及时处理所有其他 window 系统事件。