[ Calculator Image ]

Calculator


Using W within the IDE
Number Dialog / Calculator
Exercises

In this section we'll write a program which builds a calculator as a window within the IDE. In doing so we'll explore some useful tricks relating to binding events, and automated widget construction.

We start by opening a new window in the IDE. We'll enter the program in there, and will run it using the IDE's run all command.

First we have to load W together with some other useful modules. Since we'll be intercepting keystrokes, Wkeys gives us some useful constants. We'll use string to save a little typing. And, well, we want all of the math module's functions available because we're building a calculator.

import W
import Wkeys

import string
from math import *

Now we build some of our widgets: the main window and the text field which shows the expression being worked on, and the results.

w = W.Window((148,246), "Calculator")

w.display = W.EditText((4,4,-4,22), "0", fontsettings=("geneva",0,12,(0,0,0)))

We now set up a utility routine which restricts the keystrokes we allow the user to type. The calculator would be more powerful if we allowed more keystrokes, since the user could type anything in. However by restricting the input, we make the calculator safer and more user friendly - it's still possible to mess up, but it's much harder. Also, the ability to restrict user input in a textfield like this can be a very handy trick to know.

What's going on here is that bindings to events in angle brackets, like '<key>' have the option of returning something. If the result they return is true, then W stops processing the event. We can use this to stop the keystrokes from ever reaching the text widget. (XXX in the window do_char handler, if it's bound to the window it will block key events, but if it's bound to the current widget it doesn't block individual key events, only the key() method... why? To allow menu equivalents to propogate, perhaps?)

clearkey = '\033'
okkeys = ("0123456789c=+-*/() " + Wkeys.backspacekey + Wkeys.deletekey 
        + Wkeys.returnkey + Wkeys.enterkey
        + string.join(Wkeys.arrowkeys) + clearkey)

def blockkey(char, event):
    if char not in okkeys:
        return 1
    else:
        return 0

w.bind('<key>', blockkey)

Before creating the remaining widgets, we need a few event handlers. do_clear clears the display. do_eval is the heart of the calculator - it calls eval on the contents of the display, and does some simple error handling (anything major gets passed on to the IDE).

def do_clear():
    w.display.set("0")
    w.display.selectall()

def do_eval():
    expr = w.display.get()
    try:
        out = str(eval(expr))
        w.display.set(out)
        w.display.setselection(len(out),len(out))
    except ZeroDivisionError, what:
        w.display.set("Division by zero")
        w.display.selectall()
    except SyntaxError, what:
        w.display.set("Invalid expression")
        w.display.selectall()
    except OverflowError, what:
        w.display.set("Number too large")
        w.display.selectall()

Now things get a bit tricky. We could create each button and its callback individually, but this would quickly get tedious. It'd also be a problem if you wanted to change the layout of the calculator (say, to add more buttons), because then you'd have to go back and change the location of each button by hand. So instead we're going to be clever. Almost all the buttons do much the same thing: when you click on it, it inserts some text into the display widget. We could write an individual function for each one of these steps, but a much neater trick is to use a lambda to create an anonymous callback, like so:

lambda x=char: w.display.insert(x)
This is a very standard Python idiom, and using a lambda to create a callback in such a way is a common trick in Python GUI programming (technically, this is a form of closure). Another way that we could have gone would be to write a class with a __call__ method, something like this:
class MyCallback:
    def __init__(self, char):
        self.char = char
    def __call__(self):
        w.display.insert(char)
Then we could create our callback by instantiating:
MyCallback(char)
This is another very standard Python idiom. It's more flexible than the first, and it's much truer to "The Python Way" but it's also heavier - we'd be going around creating callable class instances when what we want is a fairly simple function. In our situation, it's probably best to use the first trick.

A third way to do it would be to subclass W.Button.

So now we know how to automatically create a callback that does what we want. Automatically creating a button isn't hard, we just call W.Button with the appropriate variables. So if we have a list of the characters we want to insert, and the names of the widgets we are creating, like this (actually it's a list of rows, so we can get a nice rectangular array):

buttons = [[("(", "leftpar"), (")", "rightpar"), ("pi", "pi"), ("**", "pow")],
        [("sin", "sin"), ("cos", "cos"), ("tan", "tan"), ("/", "divide")],
        [("7", "seven"), ("8", "eight"), ("9", "nine"), ("*", "times")],
        [("4", "four"), ("5", "five"), ("6", "six"), ("-", "minus")],
        [("1", "one"), ("2", "two"), ("3", "three"), ("+", "plus")],
        [("0", "zero"), (".", "point"), ("C", "clear"), ("=", "eval")]]

Then all we need to do is iterate across the rows and create our widgets. We'll also bind the appropriate keystrokes to the new button's push() method.

for i in range(len(buttons)):
    for j in range(len(buttons[i])):
        char, symbol = buttons[i][j]
        if char != "C" and char != "=":
            w[symbol] = W.Button((4+36*j,30+36*i,32,32), char,
                    lambda x=char: w.display.insert(x))
            w.bind(char, w[symbol].push)

Like so. Notice that we're automatically generating the button's location, the title and the callback. All that's left are the two buttons which don't just insert a character: clear and eval. We've already created their callbacks, so we just create these by hand:

        elif char is "C":
            w.clear = W.Button((4+36*j,30+36*i,32,32), "C", do_clear)
            w.bind('c', w.clear.push)
            w.bind('shiftC', w.clear.push)
            w.bind(clearkey, w.clear.push)
        elif char is "=":
            w.eval = W.Button((4+36*j,30+36*i,32,32), "=", do_eval)
            w.bind('=', w.eval.push)
            w.bind(Wkeys.returnkey, w.eval.push)
            w.bind(Wkeys.enterkey, w.eval.push)

And, finally, we open up the window.

w.open()

You can get the calculator to display and run by running the contents of the IDE window. Running it multiple times will create multiple independent calculator windows. The complete source code is available to save you some typing.

The calculator itself is not really practical, although it has potential. To be really useful, we'd need to add something more than what you can get from the interactive Python window - perhaps a grapher widget could be written and added, or some algebraic manipulations introduced. To be serious, we'd also need to add the remaining functions out of the math module, and possibly those from cmath as well.

Exercises

  1. Add in some new buttons to allow more scientific math, like exp, log or log10.
  2. Error-handling is not ideal, since it overwrites the text which caused the error. Make the error alerts put up dialog boxes, and perhaps even move the cursor to where the error occurred.
  3. At the moment, if both numbers in a division are integers, integer division is used, just like Python. Also, big numbers are not automatically converted to long ints. Modify do_eval to make the calculator behave more naturally. The easiest way to do this would be to preprocess the string to add extra components like appending "L" after long enough whole numbers. A more sophisticated approach would be to use modules in the standard library to get into the parse tree and modify that before evaluating.