0

I am learning tkinter and am writing a simple a Python3 program which generates a root window and adds a rectangle and a number of ovals. The ovals are animated using a callback registered with the after() function. I have also bound the keyboard and when the q key is pressed the root window is destroyed. If I run the program from the command line it works fine. If I run it from an iPython (spyder) console it runs fine the first time, but on the second attempt it generates an error which seems to be some sort of vestigial callback firing when it shouldn't. If I close the console I am using and open another one, the program again runs OK on its first invocation but thereafter gives me exactly the same error.

I suspect that I'm not cleaning up after myself at the end of the first invocation of the program. I'd assumed that calling after_cancel() followed by root_destroy() would clear everything up.

I've tried individually deleting the various ovals as part of the key callback, before calling root.destroy() but that has no effect.

Python 3.4.3 (default, Jul 28 2015, 18:20:59) 
Type "copyright", "credits" or "license" for more information.

IPython 1.2.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.
%guiref   -> A brief reference about the graphical user interface.

In [1]: runfile('/home/tim/metatron/Scratch/gr3.py', wdir='/home/tim/metatron/Scratch')
Got key 'q'
Bye!

In [2]: runfile('/home/tim/metatron/Scratch/gr3.py', wdir='/home/tim/metatron/Scratch')
Got key 'q'
Bye!
Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.4/tkinter/__init__.py", line 1536, in __call__
    return self.func(*args)
  File "/usr/lib/python3.4/tkinter/__init__.py", line 585, in callit
    func(*args)
  File "/home/tim/metatron/Scratch/gr3.py", line 165, in Animate
    b.Move(b.position + b.velocity)
  File "/home/tim/metatron/Scratch/gr3.py", line 77, in Move
    self.parent.canvas.move(self.widget, deltax, deltay)
  File "/usr/lib/python3.4/tkinter/__init__.py", line 2432, in move
    self.tk.call((self._w, 'move') + args)
_tkinter.TclError: invalid command name ".139666802660408"

In [3]: 

Any ideas what I'm doing wrong? (I've included the full source for completeness).

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, speed=0.0, angle=0.0, accel=0.0, radius=0.0, 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.radius   = radius            # Radius
        self.border   = border            # Border thickness (integer)
        self.position = numpy.complex(x, y)
        self.velocity = numpy.complex(speed*numpy.cos(angle), 
                                      speed*numpy.sin(angle))

        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} vx={:.4f} vy={:.4f} r={:.4f} t={}, px={} py={} pr={}".format(
                self.index, self.position.real, self.position.imag, 
                self.velocity.real, self.velocity.imag, 
                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
        """
        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
        """
        return int((1.0 + self.position.real) * 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
        """
        return int((1.0 - self.position.imag) * self.parent.height / 2.0 + self.parent.border)

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


    def HandleWallCollision(self):
        """
        Detects if a ball collides with the walls of the rectangular
        Court.
        """
        # Check impact with top and invert vy if it occurs
        if self.py() < self.pr() + self.parent.border:
            self.velocity = numpy.complex(self.velocity.real, -self.velocity.imag)
        # Check bottom impact
        if self.py() + self.pr() > self.parent.height + self.parent.border:
            self.velocity = numpy.complex(self.velocity.real, -self.velocity.imag)
        # Left impact
        if self.px() - self.pr() < self.parent.border:
            self.velocity = numpy.complex(-self.velocity.real, self.velocity.imag)
        # Right impact
        if self.px() + self.pr() > self.parent.width + self.parent.border:
            self.velocity = numpy.complex(-self.velocity.real, self.velocity.imag)

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=10,         # Number of rows of balls
                 cols=10,         # 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):         # Animation tick length (mS)
        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':
            self.root.after_cancel(self.afterid)
            self.root.destroy()
            print("Bye!")

    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, 
                                       numpy.random.random()*0.005, 
                                       numpy.random.random()*numpy.pi*2, 
                                       0.0, radius, border))

    def Animate(self):
        """
        Animates the movement of the various balls
        """
        for b in self.balls:
            b.Move(b.position + b.velocity)
        self.canvas.update()
        self.afterid=self.root.after(self.tick, self.Animate)
TimGJ
  • 1,584
  • 2
  • 16
  • 32
  • You might want to reference your [previous question](http://stackoverflow.com/questions/33258866/tkinter-tclerror-invalid-command-name-error-after-calling-root-destroy) about this code. Would [this question](http://stackoverflow.com/questions/26168967/invalid-command-name-while-executing-after-script) help you this time? It sounds like the root of the problem is that you're destroying the application while you still have `after()` calls scheduled – TigerhawkT3 Oct 22 '15 at 12:03
  • @TigerhawkT3 I was thinking of referencing the earlier question, but as this is a different problem and, given SO's emphasis on consision I decided it would be inappropriate. I'd also looked at various questions on SO and other sites but so far drawn a blank. – TimGJ Oct 22 '15 at 12:40
  • @TigerhawkT3 I agree that the cause would appear to be the callback continuing to fire, but I can't work out why as I think I am cancelling it correctly. Hence my question. – TimGJ Oct 22 '15 at 12:41
  • I suggest making an [mcve](http://www.stackoverflow.com/help/mcve) that has as much code removed as possible. – Bryan Oakley Oct 22 '15 at 13:25

0 Answers0