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