How to check for collision between two boxes using John Zelle's graphics.py module?
EDIT:
I have found a way to find collisions between any two classes that have x,y,width,height. Its a bit messy but it does the job:
def collided(self, collider):
if self.x < collider.x + collider.width and\
self.x + self.width > collider.x and\
self.y < collider.y + collider.height and\
self.y + self.height > collider.y:
return True
return False
If anyone has a better way of doing this, it would be greatly appreciated!
Here's a little demo program that illustrates a fairly succinct way of doing this — it contains a function named intersect() which checks whether two instances of the graphics module's Rectangle class intersect.
A slightly tricky aspect of doing that is because the two Points used to define Rectangle aren't required to be its lower-right and upper-left corners, which the logic in intersect() function requires. To handle that, a helper function named canonical_rect() is used to make that's the case for each of arguments it's passed.
from graphics import *
from random import randint
WIDTH, HEIGHT = 640, 480
def rand_point():
""" Create random Point within window's limits. """
return Point(randint(0, WIDTH), randint(0, HEIGHT))
def rand_rect():
""" Create random Rectangle within window's limits. """
p1, p2 = rand_point(), rand_point()
while p1 == p2: # Make sure points are different.
p2 = rand_point()
return Rectangle(p1, p2)
def canonical_rect(rect):
""" Return new Rectangle whose points are its lower-left and upper-right
extrema - something Zelle graphics doesn't require.
"""
p1, p2 = rect.p1, rect.p2
minx = min(p1.x, p2.x)
miny = min(p1.y, p2.y)
maxx = max(p1.x, p2.x)
maxy = max(p1.y, p2.y)
return Rectangle(Point(minx, miny), Point(maxx, maxy))
def intersect(rect1, rect2):
""" Determine whether the two arbitrary Rectangles intersect. """
# Ensure pt 1 is lower-left and pt 2 is upper-right of each rect.
r1, r2 = canonical_rect(rect1), canonical_rect(rect2)
return (r1.p1.x <= r2.p2.x and r1.p2.x >= r2.p1.x and
r1.p1.y <= r2.p2.y and r1.p2.y >= r2.p1.y)
def main():
# Initialize.
win = GraphWin('Box Collision', WIDTH, HEIGHT, autoflush=False)
center_pt = Point(WIDTH // 2, HEIGHT // 2)
box1 = Rectangle(Point(0, 0), Point(0, 0))
box2 = Rectangle(Point(0, 0), Point(0, 0))
msg = Text(center_pt, "")
# Repeat until user closes window.
while True:
box1.undraw()
box1 = rand_rect()
box1.draw(win)
box2.undraw()
box2 = rand_rect()
box2.draw(win)
if intersect(box1, box2):
text, color = "Collided", "red"
else:
text, color = "Missed", "green"
msg.undraw()
msg.setText(text)
msg.setTextColor(color)
msg.draw(win)
win.update()
try:
win.getMouse() # Pause to view result.
except GraphicsError:
break # User closed window.
if __name__ == '__main__':
main()
Related
I need to create a widget that is used to pick a time. QTimeEdit widget doesn't seem intuitive or a good design. So I decided to create a time picker similar to the time picker in smartphones.
I managed to create the clock and click that makes the pointer (something similar to the pointer in the image) move to the currently clicked position (note: it's not perfect, it still looks bad). I would like to have help with making the inner clock
Here is my code:
from PyQt5 import QtWidgets, QtGui, QtCore
import math, sys
class ClockWidget(QtWidgets.QWidget): # I want to be able to reuse this class for other programs also, so please don't hard code values of the list, start and end
def __init__(self, start, end, lst=[], *args, **kwargs):
super(ClockWidget, self).__init__(*args, **kwargs)
self.lst = lst
if not self.lst:
self.lst = [*range(start, end)]
self.index_start = 0 # tune this to move the letters in the circle
self.pointer_angles_multiplier = 9 # just setting the default values
self.current = None
self.rects = []
#property
def index_start(self):
return self._index_start
#index_start.setter
def index_start(self, index):
self._index_start = index
def paintEvent(self, event):
self.rects = []
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setColor(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
x, y = self.rect().x(), self.rect().y()
width, height = self.rect().width(), self.rect().height()
painter.drawEllipse(x, y, x + width, x + height)
s, t, equal_angles, radius = self.angle_calc()
radius -= 30
pen.setColor(QtCore.Qt.green)
pen.setWidth(2)
painter.setPen(pen)
""" pointer angle helps in determining to which position the pointer should be drawn"""
self.pointer_x, self.pointer_y = s + ((radius-30) * math.cos(self.pointer_angles_multiplier * equal_angles)), t \
+ ((radius-30) * math.sin(self.pointer_angles_multiplier * equal_angles))
""" The pendulum like pointer """
painter.drawLine(QtCore.QPointF(s, t), QtCore.QPointF(self.pointer_x, self.pointer_y))
painter.drawEllipse(QtCore.QRectF(QtCore.QPointF(self.pointer_x - 20, self.pointer_y - 40),
QtCore.QPointF(self.pointer_x + 30, self.pointer_y + 10)))
pen.setColor(QtCore.Qt.blue)
pen.setWidth(3)
font = self.font()
font.setPointSize(14)
painter.setFont(font)
painter.setPen(pen)
""" Drawing the number around the circle formula y = t + radius * cos(a)
y = s + radius * sin(a) where angle is in radians (s, t) are the mid point of the circle """
for index, char in enumerate(self.lst, start=self.index_start):
angle = equal_angles * index
y = t + radius * math.sin(angle)
x = s + radius * math.cos(angle)
# print(f"Add: {add_x}, index: {index}; char: {char}")
rect = QtCore.QRectF(x - 30, y - 40, x + 60, y) # clickable point
self.rects.append([index, char, rect]) # appends index, letter, rect
painter.setPen(QtCore.Qt.blue)
painter.drawRect(rect) # helps in visualizing the points where the click can received
print(f"Rect: {rect}; char: {char}")
painter.setPen(QtCore.Qt.red)
points = QtCore.QPointF(x, y)
painter.drawText(points, str(char))
def mousePressEvent(self, event):
for x in self.rects:
index, char, rect = x
if event.button() & QtCore.Qt.LeftButton and rect.contains(event.pos()):
self.pointer_angles_multiplier = index
self.current = char
self.update()
break
def angle_calc(self):
"""
This will simply return (midpoints of circle, divides a circle into the len(list) and return the
angle in radians, radius)
"""
return ((self.rect().width() - self.rect().x()) / 2, (self.rect().height() - self.rect().y()) / 2,
(360 / len(self.lst)) * (math.pi / 180), (self.rect().width() / 2))
def resizeEvent(self, event: QtGui.QResizeEvent):
"""This is supposed to maintain a Square aspect ratio on widget resizing but doesn't work
correctly as you will see when executing"""
if event.size().width() > event.size().height():
self.resize(event.size().height(), event.size().width())
else:
self.resize(event.size().width(), event.size().width())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
message = ClockWidget(1, 13)
message.index_start = 10
message.show()
sys.exit(app.exec())
The Output:
The blue rectangles represent the clickable region. I would be glad if you could also, make the pointer move to the closest number when clicked inside the clock (Not just move the pointer when the clicked inside the blue region)
There is one more problem in my code, that is the numbers are not evenly spaced from the outer circle. (like the number 12 is closer to the outer circle than the number 6)
Disclaimer: I will not explain the cause of the error but the code I provide I think should give a clear explanation of the errors.
The logic is to calculate the position of the centers of each small circle, and use the exinscribed rectangle to take it as a base to draw the text and check if the point where you click is close to the texts.
from functools import cached_property
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ClockWidget(QtWidgets.QWidget):
L = 12
r = 40.0
DELTA_ANGLE = 2 * math.pi / L
current_index = 9
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
R = min(self.rect().width(), self.rect().height()) / 2
margin = 4
Rect = QtCore.QRectF(0, 0, 2 * R - margin, 2 * R - margin)
Rect.moveCenter(self.rect().center())
painter.setBrush(QtGui.QColor("gray"))
painter.drawEllipse(Rect)
rect = QtCore.QRectF(0, 0, self.r, self.r)
if 0 <= self.current_index < 12:
c = self.center_by_index(self.current_index)
rect.moveCenter(c)
pen = QtGui.QPen(QtGui.QColor("red"))
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(c, self.rect().center())
painter.setBrush(QtGui.QColor("red"))
painter.drawEllipse(rect)
for i in range(self.L):
j = (i + 2) % self.L + 1
c = self.center_by_index(i)
rect.moveCenter(c)
painter.setPen(QtGui.QColor("white"))
painter.drawText(rect, QtCore.Qt.AlignCenter, str(j))
def center_by_index(self, index):
R = min(self.rect().width(), self.rect().height()) / 2
angle = self.DELTA_ANGLE * index
center = self.rect().center()
return center + (R - self.r) * QtCore.QPointF(math.cos(angle), math.sin(angle))
def index_by_click(self, pos):
for i in range(self.L):
c = self.center_by_index(i)
delta = QtGui.QVector2D(pos).distanceToPoint(QtGui.QVector2D(c))
if delta < self.r:
return i
return -1
def mousePressEvent(self, event):
i = self.index_by_click(event.pos())
if i >= 0:
self.current_index = i
self.update()
#property
def hour(self):
return (self.current_index + 2) % self.L + 1
def minumumSizeHint(self):
return QtCore.QSize(100, 100)
def main():
app = QtWidgets.QApplication(sys.argv)
view = ClockWidget()
view.resize(400, 400)
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I am creating a program in Python using Zelle graphics package. There is a moving circle that the user clicks on in order to make it return to the center of the screen. I cannot figure out how to identify when the user clicks inside of the circle. Here is the code I have written:
from graphics import *
from time import sleep
import random
Screen = GraphWin("BallFalling", 400 , 400);
Screen.setBackground('green')
ball = Circle(Point(200,200),25);
ball.draw(Screen);
ball.setFill('white')
ballRadius = ball.getRadius()
ballCenter = 0
directionX = (random.random()*40)-20;
directionY = (random.random()*40)-20;
clickx = Screen.getMouse().getX();
clicky = 0
while ball.getCenter().getX() + ball.getRadius() <= 400 and ball.getCenter().getY() + ball.getRadius() <= 400 and ball.getCenter().getX() >= 0 and ball.getCenter().getY() >= 0:
ball.move(ball.getRadius()//directionX,ball.getRadius()//directionY)
ballLocation = ball.getCenter().getX();
ballLocationy = ball.getCenter().getY();
sleep(1/15);
The main problem I am having is identifying the coordinates of the mouse click. I cannot find anything in the Zelle graphics package that says anything about this.
The major issues I see are: you are calling getMouse() before the loop when you should be calling checkMouse() inside the loop; you have no code to compare the distance of the click from the ball; you have no code to return the ball to the center of the screen.
Below is my complete rework of your code addressing the above issues:
from time import sleep
from random import randrange
from graphics import *
WIDTH, HEIGHT = 400, 400
RADIUS = 25
def distance(graphic, point):
x1, y1 = graphic.getCenter().getX(), graphic.getCenter().getY()
x2, y2 = point.getX(), point.getY()
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
window = GraphWin("Ball Falling", WIDTH, HEIGHT)
window.setBackground('green')
ball = Circle(Point(WIDTH/2, HEIGHT/2), RADIUS)
ball.setFill('white')
ball.draw(window)
directionX = randrange(-4, 4)
directionY = randrange(-4, 4)
while True:
center = ball.getCenter()
x, y = center.getX(), center.getY()
if not (RADIUS < x < WIDTH - RADIUS and RADIUS < y < HEIGHT - RADIUS):
break
point = window.checkMouse()
if point and distance(ball, point) <= RADIUS:
ball.move(WIDTH/2 - x, HEIGHT/2 - y) # recenter
directionX = randrange(-4, 4)
directionY = randrange(-4, 4)
else:
ball.move(directionX, directionY)
sleep(1/30)
There may still be subtle bugs and contants tweaking for you to sort out.
I am trying to make a program that will increase the speed of the moving blue circle by 50% if the yellow square is clicked, and decrease it by 50% if the purple square is clicked. The purple square works fine but the yellow square will not register clicks at all. Interestingly, if the black square is used in place of the yellow square, it will function as necessary. Additionally, the blue circle will register clicks within it but will not close the window as intended. I am a novice in python and am beyond confused here. Any suggestions would be greatly appreciated.
As stated previously, functionality works as intended when the black square is used to carry out the functionality of the yellow square. I have used print statements to test if clicks are being registered in different shapes. Clicks register in the blue circle, black square and purple square, but not in the yellow square.
from graphics import*
from time import sleep
def main():
win,c=make()
yellow=Rectangle(Point(495,200),Point(395,300))
yellow.setFill("yellow")
yellow.draw(win)
purple=Rectangle(Point(5,200),Point(105,300))
purple.setFill("purple")
purple.draw(win)
black=Rectangle(Point(200,5),Point(300,105))
black.setFill("black")
black.draw(win)
z=0.2
dx,dy=3,-6
while True:
c.move(dx,dy)
sleep(z)
center=c.getCenter()
dx,dy=bounce(dx,dy,center)
point=win.checkMouse()
if point != None:
if isClicked(point,yellow):
z=z-0.05
elif isClicked(point,purple):
z=z+0.05
elif isClicked(point,c):
win.close
def bounce(dx,dy,center):
if center.getX()<25 or center.getX()>475:
dx *= -1
if center.getY()<25 or center.getY()>475:
dy *= -1
if (center.getX() > 370 and center.getX() < 470) and (center.getY() >175 and center.getY() < 325):
dx *= -1
dy *= -1
return dx,dy
def isClicked(click, shape):
# verify that click is a Point object otherwise return False
if not click:
return False
# get the X,Y coordinates of the mouse click
x,y = click.getX(), click.getY()
# check if shape is a Circle
if type(shape).__name__ == 'Circle':
center = shape.getCenter()
cx, cy = center.getX(), center.getY()
# if click is within the Circle return True
if ((x-cx)**2 + (y-cy)**2)**.5 <= 25:
return True
# shape must be a Rectangle
else:
x1, y1 = shape.getP1().getX(), shape.getP1().getY()
x2, y2 = shape.getP2().getX(), shape.getP2().getY()
# if click is within the Rectangle
if (x1 < x < x2) and (y1 < y < y2):
return True
# click was not inside the shape
return False
def make():
win = GraphWin('Tester',500,500)
win.setBackground('grey')
c = Circle(Point(250,250),25)
c.setFill('blue')
c.draw(win)
return win, c
main()
As stated previously, the intended function would be that a click in the purple rectangle would slow the circle by 50% and a click in the yellow rectangle should increase speed by 50%. Currently a click in the circle does nothing, as does a click in the yellow square. Additionally, a click in the blue circle should close the circle. Thank you for any advice!
You forgot () in win.close() and it never run this function. You could also add break to exit loop because win.close() only close window but it doesn't end loop which still try to win.checkMouse() but win doesn't exists any more and it gives error.
Problem with yellow rectangle is because first X is bigger then second X (495 > 395) in
yellow = Rectangle(Point(495, 200), Point(395, 300))
and Rectangle doesn't sort these values so finally this is not true
if (x1 < x < x2) and (y1 < y < y2)
If you change it to
yellow = Rectangle(Point(395,200),Point(495,300))
then it works correctly.
BTW:
Instead of type(shape).__name__ == 'Circle' you can use isinstance(shape, Circle)
You could add some spaces and empty lines to make it more readable.
Using import * is not prefered - but this part I didn't change.
PEP 8 -- Style Guide for Python Code
Instead of if point != None: you should rather use if point is not None: or shorter if point:
from graphics import *
from time import sleep
def main():
win, c = make()
yellow = Rectangle(Point(395, 200), Point(495, 300))
yellow.setFill("yellow")
yellow.draw(win)
purple = Rectangle(Point(5, 200), Point(105, 300))
purple.setFill("purple")
purple.draw(win)
black = Rectangle(Point(200, 5), Point(300, 105))
black.setFill("black")
black.draw(win)
z = 0.2
dx, dy = 3, -6
while True:
c.move(dx, dy)
sleep(z)
center = c.getCenter()
dx, dy = bounce(dx, dy, center)
point = win.checkMouse()
#if point is not None:
if point:
if isClicked(point, yellow):
z = z - 0.05
elif isClicked(point, purple):
z = z + 0.05
elif isClicked(point, c):
win.close()
break
def bounce(dx, dy, center):
if center.getX() < 25 or center.getX() > 475:
dx *= -1
if center.getY() < 25 or center.getY() > 475:
dy *= -1
if (center.getX() > 370 and center.getX() < 470) and (center.getY() > 175 and center.getY() < 325):
dx *= -1
dy *= -1
return dx, dy
def isClicked(click, shape):
# verify that click is a Point object otherwise return False
if not click:
return False
# get the X,Y coordinates of the mouse click
x, y = click.getX(), click.getY()
# check if shape is a Circle
if isinstance(shape, Circle):
center = shape.getCenter()
cx, cy = center.getX(), center.getY()
# if click is within the Circle return True
if ((x-cx)**2 + (y-cy)**2)**.5 <= 25:
return True
# shape must be a Rectangle
else:
x1, y1 = shape.getP1().getX(), shape.getP1().getY()
x2, y2 = shape.getP2().getX(), shape.getP2().getY()
# if click is within the Rectangle
if (x1 < x < x2) and (y1 < y < y2):
return True
# click was not inside the shape
return False
def make():
win = GraphWin('Tester', 500, 500)
win.setBackground('grey')
c = Circle(Point(250, 250), 25)
c.setFill('blue')
c.draw(win)
return win, c
main()
I am currently trying to digitalize an boardgame I invented (repo: https://github.com/zutn/King_of_the_Hill). To make it work I need to check if one of the tiles (the arcs) on this board have been clicked. So far I have not been able to figure a way without giving up the pygame.arc function for drawing. If I use the x,y position of the position clicked, I can't figure a way out to determine the exact outline of the arc to compare to. I thought about using a color check, but this would only tell me if any of the tiles have been clicked. So is there a convenient way to test if an arc has been clicked in pygame or do I have to use sprites or something completely different? Additionally in a later step units will be included, that are located on the tiles. This would make the solution with the angle calculation postet below much more diffcult.
This is a simple arc class that will detect if a point is contained in the arc, but it will only work with circular arcs.
import pygame
from pygame.locals import *
import sys
from math import atan2, pi
class CircularArc:
def __init__(self, color, center, radius, start_angle, stop_angle, width=1):
self.color = color
self.x = center[0] # center x position
self.y = center[1] # center y position
self.rect = [self.x - radius, self.y - radius, radius*2, radius*2]
self.radius = radius
self.start_angle = start_angle
self.stop_angle = stop_angle
self.width = width
def draw(self, canvas):
pygame.draw.arc(canvas, self.color, self.rect, self.start_angle, self.stop_angle, self.width)
def contains(self, x, y):
dx = x - self.x # x distance
dy = y - self.y # y distance
greater_than_outside_radius = dx*dx + dy*dy >= self.radius*self.radius
less_than_inside_radius = dx*dx + dy*dy <= (self.radius- self.width)*(self.radius- self.width)
# Quickly check if the distance is within the right range
if greater_than_outside_radius or less_than_inside_radius:
return False
rads = atan2(-dy, dx) # Grab the angle
# convert the angle to match up with pygame format. Negative angles don't work with pygame.draw.arc
if rads < 0:
rads = 2 * pi + rads
# Check if the angle is within the arc start and stop angles
return self.start_angle <= rads <= self.stop_angle
Here's some example usage of the class. Using it requires a center point and radius instead of a rectangle for creating the arc.
pygame.init()
black = ( 0, 0, 0)
width = 800
height = 800
screen = pygame.display.set_mode((width, height))
distance = 100
tile_num = 4
ring_width = 20
arc = CircularArc((255, 255, 255), [width/2, height/2], 100, tile_num*(2*pi/7), (tile_num*(2*pi/7))+2*pi/7, int(ring_width*0.5))
while True:
fill_color = black
for event in pygame.event.get():
# quit if the quit button was pressed
if event.type == pygame.QUIT:
pygame.quit(); sys.exit()
x, y = pygame.mouse.get_pos()
# Change color when the mouse touches
if arc.contains(x, y):
fill_color = (200, 0, 0)
screen.fill(fill_color)
arc.draw(screen)
# screen.blit(debug, (0, 0))
pygame.display.update()
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.
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.