home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Shareware Overload
/
ShartewareOverload.cdr
/
windows
/
actortut.zip
/
TUTR4.TXT
< prev
next >
Wrap
Text File
|
1990-10-19
|
36KB
|
1,029 lines
Actor column for JOOP
Column 3 (Nov./Dec. 1989)
Zack Urlocker
Abstracting the User Interface
Although object-oriented programming has been around for
more than twenty years, its usage and interest in it have
increased tremendously in the last five years. There are
several factors which have brought OOP to the forefront.
These include: a wider range of useful and efficient tools,
greater awareness and, perhaps most significantly, a greater
need to reduce software complexity.
One of the most complex programming areas is user-interface
development. Graphical user-interfaces (GUIs), such as
Microsoft Windows or that of the Macintosh, have hundreds of
function calls in the application program interface (API)
for managing the display, controlling the mouse, managing
with fonts, printing and so on. More sophisticated
environments, such as Hewlett-Packard's NewWave, OS/2
Presentation Manager, and Unix Open Look, have over a
thousand functions in the API.
Applications developed for standard graphical environments
provide users with a consistent, easy-to-use interface with
powerful mechanisms for sharing data between applications.
However, these benefits come at the expense of a steep
learning curve and longer development times when using
traditional languages such as C. Even relatively simple
applications require hundreds of lines of code to run
properly in a graphical environment. Given the complexity
of developing for a GUI, it's not surprising that there's
been a tremendous adoption of object-oriented programming in
this area.
In this column, I describe a simple graphical application
from an object-oriented perspective. The application is
written in Actor for the Microsoft Windows environment. If
you're not familiar with GUI programming, this will be an
opportunity to see what goes on behind the scenes. I'll
also examine different approaches to applying object-
oriented programming to GUIs and provide some tips that
should make the trip a little easier --whether you're
designing your own user-interface objects or building on
existing ones.
User-Interface Objects
One approach to managing GUI programming is to build a class
library of user-interface objects. This is the approach
used in Actor. Actor builds on the graphical user-
interfaces components of Microsoft Windows and provides the
programmer with ready-to-use classes for dialog boxes, list
boxes, scroll bars, buttons and of course, windows. These
objects can be combined to provide very flexible user
interfaces.
An example of a simple graphical interface is shown in
Figure 1. An account window displays a list of accounts,
and for the selected account, a corresponding chart and text
summary. The window also has a set of pulldown menus for
opening and saving files, adding or deleting accounts and
changing chart types. Input, when required, is elicited
from the user through dialog boxes.
The account window is made up of several panes. Experienced
Microsoft Windows programmers will recognize that there are
two child windows, used to display the chart and text, and a
list box control. The account window is known as the parent
window. Each of these components is responsible for its own
behavior --illustrating that the components are, in fact,
objects. For example, when the user scrolls through the
list box or selects an item, the visual effect is managed by
the list box itself; it does not require any additional code
on the part of the application programmer. Other types of
controls include buttons, edit fields and scroll bars, as
shown in Figure 2. These controls have a similarly limited
set of behaviors that are managed automatically.
However, there is more to an application program than the
user interface. When the user clicks on an item, there must
be some effect on the application. In this case, the
selected account's data should appear in the other windows.
This behavior is managed through a protocol which specifies
that when an event occurs in a child window that cannot be
fully managed by the object itself, it sends a message to
the parent window. For example, scrolling is handled
entirely by the list box. Selection, which requires some
action on the part of the application, results in sending a
command message to the parent window.
The command message is also used in the account window to
handle other commands from menus or the keyboard. Therefore
the command message uses an argument to indicate the item
selected, either a menu ID constant or control constant.
At other times, it is necessary for the parent window to
send messages to the child windows. For example, when the
account window is resized by dragging on its borders, the
account window gets a reSize message. Thus, resizing
appears to be automatic to the user.
Implementation
There are four primary classes in the account window
application: AcctApp, AcctWindow, AcctDialog, and Account.
The application also uses standard Actor classes such as
TextWindow, List box, FileDialog; charting classes such as
ChartWindow, Chart and its descendants; and the object-
storage facilities from Language Extensions I, an Actor add-
on product. Figure 3 shows the class hierarchy of the
application.
The AcctApp class defines the application's startup
behavior. The application class's responsibility is simply
to create an AcctWindow, and, if necessary, load any file
specified as a command line argument from MS-DOS. The code
for the AcctApp class is shown in Listing 1. As you examine
the source code you'll note that Actor's syntax is more akin
to Pascal or C than to Smalltalk. Messages are in the form
message(receiver, arg1, arg2);. Note that the inherit
message specifies the ancestor and instance variables for
the class. The inherit message is not normally written by
the programmer, but is generated automatically by the
browser.
The AcctWindow is the central object in the application and
is responsible for managing most of the user-interface. It
maintains a dictionary of accounts, the current account and
has other instance variables that correspond to the child
windows. The AcctWindow manages user interaction and sends
messages to the accounts dictionary or child windows as
necessary to perform the application logic. The other
objects are completely independent of the AcctWindow and its
user interface. Figure 4 shows the division of labor in the
application.
The AcctWindow has a dictionary of menu items and
corresponding message names to respond to command messages.
For example, if the user selects the "Save As.." menu choice
from the File menu, then the menuItem argument of the
command message will have the constant value AW_FILE_SAVEAS
as defined in the application's resource file. The
AcctWindow will respond by looking up the constant in the
actions dictionary and sending itself a fileSaveAs message.
This data-driven technique fits well with object-oriented
programming and helps increase code reusability in
descendant classes.
The rest of the AcctWindow code implements the menu commands
for loading and saving files, or selecting, adding or
deleting an account. The code for the AcctWindow class is
shown in Listing 2. Upper case identifiers, such as
AW_FILE_SAVEAS, denote constants that are defined in a
header file.
Difficulties
Although a library of user-interface objects hides much of
the complexity of GUI programming, there are still some
difficulties stemming from the fact that user-interface
remains intertwined with the application logic. For
example, adding and deleting accounts requires updating both
the list box and the accounts dictionary. Certainly it's
possible to create a descendant of List box, perhaps called
AcctList, that manages the dictionary of accounts; but this
approach may not general enough to be used in other
applications. Another limitation is due to the fact that
the class library only factors out user-interface components
and does not address other mundane tasks such as file
management. As a result, code that is common to most
applications, such as prompting the user for the name of the
file to load, or warning if the user does not save his or
her work, is rewritten for each application.
Clearly, a more general solution is possible --one that
includes not only user-interface components, but other
general characteristics of applications.
Towards an Application Framework
Much research has been done in creating general-purpose
application frameworks for Smalltalk-80 and Macintosh
environments. The basic idea of an application framework is
to take the user-interface objects one step further and
provide a set of classes that defines a fully-functional do-
nothing application. The framework has "hooks" to allow an
application programmer to plug in objects that represent the
functionality unique to his application. Generic behavior,
such as user-interface control, file management, printing,
scrolling and so on, are already available in a reusable
form.
The use of an application framework has several benefits.
It reduces the code required in applications, maintenance is
easier, and consistency is encouraged. The disadvantages
are the effort of implementating the framework and a steep
learning curve to use it. Although we are only beginning to
understand the design implications for application
frameworks, it's worth looking at two current systems to see
what can be learned.
Smalltalk-80 and MVC
Smalltalk-80's application framework is based on having a
three-part representation of the application known as the
Model-View-Controller or MVC for short [Burbeck 87]. The
view and controller are based on standard classes which
define a protocol of messages between the three parts. The
controller manages all user input including the keyboard and
mouse. The view provides a graphical representation of the
application, typically in a window. The model is defined by
the application programmer and can be thought of as the data
in the application.
In an MVC approach to the account window application, the
model would be the dictionary of accounts. There would be a
separate view and controller for each of the child windows.
Whenever the user added, deleted or selected a new account,
the controller would send an appropriate message to the
accounts dictionary, which would in turn, broadcast the fact
that it had changed to the views. The views would then
update themselves, asking the model for additional
information if necessary.
The MVC approach eliminates some of the code required to
update both the account dictionary and the list box. It
also separates the behavior of windows into two distinct
roles: user-input managed by the controller, and output
provided by the view. Unfortunately, this separation does
not fit well with most GUIs where input is always associated
with a particular window. MVC's division of labor and need
for a separate controller for each view makes it difficult
to learn; it takes careful experimentation to make changes
to controller classes. Some Smalltalk vendors and users
have found that they're better off using simpler classes
than dealing with MVC's complexities.
Although MVC is most applicable to Smalltalk-80, it can be
implemented in any object-oriented language. See the
references at the end for more information on the MVC
protocol and its Smalltalk-80 implementation.
MacApp's Reusable Toolkit
Apple Computer's MacApp is a second generation application
framework for the Macintosh that refines some of the ideas
in MVC [Schmucker 86]. Although most often used with
Object-Pascal, it MacApp can be accessed from most Macintosh
programming languages. Whereas the MVC approach has a
three-part representation of the application, MacApp
provides two major components: the document (similar to the
MVC model) and the view. The functionality of the MVC's
controller is in effect hard-coded into the MacApp
application to ensure adherance to the Macintosh user-
interface guidelines.
MacApp includes other classes that provide automatic
resizing, scrolling, coordinate transformation, undo/redo of
commands, and document management. MacApp's approach
provides a higher level model than either a user-interface
library or MVC, but it is less flexible. However, MacApp is
only a framework; it does not attempt to provide a complete
class library and therefore lacks support for graphical
objects, collections and other general-purpose classes.
These facilities must be provided by the language used with
MacApp.
Effective User-Interface Strategies
Although there is no single solution that meets all needs,
class libraries and frameworks provide a tremendous
headstart to programmers developing for graphical user-
interfaces.
Even with an object-oriented language and class library,
programming for a GUI remains challenging. Whether you're
building class libraries, using a framework or are somewhere
in between, you should keep in mind the following
guidelines:
∙ Separate the user interface from application logic.
The model should be independant of the views. You should be
able to change the user interface with minimal effect on the
rest of the application.
∙ In GUIs that couple graphical rendering and user
interaction, the responsibility of the MVC view and control
can be combined into the window object.
∙ Use a consistent, general protocol between different
user-interface objects. When building new user-interface
objects use existing protocol where appropriate.
∙ The best user-interface components are those that can
be reused easily. When implementing new classes, always
test them by creating subclasses to see if the protocol is
complete.
∙ Don't shy away from tackling non-user-interface
problems with reusable classes. These can provide you with
the basis for a more complete application framework.
By following these guidelines and experimenting with
different approaches you can improve the quality of your
work and make it resilient to change. In the future we're
likely to see much richer class libraries and easier-to-use
application frameworks for graphical environments that will
pave the way for even greater productivity.
Further Information
Steve Burbeck, Applications Programming in Smalltalk-80: How
to Use Model-View-Controller, Softsmarts, Inc., 1987.
Mahesh H. Dodani, et al., "Separation of Powers", Byte,
March 1989.
Kurt Schmucker, Object-Oriented Programming for the
Macintosh, Hayden Books, 1986.
Kurt Schmucker, "Packaging User-Interface Functionality",
Journal of Object-Oriented Programming, April/May, 1988.
Glenn Krasner, Steven Pope, "A Cookbook for Using the Mode-
View-Controller", Journal of Object-Oriented Programming,
August/September, 1988.
Abstracting the User Interface page 8
Source Code
The sample application and complete Actor source code
described in this column are available in MS-DOS disk format
from the author for $5 in the United States, or $10
elsewhere. Write to Zack Urlocker, The Whitewater Group,
600 Davis St., Evanston, IL 60201, USA.
About the Author
Zack Urlocker is manager of developer relations at The
Whitewater Group, the creators of Actor, an object-oriented
language for Microsoft Windows. Mr. Urlocker has taught
object-oriented programming to hundreds of professionals and
has published articles in several computer magazines and
journals.
Abstracting the User Interface page 9
Figure 1. The account window contains three child windows.
** Screenshot of the account window application.
Figure 2. Microsoft Windows controls are user-interface
objects.
** Diagram of control objects
Figure 3. The account window application class tree.
** Diagram of class tree.
Figure 4. The division of labor in the account window
application.
** Diagram of division of labor
Abstracting the User Interface page 10
Listing 1. The AcctApp class.
/* The AcctApp class defines the application and its
initialization. The AcctApp class inherits from the
Object class and has a single instance variable, window.
*/
inherit(Object, #AcctApp, #(window), nil, nil)!!
/* The init message is sent when the application starts.
It creates the window and if a command line argument was
specified, the file is opened. An "about" box is also
shown. */
Def init(self, cmdLine | fName, dlg)
{ initSystem(self);
window := new(AcctWindow,nil,nil,"Account Window", nil);
show(window, CmdShow);
if cmdLine
fName := words(cmdLine)[1];
if size(fName) > 1
fileOpen(window,fName + ".acc");
endif;
endif;
dlg := new(Dialog);
runModal(dlg, ABOUT_BOX, window);
}
Abstracting the User Interface page 11
Listing 2. The AcctWindow class.
/* Demonstrate Actor user-interface components.
AcctWindow inherits from the Window class.
Instance variables are shown in the inherit message.
*/
inherit(Window, #AcctWindow, /* instance variables */
#(accounts /* dictionary */
curAcct /* current
account */
acctList /* list box */
notesWindow /* text window
*/
chartWindow /* for a chart
*/
chartType /* current style
*/
actions /* dictionary */
fName /* name of file
*/
dirty /* boolean flag
*/), 2, nil)
/* Create the window with min, max buttons. */
Def create(self, parent, wName, rect, style)
{
^create(self:Window, nil, wName, rect,
WS_OVERLAPPEDWINDOW);
}
/* Initialize the AcctWindow and its instance variables. */
Def init(self)
{
acctList := new(List box, AW_LIST, self);
notesWindow := newChild(TextWindow, AW_TEXTWIND, self);
chartWindow := newChild(ChartWindow, AW_CHARTWIND, self);
initMenus(self);
chartType := VBarChart;
accounts := new(Dictionary, 5);
}
/* Show the window and its child windows.
Load the demonstration data also. */
Def show(self, scrnMode)
{
setText(self, "Loading...");
show(self:WindowsObject, scrnMode);
show(notesWindow, 1);
show(chartWindow, 1);
fName := "ACCTWIND.ACC";
Abstracting the User Interface page 12
fileOpen(self, fName);
show(acctList, 1);
setText(self, caption);
}
Abstracting the User Interface page 13
/* Respond to message to resize.
Resize the child windows. */
Def reSize(self, wp, lp | bot, rt)
{
rt := right(clientRect(self));
bot := bottom(clientRect(self));
setCRect(acctList, rect(0, 0, 125, bot));
moveWindow(acctList);
setCRect(notesWindow, rect(125, bot/2, rt, bot));
moveWindow(notesWindow);
setCRect(chartWindow, rect(125, 0, rt, bot/2));
moveWindow(chartWindow);
}
/* Initialize the menus. Actions not implemented here
will be handled by the chartWindow. */
Def initMenus(self)
{
loadMenu(self, "CWMenus");
setMenu(self, hMenu);
actions := new(Dictionary,10);
addAbout(self);
add(actions, AW_LIST, #showAcct);
add(actions, CW_FILE_NEW, #fileNew);
add(actions, CW_FILE_OPEN, #fileOpenAs);
add(actions, CW_FILE_SAVE, #fileSave);
add(actions, CW_FILE_SAVEAS, #fileSaveAs);
add(actions, CW_FILE_PRINT, #printChart);
add(actions, CW_FILE_QUIT, #close);
add(actions, CW_ADDITEM, #addItem);
add(actions, AW_ACCOUNT_ADD, #accountAdd);
add(actions, AW_ACCOUNT_DELETE, #accountDelete);
add(actions, CW_HBAR, #setHBarClass);
add(actions, CW_VBAR, #setVBarClass);
add(actions, CW_PIE, #setPieClass);
add(actions, CW_HELP, #help);
}
/* Handle menu commands using a data driven approach. The
first argument, menuItem, indicates the menu item ID.
Check to make sure that the menuItem exists and, if so
perform that action, otherwise it's an error. */
Def command(self, menuItem, lParam)
{ if actions[menuItem]
perform(self, actions[menuItem]);
else
beep();
errorBox("Command not implemented", asString(menuItem));
endif;
}
Abstracting the User Interface page 14
/* Clear the accounts. */
Def fileNew(self | dlg)
{
if not(dirty) or shouldClose(self)
clearAccounts(self);
fName := nil;
endif;
}
/* Prompt the user, then read a new file of accounts by
sending a fileOpen message. */
Def fileOpenAs(self | dlg)
{
if not(dirty) or shouldClose(self)
dlg := new(FileDialog, "*.acc");
if runModal(dlg, FILE_BOX, self) == IDOK
fName := getFile(dlg);
fileOpen(self, fName);
endif;
endif;
}
/* Load some data into the accounts.
Uses the object-storage facility from Lang. Ext. I. */
Def fileOpen(self, fName | acctFile, reader)
{
showWaitCurs();
acctFile := new(File);
setName(acctFile, fName);
open(acctFile, 0);
if getError(acctFile) == 0
reader := new(StoredObjectReader);
accounts := readFrom(reader, acctFile);
clearAccounts(self);
do(accounts,
{using(acct)
addString(acctList, name(acct));
});
else
beep();
errorBox("File Error", "Cannot read file " +
asString(fName));
endif;
close(acctFile);
showOldCurs();
}
Abstracting the User Interface page 15
/* Prompt the user for a filename, then save the accounts
by sending a fileSaveIt message. */
Def fileSaveAs(self | dlg)
{
if not(fName)
fName := "ACCOUNTS.ACC";
endif;
dlg := new(InputDialog, "Save As..",
"Enter File Name", fName);
if runModal(dlg, INPUT_BOX, self) == IDOK
fName := getText(dlg);
fileSaveIt(self);
endif;
}
/* Save a file of accounts using the current name. */
Def fileSave(self)
{
if fName
fileSaveIt(self);
else
fileSaveAs(self);
endif;
}
/* Actually do the work of saving the accounts.
Uses the object-storage facilities from Lang. Ext. I */
Def fileSaveIt(self | file)
{
showWaitCurs();
file := new(File);
setName(file, fName);
create(file);
if getError(file) == 0
storeOn(accounts, file, nil);
close(file);
dirty := false;
else
beep();
errorBox("File Error", "Cannot save file " +
asString(fName));
fName := nil;
endif;
showOldCurs();
}
/* Print the current chart. */
Def printChart(self)
{ printChart(chartWindow);
}
Abstracting the User Interface page 16
/* Delete the current account. */
Def accountDelete(self)
{
if not(curAcct)
beep();
else
remove(accounts, name(curAcct));
remove(acctList, getSelIdx(acctList));
dirty := true;
showAcct(self);
endif;
}
/* Add a new account. Prompt the user for input
by running an AcctDialog. */
Def accountAdd(self | dlg)
{
dlg := new(AcctDialog);
if runModal(dlg, AW_ACCOUNT_BOX, self) == IDOK
curAcct := new(Account);
setName(curAcct, name(dlg));
setNumber(curAcct, number(dlg));
add(accounts, name(dlg), curAcct);
addString(acctList, name(dlg));
selectString(acctList, name(dlg));
dirty := true;
showAcct(self);
endif;
}
/* Add an item to the account and to the chart.
The tuple is a label, value pair. */
Def addItem(self | tuple)
{
if curAcct
tuple := addItem(chartWindow);
if tuple
addData(curAcct, tuple[0], tuple[1]);
dirty := true;
showAcct(self);
endif;
else /* the user hit cancel */
beep();
endif;
}
/* Display help from resources. */
Def help(self)
{ runModal(new(Dialog), CW_HELP_BOX, self));
}
Abstracting the User Interface page 17
/* Set the type of chart, tell the chartWindow. */
Def setHBarClass(self)
{ chartType := HBarChart;
setHBarClass(chartWindow);
}
/* Set the type of chart, tell the chartWindow. */
Def setVBarClass(self)
{ chartType := VBarChart;
setVBarClass(chartWindow);
}
/* Set the type of chart, tell the chartWindow. */
Def setPieClass(self)
{ chartType := PieChart;
setPieClass(chartWindow);
}
/* Show the selected account if it's valid,
otherwise clear the current account. */
Def showAcct(self | acctName, chart)
{
if (acctName := getSelString(acctList))
curAcct := value(assocAt(accounts, acctName));
cls(notesWindow);
show(curAcct, notesWindow);
chart := new(chartType);
setLabels(chart, dataKeys(curAcct));
setData(chart, dataValues(curAcct));
setArea(chart, point(right(clientRect(chartWindow)),
bottom(clientRect(chartWindow))));
setChart(chartWindow, chart);
else
clearCurrent(self);
endif;
}
/* Clear the current account and where it is shown. */
Def clearCurrent(self)
{
setChart(chartWindow, new(chartType));
cls(notesWindow);
curAcct := nil;
}
/* Clear the accounts and where they are shown. */
Def clearAccounts(self)
{
clearList(acctList);
clearCurrent(self);
dirty := nil;
}
Abstracting the User Interface page 18
/* Close the window. An errorbox will appear if changes
have been made since the last time the chart was saved.
The choices "Yes", "No", and "Cancel" will be presented.
*/
Def shouldClose(self | answer)
{ if dirty
then
answer := new(ErrorBox, self, "Save changes?",
"No save since last modify", MB_YESNOCANCEL);
select
case answer == IDYES
fileSaveIt(self);
^true; /* true closes window */
endCase
case answer == IDNO
^true;
endCase
default
^nil; /* nil keeps the window */
endSelect;
endif;
}
Abstracting the User Interface page 19