Reputation: 259
Started playing with python's tkinter today and ran into some problems. I created an animation that moves a ball around the screen, with a given speed. (and when it hits the screen, it goes back)
Why does my ball look bad? it's shape is not uniform? (its like blinking a lot)
Is there a better way to do it?
the code:
from tkinter import *
import time
WIDTH = 800
HEIGHT = 500
SIZE = 100
tk = Tk()
canvas = Canvas(tk, width=WIDTH, height=HEIGHT, bg="grey")
canvas.pack()
color = 'black'
class Ball:
def __init__(self):
self.shape = canvas.create_oval(0, 0, SIZE, SIZE, fill=color)
self.speedx = 3
self.speedy = 3
def update(self):
canvas.move(self.shape, self.speedx, self.speedy)
pos = canvas.coords(self.shape)
if pos[2] >= WIDTH or pos[0] <= 0:
self.speedx *= -1
if pos[3] >= HEIGHT or pos[1] <= 0:
self.speedy *= -1
ball = Ball()
while True:
ball.update()
tk.update()
time.sleep(0.01)
errors after terminating the program:
Traceback (most recent call last):
File "C:/..py", line 29, in <module>
ball.update()
File "C:/Users/talsh/...py", line 20, in update
canvas.move(self.shape, self.speedx, self.speedy)
File "C:\Users\...\tkinter\__init__.py", line 2585, in move
self.tk.call((self._w, 'move') + args)
_tkinter.TclError: invalid command name ".!canvas"
Is it normal? Am I doing anything wrong?
Upvotes: 3
Views: 1258
Reputation: 41
After suffering the same flattened leading edges of fast moving objects I am inclined to agree with @Fheuef's response, although I solved the problem in a different manner.
I was able to eliminate this effect by forcing a redraw of the entire canvas just by resetting the background on every update.
Try adding:
canvas.configure(bg="grey")
to your loop. Of course we compromise performance, but it's a simple change and it seems a reasonable trade off here.
Upvotes: 4
Reputation: 21
Basically I've found that this has to do with the way Tkinter updates the canvas image : instead of redrawing the whole canvas everytime, it forms a box around things that have moved and it redraws that box. The thing is, it seems to use the ball's old position (before it moved) so if the ball moves too fast, its new position is out of the redraw box.
One simple way to solve this however is to create a larger invisible ball with outline=''
around it, which will move to the ball's position on every update, so that the redraw box takes that ball into account and the smaller one stays inside of it. Hope that's clear enough...
Upvotes: 2
Reputation: 15226
I would imaging the problem is coming from sleep()
. The methods sleep()
and wait()
should not be used in tkinter as they will pause the entire application instead of just providing a timer.
Update: Its also not a good idea to name a method the same name as a built in method.
you have self.update()
and update()
is already in the name space for canvas. Change self.update()
to something else like: self.ball_update()
UPDATE: It looks like tikinter refreshes at a 15ms rate and trying to fire an even faster than that might cause issues. The closest I was able to get to stopping the circle from distorting while moving at the same rate as your original code was to change the timer to 30ms and to change your speed variables to 9 from 3.
Always make sure you have mainloop()
at the end of you tkinter app. mainloop()
is required to make sure tkinter runs properly and without there may be bugs caused by it missing so at the end add tk.mainloop()
You should use after()
instead. This should probably be done using a function/method as your timed loop. Something like this:
def move_active(self):
if self.active == True:
self.ball_update()
tk.after(30, self.move_active)
tk.update()
Replace your while loop with the above method and add the class attribute self.active = True
to your __init__
section. Let me know if this clears up your stuttering:
from tkinter import *
import time
WIDTH = 800
HEIGHT = 500
SIZE = 100
tk = Tk()
canvas = Canvas(tk, width=WIDTH, height=HEIGHT, bg="grey")
canvas.pack()
color = 'black'
class Ball:
def __init__(self):
self.shape = canvas.create_oval(0, 0, SIZE, SIZE, fill=color)
self.speedx = 9 # changed from 3 to 9
self.speedy = 9 # changed from 3 to 9
self.active = True
self.move_active()
def ball_update(self):
canvas.move(self.shape, self.speedx, self.speedy)
pos = canvas.coords(self.shape)
if pos[2] >= WIDTH or pos[0] <= 0:
self.speedx *= -1
if pos[3] >= HEIGHT or pos[1] <= 0:
self.speedy *= -1
def move_active(self):
if self.active == True:
self.ball_update()
tk.after(30, self.move_active) # changed from 10ms to 30ms
ball = Ball()
tk.mainloop() # there should always be a mainloop statement in tkinter apps.
Here are some links to Q/A's related to refresh timers.
Why are .NET timers limited to 15 ms resolution?
Why does this shape in Tkinter update slowly?
All that being said you may want to use an alternative that might be able to operate at a faster refreash rate like Pygame
UPDATE:
Here is an image of what is happening to the circle while its moving through the canvas. As you can see its getting potions of the circle visibly cut off. This appears to happen the faster the update is set. The slower the update( mostly above 15ms) seams to reduce this problem:
Upvotes: 6