2. The ui GUI toolkit


Although the ui GUI toolkit was invented for the Thought Experiment, it is not part of the experiment as such. In other words, it should be considered a given, entirely external to the framework produced by the experiment. Yes, it was created for this investigation, but not arbitrarily so: (1) the API is a realistic simulation of a real toolkit API, and (2) neither the API nor its implementation were modified as part of the experimental evolutionary process.

2.1 The ui package

2.1.1 __init__.py

The ui package consists of two small modules: composites and widgets. Like all Python packages, it also includes an __init__.py file, but instead of merely being an empty package marker, this file imports all public names from the two modules in the package:

from widgets import *
from composites import *

Thus one can simply "import ui", and prefix all package names with "ui." like this:

from donnal.thought import ui

x = ui.TextWidget()

This technique prevents the main namespace from being "polluted" with all the names in the package (which for ui is a small number, but for a real toolkit might be overwhelming). It also makes it easy to spot the toolkit names when reading the source code.

2.2 The composites module

The composites module exports three public classes:

__all__ = ['Panel', 'Frame', 'App']

It also defines one private class: Component.

2.2.1 The Component class

The Component class is the base class for all widgets and the composite classes Panel and Frame. The only public ui GUI class not derived from Component is App.

class Component:
     """The base class for all components."""
    def __init__(self, parent=None):
        self._parent = parent
        self._children = []
        if parent:
            parent._children.append(self)

    def GetParent(self):
        return self._parent

    def GetChildren(self):
        return self._children

The primary purpose of the Component class is to implement component binding and a hierarchical tree structure for GUI components and their containers (parents). Whenever a component is instantiated with a parent argument, the component instance is appended to the parent's child list.

x = Component()
y = Component(parent=x)
z = Component(parent=x)

Then x.GetChildren() returns [y, z], and y.GetParent() returns x. Regarding the hierarchical structure, several additional points must be made.

2.2.2 The Panel class

The Panel class provides a component on which widgets and/or panels are placed. Its ui implementation couldn't be simpler: it is just a component.

class Panel(Component):
    """A component on which widgets and/or panels are placed."""
    pass

2.2.3 The Frame class

The Frame class is nothing more than a Panel, since a frame differs from a panel only in visual appearance, tab order, and so on, aspects that are supposed to be ignored in the ui GUI toolkit.

class Frame(Panel):
    """A panel with... hmmm... well, with nothing else."""
    pass

2.2.4 The App class

The App class is the only public ui class that is not derived from Component. The actual root of the hierarchy spoken of above is a Frame instance, the top frame, which is associated with the App instance using the method SetTopFrame. The method GetTopFrame returns this root object.

class App:

    def __init__(self):
        self._top = None
        self.OnInit()

    def SetTopFrame(self, frame):
        self._top = frame

    def GetTopFrame(self):
        return self._top

    def OnInit(self):
        pass

    def Run(self):
        pass

Two methods are provided that allow one to define at design-time actions that are to be performed at runtime. By overriding OnInit, one may define actions that are to be performed when the application is initially instantiated. The Run method may be overridden to define actions that are to be performed when this method is called at runtime after instantiation (in our case, from a unit test).

2.3 The widgets module

The widgets module exports four public classes and seven (macro) functions:

__all__ = ['TextWidget', 'Button', 'Choice', 'ListBox',
           'EVT1', 'EVT2',
           'EVT_KILL_FOCUS', 'EVT_BUTTON', 'EVT_CHOICE',
           'EVT_LISTBOX', 'EVT_LISTBOX_DCLICK']

It also defines one private class: Widget.

2.3.1 The Widget class

The Widget class, derived from Component, is the base class for all widgets. It implements the ui simulated event-handling mechanism.

from composites import Component

class Widget(Component):

    def trigger1(self, event):
        self._handler1(event)

    def trigger2(self, event):
        self._handler2(event)

    def _handler1(self, event):
        pass

    def _handler2(self, event):
        pass

Calling the trigger1 method (from a unit test, for example) calls the user-defined method bound to the _handler1 of the same instance. To set this up, one could simply override the _handler1 method, except for the fact that this method is considered private. The canonical way to bind an appropriate handler is to first define the handler as an instance method, and then bind that method to the correct private name using one of the following macros (functions) in the __init__ method:

def EVT1(object, handler):
    object._handler1 = handler

def EVT2(object, handler):
    object._handler2 = handler

# aliases for EVT1 and EVT2
EVT_KILL_FOCUS = EVT1
EVT_BUTTON = EVT1
EVT_CHOICE = EVT1
EVT_LISTBOX = EVT1
EVT_LISTBOX_DCLICK = EVT2

The event paramenter can be any Python object (a string or an integer, for example) that the event handler uses for whatever purpose the programmer wishes. By way of contrived example:

class MyWidget(ui.TextWidget):
    def __init__(self, parent):
        ui.TextWidget.__init__(self, parent)
        ui.EVT_KILL_FOCUS(self, self.OnKillFocus)

    def OnKillFocus(self, event):
        self.SetValue(event)

2.3.2 The TextWidget class

The TextWidget class allows text to be edited. Actually, this widget merely allows text to be stored (with SetValue) and retrieved (using GetValue).

class TextWidget(Widget):
    """A widget that allows text to be displayed and edited."""
    def __init__(self, parent=None, value=''):
        Widget.__init__(self, parent)
        self.__value = value

    def GetValue(self):
        return self.__value

    def SetValue(self, value):
        self.__value = str(value)

2.3.3 The Button class

The Button class is no more than a basic Widget (a leaf node in a hierarchy and an event handler). The key to using the Button class, of course, is defining an appropriate event handler for each subclass.

class Button(Widget):
    """A widget that initiates an action when clicked (triggered)"""
    pass

2.3.4 The Choice class

The Choice class provides a widget for selecting a string from a list of strings. In addition, each string can be associated with an arbitrary Python object, the so-called client data. The list of strings from which to choose can be passed to the widget at instantiation, or it can be populated using the Append method (which adds a required item argument and an optional client data argument to the choice list).

class Choice(Widget):
    """A widget used to select from a list of strings."""
    def __init__(self, parent=None, choices=None):
        Widget.__init__(self, parent)
        self._choices = choices or []
        self._choices = [(i, None) for i in self._choices]
        self._selection = 0

    def Clear(self):
        self._choices = []

    def Append(self, item, clientData=None):
        self._choices.append((str(item), clientData))

    def SetSelection(self, index):
        i = int(index)
        if i < len(self._choices):
            self._selection = i

    def GetSelection(self):
        return self._selection

    def GetString(self, n):
        n = int(n)
        if n < len(self._choices):
            return self._choices[n][0]

    def GetClientData(self, n):
        n = int(n)
        if n < len(self._choices):
            return self._choices[n][1]

    def FindString(self, search):
        try:
            choices = [s for s, cd in self._choices]
            return choices.index(search)
        except ValueError:
            return -1

    def trigger1(self, event):
        self.SetSelection(event)
        self._handler1(event)

2.3.5 The ListBox class

The ListBox class differs considerably from Choice in visual appearance, but since that is not a consideration in ui GUI, the ListBox is the same as Choice except for an additional event simulating a double click selection.

class ListBox(Choice):
    """Another widget used to select from a list of strings."""

    def trigger2(self, event):
        self.trigger1(event)
        self._handler2(event)

2.4 The unit tests

It should be repeated that the unit tests published here are not evolutionary in nature like those that make up the Thought Experiment as such published in subsequent sections. The ui GUI toolkit is simply a given that is outside the Thought Experiment as such. The unit tests, therefore, are merely regression tests to assure that the classes provided actually work as advertised. Appropriately, then, there are only two import statements:

import unittest
from donnal.thought import ui

2.4.1 Procedural versus object-oriented approach

Test01.test001 below demonstrates a procedural approach to composition, and Test01.test002 demonstrates an object-oriented approach. (See Rationale 2.2 and 2.3 for further background and discussion.) The point of these two tests is to show that the parent parameter can be used using either the procedural style or the object-oriented style to establish a parent-child (hierarchical) relationship.

class MyPanel(ui.Panel):
    """Subclass to show object-oriented style"""
    def __init__(self, parent=None):
        ui.Panel.__init__(self, parent)
        self.tw1 = ui.TextWidget(parent=self)
        self.bt1 = ui.Button(parent=self)
        self.ch1 = ui.Choice(parent=self)

class MyFrame(ui.Frame):
    """Subclass to show object-oriented style"""
    def __init__(self, parent=None):
        ui.Frame.__init__(self, parent)
        self.p1 = MyPanel(parent=self)
        self.p2 = MyPanel(parent=self)

class Test01(unittest.TestCase):
    """Tests of application composition"""

    def test001(self):
        """Procedural approach to composition"""
        app = ui.App()
        f1 = ui.Frame(parent=None)
        app.SetTopFrame(f1)
        p1 = ui.Panel(parent=f1)
        tw1 = ui.TextWidget(parent=p1)
        bt1 = ui.Button(parent=p1)
        ch1 = ui.Choice(parent=p1)
        p2 = ui.Panel(parent=f1)
        tw2 = ui.TextWidget(parent=p2)
        bt2 = ui.Button(parent=p2)
        ch2 = ui.Choice(parent=p2)
        app.Run()

        self.assertEqual(app.GetTopFrame(), f1)
        self.assertEqual(p1.GetParent(), f1)
        self.assertEqual(p2.GetParent(), f1)
        self.assertEqual(tw1.GetParent(), p1)
        self.assertEqual(bt1.GetParent(), p1)
        self.assertEqual(ch1.GetParent(), p1)
        self.assertEqual(tw2.GetParent(), p2)
        self.assertEqual(bt2.GetParent(), p2)
        self.assertEqual(ch2.GetParent(), p2)
        self.assertEqual(f1.GetChildren(), [p1, p2])
        self.assertEqual(p1.GetChildren(), [tw1, bt1, ch1])
        self.assertEqual(p2.GetChildren(), [tw2, bt2, ch2])

    def test002(self):
        """Object-oriented approach to composition"""
        app = ui.App()
        f1 = MyFrame()
        app.SetTopFrame(f1)
        app.Run()

        self.assertEqual(app.GetTopFrame(), f1)
        self.assertEqual(f1.p1.GetParent(), f1)
        self.assertEqual(f1.p1.tw1.GetParent(), f1.p1)
        self.assertEqual(f1.p1.bt1.GetParent(), f1.p1)
        self.assertEqual(f1.p1.ch1.GetParent(), f1.p1)
        self.assertEqual(f1.p2.tw1.GetParent(), f1.p2)
        self.assertEqual(f1.p2.bt1.GetParent(), f1.p2)
        self.assertEqual(f1.p2.ch1.GetParent(), f1.p2)
        self.assertEqual(f1.GetChildren(), [f1.p1, f1.p2])
        expected = [f1.p1.tw1, f1.p1.bt1, f1.p1.ch1]
        self.assertEqual(f1.p1.GetChildren(), expected)
        expected = [f1.p2.tw1, f1.p2.bt1, f1.p2.ch1]
        self.assertEqual(f1.p2.GetChildren(), expected)

2.4.2 TextWidget

class ClientData(object): pass

The ClientData class defined above (which is just a Python object) is used in the Choice tests below. It is included here simply because it is defined before the Test02 class, which includes both TextWidget and Choice unit tests.

The TextWidget tests simply assure that the value put into the widget by SetValue is returned by GetValue, and that all input representations (other than string, such as integer) are converted to strings.

class Test02(unittest.TestCase):
    """Tests of the ui GUI widgets"""

    def test001(self):
        """TextWidget: SetValue and GetValue"""
        x = ui.TextWidget()
        self.assertEqual(x.GetValue(), '')
        x.SetValue('some text string')
        self.assertEqual(x.GetValue(), 'some text string')

    def test002(self):
        """TextWidget: SetValue converts value to string"""
        x = ui.TextWidget()
        x.SetValue(4321)
        self.assertEqual(x.GetValue(), '4321')

2.4.3 Choice

The Choice unit tests exercise all of the methods defined for this class. In particular, Append, with or without the optional clientData argument, is tested. Because ListBox is derived from Choice these tests do not need to be duplicated for ListBox.

    def test003(self):
        """Choice: Append adds items to widget"""
        x = ui.Choice()
        self.assertEqual(x.GetString(0), None)
        x.Append('first')
        self.assertEqual(x.GetString(0), 'first')
        self.assertEqual(x.GetString(1), None)
        x.Append('second')
        self.assertEqual(x.GetString(1), 'second')

    def test004(self):
        """Choice: Append converts item to string"""
        x = ui.Choice()
        x.Append(5634)
        self.assertEqual(x.GetString(0), '5634')

    def test005(self):
        """Choice: initializes choice list"""
        x = ui.Choice(choices = ['one', 'two', 'three'])
        self.assertEqual(x.GetString(0), 'one')
        self.assertEqual(x.GetString(1), 'two')
        self.assertEqual(x.GetString(2), 'three')

    def test006(self):
        """Choice: SetSelection. GetSelection"""
        x = ui.Choice(choices = ['one', 'two', 'three'])
        x.SetSelection(1)
        self.assertEqual(x.GetSelection(), 1)
        x.SetSelection(0)
        self.assertEqual(x.GetSelection(), 0)
        x.SetSelection(2)
        self.assertEqual(x.GetSelection(), 2)
        x.SetSelection(3)
        self.assertEqual(x.GetSelection(), 2)   # 3 is too high

    def test007(self):
        """Choice: Clear"""
        x = ui.Choice(choices = ['one', 'two', 'three'])
        x.Clear()
        x.SetSelection(1)   # now 1 is too high
        self.assertEqual(x.GetSelection(), 0)

    def test008(self):
        """Choice: FindString"""
        x = ui.Choice(choices = ['one', 'two', 'three'])
        self.assertEqual(x.FindString('one'), 0)
        self.assertEqual(x.FindString('two'), 1)
        self.assertEqual(x.FindString('three'), 2)
        self.assertEqual(x.FindString('four'), -1)

    def test009(self):
        """Choice: Append, GetString, GetClientData"""
        a = ClientData()
        b = ClientData()
        c = ClientData()
        x = ui.Choice()
        x.Append('1', a)
        x.Append('2', b)
        x.Append('3', c)
        self.assertEqual(x.GetString(0), '1')
        self.assertEqual(x.GetString(1), '2')
        self.assertEqual(x.GetString(2), '3')
        self.assertEqual(x.GetString(3), None)
        self.failUnless(x.GetClientData(0) is a)
        self.failUnless(x.GetClientData(1) is b)
        self.failUnless(x.GetClientData(2) is c)
        self.failUnless(x.GetClientData(3) is None)

2.4.4 Kill Focus event

class KillFocusTester(ui.TextWidget):
    def __init__(self, parent):
        ui.TextWidget.__init__(self, parent)
        ui.EVT_KILL_FOCUS(self, self.OnKillFocus)

    def OnKillFocus(self, event):
        self.SetValue(event)

The KillFocusTester class define above is derived from ui.TextWidget to test the kill focus or lose focus event. Ordinarily, the OnKillFocus method as defined here would not be particularly helpful, but for testing purposes if fits the bill. It simply sets the value of the widget to the event argument that is passed to the trigger1 method (as demonstrated in test001 below).

class Test03(unittest.TestCase):
    """Tests of the ui event trigger/handling mechanism"""

    def test001(self):
        """TextWidget: trigger event in subclass"""
        x = KillFocusTester(None)
        self.assertEqual(x.GetValue(), '')
        x.trigger1('dummy event')
        self.assertEqual(x.GetValue(), 'dummy event')

    def handleEvent(self, event):
        """a generic event handler"""
        self.event = event

    def ignoreEvent(self, event):
        """another generic event handler"""
        self.event = 'ignored'

    def test002(self):
        """TextWidget: external event triggers"""
        x = ui.TextWidget()
        ui.EVT1(x, self.handleEvent)
        ui.EVT2(x, self.ignoreEvent)
        x.trigger1('first one')
        self.assertEqual(self.event, 'first one')
        x.trigger2('second one')
        self.assertEqual(self.event, 'ignored')

The two functions, handleEvent and ignoreEvent, are external to the widget, demonstrating a "procedural" approach to connecting event triggers to their handlers. This is not how the AP framework will make such connections, but it is an effective testing procedure. These two methods will also be used below to test event triggers for Button, Choice and ListBox.

2.4.5 Button events

    def test003(self):
        """Button: external event triggers"""
        x = ui.Button()
        ui.EVT1(x, self.handleEvent)
        ui.EVT2(x, self.ignoreEvent)
        x.trigger1('first one')
        self.assertEqual(self.event, 'first one')
        x.trigger2('second one')
        self.assertEqual(self.event, 'ignored')

2.4.6 Choice events

    def test004(self):
        """Choice: external event triggers"""
        x = ui.Choice()
        x = ui.Choice(choices = ['one', 'two', 'three'])
        ui.EVT1(x, self.handleEvent)
        ui.EVT2(x, self.ignoreEvent)
        self.assertEqual(x.GetSelection(), 0)
        x.trigger1('1')
        self.assertEqual(self.event, '1')
        self.assertEqual(x.GetSelection(), 1)
        x.trigger2('2')
        self.assertEqual(self.event, 'ignored')
        self.assertEqual(x.GetSelection(), 1)

2.4.7 ListBox events

The ListBox event triggers differ from those of Choice only in that trigger2 (double click) automatically calls trigger1 (single click) before running its own handler. That is why the widget selection is changed to 2 below while it is not changed for Choice above.

    def test005(self):
        """ListBox: external event triggers"""
        x = ui.ListBox()
        x = ui.ListBox(choices = ['one', 'two', 'three'])
        ui.EVT1(x, self.handleEvent)
        ui.EVT2(x, self.ignoreEvent)
        self.assertEqual(x.GetSelection(), 0)
        x.trigger1('1')
        self.assertEqual(self.event, '1')
        self.assertEqual(x.GetSelection(), 1)
        x.trigger2('2')
        self.assertEqual(self.event, 'ignored') # trigger2 called
        self.assertEqual(x.GetSelection(), 2)   # trigger1 also

2.4.8 Composite components

Much of the composition interface (especially GetParent) was tested in Test01 above, but the following unit tests are primarily for GetChildren and nesting of panels.

class Test04(unittest.TestCase):
    """Test of the ui GUI composites"""

    def test001(self):
        """Panel: GetChildren"""
        mypanel = ui.Panel(None)
        tw1 = ui.TextWidget(parent=mypanel)
        self.assertEqual(mypanel.GetChildren(), [tw1])
        tw2 = ui.TextWidget(parent=mypanel)
        self.assertEqual(mypanel.GetChildren(), [tw1, tw2])
        tw3 = ui.TextWidget(parent=mypanel)
        self.assertEqual(mypanel.GetChildren(), [tw1, tw2, tw3])

    def test002(self):
        """Frame: GetChildren"""
        myframe = ui.Frame(None)
        tw1 = ui.TextWidget(parent=myframe)
        self.assertEqual(myframe.GetChildren(), [tw1])
        tw2 = ui.TextWidget(parent=myframe)
        self.assertEqual(myframe.GetChildren(), [tw1, tw2])
        tw3 = ui.TextWidget(parent=myframe)
        self.assertEqual(myframe.GetChildren(), [tw1, tw2, tw3])

    def test003(self):
        """Panel: nesting composites"""
        mypanel = ui.Panel(None)
        p1 = ui.Panel(parent=mypanel)
        self.failUnless(p1.GetParent() is mypanel)
        p2 = ui.Panel(parent=p1)
        self.failUnless(p2.GetParent() is p1)
        tw1 = ui.TextWidget(parent=p2)
        self.failUnless(tw1.GetParent() is p2)

2.4.9 Application

The following is a minimal application defined using the ui GUI toolkit. The classes defined here are all subclasses of ui classes, with "user-defined" methods overridden or specific event triggers defined.

class TheButton(ui.Button):
    """When trigger1(textwidgetObj), the evtObj is changed"""
    def __init__(self, parent=None):
        ui.Button.__init__(self, parent)
        ui.EVT_BUTTON(self, self.OnClick)

    def OnClick(self, evtObj):
        evtObj.SetValue('clicked')
        
class APanel(ui.Panel):
    """To show object-oriented style of composition"""
    def __init__(self, parent=None):
        ui.Panel.__init__(self, parent)
        self.tw1 = ui.TextWidget(parent=self)
        self.tw2 = ui.TextWidget(parent=self)
        self.bt1 = TheButton(parent=self)
        self.ch1 = ui.Choice(parent=self,
                             choices=['a', 'b', 'c', 'd'])

class AFrame(ui.Frame):
    """Subclass to show object-oriented style"""
    def __init__(self, parent=None):
        ui.Frame.__init__(self, parent)
        self.p1 = APanel(parent=self)
        self.p2 = APanel(parent=self)

class TheApp(ui.App):
    """A custom application with OnInit and Run overridden"""
    def OnInit(self):
        self.SetTopFrame(AFrame())

    def Run(self):
        f = self.GetTopFrame()
        f.p1.bt1.trigger1(f.p1.tw1) # click bt1, evtobj is tw1
        f.p2.ch1.trigger1(2)  # select third item (0 based)
        f.p1.tw2.SetValue('ahem')   # set a text widget value
        

Using the application defined above, the following test first instantiates the app and tests for the structure of the main frame created in the OnInit method defined for the app. Then the test calls Run and checks to see that the desired actions were performed.

class Test05(unittest.TestCase):
    """Tests of custom application with Run()"""

    def test001(self):
        """Custom application """

        x = TheApp()
        f = x.GetTopFrame()
        self.assertEqual(f.p1.tw1.GetValue(), '')
        sel = f.p2.ch1.GetSelection()
        self.assertEqual(f.p2.ch1.GetString(sel), 'a')
        self.assertEqual(f.p1.tw2.GetValue(), '')

        x.Run()
        self.assertEqual(f.p1.tw1.GetValue(), 'clicked')
        sel = f.p2.ch1.GetSelection()
        self.assertEqual(f.p2.ch1.GetString(sel), 'c')
        self.assertEqual(f.p1.tw2.GetValue(), 'ahem')


if __name__ == '__main__':
    unittest.main()

2.5 Discussion

2.5.1 A realistic API

Nowhere in the rest of this Thought Experiment or in the Rationale is the "real" GUI toolkit on which the ui GUI toolkit is based discussed or even mentioned. Here, however, it is disclosed that the ui GUI toolkit is and patterned after the wxPython GUI framework. The reason for this disclosure is to show that the API is fairly realistic, and that there is a reasonable expectation that the results of this Thought Experiment will apply when transferred to a GUI toolkit such as wxPython. The ui package corresponds to the (new) wx package, and the ui.TextWidget corresponds to the wx.TextCtrl class. Other class names and method names are similar.

The ui GUI toolkit is obviously limited in scope and functionality, but all we need for the Though Experiment is enough functionality to allow testing to take place.

2.5.2 Similarity and difference

The most important similarity between the ui GUI toolkit and wxPython, perhaps, is the parent paramter for instantiation of all components. The container-component (parent-child) relationship is maintained through out the hierarchical structure of the application. The one exception is the top-level frame, which is reported to the Application using a separate mechanism (one that does not involve a parent parameter, but is similar between ui and wx).

The most important difference between the two is the lack of an id parameter for instantiation of ui components. This is a pervasive feature of wxPython that is not required by the framework that is evolved in this investigation. This feature is therefore left out in ui and would be ignored in a wxPython implementation of the AP framework.


XHTML 1.0 © 2003 Donnal C. Walter,
This page updated 2003-11-14.
See About this document for information on suggesting changes.