TheCrystalShip
TheCrystalShip

Reputation: 259

tkinter - the shape isn't uniform and the animation looks bad

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)

  1. Why does my ball look bad? it's shape is not uniform? (its like blinking a lot)

  2. 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

Answers (3)

Don H
Don H

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

Fheuef
Fheuef

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

Mike - SMT
Mike - SMT

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:

enter image description here

Upvotes: 6

Related Questions