Timer with UI
I want to create something like a timer with the UI. For testing there should be a button which triggers a label to display a count down etc. This is my code. Unfortunately, it only displays the last value (0) of the count down. Why is it like this? Is there anything I can do to directly display the other values (here 3, 2, 1) as well?
import ui, time
def __init__(self): self.view_main = ui.load_view() self.lbl = self.view_main["lbl_time"] self.view_main.present("sheet") self.time = 4 def did_load(self): self["btn_start"].action = self.change_label_time def change_label_time(self, sender): while self.time > 0: self.time -= 1 time.sleep(1) self.view_main["lbl_time"].text = str(self.time)
time.sleepisn't really compatible with
ui. Instead, use
# coding: utf-8 import ui, time class Main(ui.View): def __init__(self): self.view_main = ui.load_view() self.lbl = self.view_main["lbl_time"] self.view_main.present("sheet") self.time = 4 self.view_main["btn_start"].action = self.change_label_time def change_label_time(self, sender): ui.delay(self.decrement, 1) def decrement(self): if self.time > 0: self.time -= 1 self.lbl.text = str(self.time) ui.delay(self.decrement, 1) Main()
You're also abusing a custom view class. There's no reason to use a custom view class here, it's effectively being used as a function (
__init__called). It works just as well to do
import ui, time view_main = ui.load_view() lbl = view_main["lbl_time"] view_main.present("sheet") time = 4 #the action is this because it takes a function, what this does is calls it with a delay. view_main["btn_start"].action = lambda sender: ui.delay(decrement, 1) #This is still here because ui.delay takes a function. def decrement(): global time if time > 0: time -= 1 lbl.text = str(time) ui.delay(decrement, 1)
There were other problems with your code, I don't know why did_load was called in your code or how it worked, the contents of did_load contained an error, it should've been
self.view_main["btn_start"].action = self.change_label_time
self["btn_start"].action = self.change_label_time
I don't know how your button action got set since did_load contained an error and was never called anyway. It'd've be great if you'd included the pyui file on GitHub.
His did_load looks okay to me given he is using load_view.
time.sleep can work, but it needs to be with code that has ui.in_background wrapped around it. Basically, for things on the ui thread, you will not see the ui update until the function exits. Ui.delay, or a Timer or Thread are recommended though, rather than in_background for something like this, if you care at all about precision. in_backgrounded coded gets run in a single queue, which can lead to surprising results if you think it behaves like an asynchronous thread.
Thanks a lot for the quick answers.
a) Is there a way to export the pyui file somehow and then to upload it to GitHub? Is it maybe done "manually" with the ftp server Ole posted some time ago? Or with any of the shells created for pythonista?
b) do you know why there are compatibility issues between time and ui? I am quite curious about such things...
Right now I am not familiar with asynchronous programming. But I think the take away is to skip the decorator in my case.
In console, try
print open('timer.pyui').read()and you can paste that in the forum, at the very least.
There is no incompatibility with timr and ui.
# coding: utf-8 import ui, time v=ui.View() b1=ui.Button(title='backgrounded',frame=(10,50,100,50)) b2=ui.Button(title='not backgrounded',frame=(10,150,100,50)) @ui.in_background def b1action(sender): for i in xrange(6): sender.title=str(i) time.sleep(0.5) def b2action(sender): # the ui does not update until this method exits! for i in xrange(6): sender.title=str(i) time.sleep(0.5) b1.action=b1action b2.action=b2action v.add_subview(b1) v.add_subview(b2) v.present()
the key is understanding that the ui cannot update until your callback exits. So, in the case if the non backgrounded case, if it never exits the ui will appear to hang up. In my example above, it eventually exits, and prints the final number. Use of the
in_backgrounddecorator allows the ui to update while the function runs in a background queue. However, note what happens if you use b1action for both buttons, then press both close together.... the first button has to finish before the next one starts, because ui.in_background shares the same queue. That problem can be solved using a Thread, or something like this decorator:
def run_async(func): from threading import Thread from functools import wraps @wraps(func) def async_func(*args, **kwargs): func_hl = Thread(target = func, args = args, kwargs = kwargs) func_hl.start() return func_hl return async_func
Tanks. There is only one process at a time on each thread. In this case it is the process triggered by either of the buttons. pressing b2 halts the process of b1. Or in other words uses/ reserves the threat. But not vice versa. Pressing b1 still allows for b2 which then again blocks the thread. Eventually, b2 decorated with your run_async allows for parallel use. Funnily, if I put a @ui.in_background infront of b2action b1 behaves as b2.
I have adjusted the code a little bit. The button now calls run_countdown. Now the delay function does not work any more. Why is it like this? How can I make the sport time work?
def run_countdown(self): while self.round <= self.rounds: self.time = self.seconds_active self.countdown() self.time = self.seconds_break self.countdown() self.round += 1 def countdown(self): if self.time > 0: self.time -= 1 view_main["lbl_time"].text = str(self.time) ui.delay(self.countdown, 1)
The ui will not update until the callback function exits. Look back at your code above, and think through when your callback function exits.....
Decorating your first fcn with ui.in_background would fix that problem, but you would also spawn a very large number of countdown() functions (since countdown returns instantly), not whatnyou want, i think.
I would recommend something like this:
- your button action (not backgrounded) calls a start_game function
- start_game would sets the self.time, then calls (via ui.delay) the countdown () function
- countdown does mostly what it does now, except you would add an else: which calls game_over. That method can log the final results, decide if there are remaining rounds, and then call start_game again.
4). It is a good idea within any self-calling function or thread to check for the view being on_screen, and then exiting if not. That way, when you close the view, the timer will gracefully exit. You also have to solve the issue of someone pressing the button twice, either start_game exits if a game is already running, or you would set a cancel flag and/or call ui.cancel_all_delays.
Thank you Jon. I thought the problem might be the same kind as before. So I will have to think through properly. Hope there are no further questions coming up.
I have solved it with the following code. I'd be happy to share the whole script if anyone is interested.
def run_countdown(self): self.setup_view_main() self.countdown() def countdown(self): if self.time > 0: if self.time <= 3: speech.say(str(self.time), "en-US", 0) view_main["lbl_time"].text = str(self.time) self.time -= 1 ui.delay(self.countdown, 1) elif self.round < self.data["rounds"]: self.switch_states() self.run_countdown()
- list item