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
3
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)
13
14 def pause(self):
15 self.__paused = not self.__paused
16 if self.__paused:
17 self.__unpause.clear()
18 else:
19 self.__unpause.set()
20
21 def get_time(self):
22 return self.__time
23
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()
31
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
3
4 from gtkCountdown.countdownThread import countdownThread
5
6 class cdProgressBar:
7 def __init__(self, time = 10):
8 self.tot_time = time
9 self.cd_thread = countdownThread(self.__countdown_cb, time)
10
11 self.builder = gtk.Builder()
12 self.builder.add_from_file('progress_countdown.glade')
13 self.window = self.builder.get_object('cd_window')
14
15 self.progressbar = self.builder.get_object('cd_progressbar')
16
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 }
22
23 self.builder.connect_signals(handlers_dict)
24
25
26 def main(self):
27 self.window.show_all()
28 gdk.threads_init()
29 gtk.main()
30
31 def startbutton_clicked(self, widget, data=None):
32 self.cd_thread.restart()
33
34 def pausebutton_clicked(self, widget, data=None):
35 if self.cd_thread.isAlive():
36 self.cd_thread.pause()
37
38 def quit(self, widget, data=None):
39 gtk.main_quit()
40
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))
45
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

0 comments: