ui GUI toolkitAlthough this study revolves around the notion of data persistence, we start at the other end of the development spectrum, the user interface, and we do so precisely because we want to keep our focus on the data where it belongs. It would be easy to get caught up in the details of a particular GUI toolkit and lose sight of the issues pertinent to data storage and retrieval. To avoid this scenario, then, we begin by defining an idealized toolkit, the uiGUI (ooey gooey) toolkit, in which there is:
Eliminating the event loop and ignoring the visual aspects of the user interface make it possible to investigate the AP framework with an astonishingly simple "GUI" package.
The uiGUI API consists of the following classes and methods.
| Composites | Widgets |
|---|---|
|
|
Strictly speaking, the App class is not a "composite" except in the sense that it is made up of all the components of the application. An app can have one top-level frame (which will have a parent of None). Other frames may have the top frame as their parent. The App.Run method does nothing (there is no event loop), but subclasses of App may override this method to call event triggers (simulate events) for testing purposes. A panel must have a frame or another panel as its parent. A widget must have as its parent a panel or a frame. When a widget or a panel is instantiated, it is automatically added to the list of children for its parent.
Each component has two methods, trigger1 and trigger2, which, when called, initiate a simulated event. There are also available two corresponding macro functions (EVT1 and EVT2 respectively) to connect a trigger to an "event handler" function. For readability the following aliases for EVT1 are provided as well.
The triggerN methods take one parameter (event), which in general can be any Python object. A custom handler may use this parameter for any purpose wished. For Choice.trigger1, this parameter is an integer, which selects the corresponding item.
Like the third-party framework that inspired it, the uiGUI toolkit is properly considered object-oriented, in that it provides classes of objects from which a user interface can be constructed. But also like its archtype, uiGUI can be used with a coding style that is more procedural than object-oriented (OO). While the procedural style is more direct for relatively simple projects, we will show how the OO approach allows complex projects to be wrapped in mind-sized parcels.
The following unit test demonstrates the procedural style of composition.
import unittest
from donnal.thought import ui
class Test01(unittest.TestCase):
def test001(self):
app = ui.App()
f1 = ui.Frame(parent=None, title='My Top Frame')
app.SetTopFrame(f1)
p1 = ui.Panel(parent=f1)
p2 = ui.Panel(parent=f1)
tw1 = ui.TextWidget(parent=p1)
bt1 = ui.Button(parent=p1)
ch1 = ui.Choice(parent=p1)
tw2 = ui.TextWidget(parent=p2)
bt2 = ui.Button(parent=p2)
ch2 = ui.Choice(parent=p2)
# relevant assertions.
There are four sources of complexity with which we must contend, two of which are demonstrated in the code above, and two of which are easily imagined. These are:
In the unit test above, there are ten objects with ten different names. Every object must have a name that is distinct from all the others. For ten components this is trivial, but when the number is five or ten times more, the naming problem can become overwhelming. Note also that panel p1 is similar to panel p2, and that the code defining p1 and p2is therefore nearly redundant, giving rise to the common practice of copy-paste-edit.
In the original third-party toolkit on which uiGUI is based, component naming is further complicated by the fact that internally each object is given a unique integer identifier. A commonly used technique, then, is to define the names of objects as constants for the corresponding identifier. On the other hand, it is possible to ignore these identifiers altogether, and the uiGUI idealization does so.
In the example unit test, components tw1, bt1 and ch1 are bound as children to panel p1, while tw2, bt2 and ch2 are bound to p2. The two panels are bound as children to the frame f1. The binding mechanism involves passing the parent as a parameter during instantiation. This results in the child storing a reference to its parent and the parent appending the child to its list of child objects.
As with component naming, this binding mechanism works well enough for small numbers, but with increasing numbers of components (and with increasing levels of nesting) the degree of complexity rises dramatically. Understanding and/or remembering which component is the proper parent of which child components is a daunting task.
The uiGUI toolkit has no visible display, so component layout is ignored. It is not difficult to imagine, however, the need for a way to define the size and position of each component. One way would simply be to pass these two parameters, if they are known, to each component constructor. More often than not, however, they are not known ahead of time, and even when they are, making a subsequent change in the size or position for one component usually results in changes to many other components. An alternative method would be to pass certain other parameters to an object belonging to the parent object that is responsible for flexible sizing of the components.
The latter is a powerful technique for creating attractive, platform-independent visual displays, and it is the method of choice for many GUI toolkits. There are, however, several problems associated this scheme that give it a reputation for being esoteric or arcane. First, the sizing object seems to act like a container in many regards, but it is not a container, it is only associated with one. It is easy to get confused about this, especially with nested containers and sizers. Second, there are usually several diffent styles of sizing objects, each having a slightly different API. Third, the parameters needed for adding a component to a sizing object are often not as intuitive as we might like. And finally, this technique requires two separate steps, (1) instantiating the child object and (2) adding it to the parent sizing object. This, in turn, may require several additional statements for each component.
The uiGUI toolkit has no event loop, but it is easy enough to imagine how event-handling might work for the example we are studying. In brief, the programmer would need to define a function for each event to be handled and then connect that function to the appropriate event on a given object. This is usually done by running an event-specific macro with the object and function to be linked as parameters. Even if similar objects have similar events defined for them, a separate macro statement must be written for each connection. The problems encountered with a procedural style for doing all of this is similar to the naming and binding problems discussed above, only multiplied since there are potentially three things to keep track of (the handler function, the object, and the event).
The same application could be coded in a more OO style as follows. First we define a subclass of ui.Panel, adding the desired widgets in the constructor-initializer (__init__) method.
class MyPanel(ui.Panel):
def __init__(self, parent=None):
ui.Panel.__init__(self, parent)
self.tw1 = ui.TextWidget(parent=self)
self.bt1 = ui.Button(self)
self.ch1 = ui.Choice(self)
Notice especially that the parent in this case is always self, which stands for the MyPanel instance that is created. Then we do the same thing with a custom class derived from ui.Frame, adding two instances of MyPanel just defined.
class MyFrame(ui.Frame):
def __init__(self, parent=None, title='My Top Frame'):
ui.Frame.__init__(self, parent, title)
self.p1 = MyPanel(parent=self)
self.p2 = MyPanel(self)
Now the corresponding unit test becomes simply:
def test002(self):
app = ui.App()
f1 = MyFrame()
app.SetTopFrame(f1)
# relevant assertions.
Comparing these two contrived (and simple) examples, we should first note that the total number of lines of code is about the same for each one. Furthermore, depending on one's point of view, the procedural-style might appear more direct and therefore easier to understand, at least as shown above. But keep in mind (1) that this simple application has only ten objects, and (2) that the code listings above ignore component layout and event-handling entirely. These examples are not intended to establish the superiority of one style over the other, but merely to illustrate the principles under discussion. That said, we should admit that this project clearly leans toward the OO style of composition.
It is not our claim that OO programming is a panacea for all software development problems. Nonetheless, the OO features of Python can greatly alleviate the problems associated with component naming, binding, layout and event-handling as outlined above. Python's class definition syntax deserves particular attention in this regard.
One of the most significant features of Python is the way it manages name spaces. And of the tools for dealing with namespaces in Python, one of the most powerful is class definition (Python Tutorial Section 9.2). Each class sets up its own namespace so that name conflicts can be avoided even when there are a large number of components named. Moreover the names can be partitioned into geometrical or logical groups. Components named in this manner may be accessed through the so-called dot notation (e.g., f1.p1.tw1).
Likewise, class definitions allow each composite (container) to take care of its own components without knowing anything about the components created elsewhere in the application. The parent for each component thus becomes self (the composite class instance), making it easy to keep track of the parent-child relationships. Later we will investigate a syntax to make adding components even easier. When a composite (such as MyPanel) is instantiated more than once, the same code can be "reused", avoiding the need for copying and pasting.
Class definitions per se don't simplify component layout all that much. There is, of course, the possible advantage of code reuse when a class is instantiated more than once, and the layout may also be simplified by partitioning the components into nested composite objects (subpanels, for example). But otherwise the code for placing individual components or for adding components to a sizing object is much the same either way.
There is, however, the potential for writing sophisticated code to set up sizing objects of different types and for converting a set of simple parameters into the arcane parameters required by the GUI toolkit. This code, written once, could be used by all composite classes, greatly simplifying component layout. Since layout is not part of the uiGUI toolkit, nor is it much related to data persistence, we will not explore this aspect in much more detail (barely touching on it again, in fact). Nevertheless, keep in mind that this trick is potentially a big deal.
Most of the events in which we have an interest are generated by widgets rather than by composite objects. To take advantage of OO features, therefore, the widget classes need to be subclassed. The advantages of doing this are (1) that the event handler(s) can be defined along with the subclass, and (2) the macros for connecting the events to the handlers can be run during instantiation. Since this aspect of the GUI is related to capturing data (for possible storage and retrieval), it is an aspect that clearly must be investigated further in this study.
One could make the case that event-handling is more relevant to data persistence than component naming, binding and layout put together. It is through user-initiated events that the application receives the data entered, and through such events that the data are further manipulated and ultimately stored and retrieved. More especially, one or more events are required to initiate data validation and database transactions.