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.
ui packageThe 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.
composites moduleThe composites module exports three public classes:
__all__ = ['Panel', 'Frame', 'App']
It also defines one private class: Component.
Component classThe 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.
Panel classThe 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
Frame classThe 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
App classThe 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).
widgets moduleThe 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.
Widget classThe 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)
TextWidget classThe 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 bedisplayed andedited.""" 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)
Button classThe 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
GetParent inherited from Component via Widget.Choice classThe 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)
GetParent inherited from Component via Widget.ListBox classThe 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)
Choice class.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
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)
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')
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)
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.
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')
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)
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
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)
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()
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.
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. |