home *** CD-ROM | disk | FTP | other *** search
Text File | 1990-10-19 | 34.8 KB | 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