Tuesday, 18 January 2011

Simple countdown progress bar remake

I've been warned on reddit my countdown progress bar implementation explained here is messy and wouldn't work on Windows OS because changing widgets is valid only from the GUI thread and I must admit, a special thread object for the purpose of updating the widget really looks messy, but I really didn't test it to see if it works on Windows. Today I reworked the code anyway, merging all the previous stuff in one object that builds GUI and acts as a timer at the same time. Also I made some minor improvements. Simple countdown timer is now even more simpler:

 1 import time, gtk 
2 from threading import Thread, Event
4 gtk.gdk.threads_init() # initialize threads right away
6 class cdProgressBar(Thread):
7 def __init__(self, time = 10):
8 Thread.__init__(self)
9 self.time = self.tot_time = time
11 self.builder = gtk.Builder()
12 self.builder.add_from_file('progressbar_countdown.glade')
13 self.window = self.builder.get_object('cd_window')
14 self.progressbar = self.builder.get_object('cd_progressbar')
15 self.set_progressbar()
17 self.builder.connect_signals({
18 'on_cd_window_delete_event' : self.quit,
19 'on_cd_startbutton_clicked' : self.startbutton_clicked,
20 'on_cd_pausebutton_clicked' : self.pausebutton_clicked
21 })
23 # threading
24 self.unpause = Event()
25 self.restart = False
26 self.setDaemon(True) # stop the thread on exit
28 def main(self):
29 self.window.show_all()
30 self.start() # start the thread
31 gtk.main()
33 def quit(self, widget, data = None):
34 gtk.main_quit()
36 def startbutton_clicked(self, widget, data = None):
37 if not self.unpause.isSet():
38 self.unpause.set()
39 self.restart = True
41 def pausebutton_clicked(self, widget, data = None):
42 if self.unpause.isSet():
43 self.unpause.clear() # pause the countdown timer
44 else:
45 self.unpause.set()
47 def set_progressbar(self):
48 self.progressbar.set_text(str(self.time))
49 self.progressbar.set_fraction(self.time/float(self.tot_time))
51 def run(self):
52 while True:
53 self.unpause.wait() # wait the self.unpause.isSet()
54 if self.restart:
55 self.time = self.tot_time
56 self.restart = False
57 self.set_progressbar()
58 time.sleep(1)
59 if self.time != 0:
60 self.time -= 1
62 cd_window = cdProgressBar()
63 cd_window.main()

Countdown Progress Bar is designed using glade interface designer like this. Code is tested and it's working under Windows and under Linux as well.

Saturday, 15 January 2011

Simple countdown progress bar using pygtk

Here I will explain how to build a progress bar that behaves like a simple countdown timer. This kind of progress bar at the start will be 100% filled and will empty itself every second until it's empty and reaches zero. Buttons to start and pause the countdown will be implemented as well. The picture below demonstrates the final product:

For this to work, a background countdown timer thread must be implemented. Its also important to implement pause and restart function for the timer. Here is the full code for the countdownThread object (countdownThread.py):

 1 from threading import Thread, Event 
2 import time
4 class countdownThread(Thread):
5 def __init__(self, callback_func, time = 10):
6 Thread.__init__(self)
7 self.__time = self.__fulltime = time
8 self.__unpause = Event()
9 self.__paused = False
10 self.__restart = False
11 self.__callback_func = callback_func
12 self.setDaemon(True)
14 def pause(self):
15 self.__paused = not self.__paused
16 if self.__paused:
17 self.__unpause.clear()
18 else:
19 self.__unpause.set()
21 def get_time(self):
22 return self.__time
24 def restart(self):
25 if not self.isAlive():
26 self.start()
27 else:
28 self.__restart = True
29 if self.__paused:
30 self.pause()
32 def run(self):
33 self.__unpause.set()
34 while True:
35 if self.__time < 0:
36 self.__time = 0
37 self.__unpause.wait()
38 if self.__restart:
39 self.__time = self.__fulltime
40 self.__restart = False
41 self.__callback_func()
42 time.sleep(1)
43 self.__time -= 1

Countdown timer object is inherited from Thread object, and more functionality is added. Its constructor accepts 2 arguments: callback_func (a callable object) and time (integer). They are used to initialize timer's member variables. The self.__time is total time to start the countdown from, and it should decrement every second by 1; variable self.__fulltime is used to restart the counter.
Furthermore an Event object self.__unpause is used internally for pausing/unpausing the counter. Callback function passed to constructor is function that should execute every one second till time hits zero. In this case it's used to update the progress bar in GUI.
Timer is set to daemon thread using self.setDaemon(True) which means the main thread doesn't need to wait for the timer to quit.
Pause function on line 14 changes the self.__paused, and sets/clears self.__unpause Event object. When this object calls its set() function, it sets its internal flag, and any thread waiting for this event to happen continues its work until the clear() function 'unsets' the internal flag again. In this case. CountdownTimer repeatedly checks the self.__unpause event by calling self.__unpause.wait(). If internal flag is set timer continues the loop, and if it's not it waits for the Event to 'unset' the flag so it can continue the work.
Restart function at line 24 is used to start the thread or restart the counter if the thread is already running. Run function is the timer itself, at the beginning it calls self.__unpause.set(), meaning timer is unpaused at the start. While loop loops till the program exits, performs a couple of checks: sets time to full again if self.__restart is true and sets time to 0 if it tries to go negative. Finally calls the callback function, sleeps a second and decrements time by one.

Next, it is time to build a GUI. I did it using Glade Interface Designer program. Just try to build something similar to the pic above using GtkButton, GtkProgressBar. GtkHBox, and GtkVBox. Important thing is to set the handler names under 'Signals' properties of each button so they can be connected to appropriate callback functions. It should look like something like this.
And here is the code for the gui (gui.py):

 1 import gtk
2 from gtk import gdk
4 from gtkCountdown.countdownThread import countdownThread
6 class cdProgressBar:
7 def __init__(self, time = 10):
8 self.tot_time = time
9 self.cd_thread = countdownThread(self.__countdown_cb, time)
11 self.builder = gtk.Builder()
12 self.builder.add_from_file('progress_countdown.glade')
13 self.window = self.builder.get_object('cd_window')
15 self.progressbar = self.builder.get_object('cd_progressbar')
17 handlers_dict = {
18 'on_cd_window_delete_event' : self.quit,
19 'on_cd_startbutton_clicked' : self.startbutton_clicked,
20 'on_cd_pausebutton_clicked' : self.pausebutton_clicked,
21 }
23 self.builder.connect_signals(handlers_dict)
26 def main(self):
27 self.window.show_all()
28 gdk.threads_init()
29 gtk.main()
31 def startbutton_clicked(self, widget, data=None):
32 self.cd_thread.restart()
34 def pausebutton_clicked(self, widget, data=None):
35 if self.cd_thread.isAlive():
36 self.cd_thread.pause()
38 def quit(self, widget, data=None):
39 gtk.main_quit()
41 def __countdown_cb(self):
42 curr_time = self.cd_thread.get_time()
43 self.progressbar.set_text(str(curr_time))
44 self.progressbar.set_fraction(curr_time/float(self.tot_time))
46 cd_window = cdProgressBar(time = 30)
47 cd_window.main()

Notice I put those files in the same python package called gtkCountdown.
GUI construction needs only one parameter and it's time, which will be used along with self.__countdown_cb callable object to construct a countdown timer thread self.cd_thread. GUI is build and appropriate signals are connected in lines from 11 to 23. Function main() is used to start the gtk main loop. Important thing is to initialize the threads with gdk.threads_init() before gtk.main() is called.
Clicking on the 'start' button, restart() function of the countdown time thread is called, and by clicking on 'pause' button timer is paused. Function __countdown_cb as said before, is used by the timer and executes every second. It gets current time from self.cd_thread, prints its value onto progress bar and updates it. GUI is initialized and ran in last two lines of code.

Notice that this code shouldn't be used as reference. Changing widget from thread outside the GUI thread is probably bad idea.

See the improved countdown progress bar code here