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.
exp
, log
or log10
.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.