A simple GUI stopper

Hello again
In this short post I will present a simple and useful stopper made with Tk and Python.


A friend of mine called me the other day and asked for help. He works in a Call-Center where he and his colleagues answer phone-calls from customers of their company.
He wanted to know exactly how long was every phone call. He wanted a stopper application.

In this post I'll lead you through the requirements we decided on and the solution I gave him.
Hope you enjoy!


Well, although my friend needed to measure repetitive measurements, means he didn't want to summarize the time of more than one call each time, I've decided to enable this option with a button called "Continue". When this button is pressed, the stopper doesn't reset after every measurement.
In addition, we would like to give the user time to see the measured time before s/he resets the stopper and after reset we would like to hold until the next count.
Hence, we have 3 states the stopper can be at:
  1. Stop, when launching the application and after every reset. We shall see "000:00:00.00".
  2. Run, after start command, the stopper is counting up.
  3. Freeze, when we pause the counting, the digits are shown but they don't "move".

Having these three states in mind, I came up with the following User-Interface design:
  • The stopper will be activated conveniently with key-presses, either ENTER or SPACE
  • One press will start the measuring (from Stop to Run), the second will freeze it (Run to Freeze) and the third one will reset to zero or continue counting (Freeze to Stop or Freeze to Run), this is for the user to decide.
  • There should be an option to stick the stopper above all other windows so it won't get lost during work.
  • Pressing 'r' will reset the stopper regardless at what state it's in. This is especially important when working in "Continue" mode when Space/Enter key-presses move the stopper from Run to Freeze and back to Run without clearing the previous measurement.


I chose Python and Tk for the implementation since my "mother tongue" is Python and I'm relatively familiar with Tk after improving the code of the free and open-source GUI package for Python, easygui - A link to this other work

Here is a picture of the stopper.
As you can see, it has the format HHH:MM:SS.hh 
In addition, the button "Stick" on the right side determines whether the stopper stays above all other windows or not.
The button "Continue" on the left side determines whether the stopper resets with every freeze or continue counting from the same point.
Pressing 'r' at any point, resets and initializes the stopper.


- Download Link -
There are 2 files there:
  • stopper.pyw : the script file, run it to get the stopper (you should have Python installed).
  • stopper_icon.ico : the clock icon of the stopper's left-top corner. You can of course use your own icon by overriding this file with your own icon.


Here is the source code of stopper.pyw

# This is a very simple GUI stopper
# Run the file
# Press either ENTER or SPACE to start the count
# Press again to freeze
# Now when you press again it depends in which mode you're working in
# The stopper has two modes, reset on every stop or continue counting
# This is depended whether the "Continue" button is pressed on not
# To reset when you're working in Continue mode, press 'r' (you can use in both modes)
# To quit, just press ESC or click at the X button at the upper-right corner
# Enjoy!
#                )
#                  (
#      _ ___________ )
#     [_[___________#

from Tkinter import *
import time, sys, os

class Stopper():
 def __init__(self, tkroot):
  self.STOP = 0
  self.RUN = 1
  self.FREEZE = 2
  self.state = 0
  self.startTime = None
  self.freezeTime = None
  self.sticked = False
  self.continueGrow = False
  # create the continueGrow_widget widget
  self.continueGrow_widget = Label(tkroot)
  self.continueGrow_widget.bind('', self.continue_grow)
  labelfont = ('Comic Sans MS', 10, 'italic')
  self.continueGrow_widget.config(bg='CadetBlue', relief=RAISED, width=7)
  self.continueGrow_widget.config(text = 'Continue', font=labelfont)
  self.continueGrow_widget.pack(expand=YES, fill=BOTH, side=LEFT)
  # create the digits_widget widget
  self.digits_widget = Label(tkroot)
  self.digits_widget.bind('',  self.change_state)
  self.digits_widget.bind('',  sys.exit)
  self.digits_widget.bind('',  self.reset_count)
  labelfont = ('Comic Sans MS', 25, 'bold')
  self.digits_widget.config(bg='goldenrod', font=labelfont)
  self.digits_widget.config(text = '000:00:00.00')
  self.digits_widget.pack(expand=YES, fill=BOTH,side=LEFT)
  # create the sticky_widget widget
  self.sticky_widget = Label(tkroot)
  self.sticky_widget.bind('', self.float_above_everything)
  labelfont = ('Comic Sans MS', 10, 'italic')
  self.sticky_widget.config(bg='IndianRed', relief=RAISED, width=7)
  self.sticky_widget.config(text = 'Stick', font=labelfont)
  self.sticky_widget.pack(expand=YES, fill=BOTH, side=RIGHT)
 def continue_grow(self, event):
  if self.continueGrow == False:
   self.continueGrow = True
   self.continueGrow_widget.configure(relief=SUNKEN, bg='CornflowerBlue')
   self.continueGrow = False
   self.continueGrow_widget.configure(relief=RAISED, bg='CadetBlue')
 def float_above_everything(self, event):
  ''' toggle sticky_widget window state(keep GUI above all other windows)
   currently supports only Windows! '''
  if self.sticked==False:
   self.sticked = True 
   self.sticky_widget.configure(relief=SUNKEN, bg='HotPink3', text='Stuck')
   self.sticked = False
   self.sticky_widget.configure(relief=RAISED, bg='IndianRed', text='Stick  ')
  # toggle sticky_widget state
  if 'win' in sys.platform:
   tkroot.wm_attributes("-topmost", 1 if self.sticked else 0) 
  elif 'inux' in sys.platform:
   #to do
 def add_zerros(self, s, min=2):
  '''add zerro chars before a string
   e.i. 7 to  07 when min=2
     7 to 007 when min=3 '''
  if len(s) < min:
   s = '0' * (min - len(s)) + s
  return s

 def clock_like_time(self, startTime):
  "convert time in seconds to a stirng like HH:MM:SS.hh "
  t = time.time() - startTime
  #capture the first two digits_widget after the dot for the hundreths
  str_hunds = str(t - int(t)).replace("0.",'')
  if len(str_hunds) > 2: 
   str_hunds = str_hunds[:2]
  elif len(str_hunds) < 2: 
   #add '0' before the digit to keep the form of 0X instead of just X
   str_hunds = self.add_zerros(str_hunds)
  # we skip the case of len(str_hunds)==2 because this is what we want
  str_mins_and_secs = time.strftime('%M:%S', time.gmtime(t))   
  # we can't use %H for hours in the above row 
  # b/c for 25 hours we'll get 1 hour and one day
  str_hours = self.add_zerros(str(int(t)/3600), min=3)
  return "%s:%s.%s" % (str_hours, str_mins_and_secs, str_hunds)

 def reset_count(self, event=None):
  self.state = self.STOP
  self.digits_widget.configure(text = '000:00:00.00')
 def change_state(self, event):
  if self.state == self.STOP:
   self.state = self.RUN
   self.startTime = time.time()
  elif self.state == self.RUN:
   self.state = self.FREEZE
   self.freezeTime = time.time() - self.startTime
  elif self.state == self.FREEZE:
   if self.continueGrow:
    # increasing startTime by the time of freezing
    self.startTime = time.time() - self.freezeTime
    self.state = self.RUN

 def iter(self):
  if self.state == self.RUN:
   # self.startTime=self.startTime-90000 # uncomment for debug a long time test
   self.digits_widget.config(text = self.clock_like_time(self.startTime))
  # else pass, when self.state is FREEZE or STOP
  self.digits_widget.after(100, self.iter)   

#====================== MAIN ======================#
if __name__=="__main__":
 tkroot = Tk()
 # here we load the icon for the GUI window
 # you can use mine or create your own icon file
 if os.path.exists('stopper_icon.ico'): 
 stopper = Stopper(tkroot)


Improving easygui for python

In this post I'll present an improvement to the wonderful GUI module for python, easygui.

Introducing easygui

On my first days as a programmer, I needed to create a GUI application for an automation project I was involved in.
I searched the web for help and came up with easygui.

Easygui gave me what I needed, the ability to invoke GUI without knowing anything at all about GUI creation, frames or Tk.
With easygui you just call a simple synchronous function and wait for the answer.
Here is an example

import easygui

choice = easygui.buttonbox("body","title",["Yes","No"])
if choice == "Yes":

This is it! and here is what you get:

easygui project homepage: open in a new tab

The problem

Now I would like to point out a very annoying behavior of easygui, it invokes a new window for every single user input.
If you ever needed to get sequential inputs from a user with it, you have probably noticed this problem by yourself.
I'll explain the problem with an example. Let's say we have an external device we want to control, a robot. We want to be able to move it in all directions and to switch it on and off.
You're probably thinking of something like this:
import easygui

choices = ["on", "off", "forward", "backward", "right", "left"] 
input= '' 
while input != "None": #happens when the user presses ESC  
    input = easygui.buttonbox("controller","robot", choices)
    if input == "forward":   
    elif input == "backward":
    elif input == "off":   


Problems :
  1. User can't move the GUI around the screen because after every input, the window closes and a new one appears instead but in the original place.
  2. Flickering happens after every choice, till the next window opens.
  3. Every different window gets a new task-number or pid. This makes it harder to follow the window instances 


Creating a callback mechanism in which we provide an easygui-function with a callback-function and the callback-function will be called for every user input.
The window should stay the same, in the same position with no flickering and with the same pid/task-number.

An example of how the new robot controller should look:
import easygui_callback

def controller(user_input):
 if user_input == "forward":
 elif user_input == "backward":
 elif user_input == "off":
  return "terminate" #this terminates the callback loop
choices = ["on", "off", "forward", "backward", "right", "left"]
easygui_callback.buttonbox("controller","robot", choices, callback=controller)

My work

The goal was to introduce the new ability without breaking the old API so everybody can continue enjoying their old code in the same way they have used it before, without worrying at all about the callback option.
Only programmers who are interested in such capability, will use it.

I've modified the code in order to enable the solution above.
This works wonderfully and it's totally backward-compatible.
Now the GUI can live forever and the logic applied to the options is being taken care separately.
Again, you can use both ways. You can put your logic in a separate function like in the example above but you can also use easygui-function (buttonbox for instance) the same way you used to do before. It supports both ways.

All three problems mentioned above are now solved, you can move the window around the screen and it won't flicker between user-inputs. The pid/task-number stays the same.

The result:


You can download the modified version here (please drop a short nice comment if you do :)
The modified version contains some bug fixes and it is based on easygui version 0.96
All modifications have been sent to the original developer and are being reviewed by him now in order to merge them with the next easygui formal version.

Note! Not all the functions support callback!
For some of them it makes no sense to support it
 - Functions supporting callback:

 - Not supporting callback:
textbox (i'm considering adding callback to this one)


Diff explanation

Summary of changes:
I recommend on viewing both files together, the original easygui and the modified version easygui_callback, with a diff program like WinMerge/Araxis

I've created a new generic function called "_buttonbox", it is based on the old function buttonbox
many other functions, including buttonbox (the original one) call it
most of the logic lies there now (in _buttonbox)

I'm gonna treat the current published version of easygui as the "old easygui" and my proposal "new easygui"

callback added
Because ynbox is by definition a choice between "yes" and "no", it makes more sense to not let the user specify
his own choices. If he wants to do so, he better uses the choicebox/boolbox and specify his choices there.
Added feature: you can now ALT+F4 or ESC instead of choosing between "yes" and "no", in this case None will be returned

callback added

callback added
Typo fixed "The returned value is calculated this way::" -> "The returned value is calculated this way:"
Bug fixed: in current easygui there is no validation of "choices" input parameter.
This is a serious bug because

  1. you can send more than 2 options to boolbox -> choosing the third or higher choice will always return 0.
  2. you can send only one parameter or empty tuple -> you get an unclear inner python trace

Added feature: you can now ALT+F4 or click on X instead of choosing between "yes" and "no", in this case None will be returned

callback added
The logic has been moved inside _buttonbox and we signify it by adding the parameter "index_only"

callback added
All old logic and Tk work has been moved to the new function _buttonbox
now it simply calls _buttonbox like other simple functions
Bug fixed: in old easygui there is no way to kill the GUI otherwise than choosing one of the choices
in the new one, you can kill it with 'X' button (need also to bind it to ESC)

_buttonbox: (the new function's logic comparatively to the old buttonbox)

  1. Default value, the initialization, is now set to None instead of the first choice. In the old easygui, you blocked the option of "X" button (windows manager close) so there is no need for default value anyway because the only way to make the GUI disappear is by choosing one of the choices. In the new one, this is the only way to close the GUI if we use callback. Anyway, this makes more sense for my opinion, it lets the GUI-user to say "i don't understand"or "i don't want either of the choices". The programmer should think about this option as well and we can't treat the "X" button the same as it was the first choice. This comment also explains the removal of the line "boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose )"
  2. We split all cases to three, indexbox, boolbox and all other cases. We treat them separately, in each case we check if callback is needed. When it is, we call a "while" loop which ends in only two conditions. One is when the user closes the window with Windows-Manager's X button or ALT+F4 or ESC. The second is when the callback function returns "terminate". Each case(indexbox,boolbox and other) modifies the reply as needed (index for indexbox, 1 or 0 for boolbox or just raw reply for other cases)
  3. We destroy "root" only if the user chose a choice and the callback returned "terminate". Otherwise means the user closed the window with Windows-Manager's X button or ALT+F4 or ESC and therefore "root" is already destroyed

callback added
lowerbound and upperbound logic has been moved inside __fillablebox
we signify it to enterbox with an input parameter "integer_only=True"

callback added

inner function "__multfillablebox":
callback added

callback added
The stripping part is being taken care inside "__fillablebox"

Input parameter callback has been added
Input parameter strip has been added for enterbox
Input parameter integer_only has been added for integerbox
Input parameters upperbound and lowerbound have been added for integerbox option

We treat 2 cases in __fillablebox:
1) integerbox
    We have 3 options now
1) User canceled the GUI with "X", ALT+F4 or ESC
- In this case, we just terminate the GUI and return None
2) User chose too high or too low integer value or non-integer value
- In this case, we modify the entry with the appropriate error and reinvoke the GUI
3) User chose an appropriate value, integer between the specified boundries
- In this case we can return this int-value and terminate the GUI or, if callback is needed, call callback

2) enterbox
We accept everything and we strip it if specified

a new function, it gets a value, entryWidget, upper and lower bounds for the integer
It checks whether the value is an integer and between the boundries if given
If one of the 2 conditions aren't met (not an integer, out of boundries), it sets an error
message inside the entryWidget and returns None. otherwise it returns a string saying "ok"

callback added

callback added

callback added
Bug fixed: in old easygui we focus and select the first choice. I've disabled it in the new one.
This causes a problem when using multchoicebox. In multchoicebox we click on every choice we want to choose and when we click on the "ok" button, all choices are sent back together as a list.
Well, when we defaultly choose the first choice for the user, s/he might not notice it.
I'll explain:
if we have 5 options, we invoke the GUI with 1st choice already marked, the user chooses the 4th and the 5th.
We send back [1,4,5]. To avoid they need to click on the first choice to unmark it or click on "clear all" button.
Same would happen when user chooses nothing.
So we make users click on something they for sure don't want just to avoid it.