Bob has worked with user interface design since the days when menus were considered a pretty neat idea. He is currently responsible for multimedia tools development at Oracle, and can be contacted at 500 Oracle Pkwy., M/S 4 OP 12, Redwood Shores, CA 94065.
For software developers, the proliferation of windowing systems -- Macintosh, MicrosoftWindows, Presentation Manager, Motif, Open Look, NeXTStep -- is a major headache. Develop a product for one GUI, and you miss out on markets for all the others. What's worse, the GUI you pick might be the once-promising contender that later falls by the wayside.
Some developers hedge their bets by writing for several of the most popular systems. But the GUI toolkits are all different, hard to learn, and cumbersome to use. Development for multiple systems is both risky and expensive. And products that are converted from one GUI to another often end up with a "foreign" feel that hinders acceptance by users.
One solution to this dilemma is to design a portable toolkit for implementation on top of each "native" windowing system. This toolkit can provide application programmers with a standard library of routines that map to all underlying platforms, thus allowing products to be ported without changes to the application's source code.
Ideally, applications written with the portable toolkit should make full use of all native windowing system capabilities, and have the look and feel of applications written specifically for that platform. It is hard to write full-featured, competitive applications if your toolkit restricts you to a "least common denominator" intersection of GUI features.
Additionally, the toolkit should have little impact on an application's performance. And, just to make things challenging, a reasonable subset of the toolkit should be available on character-mode terminals, so that simple applications using menus and dialog boxes can work equally well on a VT220 and a Macintosh.
These were our goals when we decided to develop a portable user interface toolkit. But we quickly learned that the task is much more difficult than it seems. The same problems that make it tough to port applications from one GUI to another make it very difficult to write a portable toolkit.
In developing the toolkit, the challenges we faced ranged from dealing with fundamentally different event models to nagging details such as deciphering the various ellipse-drawing algorithms.
One of our thorniest problem areas dealt with look-and-feel, in which native GUIs have differing notions of how to perform similar tasks. Most systems, for example, permit a menu in each window, while the Macintosh only allows for a single menu bar. How could our toolkit allow people to write applications that would look right in any environment?
Typefaces were another problem area. On the Macintosh, typefaces are described by three completely independent properties: A font (Times or Helvetica, for example), a point size, and a style or combination of styles (underline, Italic, bold, and so on). In theory, any font can be represented in any style and in any size. (In practice, only a few sizes are stored as screen bitmap files; other sizes are derived by bitmap scaling, with visual results that are often unacceptable.)
On other systems, typefaces are only provided for specific combinations. The existence of a 12-point Helvetica Italic, for example, doesn't imply that a 10-point Helvetica bold exists. Some systems also support an additional property, the font weight, which may range from ultra-light to ultra-bold. Finally, character-mode terminals provide their own (very limited) typeface model, which includes such attributes as reverse-video and blinking.
Another major GUI difference involves coordinate systems. Each GUI supports one or more different display resolutions. Often, the horizontal and vertical dimensions are not the same, which complicates things like drawing circles that are truly round. And while most GUIs place their origin at the upper-left corner of the drawing area, Presentation Manager's origin is at the lower-left, following the coordinate system commonly used in mathematics. Because coordinates are not used just for graphical shapes, but also for positioning controls and text within windows, the toolkit's coordinate space had to be mapped onto character-mode devices as well.
Even with a uniform coordinate system, there is no guarantee that a drawing will appear the same across GUIs. Two curves that just touch on one platform may be a pixel apart, or even overlapping one another, due to minor differences in drawing algorithms.
Rather than tackling these problems on a case-by-case basis, and possibly ending up with a chaotic mess, we evolved five formal design principles that were applied across the entire toolkit: overspecification, abstraction, augmentation, exclusion, and qualification. These are discussed in the sections that follow.
Each GUI toolkit has features and options that the others lack. Because our toolkit must have the necessary information to pass on to each of the underlying toolkits, the application programmer must supply the superset of that information.
For example, to create a window, the programmer supplies the minimum size, maximum size, resize increment, window style, title, and so on, even though much of this may be ignored by some platforms. When this specification system proved burdensome to the application programmer, we relieved the burden by providing reasonable defaults for all attributes.
Thus, if the programmer is concerned with the window's minimum size, he can specify it, and the toolkit will pass this on to the underlying GUI. If the programmer doesn't care, the toolkit will supply a default value that is consistent with the look and feel of the underlying platform (if the attribute is relevant to that platform).
Rather than passing attributes as strings of parameters, we created an attribute structure for each GUI entity (windows, views, text fields, buttons, and so on). Each attribute structure has a mask field, with a bit corresponding to each attribute. By setting the mask bits before passing the attribute structure to a toolkit routine, the programmer specifies which attributes of the entity are to be set explicitly, and which will default (see Listing One). The attribute structures have the added advantage of being extensible, should some future GUI require the specification of another attribute.
In cases where the implementation of a particular feature differs from one platform to another, the portable toolkit presents a uniform abstract interface to the application programmer. A good example of this is the menu bar problem mentioned earlier.
There are at least three ways that native GUIs handle menu bars:
In cases where a native windowing system lacks a particularly useful feature, the portable toolkit actually implements that feature on the deficient platform, so that application programmers can make use of it. Examples of this are: A help facility, a text list box, and "Open" and "Save" file dialogs, all of which were missing from at least one GUI. In this way, we avoid becoming a "least common denominator."
Some features are found on only one or two platforms, and would be too difficult to implement on others. Here, our toolkit simply excludes the feature.
In general, the toolkit uses exclusion only when there is no reasonable alternative. A good example is the Macintosh sound manager, which has no parallel in any other major windowing system. Should this feature become more common across GUIs, we can add a portable sound facility using the design techniques already discussed.
If a feature is supported in a very specific way on some platforms and not on others, and if abstraction offers no reasonable way to conceal the difference in implementation, the toolkit allows the programmer to qualify requests based on the capabilities of the underlying windowing system.
An example of this is the "About" menu item, which, by convention, most Macintosh applications display in the "Apple" menu. Unfortunately, the Apple menu has no parallel on some other systems, so if applications want an About menu item, they have to display it somewhere else.
The toolkit allows the application to make a qualified request for a platform-specific About menu item. On the Macintosh, the portable toolkit displays the item in the Apple menu. On other platforms, the toolkit refuses the request; the application can then place the About item in one of its own menus.
One of our biggest problems was the whole area of event distribution. Events are central to GUI programming. Unlike traditional programs, which choose when and where to accept user input, programs on window systems must respond to a variety of user- or system-generated events (such as mouse clicks or window activations) at any time.
Unfortunately, the various GUI event models differ in three fundamental ways: How events are distributed, who receives events, and what types of events are supported. We'll briefly discuss these differences, and then show how the toolkit design techniques resolved them.
In some GUIs (the Macintosh, for example), events are queued by the system. The application program is responsible for frequently calling a Next-Event routine to see if any events are waiting to be processed. This occurs within an event loop -- the heart of a Macintosh application -- which does nothing but get events and call appropriate routines to process them.
In other GUIs, the application registers event-handler routines with the system. When events occur, the system calls the appropriate event handler. Thus, the application has control only when one of its event routines is called.
Microsoft Windows takes a hybrid approach. The application's event loop first calls a routine, GetMessage( ), to get the next event from the event queue. It then calls another system routine, DispatchMessage( ), which in turn invokes the application's preregistered event handlers.
These various event distribution models are related to the notion of an event's destination. On the Macintosh, the application's event loop gets all events. Some events are marked with additional destination information. For example, an activate event specifies the window to be activated, while a mouse button down event includes the position of the mouse. However, it is the application's responsibility to determine the event's ultimate target.
At the other end of the spectrum we have the "callback" model of OSF Motif, based on the X Window intrinsics. In a callback system, each object on the screen, from a checkbox to a text field, may have its own event handler; events are dispatched directly to the entity to which they pertain by calling its event handler.
Event targets are also affected by the types of events each GUI supports. Although common events such as keystrokes or mouse clicks have very similar destinations on all GUIs, targets of more abstract events such as activation and deactivation vary. On some GUIs, an activation event may be delivered only to the window; on others, it may go to a particular entity, such as a text control within the window.
Creating a uniform programmatic interface on top of these different models required careful application of all toolkit design techniques.
To begin with, we chose to support callback-style event distribution, because GUIs that use an event loop model could easily be augmented by building the event loop into our toolkit and adding a dispatcher. This was much simpler than the alternative: Replacing native GUI event dispatchers with a routine that funnels all events into a single queue, where an application event loop can retrieve them.
The next task was to create a uniform system of event targets that could be mapped onto the various GUIs. The toolkit can dispatch events to three classes of entities: the application itself, individual windows, and views. Views are objects that appear within windows, such as text fields, scroll bars, and checkboxes.
The application must register an event handler routine for each instance of a window or view (and of course, one for the application itself). Registration is accomplished by setting the EVENT attribute in an entity's attribute structure when it is created. The application can also register a pointer to a private data area; this pointer is passed to the event routine when it is called. Applications can then provide event routines with additional application-specific data.
Note that many entities can share the same event handler routine, because the routine is passed the target (or "recipient") ID in the event record, whenever it is called. The event record may also contain data specific to the type of event; for example, a KEY event record contains the actual key that the user pressed. To provide all of the event-specific data without becoming too large, the master event record passed to event handler routines is actually a union of type-specific event records; the individual variants overlap only in the "type" and "recipient" fields, which are provided for all types of events. Listing Two shows an application-event routine that responds to push-button events for three different buttons in a dialog.
Actually implementing this scheme on the various GUIs required extensive abstraction and augmentation. The entire view scheme is itself an abstraction, because many GUIs group objects into numerous distinct classes, rather than a single, all-encompassing class. Augmentation was required to implement uniform view behavior, including event dispatching, on top of the many different object classes on each of the GUIs.
All three classes of toolkit event targets (application, windows, and views) were mapped directly onto those GUIs that already allow individual entities to register their own event handlers. Other GUIs were augmented to allow registration of event routines, and to send events to the appropriate entity through the event dispatcher. (When we say that the toolkit is "sending an event" to an entity, we simply mean that it is calling that entity's event handler.)
The actual target for any particular event is based on the event type. For example, when the toolkit receives a mouse button press event from the underlying GUI, it may have to determine the view that contains the current mouse position, and then dispatch a MOUSEDOWN event to that view's event handler. A KEY event, in contrast, is sent directly to the view or window which is currently the focus for keyboard input. The user may be able to change the keyboard focus by tabbing to, or clicking in, another view. In this case, the view that previously had the keyboard focus is sent a FOCUSOUT event, and the view that has just become eligible for keyboard input is sent a FOCUSIN event.
The FOCUSIN and FOCUSOUT events are examples of events not supported on some GUIs, but necessary to writing well-behaved applications. On some GUIs, for example, the system may highlight text selected by the user. If the user moves to another view by tabbing or clicking, the text in the previous field must be unhighlighted. To support this functionality, the application must be notified when the keyboard focus shifts. Providing a standard set of events across GUIs required extensive augmentation.
No software project is ever 100 percent successful, but by consistently applying toolkit design techniques, we met all of the goals described in the beginning of this article, and have implemented some or all of the toolkit on character-mode terminals, Microsoft Windows, Presentation Manager, Macintosh, X Window, and Motif.
Two very large software products, a forms/data entry package and a presentation graphics/charting package, were built on top of the first version of the toolkit. These were successfully ported across native GUIs with an investment of less than two percent of the original coding effort.
While the effort to design and code for GUI independence is not small, it can pay off: Applications will reach a far wider audience, and will not be locked in to one platform as GUIs continue to advance and proliferate.
Credit for the toolkit belongs to: Ed Screven, who is chiefly responsible for the current design; Bruce Daniels, who provided the initial vision and inspiration; and all of the outstanding individuals who have nursed the toolkit through many revisions.
_DESIGNING A PORTABLE GUI TOOLKIT_ by Robert T. Nicholson[LISTING ONE]
/* make_my_window -- Application code to create a window demonstrates use of
attributes for overspecification. The programmer can specify values for
window properties applying to any underlying GUI, or can accept
platform-specific defaults. Routines and constants prefaced with the
letters "ui" are toolkit routines.
*/
uiWindow make_my_window(private_data)
ptr_t private_data;
{
struct
uiWindowAttr window_attr; /* window attributes */
uiWindow the_window; /* The window created */
/* Set the attibute "mask" for the attributes we're concerned with */
window_attr.mask = uiWINDOW_A_EVENT |
uiWINDOW_A_TYPE |
uiWINDOW_A_MODALITY |
uiWINDOW_A_POSITION |
uiWINDOW_A_SIZE |
uiWINDOW_A_TITLE |
uiWINDOW_A_CANRESIZE |
uiWINDOW_A_CANCLOSE |
uiWINDOW_A_CANMINIMIZE |
uiWINDOW_A_CANMAXIMIZE |
uiWINDOW_A_CANTITLE |
uiWINDOW_A_VSATYPE |
uiWINDOW_A_HSATYPE;
/* Set the desired attribute values */
/* EVENT - register the event handler, and a pointer to an application data
structure that will be passed to the event routine when it is called. */
window_attr.eventproc = my_event_routine;
window_attr.eventarg = private_data;
/* TYPE & MODALITY - a non-modal document window */
window_attr.type = uiWINDOW_DOCUMENT;
window_attr.modality = uiWINDOW_MODALITY_NONE;
/* POSITION, SIZE, and TITLE */
window_attr.x = 200;
window_attr.y = 100;
window_attr.width = 500;
window_attr.height = 300;
window_attr.title = "Untitled";
/* CANRESIZE, CANCLOSE, CANMINIMIZE, CANMAXIMIZE, CANTITLE - these permissions
determine how the user can manipulate this window. We could simply accept
platform-specific defaults for these, to maintain local look and feel. */
window_attr.canresize = TRUE;
window_attr.canclose = TRUE;
window_attr.canminimize = TRUE;
window_attr.canmaximize = TRUE;
window_attr.cantitle = TRUE;
/* VSATYPE & HSATYPE - window will have vertical and horizontal scrollbars */
window_attr.vsatype = uiWINDOW_SATYPE_BAR;
window_attr.hsatype = uiWINDOW_SATYPE_BAR;
the_window = uiWindowCreate(&window_attr);
return the_window;
}
[LISTING TWO]
/* button_routine -- A sample of an event handler routine that processes
events for three buttons in a dialog box. Data structures prefaced with
the letters "ui" are toolkit structures.
*/
void button_routine(the_event, private_data)
uiEvent *the_event; /* Event record pointer */
ptr_t private_data; /* Dialog's data pointer */
{
switch (the_event->type)
{
/* Handle button-push events for all three of the
dialog's buttons. */
case uiEVENT_PUSHB:
if (the_event->recipient ==
((dialog_data_struct *) private_data)->yes_button)
do_yes_action(private_data);
if (the_event->recipient ==
((dialog_data_struct *) private_data)->no_button)
do_no_action(private_data);
if (the_event->recipient ==
((dialog_data_struct *) private_data)->cancel_button)
do_cancel_action(private_data);
break;
/* Handle key events - if the user pressed the Return
key, treat it the same as the "Yes" button, which
is the default button for this dialog. */
case uiEVENT_KEY:
if (((uiEventKey *) the_event)->key) == '\n')
do_yes_action(private_data);
else
uiWSBeep();
/* (Not interested in any other events that the buttons
may receive.) */
}
return;
}
Copyright © 1991, Dr. Dobb's Journal