-->

“tkinter.TclError: invalid command name” error aft

2019-07-14 07:29发布

问题:

I am in the process of learning tkinter on Python 3.X. I am writing a simple program which will get one or more balls (tkinter ovals) bouncing round a rectangular court (tkinter root window with a canvas and rectangle drawn on it).

I want to be able to terminate the program cleanly by pressing the q key, and have managed to bind the key to the root and fire the callback function when a key is pressed, which then calls root.destroy().

However, I'm still getting errors of the form _tkinter.TclError: invalid command name ".140625086752360" when I do so. This is driving me crazy. What am I doing wrong?

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()

I've included the full listing for completeness, but I'm fairly sure that the problem lies in the Court class. I presume it's some sort of callback or similar firing but I seem to be beating my head against a wall trying to fix it.

回答1:

You have effectively got two mainloops. In your Court.__init__ method you use after to start the Animate method and then start the Tk mainloop which will process events until you destroy the main Tk window.

However the Animate method basically replicates this mainloop by calling update to process events then time.sleep to waste some time and repeating this. When you handle the keypress and terminate your window, the Animate method is still running and attempts to update the canvas which no longer exists.

The correct way to handle this is to rewrite the Animate method to perform a single round of moving the balls and then schedule another call of Animate using after and provide the necessary delay as the after parameter. This way the event system will call your animation function at the correct intervals while still processing all other window system events promptly.