Using tkinter after to produce an animation - python

Background Information - I'm attempting to create somewhat of animation for a frame object with TKinter with the following code:
from tkinter import Frame, Tk, Label, Button
import time
def runAnim():
for width in range(0, 200):
app.after(5000, lambda width = width: test_label.config(width=width))
app = Tk()
app.geometry("500x500")
test_label = Frame(bg="#222", width=0)
test_label.pack(side="left", fill="y")
test_button = Button(text="toggle", command=lambda: runAnim() )
test_button.pack(side="right")
The problem is that it this is not producing the desired behaviour. My understanding is that this should gradually increase the width every 5 seconds, however the 0-200 range seems to complete within these 5 seconds, rather than it being an increased width of 1 every 5 seconds.
Any solutions would be appreciated!

That after(5000, …) means 5 seconds after right now, as after is being called, not 5 seconds after some future point in time that tkinter can only guess by reading your mind.
So, you're just creating 200 callbacks, and scheduling them all to run 5 seconds from now. That's obviously not what you want, but it's what you're asking for, so that's what you get.
In general, you can't do loops like this in event-based programming. What you need to do is turn the loop inside-out: each step does one iteration, then schedules the next call for the next one.
The fully-general transformation looks like this:
def runAnim():
iterwidth = iter(range(0, 200))
stepAnim(iterwidth)
def stepAnim(iterwidth):
try:
width = next(iterwidth)
except StopIteration:
return
test_label.config(width=width))
app.after(5000, stepAnim, iterwidth)
While that works for any iterable, when you're just iterating over numbers, it's usually a bit nicer to turn the for loop into an explicit counter, which is easier to invert. (Yes, that's the opposite of the "usual for instead of while and += 1 when you're not inverting things. The difference is that here, we can't access the magic of for or while, and while is a lot less magical, and therefore easier to invert.)
def runAnim():
stepAnim(0, 200):
def stepAnim(width, maxWidth):
test_label.config(width=width))
width += 1
if width < maxWidth:
app.after(5000, stepAnim, width, maxWidth)
However, in this particularly simple case, you might be able to get away with scheduling 200 callbacks, ranging from 5 to 1000 seconds into the future:
def runAnim():
for width in range(0, 200):
app.after(5000 * width, lambda width = width: test_label.config(width=width))
This might cause the timer to drift a lot more badly, or it might even choke up the scheduler and add lag to your program, but it's at least worth trying.
Speaking of drift:
Back at the start, I mentioned that after(5000, …) means 5 seconds after right now.
An after can fire a bit late. As the docs say: "Tkinter only guarantees that the callback will not be called earlier than that; if the system is busy, the actual delay may be much longer."
So, what happens if it fires after, say, 5.2 seconds? Then the second tick happens 5 seconds after that, at 10.2 seconds, not at 10 seconds. And if they're all firing a bit late, that adds up, so by the end, we could be 20 seconds behind.
Worse, what if after fires exactly at 5.0 seconds, but the Label.config takes 0.2 seconds to run? Then we're absolutely guaranteed to be 20 seconds behind. (Plus any additional error from after itself.)
If this matters, you need to keep track of the desired "next time", and wait until then, not until 5 seconds from whenever it is now. For example:
import datetime as dt
def runAnim():
stepAnim(0, 200, dt.datetime.now() + dt.timedelta(seconds=5):
def stepAnim(width, maxWidth, nextTick):
test_label.config(width=width))
width += 1
if width < maxWidth:
now = dt.datetime.now()
delay = (nextTick - now).total_seconds() * 1000
nextTick += dt.timedelta(seconds=5)
app.after(delay, stepAnim, width, maxWidth, nextTick)

Related

tkinter after() lagging after period of time

I was writing code to show time every second and then noticed that eventually after some time(approx 30 seconds) time starts lagging behind the original time. I was able to reproduce the issue, so was someone I know. Hopefully you guys too can reproduce this.
Code:
from tkinter import *
import time
win = Tk()
win.geometry('400x400')
frame=Frame(win)
frame.grid()
labelTD=Label(frame)
labelTD.grid(row=2,column=0)
def countdown(n):
mn, secs =divmod(n, 60)
hr, mn = divmod(mn, 60)
labelCD.config(text=f"{hr:02}:{mn:02}:{secs:02}")
labelCD.config(font='infra 50 bold',foreground='black',background='white')
labelCD.grid(row=0)
if n >= 0:
labelCD.after(1000, countdown, n-1)
else:
labelCD.destroy()
def clock():
t=time.strftime('%A''\n''%D''\n''%I:%M:%S',time.localtime())
if t!='':
labelTD.config(text=t,font='infra 50 bold',foreground='black',background='white')
Hr = time.strftime('%H')
Mn = time.strftime('%M')
Sc = time.strftime('%S')
if int(Hr)==8 and int(Mn)==41 and int(Sc)==0: ### you can trigger it at any time you want
countdown(3600)
labelTD.after(1000,clock)
labelCD = Label(frame)
labelCD.grid()
clock()
win.mainloop()
Could this be due to usage of after() or calling the function every second for a long time? If so, any alternatives to show timers? Thanks in advance :D
The after method doesn't guarantee any sort of precision. The only guarantee it makes is that the function will be called some time after the requested timeout. It could be at 1000 ms, it could be at 1001, 1010, or something else. It depends on what else is going on in the system.
If you need something more accurate, you can save the current time in ms in your function, and do some math from the previous time your function ran, and then use that delta to adjust your calculations.

Is there a method to count time without imports?

I've got a CASIO fx-CG50 with python running extended version of micropython 1.9.4
Decided to make a game but I really need a sleep function, I cannot use any imports as everything is pretty barebones. Any help would be greatly appreciated.
I've tried downloading utilities but they're just extra applications, nothing seems to really exist for the casio.
Cheers!
If you cannot import time (or utime) in your code, you could always implement a simple function that loops for a certain number of steps:
def wait(step):
for i in range(step):
pass
wait(999999)
In that case, the actual time spent in the function will depend on the computational power of your device.
I am trying to do the same exact things and I was trying to benchmark a wait function by animating a square accross the scree. Here is what I have come up width:
from casioplot import *
def wait(milli):
time = milli*50
for i in range(time):
pass
def drawSquare(x,y,l):
for i in range(l):
for j in range(l):
set_pixel(x+j,y+i,(0,0,0))
draw_string(0, 0, "start.", (0, 0, 0,), "small")
show_screen()
waitMillis = 1000
screenWidth = 384
screenHeight = 192
xPos = 0
yPos = 0
squareSide = 24
while xPos + squareSide < screenWidth:
clear_screen()
drawSquare(xPos,yPos,squareSide)
show_screen()
wait(waitMillis)
xPos = xPos + 5
draw_string(int(screenWidth/2), 0, "done", (0, 0, 0,), "small")
show_screen()
So when just using a manual stopwatch app, I noticed that the wait function has a 1 second turnaround time for every 50k iterations. So I set a ratio to the value in order
to have a comfortable millisecond parameter. Unfortunately, performance degrade drastically probably do to the drawing utilities and the constant clear and setting of pixels. The performance degradation is exponential and it is hard to capture and control. The square moves well up to half of the screen after which it moves very sluggishly so. I am not sure if we can fix this problem easily...

How to accurately sample in python

At work, I have a need: to do sampling every 0.08 seconds in 10 seconds.
I use while loop but it fails.
import time
start_t =time.time()
while time.time() -start_t <=10:
if float(time.time() -start_t) % float(0.08) == 0:
"""do sample record""
finally, I got no data at all, I think the if float(time.time() -start_t) % float(0.08) == 0: does not work.
I am confused how to set the condition to enter the sampling code.
The easiest way is to use time.sleep:
from time import sleep
for i in range(125):
"""do sample record"""
sleep(0.08)
You probably get no data because you collect the time only at discrete moments. In these moments, they will never be perfect multiples of 0.08.
Q : "How to accurately sample in python"
At work ( Chongqing ),I have a need: to do sampling every 0.08 seconds in 10 seconds.
Given the python is to be used, the such precise sampling will need a pair of signal.signal()-handlers on the unix-systems,
import signal
#------------------------------------------------------------------
# DEFINE HANDLER, responsible for a NON-BLOCKING data-acquisition
#------------------------------------------------------------------
def aSIG_HANDLER( aSigNUM, aPythonStackFRAME ):
... collect data ...
return
#------------------------------------------------------------------
# SET THE SIGNAL->HANDLER MAPPING
#------------------------------------------------------------------
signal.signal( signal.SIGALM, aSIG_HANDLER )
#------------------------------------------------------------------
# SET THE INTERVAL OF SIGNAL-ACTIVATIONS
#------------------------------------------------------------------
signal.setitimer( signal.ITIMER_REAL, seconds = 0, # NOW WAIT ZERO-SECONDS
interval = 0.08 # FIRE EACH 80 [ms]
)
#------------------------------------------------------------------
# ... more or less accurately wait for 10 seconds, doing NOP-s ...
#------------------------------------------------------------------
#----------------------------------------------------------------
# AFTER 10 [s] turn off the signal.ITIMER_REAL activated launcher
#----------------------------------------------------------------
signal.setitimer( signal.ITIMER_REAL, seconds = 0, # NOW WAIT ZERO-SECONDS
interval = 0.0 # STOP SENDING SIGALM-s
)
or,for a Windows-based systems,there is a chance to tweak ( and fine-tune up to a self-correcting, i.e. non-drifting ) Tkinter-based sampler as shown in this answer.
class App():
def __init__( self ):
self.root = tk.Tk()
self.label = tk.Label( text = "init" )
self.label.pack()
self.sampler_get_one() # inital call to set a scheduled sampler
self.root.lower() # hide the Tk-window from GUI-layout
self.root.mainloop()
def sampler_get_one( self ):
# \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
#
# DEMO to show real plasticity of the Tkinter scheduler timing(s)
#
# /\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/
... review a drift of the activation + adapt the actual delay for a next .after()
# SET .after() vv-----------# re-calculate this value to adapt/avoid drifting
self.root.after( 80, # re-instate a next scheduled call,
self.sampler_get_one
) # .after a given ( self-corrected ) delay in [ms]
#-------------------------------#-NOW--------------------------------------------
... acquire ... data ... # best in a non-blocking manner & leave ASAP
You use float number divide by float number, and time.time() will return a long decimal number so you get no data because your result always 0.00001234 or something like that. I think you should use round to get 2 decimal number
temp = time.time()-start_t
if round(temp,2) % 0.08 == 0:
"""do sample record"""
However, this script will return about 27000 result in 10 second. Because you will have 0.08, 0.081,0.082,etc and they all do your recording work.
So I think you should work with Maximilian Janisch solution (using sleep function) is better. I just want to explain why you reach no solution.
Hope this helpful!
EPILOGUE :
With all due respect, the proposed code is awfully dangerous & mis-leading.Just test how naive it gets : 8.00 % 0.08 yields 0.07999999999999984 that is by no means == 0,while the if-condition ought be served & sample taken, if it were not for the (known) trap in real-numbers IEEE-754 handling.So as to see the scope of the disaster, try :sum( [ round( i * 0.08, 2 ) % 0.08 == 0 for i in range( 126 ) ] )+ compare it with 125-samples the task was defined above to acquire.Get 8 samples instead of 125 # regular, 12.5 [Hz] samplingis nowhere near a solution! – user3666197 22 hours ago
#user3666197 wow, a very clear explanation, I think I should delete this answer to avoid misleading in the future. Thank you! – Toby 5 hours ago
Better do not remove the Answer, as it documents what shall never be done,which is of a specific value to the Community- best to mention the rationale, not to use this kind of approaches in any real-life system.The overall lesson is positive- all learned a next step towards better system designs. I wish you all the best, man! – user3666197 4 mins ago
this will probably never exactly be true as checking for equality with floats like this will need to be very precise.
try doing something like:
start_t =time.time()
looped_t = start_t
while time.time() - start_t <= 10:
if time.time() - looped_t >= 0.08:
looped_t = time.time()
"""do sample record""
The sleep answer from Maximillian is fine as well, except if your sampling takes a significant amount of time (several hundreds of a second) then you will not stay near the 10 second requirement.
It also depends on what you prioritize as this method will at most provide 124 samples instead of the exact 125 you would expect (and do get with the sleep function).

detecting keyboard overrun in tkinter event handler when autorepeating to avoid lag

In my app I want to allow the user to scroll through images by holding down an arrow key. Not surprisingly with larger images the pc can't keep up, and builds up a potentially large buffer that carries on being processed after the key is released.
None of this is unexpected, and my normal answer is just to check the timestamp in the event against the current time and discard any events that are more than (say) .2 seconds old. This way the backlog can never get too large.
But tkinter uses some random timebase of events so that comparing with time.time() is meaningless, and I can't find a function to get hold of tkinter's own clock. I'm sure its in there, it's just most of the pythonised tkinter documentation is a bit naff, and searching for time or clock isn't helping either.
def plotprev(self,p):
if time.time() - p.time > .2:
return
Sadly this test always returns true, where is tkinter's pseudo clock to be found?
Any other method will be complex in comparison.
well it's nor very nice, but it isn't too tedious and seems to work quite well: (with a little bit of monitoring as well)
def checklag(self,p):
if self.lasteventtime is None: #assume first event arrives with no significant delay
self.lasteventtime = p.time
self.lasteventrealtime = time.time()
self.lagok=0
self.lagfail=0
return True
ptdiff = (p.time-self.lasteventtime) / 1000
rtdiff = time.time() - self.lasteventrealtime
lag = rtdiff-ptdiff
if lag < .3:
self.lagok += 1
if self.lagok %20 == 0:
print("lagy? OK: %d, fail: %d" %(self.lagok, self.lagfail))
return True
else:
self.lagfail += 1
return False

Having a certain piece of code run for a specified amount of time

I'm working on a galactica type of game using pygame and livewires. However, in this game, instead of enemy's, there are balloons that you fire at. Every 25 mouse clicks, I have the balloons move down a row using the dy property set to a value of 1. If a balloon reaches the bottom, the game is over. However, I'm having some trouble figuring out how to get this to run only for, say, 1 second, or 2 seconds. Because I don't have a way to "time" the results, the dy value just indefinitely gets set to 1. Therefore, after the first 25 clicks, the row just keeps moving down. This is ok, but like I said, it's not my intended result.
Here is the code I have so far for this action:
if games.mouse.is_pressed(0):
new_missile = missile(self.left + 6, self.top)
games.screen.add(new_missile)
MISSILE_WAIT = 0 #25
CLICKS += 1
if CLICKS == 25:
SPEED = 1
CLICKS = 0
CLICKS, and MISSILE_WAIT are global variables that are created and set to an initial value of 0 before this block of code. What I'm trying to figure out is the algorithim to put underneath the if CLICKS statement. I've looked through the python documentation on the time module, but just can't seem to find anything that would suit this purpose. Also, I don't think using a while loop would work here, because the computer checks those results instantly, while I need an actual timer.
I'm not sure if I got your question but what I can suggest is that:
class Foo():
def __init__(self):
self.start_time = time.time()
self.time_delay = 25 # seconds
def my_balloon_func(self):
if(time.time() - self.start_time) > self.time_delay:
self.start_time = time.time()
else:
# do something

Categories

Resources