objects that are descendents of the root object will be visible. ... descendents in depth-first order, frees the window's resources and initiates the refreshing of the ...
Iris: A Class-Based Window Library E.R. Gansner AT&T Bell Laboratories Murray Hill, NJ 07974 ABSTRACT The Iris library provides a basis for constructing user interfaces on bitmap displays. Written in C++, it provides a window class as the main data type, along with support for such graphical classes as point, rectangle, font, icon, etc. Windows can be created within windows; this property, along with the derived class notion in C++, allows the user to design complex interface objects built from simpler components in a modular fashion. In addition, a window can specify which input events, ranging from a change in a specific mouse button to a move request, it will accept. This paper provides an introduction to Iris and a discussion of its use. 1. Introduction The Iris library provides a basis for constructing user interfaces for bitmap displays. It implements a flexible hierarchical window system, integrated with the standard bitmap graphics primitives such as bitblt, line drawing and text display. In addition, the Iris model naturally supports multiple input and output contexts. The model is object-oriented; the programmer using Iris deals with ‘‘things’’ that have a specific set of associated operations. The model, with its emphasis on building an interface from pieces with welldefined interfaces, facilitates the design and construction of user interfaces, and provides a base for building interface toolkits. The principal design goals of Iris were simplicity and ease of use and reuse. For this reason, Iris is a small, basic library, meant to be understood. It is based on a simple graphics model, an extension of the one used on the Blit [PI1,PI2] terminal. If desired, the programmer can directly access the window system and the graphics primitives, without intervening software layers adding bias and preventing a simple action from being done simply. System details and hardware incongruities are hidden as much as possible, especially when they exist on a single machine. Dealing with the pieces of an interface, such as a scroll bar, on an ad hoc basis can be cumbersome. The programmer has to deal with how the parts combine and communicate with each other; how input is handled; how objects can be refreshed or reshaped. To get the full value out of an interface tool, it is useful if it can be reused, extended or modified in some natural fashion. Iris has adopted an object-oriented approach to address these concerns. In keeping with the goal of simplicity, the number of basic objects and their associated actions is minimal. The objects are sufficiently general as to not constrain the programmer, while providing a common thread for the later combination of objects. The objects are uniform and reflect common functionality. For example, the bitblt function is bound to a single name that works regardless of whether the source and target bitmaps are on screen or off. Similarly, the process of window creation is the same for all windows. A corollary of Iris’s design goals is that an object-oriented system should use a language that supports object-oriented programming and integrate itself with the language’s features and support. Indeed, a significant part of Iris’s simplicity derives from its implementation in C++, a language sympathetic to its model. The language’s data abstraction facilities supports Iris’s notion of a graphical object. Class inheritance, especially from multiple base classes, combines with the Iris window hierarchy to provide a simple yet powerful mechanism for creating new objects from old. The virtual function polymorphism gives the library a means of implementing the generic window operations in a uniform, type-safe manner for all graphical objects. A related benefit is that Iris avoids the mess of names and types, and the type 1
insecurity, that plague systems implementing an object paradigm without language support. Another aspect of the ease of use of a window system involves the run-time environment of the program, and the burdens it places on the programmer and the program. Iris is designed to operate in a standard UNIX† environment, not in a rarefied graphics environment. There are no assumptions that, for the graphics part, there are actions a program would not or should not take. The familiar set of tools, libraries and facilities for constructing and analyzing programs are all available for the interface code. 2. The Iris Model Iris was designed to provide a foundation for constructing user interfaces from high-level components while allowing access, when necessary, to low-level graphics primitives. In this, it follows a bitmap graphics analogue to a design consideration of C++ for general programming. The C++ language does not include such constructs as run-time typing or a list class, but it allows them to be built. Similarly, Iris provides a collection of simple, generic constructs for bitmap graphics. The programmer can extend this base as desired with the creation of additional graphical classes. The idea of a graphical object (or widgets, in X terminology [SA]) is central to the Iris model. The programmer should view a graphical object as a physical construct, with limited, well-defined interactions with the rest of the world. A graphical object might closely mimic ‘‘real’’ components, such as meters, switches, buttons and oscilloscopes; provide a graphics abstraction of ‘‘real’’ components, such as a scroll bar or a virtual terminal; or instantiate an object with no physical analogue. New components can be made by tuning the parameters of extant objects, or building a new object out of one or more available objects. In this model, the construction of an interface involves gathering an appropriate collection of graphical objects and specifying how they interact, within the constraints of an object’s interface. This model provides an attractive base for using high-level languages and interactive editors in the specification of interfaces. This view of an object as a physical reality is one of the paradigms for object-oriented programming. In the Iris context, this paradigm works particularly well. First, the programmer can actually see the objects. Second, since Iris objects are built using a language that supports object-oriented programming, the manipulation and derivation of graphical objects as objects comes naturally. The basic object supplied by Iris is the window. (In other systems, a similar object might be called a viewport, a canvas, a layer, or a pad.) It provides a surface for all graphical output and a context for input. Lines can be drawn, text written, areas filled in a window, among other primitive graphics operations. A window can specify its interest in certain input events, and provide handlers to deal with the events when they occur. The coordinate system used to specify graphics output and report user input are window coordinates, i.e., the origin occurs in the upper left-hand corner of the window, with increasing y values moving the point down, and increasing x values moving the point to the right. Section 3 gives additional detail concerning the attributes and functionality of Iris windows. Iris windows form a base class in C++: they are meant to be used to derive higher level graphical objects. As is usual in C++, some derivations may simply add functionality to the base class, allowing the base class to show through. Other derived objects will totally hide its base class behind a layer of abstraction or replace some base class functions with its own. Typical Iris objects formed in this way include scroll bars, buttons, switches, banners, virtual terminals, text editors, list viewers, tile editors and dialogue boxes. In addition to being created as derived classes, new objects can come about through the Iris window hierarchy. Basically, one Iris object can include a variety of other Iris objects as children or subwindows. This technique allows an object to use the well-defined services of other objects to enhance its interface. A canonical example of this technique is a virtual terminal object, which contains three children: a text editor object, for displaying character output and receiving keyboard input; a banner object, for displaying status __________________ † Registered trademark of AT&T.
2
information and providing certain window services; and a scroll bar, for allowing the user to move the text view. In turn, some of these objects may be further subdivided. For example, the scroll bar might consist of a slider object and two arrow button objects, while the banner might contain buttons for deleting or resizing the window. Figure 1 show some graphical objects built using various combinations of C++ subclasses and the Iris hierarchy. The objects include a sketch pad, a tile editor and a virtual terminal.
Figure 1. Some graphical objects. A parent object’s role can be passive, in which it creates the children, hooks them together and gets out of the way, except for the occasional special service. In particular, a parent is usually responsible for repositioning its children when it is resized. At the other extreme, a parent object might take an active role, continually providing specific services or managing its children and their communications. Iris is initialized by creating the root window object corresponding to the display. Additional objects can then be created, optionally specifying a parent and a location in the parent’s coordinates. Only Iris objects that are descendents of the root object will be visible. A child object need not intersect with its parent, but only that portion of the child which overlaps with the parent is visible. A child object always lies on top of its parent; a child cannot be moved behind its parent. Sibling objects, however, can be freely shuffled among themselves. In addition, invisible Iris objects can be created; if a window is invisible, all of its descendents are invisible. Children of the root window, which we shall refer to as base windows, maintain a backing store, so that even if they are obscured by a sibling window, the obscured portions are stored in off-screen bitmaps. This allows Iris to handle automatically occasions when part of the window becomes exposed, without requiring the object to assist in the refresh. In addition, these objects can always act as the source of a bitblt operation without additional support from the object itself. The availability of backing store greatly
3
accelerates most screen refreshes, and simplifies the programmer’s job when creating new objects. Though any window can have a backing store, a non-base window usually shares its parent’s bitmap. In many cases, base windows will be tiled by their children, essentially providing the benefits of a backing store to the children without additional memory overhead. If a window can become corrupted, it is responsible for redrawing the corrupted portions by supplying a refresh function. Iris maintains the integrity of its windows, in that output in a given window will always be clipped to that window (and its parent) and will not affect any sibling windows or their descendents, even if the window is obscured by some siblings. In general, output in a window can only corrupt a window’s ancestors and descendents. When the destructor operator is applied to a window, it calls the destructor of all the window’s descendents in depth-first order, frees the window’s resources and initiates the refreshing of the display. Note that the display is refreshed only once, after the entire subtree is freed. This avoids the visual unpleasantness and time used to update windows that are themselves slated to be destroyed. Control flow in Iris is, by default, event-driven. Objects specify the types of events they are willing to handle, and supply a handler for such events. When an event occurs, such as a mouse button click or a timer event, Iris takes the event, determines who should get the event and passes it to the object using the object’s event handler. In most cases, the object handles the event as expeditiously as possible and returns control back to Iris. In this way, the user is presented with an interface built of multiple active contexts cooperating with each other. There are times when an object decides, for efficiency or for clarity of code, that pure event-driven control is inappropriate. Iris allows a program to forego the built-in scheduling entirely, to use it only periodically or to ‘‘grab’’ the event stream for certain critical periods. The Iris library itself makes use of this in its implementation of pop-up menus and other high-level input routines. An object’s event processing in Iris has an all-or-nothing flavor: accept one event at a time or temporarily take control of all events. This can sometimes constrain the programmer’s coding style, forcing object state information to be stored explicitly in the object when it would normally be stored implicitly in the stack frame . In addition, events targeted for other objects living in the same process can be delayed in delivery. The solutions offered by other interface libraries to similar problems are not totally satisfactory. The correct answer is probably to incorporate some multiple thread mechanism, as is done in NeWS [GO]. There are at present several general methods that could be used to support multiple threads in Iris. Specifically, one could use either the C++ task library [SH] or the C++ version of the Concurrent C language [GR]. We are currently pursuing a more radical approach (cf. [RG]). 2.1 Related Systems The interface programmer has a wealth of window systems, libraries and toolkits (cf. [EV,GO,LC,PA,RW,SA,SG], to mention a few) from which to choose. In terms of graphics functionality and networking, Iris is not the most developed. However, the two aspects that particularly distinguish Iris from similar work are its simplicity and its reliance on C++. Compared to Iris, the models of many window systems are more complex, some to the point of incomprehensibility. To do something simple might require much head scratching and programming overhead. Some packages insist the programmer deal with several layers of system tasks before actually being able to draw something in a window. This complexity can lead to a lack of uniformity, in which the programmer must do different things at different times to achieve a similar effect. At other times, the software layers impose restrictions preventing certain things from being done. Concerning graphics systems, the word sophisticated can sometimes be used in a pejorative sense. Iris is written in and for C++. Other interface packages, especially toolkits, are object-oriented in flavor, but provide little language support for maintaining the object paradigm. For this reason, they are cumbersome and limiting for the programmer, who must emulate or provide explicitly what would come for free from a compiler. Without language support, the names and types tend to become cluttered, and any use of inheritance will not benefit from type-checking. Also, transliterating a library’s header files into C++ 4
is not the same as designing and writing the library using C++. 3. Basic Classes Any bitmap graphics system needs some notion of points and rectangles. Thus, Iris provides the following structures: struct Ipt { short x; short y; }; struct Irect { Ipt origin; Ipt corner; }; The Ipt structure stores the x and y coordinates of a point. The origin in Irect specifies the upper left corner of the rectangle. The corner specifies the point one below and to the right of the bottom right point in the rectangle. Thus, the width and height of the rectangle are given by (corner.x - origin.x) and (corner.y origin.y), respectively. A pleasant bonus of using the C++ language is the ability to have operator arithmetic with points and rectangles. Instead of calling a function to add two points or to translate a rectangle, one sees the following, more aesthetic code: Ipt pt1, pt2, pt3; Irect r1, r2; . . pt3 = pt1 + pt2; r2 = r1 + pt3; . .
// Add two points. // Translate r1 by the vector pt3.
3.1 The Bitmap Class The class Ibitmap provides the target for graphical operations. An Ibitmap corresponds to a bitmap, along with a set of output member functions. These include the usual graphics operations such as marking a point, tiling a rectangle, drawing a line, writing text, and bitblting from one bitmap to another. All graphics operations are modified by a boolean function corresponding to how the bits are to be combined. In addition, the operations are clipped to the intersection of the bitmap’s boundary and its associated clipping rectangle. The geometric parameters passed to graphics routines are always in the bitmap’s coordinate system. This is the only convention that makes sense for the programmer, especially in an object-oriented, multi-window environment. Each Iris bitmap has associated with it various attributes. Most attributes have both a get function, for querying the attribute value, and a set function for setting the value, except in certain cases where the attribute is read-only. For example, each bitmap has a clipping rectangle to limit the effect of graphics operations, but is only used when clipping is activated. Iris provides the Set_clip_rect() function for setting the clipping rectangle, and the Set_clip() function for turning clipping on and off. In addition, each bitmap possesses a current point. This point is used as the initial point in line drawing and text operations. When drawing lines, Iris resets the current point to the final point supplied; when writing text, Iris resets the current point to the position where the next character would be written. 3.2 The Window Class The fundamental Iris class is the window object Iws. This provides the base class for most of the graphical objects using the Iris library. In essence, a window is a subclass of both Ibitmap and a tree class. This provides a window’s output functionality and the parent-child relationship between windows. In
5
addition, the window provides a context for user input. The window class adds to the functionality it inherits from the bitmap class with attributes and functions for handling the window hierarchy and input. The functions Top() and Bottom() move a window in front of and below all of its sibling windows, respectively. The Move() function moves the window so that its new origin corresponds to the argument point, given in the parent’s coordinate system. The Resize() function reshapes the window to a given rectangle, again in the parent window’s coordinate system. An Iris window can dynamically specify which input events it is interested in receiving. (Iris input is discussed below.) Other attributes associated with windows include cursor shape and position, various sizes related to the window, the window’s parent and whether the window is obscured. 3.3 Getting User Input Iris events are divided into several categories: keyboard events, mouse events, window events and system events. Mouse events include not only button clicks and mouse motion, but also the action of the mouse entering or leaving a window. Window events are high-level events connected to requests to change the external state of a window. System events involve timeouts and file I/O. All events are received by a window through an appropriate virtual function. The use of separate handlers for each type of event, as well as for refreshing, simplifies event handling somewhat. Keyboard events occur when a key is depressed; there are no provisions for handling up transitions. Keyboard events are not totally raw, in that the depression of metakeys (control, shift, etc.) are not reported when they occur, but only in the context of the depression of another key. A class derived from Iws must supply a virtual function Keybd_fn to receive these events. All mouse events are received through a virtual function Mouse_fn. The arguments specify the type of mouse event that occurred and the point at which the event occurred, in window coordinates, as usual. Iris provides other functions to query the current mouse state, buttons and position, at any time. For efficiency and consistency, it is recommended that when tracking the mouse, an object should poll the mouse’s position rather than rely on a stream of mouse motion events. With window events, a window is informed that someone would like to move, reshape, shuffle or delete it. This gives the object an opportunity to perform various cleanup operations. For example, an edit window about to be deleted might wish to give the user the opportunity to write its contents into a file. In addition, the window can use the return value to indicate its acceptance or rejection of the proposed action. The final category of events is system events. These include timeout events, in which a window asks to be notified after a certain time period; file events, corresponding to the need to service some open file; and requests for notification of the deletion of some object. Certain common input methods are supplied as member functions of Iris windows. The Get_pt() routine allows the user to pick a point within the window, using a specified mouse button. The routine can be set to return immediately when a button is pressed or to wait for the button to be released. The Get_rect() routine allows the user to specify a rectangle within the window, using a given button. The user moves the cursor to some position, corresponding to one corner of the rectangle, depresses the button, moves to another position corresponding to the opposing corner and releases the button. The current rectangle shape is indicated by rubber banded lines. The Move_rect() routine allows the user to place a given sized rectangle within the window, again using a given button. If the user depresses one of the unspecified buttons, all of these routines return immediately. By convention, this indicates the user’s desire not to make a choice and, by extension, not to invoke any related action. Another useful input operation, Track(), allows an object to loop on the state of a given mouse button while receiving the current mouse position. Iris also provides a pop-up menu class Imenu, with semantics roughly modeled on the menu library of the Blit. A menu is created by supplying the constructor with a NULL-terminated array of strings. There are general facilities for inserting, removing and replacing menu items. To display the menu, its Use() member function is invoked, supplied with a mouse button. As long as the button is down, the user can move the mouse cursor over the menu items, each highlighted when it is under the cursor. When the button
6
is released, the index of the current item is returned, with 0 being the index of the first item. If no item is selected, -1 is returned. The library uses basic Iris functions to implement all of the high-level input routines described above. They are provided as part of the library because of their frequent use, especially in providing the interface designer with basic input tools for prototype software. It is a simple matter to add alternative input methods and menu systems in the Iris environment. 3.4 Damage Control All window objects should supply a Refresh_fn virtual function. This routine will be called by Iris whenever it needs help restoring a window. This may occur if the window does not have an associated backing store, or if the window’s size has been changed, or if a child window is externally altered. One argument to Refresh_fn specifies the type of restoration necessary, in particular indicating whether the window has been resized. This alerts the window to reset any size dependent parameters it uses and, if necessary, to reshape its children. After being reshaped, each child will have its own Refresh_fn called. Another argument specifies the region to be repaired. 3.5 Additional Objects Other useful Iris classes include Itile, Icursor and Ifont. An Itile represents a pattern of bits that can be used to fill a closed polygonal region. An Icursor is an icon that can be used to represent the mouse location on the display. An Icursor can be associated with any Iris window and is typically used to represent the current state of the window. If a window does not choose its own cursor, it inherits the one used by its parent. The Itile and Icursor are derived from the Ibitmap class, and can therefore be used and altered with bitmap member functions. Normally, though, the programmer will use bitmap constructors that take a character-based representation of the bitmap or the name of a file containing such a representation. The Iris font class represents a font used for writing text. The user creates an Ifont by specifying the pathname of the file containing the font’s representation. Once a font exists, the user can ascertain the maximum height and width of the characters in the font, as well as other information. There is also a function for determining the dimensions of a string if it were printed using the specified font. Iris provides a default font Idefont. 4. Using Iris The structure of a program using Iris can be very simple: create a global object, create some initial graphical objects and turn control over to Iris. Using Iris, the traditional program to print ‘‘Hello, world’’ looks like: #include main () { Iws Irect Iws
*globalWin = new Iws (); globalRect = globalWin->Get_rectangle (); *win = new Iws (0, globalWin, globalRect>>10);
win->Text (Ipt(2,2), "Hello, world", Idefont, Istore_op); Iris_loop (); } This creates the default global window, determines its rectangle and creates a base window inset by 10 pixels from the global window. The string is then written in the base window, translated slightly from the window’s origin. Finally, the program passes control of the program over to Iris. The routine Iris_loop() collects events and dispatches them to the appropriate objects.
7
Of course, a more typical program would use a variety of graphical objects, and establish a more complex initial scene before calling Iris_loop(). Frequently, the global window itself is some derived class. Figure 2 illustrates a prototype browser for trees built using Iris. It uses some ten different subclasses of Iws.
Figure 2. A tree browser. There are various nuances in creating graphical objects using Iris. The code above exhibits a standard way of composing one object out of others using the window hierarchy. This technique applies well for a container class that knows what it ‘‘looks like’’ and creates its own children, a top-down approach. It is also possible to create child objects first, essentially unparented, and later insert them in a container class. Creating new objects using C++ is done with the standard derivation syntax: class FormField : private Iws { ... }; But there are times when the programmer may find it advantageous to use a reference instead: class FormField { Iws *win; ... }; with less binding between classes. For certain applications, an object may need to have more flexibility than is provided by Iris_loop(). In these cases, the program can read directly from the stream of Iris input events using the function Iget_event(). This is the routine used by Iris_loop() when it distributes events.
8
5. Nuts and Bolts Iris has been used as the interface library for a variety of programs, including a C source analyzer, an economics analysis package and a programming environment generator. It has also been used as the foundation for the windowing system in the Pegasus environment [RG]. At present, Iris runs on AT&T 5620 or 630 terminals, paired with hosts running the UNIX operating system, and on various UNIX-based workstations. It consists of about 4000 lines of C++ code, with an extra 4000 lines of C code necessary for the terminal server when used with the 5620 or 630. 5.1 Maintaining Windows A major design decision in most window systems is how to maintain window integrity, especially if a window can be written on when partially obscured. Iris uses an extension of the layerop() method [PI]. The Blit uses this method to provide a single level of windows with backing store. The method is elegant, lends itself easily to most primitive bitmap graphics operations, and, in some sense, uses the minimum cost to maintain a backing store. In addition, it can be naturally extended to support the general hierarchy and semantics of Iris windows. 5.2 Iris and C++ Iris was designed with C++ in mind. It reflects the syntax and semantics of the language and, as explained in Section 1, attempts to use language features to the fullest to support its model. In particular, inheritance in C++ complements the Iris hierarchy in the creation of new objects. With this mixture of types, Iris depends critically on the virtual functions in C++ in order to cleanly manipulate objects as windows. The data hiding provided by classes and member functions has been a help for writing portable Irisbased programs. Specifically, Iris has been implemented using a library model on workstations while using a client-server model with the terminals. Yet, program source can be moved unchanged between machine types. The biggest disadvantage in using C++ has been its mutability. In attempting to remain current with the language design and fixes to the translator, one found that legal code one week might be illegal the next, and maybe legal again the week after. In addition, as useful new features appeared, it had to be decided if, when and how they could be incorporated into the design. 6. Conclusions The Iris library presents a flexible and simple model for constructing bitmap graphics interfaces. The model is general enough so that many graphics interfaces could be built using Iris. This generality is tempered by the object-oriented nature of Iris, especially as it is supported by C++. This nature encourages the construction of interfaces using ‘‘graphical objects’’, components whose reuse, extension and combination are facilitated by the C++ class and Iris window hierarchies. In this context, Iris provides a good example of the desirability of building object-based systems using a language that supports an object paradigm. Finally, we note that Iris blends well with the standard UNIX tools and techniques. Acknowledgments The author would like to acknowledge his debt to Jonathan Shopiro, who was a pioneer in the use of graphical objects in C++ and whose ideas provided a strong impetus for Iris. The comments and complaints of Fahim Jalili, Tom Kirk, Dave Korn, Minsky Luo and John Reppy, among others, were very helpful in providing additional direction for Iris.
REFERENCES
9
[EV] Evans, Steve, "The Notifier", Proc. USENIX Tech. Conf., Atlanta, GA, June 9-13, 1986, pp. 344354. [GO] Gosling, James, "SunDew - A Distributed and Extensible Window System", Methodology of Window Management, F. R. A. Hopgood et al., eds., Springer-Verlag, Berlin, 1986, pp. 47-57. [GR] Gehani, N.H. and W.D. Roome, "Concurrent C", Software-Practice and Experience, 16(9), Sept. 1986, pp. 821-844. [LC]
Linton, Mark A. and Paul R. Calder, "The Design and Implementation of InterViews", Proc. USENIX C++ Workshop, Santa Fe, New Mexico, 1987, pp. 256-267.
[PA]
Palay, Andrew et al., "The Andrew Toolkit - An Overview", Proc. USENIX Tech. Conf., Dallas, TX, February 9-12, 1988, pp. 9-21.
[PI1] Pike, R., "Graphics in Overlapping Bitmap Layers", ACM Trans. Graphics, 2(2), 1983, pp. 135160. [PI2] Pike, R., "The Blit: A Multiplexed Graphics Terminal", AT&T Bell Laboratories Tech. Journal, 63(8), 1984, pp. 1607-1631. [RG] Reppy, J.H. and E.R. Gansner, "A Foundation for Programming Environments", Proc. 2nd ACM SIGSOFT/SIGPLAN Symposium on Practical Software Development Environments, Palo Alto, CA., 1986, pp. 218-227. [RW] Rao, Ram, and Smokey Wallace, "The X Toolkit: The Standard Toolkit for X Version 11", Proc. USENIX Tech. Conf., Phoenix, AZ, June 8-12, 1987, pp. 117-129. [SA]
Swick, Ralph R. and Mark S. Ackerman, "The X Toolkit: More Bricks for Building User-Interfaces or Widgets for Hire", Proc. USENIX Tech. Conf., Dallas, TX, February 9-12, 1988, pp. 221-228.
[SG]
Scheifler, R.W. and J. Gettys, "The X Window System", ACM Trans. Graphics, 5(2), April, 1986, pp. 89-97.
[SH]
Shopiro, J., "Extending the C++ Task System for Real-Time Control", Proc. USENIX C++ Workshop, Santa Fe, NM, Nov. 9-10, 1987, pp. 77-94.
10