3

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?

Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59
TheCrystalShip
  • 249
  • 2
  • 4
  • 12
  • I don't believe tkInter has "flicker free animations" as a design goal. Flicker is part and parcel of using tkInter. – Warren P Sep 05 '17 at 18:36
  • 2
    Maybe use PyGame instead. – Warren P Sep 05 '17 at 18:37
  • for animation/game like gui pygame is significantly more recommended. Tkinter is designed to make applications that don't change layout much. – Tadhg McDonald-Jensen Sep 05 '17 at 18:39
  • 1
    I don't think the comments about smooth animation are true. You can do smooth animations with tkinter. Part of the problem with this specific code is that it's calling sleep, which forces the GUI to freeze. – Bryan Oakley Sep 05 '17 at 18:43

3 Answers3

5

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

Mike - SMT
  • 14,784
  • 4
  • 35
  • 79
  • unfortunately it doesn't help. It still looks bad. For some reason, when the ball is going to hit the wall, its like a part of the ball disappears. – TheCrystalShip Sep 05 '17 at 18:06
  • @TheCrystalShip It might be just to much for your computer to manage. The time it takes to delete the object and redraw it might be 2 long for it to look decent moving that fast. I will dig around a bit to see if there is a similar issue out there with a solution. – Mike - SMT Sep 05 '17 at 18:07
  • @TheCrystalShip it appears that for Windows at least the default refresh rate of tkinter operates at around 15ms. So trying to call an event faster than that may cause issues. Try adjusting your timer to 20ms and then adding the needed distance change in cords to compensate for movement. I will update my answer. – Mike - SMT Sep 05 '17 at 18:29
  • @TheCrystalShip I was able to reduce any distortions by change a few number see my updated answer and let me know if it helps. – Mike - SMT Sep 05 '17 at 18:34
  • 2
    You shouldn't need any calls to `update` when using `after`. The `update` will automatically happen after you call `after`, but before the next time it runs. – Bryan Oakley Sep 05 '17 at 18:37
  • @BryanOakley I forgot to remove those update calls from my code. I was testing to see If I could use update to fix the distortion I was seeing. Thanks for pointing that out. – Mike - SMT Sep 05 '17 at 18:39
  • Also, you'll get smoother animation if you make speed as small as possible. Speed should be 1 or 2 to get the smoothest animation, then you can affect the speed by changing how often `move_active` is called. The bigger the amount you move in each frame of animation, the more jumpy it will look. The "speed" variable in this code is probably better named "direction". – Bryan Oakley Sep 05 '17 at 18:44
  • @BryanOakley the issues the OP was having was parts of the circle were being cut off. The leading potion of the circle would have 2 flat surfaces as it moved around. The faster the speed the more this happened. – Mike - SMT Sep 05 '17 at 18:47
  • @SierraMountainTech: I don't understand that comment. I don't think anything you said has anything to do with the speed or direction. This is all just basic math. If you move something 9 pixels at a time it will appear to jump; if you move it one pixel at a time it will be smooth. Naturally, moving it 9x more on every iteration means it will run 9x faster, but at the expense of a less-smooth animation. – Bryan Oakley Sep 05 '17 at 18:50
  • @BryanOakley I have added an image of what is happening to the circle as it moves around the canvas. I don't think his overall issue was smothness but rather the issue with the circle not being fully displayed as it moves around the canvas. – Mike - SMT Sep 05 '17 at 18:54
  • Interesting. I don't see that on my machine. – Bryan Oakley Sep 05 '17 at 18:57
  • @BryanOakley It may have something to do with the processing speed of the PC being used. Maybe the computer cant draw fast enough before each refresh. At least on my end this is what is happening. I did not get the stuttering or flickering the OP was having issues with. – Mike - SMT Sep 05 '17 at 18:57
4

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.

S.S. Anne
  • 15,171
  • 8
  • 38
  • 76
Don H
  • 41
  • 4
  • To the fine folks who recommend deletion of this response, can you offer a little more explanation please? Obviously I am a new and hope to contribute properly. Is it my mention of another answer? Or is my suggestion too preposterous? – Don H Jan 05 '20 at 23:02
  • I think your answer's a little unclear. It seemed as if you were just saying that you agreed with the other answer and you were restating what it said. Let me look over it a bit more and try to understand it, – S.S. Anne Jan 05 '20 at 23:05
  • I've made an edit. If you disagree with any changes I've made, please let me know. – S.S. Anne Jan 05 '20 at 23:07
  • Well, you eliminated the reference to the notion that the canvas update is limited to the bounds of the old position as explained in the other answer. But I'll leave it as you prefer. Thank you for your attention. – Don H Jan 05 '20 at 23:49
  • Well, let's see. If I added "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." would that be alright? It removes the unneeded "I'm very new to tkinter" but leaves the main point and points out that you solved the problem in a different way. – S.S. Anne Jan 06 '20 at 00:02
  • 1
    Yes, I believe so. As an avid user of stackoverflow, I think the reference is helpful to understanding why the solution works. Over the years I've learned so much from these pages and I hope I can contribute as well. Thank you again. – Don H Jan 06 '20 at 00:09
2

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

JJJ
  • 32,902
  • 20
  • 89
  • 102
Fheuef
  • 21
  • 2