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)