Llaisdy

A simple calculator in Tkinter and wxPython

Author: Ivan A. Uemlianin
Contact: ivan@llaisdy.com

Overview

This article describes my experiences writing a very simple calculator GUI - the 'Hello World!' of GUI development - in two of the main GUI toolkits for Python: Tkinter and WxPython. Even with such a simple applcation I found it possible to evaluate the toolkits relative to each other.

In order to try and follow 'best practice' (and to avoid evaluating my own programming ability ;), I sourced initial implementations externally: my Tkinter-based calculator is based on the two calculator GUIs given in Chapter three of "Python and Tkinter Programming" [11], my WxPython-based calculator is based on several demo calculators from the web [1]. Figure 1 shows screenshots of the two calculators at launch.

Figure 1: Screenshots
fig1a_tkCalc.png

Figure 1a: TkCalc

fig1b_wxCalc.png

Figure 1b: WxCalc

There is also an informal, objective, though obviously biased, discussion of TKinter and WxPython at the WxPython wiki [2].

Requirements & downloads

The following scripts require python, tkinter and wxPython. On most linux systems, the package management software will install these for you (on Debian they are called python, python-tk and python-wxtools, and there is a handful of other packages available). On Windows, you have to install python yourself: download an .msi from the python website [3]. This includes tkinter. For wxPython, go to the wxPython website and download their .msi [4].

Although not a requirement, the Boa Constructor IDE [5] is useful for debugging scripts which use WxPython widgets, as it catches and reports on wx exceptions.

This article and all the calculator scripts are included in this tarball | zipfile.

Code overview

myCalc.py contains the class AbstractCalculator, which is a superclass for TkCalc (in myTkCalc.py) and WxCalc (in myWxCalc.py). I've aimed to put everything that the other two can hold in common into myCalc.AbstractCalculator, making it easier to compare the two widget toolkits by comparing TkCalc and WxCalc. TkCalc and WxCalc contain the same methods: __init__(self), buttonPress(self, event), getDisplay(self), setDisplay(self, value), and quitApp(self).

In the last three of these, the difference between Tkinter and wxPython is purely lexical (see Table 1).

Table 1: Setters, getters and quitters
AbstractCalculator                TkCalc                         WxCalc

def getDisplay(self):             def getDisplay(self):              def getDisplay(self):
    return self.display               return self.display.get()          return self.display.GetValue()

def setDisplay(self, value):      def setDisplay(self, value):       def setDisplay(self, value):
    self.display = value              self.display.set(value)            self.display.SetValue(value)

display = property(getDisplay, setDisplay)  [also in each subclass]

def quitApp(self):                def quitApp(self):                 def quitApp(self):
    pass                              self.quit()                        self.Destroy()

I discuss the differences between init and buttonPress below.

Known bugs

In practice, the term 'known bug' seems to refer to a bug that no-one can be bothered to fix. The bugs I list here are not central to my purpose for writing these scripts (which was to compare tkinter and wxPython). Fixing them is left as an exercise for the reader.

Display

The display displays operators as well as numbers (e.g. '123 + 45'), only refreshing after an '=' or 'C'/'CE'. Of course, real calculators don't do this.

The toggleSign() method

The toggleSign() method (triggered by pressing the '+/-' key) just prepends/removes a'-' at the beginning of the display. This is too primitive (see Table 2).

Table 2: Primitive +/-
Display toggleSign(Display)
123 -123
-123 123
123 - 45 -123 - 45 (should be '123 + 45')

Comparison of Tkinter and wxPython

SLOCs

The two scripts are about the same size.

$ wc my*Calc.py
120  351 3891 myCalc.py
 89  191 2396 myTkCalc.py
 85  195 2306 myWxCalc.py

imports

The Tkinter and wx packages are imported slightly differently.

>>> from Tkinter import *
>>>

but

>>> from wx import *
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: 'module' object has no attribute '__DocFilter'
>>> import wx
>>>

Note that from module import * is discouraged, especially on Windows and Mac platforms [7].

buttonPress()

Because of the design of the superclass (see above), I have had to overload only five methods, and these show only minor idiomatic differences.

buttonPress() is the method bound to key-press events and, although the two versions differ only a little, it demonstrates the different approach to key events in the two toolkits (see Table 3).

Table 3: buttonPress(self, event) in MyTkCalc and MyWxCalc
TkCalc                                       WxCalc

keysym2label = {'plus': '+',                 keyCode2label = {wx.WXK_RETURN: '=',
                'minus': '-',                                 wx.WXK_BACK: 'C',
                'asterisk': '*',                              wx.WXK_DELETE: 'C',
                'period': '.',                                17: 'OFF' }
                'slash': '/',
                'equal': '=',
                'Return': '=',
                'Delete': 'C',
                'BackSpace': 'C',
                'q': 'OFF' }

def buttonPress(self, event):                def buttonPress(self, event):
    k = event.keysym                             c = event.GetKeyCode()
                                                 k = chr(c)
    if k not in '1234567890':                    if k not in '1234567890+-*/.=':
        k = self.keysym2label.get(k)                 k = self.keyCode2label.get(c)
    self.keyAction(k)                            self.keyAction(k)

In Tkinter, event objects of any kind have a standard set of attributes [8] including (for keyboard events) char, keycode and keysym. Keycode is a code relating to the key pressed (not the ASCII character code); char and keysym both return strings - for alphanumerics both return the string of the key pressed, for other keys (e.g., '=', <left-ctrl-key>, or Return) keysym returns a description (i.e., 'equal', 'Control_L', 'Return'). Note that two-key combinations (e.g., shift-8) count as two keypress events. Keycode will give the same code for both events; keysym and char show the effect of the combinations (i.e., with shift-8 keysym returns 'Shift_R' then 'asterisk').

In WxPython, there are three different kinds of keypress events [9]: wx.EVT_KEY_DOWN, wx.EVT_KEY_UP and wx.EVT_CHAR. These event types have the same methods, the relevant one here being GetKeyCode(), which returns the ASCII value of the key pressed, or a wxWidgets constant [10] for non-alphanumerics. However, wx.EVT_CHAR.GetKeyCode() shows combination effects (like keysym above), while wx.EVT_KEY_DOWN/UP.GetKeyCode() do not.

__init__()

The __init__() method is where the two calculators differ most. See Table 4 for a side-by-side comparison.

Table 4: __init__(self) in TkCalc and WxCalc
TkCalc                                                                               WxCalc

def __init__(self):                                                                  def __init__(self):
    AbstractCalculator.__init__(self)                                                    AbstractCalculator.__init__(self)
                                                                                         self.app = wx.PySimpleApp()
    Frame.__init__(self)
    self.pack(expand=NO, fill=NONE)                                                      wx.Frame.__init__(self, None, -1, "Simple WxP Calc")
    self.master.title('Simple Tk Calc')                                                  sizer = wx.BoxSizer(wx.VERTICAL) # Main vertical sizer
    self.master.resizable(0,0)
                                                                                         self._display = wx.TextCtrl(self, -1, '',
    self._display = StringVar()                                                                                      style = wx.TE_READONLY | wx.TE_RIGHT)
    self._display.set('0')                                                               self.display = '0'
    Entry(self, justify=RIGHT, relief=SUNKEN,                                            sizer.Add(self._display, 0, wx.EXPAND) # Add to main sizer
          disabledforeground='black',
          disabledbackground='white',                                                    gsizer = wx.GridSizer(4, 4)
          state=DISABLED,                                                                for row in self.keyLayout:
          textvariable=self._display).pack(side=TOP, expand=YES, fill=BOTH)                  for key in row:
                                                                                                 b = wx.Button(self, -1, key)
    for row in self.keyLayout:                                                                   b.Bind(wx.EVT_BUTTON,
        rowFrame = Frame(self)                                                                          lambda e, k=key: self.keyAction(k))
        for key in row:                                                                          b.SetFocus()
            Button(rowFrame, text=key,                                                           gsizer.Add(b)
                   command=lambda k=key: self.keyAction(k),                              sizer.Add(gsizer, 1, wx.EXPAND)
                   width=4).pack(side=LEFT, expand=NO, fill=NONE)                        self.SetSizer(sizer)
        rowFrame.pack(side=TOP, expand=YES, fill=BOTH)                                   sizer.Fit(self)

    self.bind('<KeyPress>', self.buttonPress)                                            self.Bind(wx.EVT_CHAR, self.buttonPress)
    self.focus_set()
                                                                                         self.Show()
    self.mainloop()                                                                      self.app.MainLoop()

The toolkits differ in the way they add widgets to the main frame. Both use the first parameter of a widget's __init__() for the widget's parent. However, wxPython uses an explicit sizer to place widgets inside the parent (i.e., with sizer.Add() methods); tkinter uses the widget's pack() method to place it. Similarly, in Tkinter commands can be bound to widgets as part of their initialisation, while wxPython calls a separate Bind method (see the loop to set up the keys in Table 4).

Because the documentation was not 100% comprehensive (see below) and because I decided to limit the time spent on these GUIs, there are some minor details which remain mysterious:

  • For the calculator display, I found it necessary to use read-only text entry boxes to disable the user from directly entering text into the display (i.e., a disabled Entry on tkinter, and a READONLY TextCtrl on wxPython). The documentation points to Label or Panel widgets as being more appropriate, but I couldn't get these to work properly (e.g. tkinter Label insisted on central justification, whatever I specified in initialisation).
  • In wxPython, the lambda function bound to each Button - lambda e, k=key: self.keyAction(k) - must carry the first argument e for the event object, even though it's not used (the tkinter lambda is otherwise identical).

Once the GUI has been initialised, both toolkits use a mainloop method to set it going, and waiting for events. Here again, wxPython is slightly more complicated. In wxPython the mainloop is not run by the GUI object itself, but by a separate application object (in WxCalc, the application object wx.PySimpleApp is attached to the calculator at the beginning of __init__()). This helps abstract overall management of the application away from particular software objects or GUI widgets.

documentation

Each toolkit has a book devoted to it published by Manning: tkinter has "Python and Tkinter Programming" [11]; wxPython has "wxPython in Action" [12]. The two books are interestingly different in style. I found Garyson's book on tkinter quite inspiring about the way I wrote python in general, beyond GUI coding. The wxPython book is a huge FAQ: it's well-written and very useful, but not an inspiring read. Conversely, the tkinter book has 250 pages of fairly comprehensive appendices and a 30 page index; the wxPython book does not attempt to be comprehensive, has no appendices and only six pages of index.

For wxPython, the book is nice to get started, but you really have to use the online documentation [13]. This is comprehensive /but/ it's not for wxPython: the wxPython online docs page is actually a frame wrapped around the wxwidgets online docs page (n.b.: true up to 03/01/08). Translating between C++ and python is a bit of a drag but fairly trivial - for example, the class and method names and parameters and all the semantics are the same.

Tkinter also has good online documentation [14]. I haven't used it much, as [11] answers most of my questions.

The documentation for neither system was fully reliable, so I occasionally had to use the absolute default and spend some time with Google. This seems to be par for the course with most software these days, open or closed source.

Conclusion

For simple GUIs I have found Tkinter easier to work with: less code is needed, there are fewer idiosyncracies, there is more/better documentation. WxPython seems to be designed with at least the possibility of more complex applications in mind. For example, Tkinter does not provide a tree widget (e.g., for browser GUIs), and Grayson has to code one up (in Example_8_10.py); wxPython provides TreeCtrl, a direct wrapper around the C++ wxWidgets class wxTreeCtrl. Consequently, for more complex GUIs I prefer wxPython. Presumably any porting from Python to C++ would be simpler from wxPython from Tkinter, but I have yet to investigate porting a wxPython application to wxWidgets.