Reputation: 561
I wrote this really simple code that move an oval trough the screen:
from tkinter import *
""" GLOBAL VARS """
H, W = 400, 600 # default height / width
DELAY = 30 # frame rate in milliseconds
R = 20 # radius of the ball
class App:
def __init__(self, parent):
""" app init """
self.root = parent
self.canvas = Canvas(self.root, bg="white", height=H, width=W)
self.canvas.pack()
self.pos = [W / 2, H / 2] # starting position
self.speed = [2, 2] # x / y speed
self.canvas.create_oval(self.pos[0] - 20, self.pos[1] - 20, self.pos[0] + 20, self.pos[1] + 20, fill="blue")
self.loop()
def loop(self):
""" main loop """
coords = self.canvas.coords(1)[:2] # actual oval coordinates
if not 0 < coords[0] < W - 20: # the oval bounce off the window
self.speed[0] *= -1
if not 0 < coords[1] < H - 20:
self.speed[1] *= -1
self.canvas.move(1, *self.speed)
self.root.after(DELAY, self.loop)
""" GUI SETUP """
root = Tk()
App(root)
root.mainloop()
The problem is that when I try to increase the frame rate, the oval half disappears (I tried to take some screenshots, but when I do, the app freezes for one frame and in the screenshots the oval appears correctly, btw you can try yourself by running the code above).
My question is: is there a way to optimize the motion or do I have to accept the fact that Tkinter can't handle high fps?
Upvotes: 3
Views: 1477
Reputation: 1857
This is what I feel to be an under-documented Tkinter performance issue. The reason that you see half the oval disappear is that, when detecting what to update on the canvas, it obtains the bbox of whatever needs to be updated, in your case the oval, and then refreshes just that portion of the canvas (for optimization reasons).
However, when you move the oval across the canvas quickly, it leaves the section of the canvas to be updated, meaning that part of the oval will be cutoff, as you see and describe.
This is the Damage/Repair model.
In my opinion, the simplest way to deal with this and fix it is to create a larger, invisible (to the user) object around what you wish to update. This means that Tkinter detects the larger object, and updates the portion of the widget (canvas) around that. The trick is to have the new updated section encompass what you wish to appear, the oval.
You can easily move both the larger invisible object and your oval via a tag system.
To put this in action, you can add the tag
of "circle"
to your oval, and then create a new oval with the same tag, but coordinates which cover the entire oval. It is also important to specify fill=""
and outline=""
so that you cannot see the new object.
This looks like:
self.canvas.create_oval(self.pos[0] - 20, self.pos[1] - 20,
self.pos[0] + 20, self.pos[1] + 20, fill="blue", tag="circle")
self.canvas.create_oval(self.pos[0] - 40, self.pos[1] - 40, self.pos[0] + 40,
self.pos[1] + 40, fill="", outline="", tag="circle")
Where the first created oval is your current oval now with a tag, and the second oval is the invisible, larger one.
When you move the oval, instead of moving 1
(which is the ID of your oval at the moment because you create it first), you can move "circle"
:
self.canvas.move("circle", *self.speed)
After you implement this, your oval shouldn't show any cut-offs. If you do still see cut-offs, then you can increase the size of the invisible oval, e.g. offsets of 60
instead of 40
.
Upvotes: 5