6.1.13

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":   
        break


video

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 

Solution


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:
video

Download


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:
boolbox 
buttonbox
ccbox
choicebox
enterbox
indexbox
integerbox
multchoicebox
ynbox
multenterbox

 - Not supporting callback:
diropenbox
fileopenbox
filesavebox
codebox
exceptionbox
multpasswordbox
msgbox
passwordbox
textbox (i'm considering adding callback to this one)

 Enjoy! 


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"

ynbox:
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


ccbox:
callback added


boolbox:
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


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


buttonbox:
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


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


multenterbox:
callback added


inner function "__multfillablebox":
callback added


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


__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


validate_integer:
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"


multchoicebox:
callback added


choicebox:
callback added


__choicebox:
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.

10 comments:

  1. This is great! Thanks. --Karl

    ReplyDelete
    Replies
    1. You are welcome... Enjoy!
      --Robbie

      Delete
  2. Thanks Robbie this is very useful for building GUIs that look decent without much effort. One question: I have a multenterbox where one field can be left blank; if left blank, the program will autogenerate a number for the user. I would like to update the onscreen field with this number without closing the window (remain inside the callback). How can this be achieved?

    ReplyDelete
    Replies
    1. Hi Andrew,
      What you're asking to do is actually populate the values in easygui.multenterbox after you sent them. This feature isn't supported.
      I would go for a simple solution like:
      ==============================================
      import easygui_callback
      import random

      def get_auto_value():
      ----# your logic here
      ----return random.random()
      ----
      choices = ["name", "address", "auto-field"]
      values= ['default-name','default-address','']
      input= ''

      while True:
      ----input = easygui_callback.multenterbox("auto field","generator", choices,values)
      ----if input == None: break
      ----if input[2] == "":
      --------values= [input[0],input[1],get_auto_value()]
      ----else:
      --------# save data somewhere or whatever
      --------pass
      ==============================================
      If you really have to work with callback, you can hack easygui_callback.multenterbox to send also 'values' list to callback and set it there. On return from callback, you'll need to set this value list into its matching widget.

      Delete
  3. So nice idea and thank you for coding :D too

    ReplyDelete
    Replies
    1. I'm glad you like it ^^
      You're welcome

      Delete
  4. Thanks for this fabulous modification. How does one install the two files in the rar? I already have easygui 0.98.

    ReplyDelete
    Replies
    1. I dropped the files into the lib folder and it worked. Thanks

      Delete
  5. Thanks a lot for this awesome package!

    ReplyDelete
  6. Your download link is broken.

    ReplyDelete