Object Oriented Programming with C++ and OSF Motif 2nd Edition_djvu
Object Oriented Programming with C++ and OSF Motif 2nd Edition_djvu
Object-Oriented
Programming with C++
ON
go
Object Oriented Programming E
with C++ and OSF™/Motif
2ND EDITION
Douglas A. Young
pete
http://www.prenhall.com
CIP
The publisher offers discounts on this book when ordered in bulk quantities. For
more information, contact:
Corporate Sales Department, PTR Prentice Hall, One Lake Street, Upper Saddle River,
NJ 07458
Phone: 800-382-3419 Fax: 201-236-7141 E-mail (Internet); corpsales @prenhall.com
All rights reserved. No part of this book may be reproduced, in any form or by any
means, without permission in
writing from the publisher. All trademarks and registered trademarks are the
property of their respective owners.
Trademarks
vi
D-
AÍ
gee 7%,
pi
Contents
3.5 Summary
6.1 An Overview
119
120
123
127
136
145
146
147
147
149
153
161
167
168
169
174
186
188
192
205
208
210
214
215
219
221
229
234
235
236
240
Contents vii
viii Contents
12.5 Actors
Bibliography
Getting Source Code
index
393
393
396
402
409
417
424
425
430
433
435
437
439
Preface
This book grew out of a simple question: “How do I use the Motif user interface
toolkit when
programming in C++?” The most direct answer to this question, one that provides
only the
mechanics of the solution, is fairly simple. But fully addressing all the
surrounding issues is a more
complex task. In particular, a complete answer must include ways to coordinate the
programming
model supported by X and Motif with the C++ object-oriented programming model. This
is the
into account current versions of X and Motif, with their increasing level of C++
support. However,
the material in this book is largely independent of any specific version of X or
Motif. While most
Motif books necessarily dwell on explanations of how to use the features of
individual widgets, this
book concentrates on how Motif can be used within the context of an object-oriented
architecture.
This includes not only the mechanics of using C++ with Motif, but also includes
stylistic and
design issues. These issues tend to remain the same from version to version of
Motif.
This book has another underlying theme that is at least as important as how to use
Motif with
C++. In the past few years there has been a fundamental shift in the type of
software programmers
develop. The textbooks we study in school often focus on algorithms and basic data
structures.
While no one would dispute that data structures and algorithms are the heart of any
application,
most modern programs cannot be considered complete without additional elements. For
most
applications written today, choosing appropriate data structures and determining
the right
algorithms are only the beginning.
One of the goals of this book is to help the reader understand how to structure
applications to
address some of these issues and how using C++ can help. Simply understanding how
to display
and manipulate user interface components such as Motif widgets is only the
beginning. It is also
necessary to understand the subtle constraints X and Motif impose on an
application’s architecture
as well as the way an object-oriented approach affects an application’s design and
implementation.
Someone is sure to ask, “Why should I use an Xt-based toolkit like Motif with C++
in the first
place? If I am writing an object-oriented program, shouldn’t I use a ‘true’ object-
oriented toolkit?”
There are many X-based, object-oriented user interface toolkits available to those
who feel that
object-oriented software must use nothing but pure object-oriented libraries. For
example, the
Interviews toolkit and its successor, Fresco, as well as the OI toolkit developed
by Solbourne are all
written in C++. Each of these toolkits has strong points and may be useful to C++
programmers.
Some may have promising futures. But for now, none of these have Xt’s and Motif’s
momentum,
support, and wide acceptance by both programmers and users.
There are some who will maintain that it is sacrilege to use a C-based toolkit like
Motif with
C++. If you fall into that camp, I can only say that this book is not written for
you. There are
literally hundreds of user interface toolkits available today, supporting a variety
of languages and
programming styles. You must decide which toolkit you want to use, based on your
needs and the
merits of each. There is much more to consider than the implementation language
used internally
by a toolkit. Issues such as present and future availability, acceptance by users
and programmers,
Preface xi
support, reliability, and so on must be considered. You must also consider the
environment in which
your application will be used and whether the user interface style supported by any
given toolkit
will be compatible with other applications in that environment. In many cases, or
perhaps even
most cases, these issues will be the most critical factors in your decision. This
book is written for
those who decide to use Motif and would also like to take advantage of the object-
oriented facilities
offered by C++. For those who choose C++ and Motif, I hope this book is useful.
All software in this book was tested on a Silicon Graphics Indy workstation running
IRIX 5.3.
The examples were compiled using Silicon Graphic’s native C++ compiler. All
examples were
tested with the X11R6 Xlib and Xt Intrinsics and Motif 1.2.3. The examples are
designed to be self-
sufficient and depend on nothing but standard UNIX and C++ libraries, X, and Motif.
Notice that
self-sufficiency places some limitations on the design of any software. For
example, many of the
classes described in this book could be improved or simplified by using a good
library of general-
purpose data structures. For those examples that could use such generic data
structures, my
available choices were to diverge from the central topic of the book to implement
the necessary
classes, to use a library that might not be available to the reader, or to
implement the examples
without the benefit of such classes. Because the topic of reusable classes that
implement common
data structures could be a book in itself, I chose the last approach in most cases.
For the purposes of
this book, the internal implementation of any individual class is of less interest
than the external
behavior of each class and the collective architecture of all classes in a system.
Writing a book is very much like developing a large piece of software. In both
cases, the idea
that one can ever find the “last bug” is an illusion. I have tried to make this
book as error-free as
possible, and I am grateful for the assistance of many people, mentioned below, who
helped in that
effort. However, I have no doubt that errors and deficiencies remain, in spite of
my best efforts.
Readers, of course, have an uncanny knack for immediately spotting errors that the
author has
overlooked repeatedly! My primary goal has been to produce a book that conveys
useful infor-
mation. I hope this book achieves that goal in spite of any errors you encounter.
Acknowledgments
I would like to thank the many people who helped me with this book. Jolly Chen,
Jean-Daniel
Fekete, Ken Fischer, Steve Friedl, Oliver Jones, Rick Kelly, David Lewis, Howard
Look, Anil Pal,
Michael Portuesi, Kim Rachmeler, and Rebecca Wirfs-Brock helped by reviewing early
drafts. Their
suggestions were invaluable in defining the final form of this book. My wife,
Teresa, helped by copy-
editing at all stages. I owe a special thanks to Shiz Kobara, who provided the
visual design of the
TicTacToe game described in Chapter 4. The portions of Chapters 4 and 5 that
address the interface
design of TicTacToe are based on his work and our discussions. This revised edition
also reflects the
feedback of readers who shared their ideas and experiences with me as they applied
the ideas in this
book to develop object-oriented systems using C++ and Motif.
Douglas A. Young
ių a a Sins, =p
x i WE
oa na i
rau
hue: Oe dain
ie pi y
ua.
ds E sii
g Ma
e i 2 i if
E oa A m
ELEA
dy 4. een
¿qu Ee
ie
a
g >
va
o WUN
oy
å y
E ” pr NE
JA ie a
HEHE EE:
A
Part |
Introduction to C++ and Motif
When using Motif and C++, the programmer must first overcome the feeling that
everything in
a program, including Motif widgets, must be a C++ object. Objects and classes are,
of course, a
central feature in any object-oriented program, but C++ programs usually involve
other data struc-
tures and functions as well. For example, most UNIX programs make system calls, use
various
UNIX library functions, and so on, even when they are written in C++. Although one
could create
an object-oriented interface to the file system or the operating system, it is
usually sufficient to call
the functions provided by various C-based UNIX libraries.
In many cases, the key benefit of object-oriented programming lies in the impact
this technique
has on an application’s architecture. Whether each individual element of a program
is implemented
as an object is seldom of critical importance. However, to fully realize the
benefits of object-
oriented programming, it is important that the programmer, designer, or architect
understand and
apply object-oriented principles when identifying and implementing a system’s major
components.
To get the most out of this book, the reader needs some basic knowledge of both
Motif and C++.
Chapter | provides a very brief and high-level introduction to X and Motif. The
material in this book
assumes the reader has already been introduced to, and can at least read, C++. Many
useful books
have been written about X, Motif, and C++, and programmers who need more
information about
these topics have a large selection from which to choose. In particular, the books
listed below
represent the definitive documentation for Xt, Motif, Xlib, and C++, respectively,
as this book goes
to press.
Asente, Paul, and Ralph Swick, The X Window System Toolkit, Digital Press, 1990.
Scheifler, Robert W., and James Gettys, X Window System, 2nd Edition, Digital
Press, 1990,
Stroustrup, Bjarne, The C++ Programming Language, 2nd Edition, Addison-Wesley,
1991.
These books provide comprehensive and authoritative coverage of topics that are
outside the scope
of this book, and are highly recommended as reference material to augment the
discussions in the
following chapters. The Bibliography on page 434 lists additional sources of
information. This book
focuses primarily on ways the programmer can structure an application to take
advantage of C++
and Motif. Except when crucial to the main point of the discussion, this book does
not provide
extensive explanations of how individual X or Motif functions work because that
information is
readily available elsewhere (for example, in the books listed above).
This book is divided into two sections. Part I introduces the mechanics of using C+
+ and
Motif with an emphasis on object-oriented techniques. Part II explores additional
ideas for using
Motif and C++, while developing a small library of useful classes that support
applications based
on Motif.
Many of the examples in this book place a strong emphasis on the thought process
behind the
development of each program. In many cases, the discussion includes reasons for
various decisions
and proposes alternate approaches. The intent is not to present a single “right”
way to develop
Motif applications with C++, but to explore and discuss various approaches and
possibilities.
This book is written with the assumption that it will be read from beginning to
end. However,
because readers’ backgrounds and interests vary widely, it is likely that many
readers will not do
that. The following chapter summaries serve as a guide for those who want to get to
the essential
material quickly or who do not need the background information discussed in the
opening chapters.
Chapter 1, A Motif Tutorial Using C++, introduces the X Window System, the Xt
Intrinsics and the
Motif widget set from the perspective of a C++ programmer. Those who have used
Motif with ANSI
C may wish to skip this chapter.
Chapter 2, C++ Classes and Widgets, introduces the approach used in this book to
take advantage
of both C++ classes and Motif widgets. This chapter provides essential information
that serves as a
basis for the remainder of this book.
Chapter 3, Designing with Objects, discusses some techniques for designing object-
oriented appli-
cations. The design techniques and notation described in this chapter are used in
examples
throughout the rest of this book.
Chapter 7, Dialogs, discusses ways dialogs can be used in Motif applications and
presents some
classes that make it easier for applications based on MotifApp to use dialogs.
Chapter 9, A Simple Menu System, presents the MotifApp menu system, which is based
on the
classes introduced in Chapter 8.
Chapter 10, Lengthy Tasks, discusses the challenge of supporting an operation that
consumes an
extended period of time in an interactive program. This chapter describes some
facilities a
framework like MotifApp can provide to help programmers handle this common problem.
Chapter 12, A MotifApp Application, presents an extended example that uses many of
the features
of the MotifApp framework developed in earlier chapters.
This book follows several naming and coding conventions common in C++ and object-
oriented
programming. All variable and class names use a mixed case convention in which
words are
capitalized and concatenated. Names of classes begin with a capital letter, and
names of instances
begin with a lowercase letter. For example:
MainWindow *myWindow;
Data members and member functions also use mixed case and start with a lower-case
letter. For
example:
myWindow->className () ;
Application *theApplication;
All classes in this book use encapsulation to separate the class’s external
protocol from its
implementation. Therefore, classes contain no public data members. Following a
common
convention used by C programmers to indicate private symbols, data members are
preceded by a
“_” character as a further reminder that the member is private. When other classes
need access to a
data member, the class provides an inline access function. Using an underscore for
the private data
member allows the access function to have the same name as the data member, but
without the
underscore. So, a sample class that contains some data needed outside the class can
be written as:
class SampleClass {
public:
SampleClass(); // Constructor
// Access functions
If a class needs to allow an outside entity to set the value of a data member, the
class can
provide a function that assigns a new value to the protected data. In the above
example, if we want
to extend the previous example to allow the _someData member to be changed, we can
provide a
setSomeData () member function like this:
class SampleClass {
private:
public:
SampleClass() ; // Constructor
// Access functions
This book presents many C++ classes. It is usually most convenient to maintain each
C++
class separately, in two files. The class is declared in a header file, while the
member functions are
implemented in a separate source file. The name of the header file is usually the
name of the class,
with a .h suffix (e.g., Stack.h), while the name of the source file is the name of
the class with a .C
suffix (e.g., Stack.C). Unfortunately, there is no standard source file suffix for
C++, and your
environment may use .C, .c, .cc, .cxx, or some other convention. Some programmers
also use a .H
suffix for the header file. This book uses .h and .C.
This book displays line numbers to the left of most code listings. Code may be
discussed in
small segments, usually consisting of individual functions. However, the line
numbering is
continuous within any given file and restarts at 1 for each new file. Occasionally,
there may be
small examples that are incomplete or are otherwise not associated with a file.
These code segments
do not have line numbers to distinguish them from the others.
To prevent problems with multiple declarations, which can easily occur when header
files are
nested, all class header files have the form:
#ifndef CLASSNAME_H
#define CLASSNAME_H
// class declaration goes here
#fendif
In each file, CLASSNAME is replaced by the name of the class. For example, a header
file for a
class named Stack would look like this:
3 PEELA ENA ES IAI SAA RRA A LALA AAA AAA EAT AAT AAT TAT
4 #ifndef STACK_H
5 #define STACK_H
6
class Stack {
public:
10
11 Stack(); // Constructor
12 // Miscellaneous members
13 by
14 tendif
The file Stack.C contains the implementation of all Stack member functions, as
follows:
tinclude "Stack.h"
Stack: :Stack()
{
1
2
3
4
5
6
7
8 // Various initialization statements
9
Notice that Stack.C includes Stack.h, which contains the class declaration. Any
other class that
references an object belonging to the Stack class needs to include the file Stack.h
as well. Because
class headers may be included in many different source files in a program, it is
important that the
header contain only declarations and not define functions (except for inline
functions) or variables.
The terminology used by C++ differs slightly from that used by many other object-
oriented
languages. C++ defines member functions instead of methods, calls a member function
for an object
instead of sending a message, creates base classes and derived classes instead of
superclasses and
subclasses, and so on. This book primarily uses the C++ terminology, but
occasionally slips into
the more traditional object-oriented mode when these terms make more sense in the
context of a
discussion.
Chapter 1
An X/Motif Tutorial Using C++
This chapter introduces the X Window System and the Motif widget set. The X Window
System,
also known simply as “X,” is an industry-standard window system that provides a
portable base for
applications with graphical user interfaces. Motif is a higher-level toolkit that
provides common user
interface components needed by X-based applications. Motif is written in C, based
on the object-
oriented architecture of the Xt Intrinsics, but it can be used by applications
written in C++. This
chapter uses C++ for all examples but does not address object-oriented programming
techniques.
Chapter 2 presents an object-oriented approach to using Motif and C++.
Section 1.1 introduces the architecture of X and the Motif widget set. Next,
Section 1.2
discusses function prototypes and shows how to link C libraries like X and Motif
with C++ applica-
tions. Section 1.3 introduces the basic programming model used by all Motif
applications. Section
1.4 provides an overview of the user interface components in the Motif widget set.
Finally, Section
1.5 introduces the X resource manager, a facility for customizing applications and
widgets.
Motif applications are built on three distinct layers. Xlib provides a low-level
interface to the basic
services of the underlying window system, the Xt Intrinsics library provides a
higher-level toolkit
architecture, and Motif provides a collection of components for creating user
interfaces. This section
discusses the basic features of the X Window System and shows the relationships
between X, Xt,
and Motif.
The server notifies each client when input is available by sending the client an
event. X
presents each event to clients as a union of C structures, Each event structure
indicates the specific
type of event and also supplies additional information about the event. For
example, when the user
presses a key on the keyboard, the X server generates a KeyPress event. Besides the
type of the
event, the KeyPress event structure reports what key the user pressed, the location
of the mouse
cursor when the event occurred, and other relevant information.
The X server places events in a per-process FIFO event queue, where the application
can read
the event. The server reports each event only to those clients that have
specifically requested events
of that type for the window in which the event occurred. Although the server
reports all events in
the order in which they occurred, clients do not necessarily process the events
immediately. Appli-
cations can read their event queues at any time, and they may be busy when the
event occurs.
Windows
The X server’s primary responsibility is to display and manipulate windows. In X, a
window is a
rectangular region on the screen. The server creates, destroys, and manipulates
windows at the
request of a client. Each window has a unique identifier, which clients must
provide in all requests
to manipulate that window.
X supports an overlapping window model in which windows can be stacked much like
papers
on a desk. Each set of sibling windows (windows that share the same parent) has a
stacking order.
The stacking order determines which window or windows appear to be “on top” and
which appear
to be partially or completely covered by the others.
Windows do not necessarily appear on a display device (a CRT or screen). When the
server
first creates a window, it is not visible. Clients can request the server to
display a window on the
screen by issuing a map request. Windows can be removed from the screen by
requesting the server
to unmap the window. Windows can be manipulated (moved, resized, destroyed) whether
they are
mapped or not.
Clients may request the server to manipulate windows on their behalf. This includes
raising or
lowering windows within their stacking order, moving their positions relative to
their parents,
mapping and unmapping them, and so on. Windows are always created at the request of
an
individual client and generally go away when that client exits or disconnects from
the server.
However, while a window exists, any client that knows that window’s ID can request
the X server
to manipulate the window.
X Graphics Facilities
X provides some basic drawing capabilities. Clients can request the server to draw
lines, circles,
rectangles, or polygons in any window. X allows lines to be drawn in various
widths, in any color
supported by the display device, and using various patterns. Figures can also be
filled with a pattern
or solid color. The basic X protocol supports only an integer coordinate system,
although this
protocol can be extended by packages like PEX (Phigs Extension to X) or OpenGL.
All graphics requests must be made relative to a drawable, which can be either a
window or a
pixmap. A pixmap is just a chunk of off-screen memory that can be the target of
graphics requests.
Pixmaps can be used to store or compose an image off screen before copying it to a
window.
Pixmaps are maintained in the X server and have unique identifiers, the same as
windows.
Window Managers
In X, the term window manager refers to an X client that allows the user to
manipulate each appli-
cation’s topmost window. A window manager allows the user to move windows, raise or
lower
windows relative to the others on the screen, iconify windows, and so on. An X
window manager is
simply a client that uses the server to manipulate other clients’ windows. All
window managers are
expected to obey a specific protocol for interacting with clients. This protocol is
documented in the
InterClient Communications Conventions Manual (ICCCM). The images in this book show
the
Motif Window Manager (mwm), but any ICCCM-compliant window manager can be used with
X
and Motif applications.
Applications based on Motif use at least three distinct libraries. The highest-
level library is the Motif
widget library, which contains common user interface components such as buttons,
scrollbars, and
menus. The second library is the Xt Intrinsics, which this book usually refers to
simply as “Xt.” This
library provides some common facilities needed to construct the components defined
by Motif, and
defines the architecture used by all widgets. The lowest-level library is the Xlib
C language interface
to X, which provides primitive operations that allow applications to create
windows, draw text and
simple graphics, and so on.
Figure 1.1 shows how these libraries relate to one another. The X server is a
process that
normally runs on a local machine and communicates with clients across a network
protocol. The
Xlib (libX11.a) layer handles the network traffic on the client side and presents a
low-level C
language interface to the facilities of the X server. The Xt Intrinsics library,
libXt.a, is built on top
of Xlib and hides many of Xlib’s lower-level details. Motif is built primarily on
the Xt layer but
occasionally calls Xlib functions directly. Motif applications typically use
functions from all three
libraries.
Application
Network Connection
~ X Server
Figure 1.1 The architecture of the X Window System
Xlib
Xlib provides the principal C language interface to the services supplied by the X
server. This library
encapsulates the mechanisms for connecting to the X server, sending requests to the
server, and
receiving events back from the server. Xlib contains approximately 290 functions
that allow appli-
cations to create windows, move and resize windows, draw text and graphics to
windows,
communicate with other clients, and much more. A detailed discussion of Xlib is
beyond the scope
of this book. For more information, the definitive book on Xlib is X Window System,
C Library and
Protocol Reference, by the creators of X, Robert Scheifler and J. Gettys
[Scheifler90]. For a more
tutorial introduction to Xlib programming, see /ntroduction to the X Window System,
by Oliver Jones
[Jones88].
At the Xlib level, one notable attribute of X is its “mechanism, not policy”
philosophy. X does
not dictate the look or feel of any application. Windows are simply rectangular
regions on the
screen, with no identifying decorations. Xlib also does not impose any strict
programming style on
applications that use it. But there are preferred ways to accomplish various tasks,
and X
programmers have developed many stylistic conventions. Some of these are specified
in the /nter-
Client Communications Conventions Manual (1ICCCM).
The Xt Intrinsics
Xlib defines a programmatic layer that is lower than many programmers prefer to
deal with. In
relation to the other libraries discussed in this section, one could compare Xlib
to the assembly code
level. Even a program that simply displays a short string on the screen can consist
of hundreds of
lines of code. Xt was designed to make it easier to write X applications by
providing more support
for the kinds of things programmers want to do with a window system, while hiding
many of the
details of Xlib.
The user interface components supported by Xt are called widgets. Typical widgets
include
buttons, scrollbars, and menus. A widget consists of a structure that contains data
and supports
functions that operate on those data. Each widget has an X window associated with
it, in which the
widget displays itself.
Many Xt-based widget sets have been written since Xt first became available,
although the
OSF/Motif widget set is currently the only widely used commercial widget set based
on Xt. The
Athena widget set, which is distributed as part of the standard X Window System
distribution, is
also based on Xt and remains popular because it is freely available. This book uses
the Motif
widget set, although most of the techniques described in the following chapters
apply equally to
any Xt-based widget set. The Xt library is usually installed as /usr/lib/libXt.a,
and its associated
header files are usually kept in /usr/include/X11.
l The location of the X and Motif header files and libraries is subject to wide
variations among systems. The locations dis-
cussed here are only likely places to look, and it is important to check local
conventions. Some installations may support
multiple versions of headers and libraries, and some sites use different library
naming conventions. Environments that sup-
port shared libraries may also have sharable versions of the X libraries. A local
expert should be able to locate the correct
libraries and header files.
Motif is a standard user interface toolkit developed and supported by the Open
Software Foundation
(OSF) and its member companies. Motif consists of several parts:
* The Motif widget set is based on the Xt Intrinsics. The standard Motif widgets
include compo-
nents that support user interaction, such as buttons, sliders, and popup and
pulldown menus.
The Motif widget set also includes components that handle assorted screen layouts,
dialogs
that can be used to display error and warning messages, and so on.
The Motif Style Guide contains rules and recommendations for achieving visual and
behav-
ioral consistency with other Motif applications.
This book uses the Motif widget set and does not discuss the Motif window manager,
the Motif Style
Guide, or UIL.
Unlike Xlib and Xt (which provide only the underlying mechanisms), the Motif widget
set
Supports a very specific appearance and behavior. Motif has a distinctive three-
dimensional
appearance and provides strong support for the interface style described in the
Motif Style Guide.
Section 1.4 introduces some specific Motif widgets and shows how they can be used
to build a
simple program.
Unlike Xlib and Xt, Motif is not freely available, but many system vendors include
Motif as
part of their system software, at little or no additional cost. Versions are also
available from the
OSF and many other sources for a fee. The Motif library is normally found in
/usr/lib/libXm.a, and
the header files are commonly installed in /usr/include/Xm. Motif Version 1.1.3, or
later, includes
headers that are compatible with C++. Later versions provide increasingly better C+
+ support. All
examples in this book assume Motif 1.2.
These three libraries, Xlib, Xt, and the Motif widget library, provide a powerful
and flexible
base upon which to create user interfaces. The following sections discuss the
programming model
used by applications based on Xt and Motif from the perspective of the C++
programmer.
However, there are a few simple things to watch out for. The first thing to be
aware of is
function prototypes. Function prototypes are used to declare the types of all
arguments to C++
functions.
X and Motif were originally written in pre-ANSI C. Functions are defined in pre-
ANSI C like
this:
void aFunction ( x, y, Z )
LE St; Y By
/* Empty Body */
This code segment defines a function that takes three integer arguments and has no
return value.
C programs sometimes declare functions before they are used, like this:
void aFunction();
In C++, all functions must be declared before they are referenced in any way. In
addition, the
type of each argument must also be declared. In C++, the above function would be
declared like
this:
This states that aFunction() is a function with no return value that takes three
integers as
arguments. This declaration is referred to as a function prototype. Notice that the
declaration
void aFunction();
is still valid, but in C++ this declaration means that aFunction() is a function
with no return
For example:
Like the C version, this function takes three integer arguments and has no return
value.
Before a C function can be called from C++, the function must have a prototype. As
of Motif
Version 1.1, all public Motif header files include function prototypes for all
public functions. The
prototypes are ifdef’d with the flag _NO_PROTO. When using C++, the prototypes are
used
automatically. For example, an excerpt from the file Xm.h might look something like
this:
#ifndef _NO_PROTO
Widget XmCreateForm() ;
#Hendif
Function prototypes are also necessary for any Xt and Xlib functions called from C+
+. Fortu-
nately, as of X11R4, all standard X and Xt header files also supply function
prototypes, which are
automatically activated when C++ is used.” Note that Motif or Xt header files whose
names end in
a “P” (IntrinsicP.h, for example) are private. These files are not meant to be
included by applica-
tions and may not include function prototypes for all functions.
Specifying C Linkage
In addition to providing prototypes for all functions, all X and Motif functions
must be declared to
be external C-language functions because the C++ preprocessor mangles the names of
nearly all
C++ functions. If the C++ preprocessor or compiler is allowed to mangle the names
of X or Motif
functions called from a C++ program, the linker will not be able to find these
functions in the X and
Motif libraries. This problem can be solved by surrounding all C function
prototypes or the header
files for any C libraries with an extern "C" declaration. For example,
extern "C" {
#include "aCheader.h"
extern void aCfunction ( int, int, char * y);
Notice that simply bracketing a function declaration with an extern "C" declaration
does not
eliminate the need for function prototypes.
As of Release 1.1, all Motif header files automatically include extern "C"
declarations.
Normally, this declaration does not need to be made explicitly in an application’s
code. However, if
a Motif or X header file does not include this statement, the extern "C" statement
can be placed
around the problem header file in the application code. The same technique can also
be used to call
functions from other C libraries within C++.
1.3
The Xt Intrinsics and all toolkits based on Xt define a strong programming model
that must be
followed when writing applications. Because Motif is based on Xt, Motif
applications follow the
same model. Most Motif applications perform the following basic steps:
Initialize Xt. This step initializes some internal data structures defined by Xt.
Every appli-
cation must also open a connection to the X server and load a resource database
containing
customization information. Applications must also create a data structure known as
an appli-
cation context.
Create a shell. All applications must create one or more shell widgets to serve as
containers
for all other widgets in the program. A shell widget acts as an intermediary
between the appli-
cation and the window manager. Every application that displays windows has one
shell
widget for each top-level application window. When the Xt functions described in
the
following section are used, this step is performed by the same function that
initializes Xt.
Create widgets. Widgets are the user interface components with which the user
interacts.
Most widgets can be customized by passing various parameters as arguments at
creation time
or after the widget has been created.
Register callback functions. Callbacks are functions that perform some application-
specific
action in response to some user input. For example, a program might register a
function to be
called when the user presses a button or selects an item from a menu.
Manage all widgets. Widgets form a hierarchy similar to the X window tree. Widgets
that
have children act as containers that group other widgets into some logical
collection. Each
container is responsible for determining the size and position of its children.
This process is
known as managing widgets. All widgets must be managed before they can appear on
the
screen. Programmers often use a convenience function that creates and manages a
widget in
a single step.
Realize all widgets. Realizing a widget creates an X window on the screen in which
the
widget can display itself. Widgets can be created, manipulated, managed, and so on
without
being visible on the screen. Once everything is ready, realizing all widgets makes
the appli-
cation’s windows appear on the screen.
Handle events. All Xt applications must enter an event loop to receive events from
the X
server. The event loop never returns and just continuously checks the event queue
for new
events. When an event occurs, the event loop removes the first pending event and
dispatches
the event to the appropriate widget for further handling.
A Simple Example
La 50 O <d ¡O UTA a BS PA
Arg args[10];
int n;
// Initialize Xt
n = 0;
XmStringFree ( xmstr );
XtRealizeWidget ( shell );
XtAppMainLoop ( app );
Now let's look at this program, step by step. We can break the program down into
the steps
Initialization
Every C or C++ program includes a certain amount of code that is nearly the same
from program to
program. This includes header files, declarations, and other statements that are
required by all
programs. The hello.C program begins by including Xm.h, the primary Motif header
file. Xm.h must
be included before any other Motif header file in all files that reference Motif
functions or widgets.
Xm.h includes the required Xt header files, such as Intrinsic.h, and also includes
all required Xlib
header files, such as Xlib.h. Programs that use Motif do not need to include these
files directly.
Following Xm.h, each file must include the header file for any widget referenced in
the file. Each
widget has its own header file. This example uses only the Motif XmLabel widget, so
it includes only
the file Label.h.
The main body of the program declares several variables needed by the example and
then, on
line 17, calls the function
Widget XtAppInitialize ( XtAppContext *appContext,
const String className,
XrmOptionDescList options,
Cardinal numOptions,
Cardinal *argc,
char **argv,
const String *fallbackResources,
ArgList args,
Cardinal numArgs );
The second argument, className, is a string that specifies the class of the
application. By
convention, the application’s class name should be the intended name of the
program, with the first
letter changed to uppercase. Note that this does not mean the programmer should
changeargv [0]
to uppercase. It means that if the program’s nominal name is “emacs”, the class
name should be
“Emacs.” The className argument identifies the program even if the user renames the
application.
The second and third arguments supply a description of command-line arguments that
should
be recognized by the application. All Xt applications recognize a number of common
command-
line options. The options argument allows applications to add application-specific
arguments to
The fifth argument must be a pointer to argc, followed by the array argv, as passed
to the
application from the command line. Xt searches the argv array for command-line
options it recog-
nizes. If any are found, Xt removes them and places them in the application's
resource database.
The last two arguments to XtAppTnitialize() allow the program to specify additional
arguments, in the form of an ArgList, to be passed to the shell widget created by
XtAppIni-
tialize(). We will discuss how to use an ArgList to customize widgets later in this
chapter.
There are two ways to create shells for multiwindow applications. The first is to
use the shell
returned by XtAppInitialize() for the first window and to create additional windows
by
calling XtCreatePopupShell (). This approach works best when the application has
one
window that clearly serves as the primary window, with all other windows playing a
secondary
role. When an application has multiple top-level windows that act as equals, it is
best to create all
windows as popup children of the initial shell returned by XtAppInitialize(). When
this
approach is used, the parent shell returned byXtAppInitialize() is not realized and
does not
appear on the screen.
The first argument to XtCreatePopupShell () specifies the name of the popup shell,
and
the second argument indicates the widget class of the shell to be created. The
third argument
specifies a parent for the shell. Popup shells must be children of some other
widget. The final two
arguments allow additional parameters to be passed to the shell widget in
anArgList.
Creating Widgets
The next major step is to create the widgets that form the application’s user
interface. The hello.C
example creates only one widget in addition to the shell, a Motif XmLabel widget
that displays the
“Hello World” string. The function XtCreateWidget () provides the simplest way to
create a
widget:
Widget parent,
ArgList args,
Cardinal numArgs );
The third argument to XtCreateWidget () specifies the parent of the new widget.
Except
for shell widgets, every widget must have a parent. In this example, the shell
widget, shell,
serves as a parent for the XmLabel widget. The last two arguments specify
additional arguments
that modify the widget’s behavior. We will discuss this below.
Motif also provides a convenience function for each widget class, which can be used
instead of
XtCreateWidget (). These functions have the form
where <widget> is replaced by the class of the widget to be created. For example,
the function
XmCreateLabel () creates a new XmLabel widget, and the function XmCreateRow-
Column () creates an XmRowColumn widget. Notice that the argument order of these
convenience
functions differs from the argument order of XtCreateWidget (). In XtCreateWidget
(),
the name of the widget precedes the parent, whereas the convenience functions
reverse this order.
All widgets except shell widgets must be managed by a parent widget. A widget’s
parent
manages the widget’s size and location, determines whether the widget is visible,
and may also
control input to the widget. Widgets can be managed by calling a separate function
after the widget
has been created, or they can be created and managed in a single step, using the
functionXtCre-
ateManagedWidget(). This convenience function has the same arguments as
XtCreateWidget ().
Customizing Widgets
Programmers can alter the default behavior of most widgets by providing additional
parameters
when the widget is created. These additional arguments can be passed
toXtCreateWidget () or
XtCreateManagedWidget(), using an ArgList. An ArgList is an array of type Arg,
which is declared as:
typedef struct {
String name;
XtArgVal value;
} Arg, *ArgList;
The name member of the Arg structure is a string that specifies a customizable
parameter supported
by the widget; the value member indicates a value for this parameter. These
customizable widget
parameters are called resources . If the size of the value stored in the value
member is less than or
equal to the size of XtArgVal, the value is stored directly in the Arg structure.
Otherwise, the
value member of a structure must represent a pointer to the value. The definition
of XtArgVal is
system-dependent, but is generally the size of an address.
This macro is often used with a counter, as shown in the code segment below.
Because
XtSetArg() is a macro that evaluates its first parameter twice, the counter must
not be incre-
mented inside the macro.
int ñ;
Arg args[10];
//
n= 0;
This code segment specifies values for two resources in an Arg array. The second
argument to each
XtSetArg() macro is a macro that represents a character string. These are defined
in Xm.h. For
example, the macros used above are defined as:
Using these macros allows the compiler to catch spelling errors that might go
unnoticed if character
strings were used directly.
Beginning with X11R4, Xt also supports vararg versions of many functions. These
functions
accept a variable number of name/value pairs in place of anArgList. The names of
all vararg
functions begin with the prefix XtVa. One such function is XtVaCreateWidget (),
which can
be used in place of XtCreateWidget (). Using the vararg version, the XmRowColumn
example above could be written as:
Many widget resources can be altered after the widget has been created by
constructing an
ArgList and calling the function
int n;
Arg args[10];
Widget rc;
Pe
n = 0;
Widget rc;//
rc = XtCreateWidget ( name, xmRowColumnWidgetClass, parent, args, n );
XtVaSetValues ( rc,
XmNwidth, 200,
XmNheight, 300,
NULL );
Compound Strings
creates a compound string from an array of ASCII characters. The “Hello World”
example creates a
compound string on line 22, as follows
The “Hello World” program uses the macro XtSetArg(), on line 27, to construct an
ArgList that specifies a compound string as an XmNlabelString resource. The XmLabel
widget uses this resource as the string to display in its window. Finally, because
the XmLabel
widget makes its own copy of the compound string, the program frees the compound
string on line
33.
Registering Callbacks
The next step in most Motif applications is to register any callback functions or
other similar
functions defined by the application to handle input or events. The hello.C example
does not define
any callbacks. The XmLabel and Shell widgets created by the “Hello World” program
handle events,
but they do so without requiring any action by the programmer. The Label widget
automatically
redraws the contents of its window when Expose events occur, repositions the
displayed text when
the window is resized, and so on. Callback functions are useful when the
application needs to take
some action in response to user input. The hello.C program is so simple that there
are no actions to
be taken.
Managing Widgets
All widgets except shell widgets must be managed by a parent widget. A widget’s
parent manages
the widget’s size and location, determines whether the widget is visible, and may
also control input
to the widget. For example, some widgets arrange their children to form rows and
columns, whereas
others group their children into resizable panes. Still others allow the user or
the programmer to
specify the location of each child widget.
To add a widget to its parent’s managed set, applications must call the function:
The “Hello World” example creates and manages the XmLabel widget in a single step,
using
the function:
Widget parent,
ArgList args,
Cardinal numArgs );
Realizing Widgets
Before a widget can be seen on the screen, it must create an X window in which to
display itself. The
process of creating an X window is called realizing the widget. A widget can be
realized by passing
itas an argument to the function:
Once the “Hello World” program has created the widgets it uses, it enters an event
loop on line 38.
The event loop is encapsulated by the Xt function:
Compiling a C++ Motif program is much the same as building a comparable C program.
The shell
command CC usually invokes the C++ compiler or translator. The program described in
the previous
section can be built with the following shell command:
This is, of course, nearly the same way an equivalent C program would be built,
except that
this command uses the C++ compiler. Notice also that the source file in this
example uses a capital
C as the file suffix. Normally, the C++ compiler does not care what file extension
is used, although
auxiliary tools, such as make, and editors, such as emacs, may not recognize
arbitrary suffixes.
Some systems may require other libraries, in addition to those shown here. For
example, the X
extension library (libXext.a) may need to be linked before the Xlib library. On
some systems, Motif
applications that use the FileSelectionBox widget may also require libPW.a or
libgen.a to be linked.
Some systems may also require additional libraries for network support.
To test the program, go to a terminal emulator window (xterm or similar) and type:
% hello
If all goes well, a window that looks like Figure 1.2 should appear on the screen.
[helo] + [I
The hello.C program creates only the middle portion of this window, the “Hello
World” label,
and the flat area around the label. The Motif window manager adds the surrounding
decoration.
Using a different window manager may produce a different appearance.
Callback Functions
Most widgets provide hooks that allow applications to define functions to be called
when some
widget-specific condition (usually related to user input) occurs. These hooks are
called callback lists.
The application’s functions are known as callback functions, or simply callbacks,
because the widget
callbacks a particular type of widget supports can be found in the man page for
that widget or in a
Motif reference guide. Callback lists are identified by strings, although
programmers usually use
macros defined in the file Xm.h instead. For example, every widget supports an
XmNdestroyCallback callback list. If a widget’s XmNdestroyCallback list is not
empty,
each callback function on the list is invoked before the widget is destroyed.
Applications can use the following function to add a callback to a widget’s
callback list:
The first argument specifies the widget to whose callback list the function is to
be added. The second
argument, call backName, identifies the callback list to which the third argument,
the callback
function proc, is to be added. The application can use the final argument, cl
ientData, to specify
some application-defined data to be passed to the registered callback function when
it is called.
Because of the strong typing provided by C++, extra care must be taken when
callback
functions are declared. Both the type of each argument passed to the callback and
the function's
return type are important. Xt defines the prototype for a callback function as:
{
exit (0);
The C++ compiler or translator will probably give warning messages about the w,
callData, and clientData parameters being unused in the quitCallback() function.
These warnings are harmless, but they can be eliminated by removing the unused
parameters,
leaving only the type declarations. Making this change, the above callback could be
written as:
{
Oxi ( P yi
The first argument passed to every callback function is the widget for which the
callback is
called. (There is nothing to prevent the programmer from registering any given
function as a
callback for multiple widgets.) The second parameter is theclientData specified by
the appli-
cation in the call to XtAddCallback ().
The last argument contains data provided by the widget. The type and purpose of
this argument
can be determined by checking the documentation for the specific widget class. In
Motif, the
callData argument is always a pointer to a structure. At a minimum, the callData
structure
contains a widget-specific code that indicates the reason the function was called
and a pointer to the
X event that indirectly triggered the callback. Some callbacks cannot be easily
related to a specific
event, in which case the event member is NULL. The structure that contains this
basic infor-
mation is defined as:
typedef struct {
int reason;
XEvent *event;
} XmAnyCallbackStruct;
Some Motif widgets define more complex structures to report additional information
to callback
functions, but these structures always include the reason and event fields as the
first two
members of the structure.
if ( cbs->reason == XmCR_ACTIVATE )
{
exit (0);
Let's see how callbacks work with a brief example. The following program, pushme.C,
is very
similar to the “Hello World” program discussed earlier. However, this program
creates a Motif
XmPushButton widget instead of an XmLabel widget and registers a function to exit
the program
when the user clicks on the button. The XmPushButton widget class supports three
callback lists:
When the user presses a mouse button while the mouse cursor is inside an
XmPushButton widget,
the widget invokes the functions on the XmNarmCallback list. If the user releases
the mouse
button while the mouse cursor is within the bounds of the XmPushButton widget, the
functions on
the XmNactivateCallback list are called, followed by the functions on the
XmNdisarm-
Callback list. If the user moves the mouse cursor out of the XmPushButton widget's
window
before releasing the mouse button, only the functions on the XmNdisarmCallback list
are
invoked.
Let’s look at the code before discussing the example.
% PLIST ESELA LALL EL I PETTITT IAAT IAAT LAL EAT TALL TES ATT
4 +#include <stdlib.h> // Needed for exit prototype
5 #include <Xm/Xm.h>
6 #include <Xm/PushB.h>
Lina
13 XtAppContext app;
14 XmString xmstr;
15
16 // Initialize Xt
17
22
24
30 XmNlabelString, xmstr,
3% NULL );
32
35 XmStringFree ( xmstr );
36
39
40 XtAddCallback ( button,
41 XmNactivateCallback,
42 quitCallback,
44
47 XtRealizeWidget ( shell );
48 XtAppMainLoop ( app );
49
$970)
51
92
54
56 {
57 exit (0);
58 }
There is very little difference between this example and the previous one, and
there are only a
few new items to discuss in pushme.C. First, instead of creating an XmLabel widget,
the pushme
program creates an XmPushButton widget on line 27. This example uses the vararg
function
Notice that this program uses the XmNlabelString resource to specify the label
displayed
by the XmPushButton widget. The XmLabel widget in the previous example accepted the
same
resource. Motif uses an object-oriented architecture, implemented in ordinary C,
that supports a
type of inheritance. The XmPushButton widget is a subclass (an Xt-style subclass,
not a C++
derived class) of XmLabel. Therefore, the XmPushButton widget inherits most of the
behavior and
resources supported by the XmLabel widget and also adds some behavior of its own.
Another difference between pushme and the earlier example is the call to
XtAddCallback () to register quitCallback (). The quitCallback () function is
regis-
tered with the XmPushButton widget's XmNactivateCallback list and invoked when the
user
presses and releases the left mouse button while the mouse cursor is over the
widget.
Notice that the structure of this program is unchanged from the “Hello World”
example on
page 16. Both programs call XtAppInitialize() to open a connection to the X server
and
create a shell widget. Then, both create widgets that serve as each application’s
primary interfaces.
Finally, all widgets are managed, the shell is realized, and each program enters an
event loop. Even
more complex programs follow this same model.
We can compile the pushme.C example the same way as for the previous program:
Figure 1.3 shows the window created by running the pushme program.
The architecture of X and Xlib imposes several subtle but powerful constraints on
an application’s
structure. Applications must be aware of the client-server model, especially the
fact that applications
run asynchronously with respect to each other and with respect to the server.
Programmers must also
be aware that the server is a shared resource and that clients must cooperate by
not burdening the
server unnecessarily. Most of all, it is important to understand the ramifications
of the event-driven
model used by X and Xt. All X and Motif applications must be designed to process
events continu-
ously. This has several implications:
e Programs cannot block while waiting for input, as older-style programs might do
when
calling a function like scanf (). If a program blocks while waiting for a response
to a
question asked of the user, it cannot receive events. If an application does not
process the
event queue in a timely manner, the application’s windows will not be redrawn when
a
window is exposed. The application’s buttons and menus will not respond to user
input, and
so on. Applications can usually be structured to avoid the need for blocking input.
It is
possible to simulate blocking behavior in such a way that the user believes the
program is
blocked while waiting for input. Chapter 7 discusses this topic in more detail.
° Ifa program is busy performing some task and does not read the event queue, there
is no user-
friendly way to interrupt the application. Users in a UNLX environment take for
granted the
ability to type a Control-C at any time to interrupt a process. To an X
application, a Control-
C key sequence typed into an application’s window is just another sequence of
events for the
server to send to the application. If the application never reads its event queue,
it has no way
to detect an interrupt key. Chapter 10 treats this topic in more detail.
* The program should avoid performing any task that will take longer than the
user's perception
of a reasonable response time. If the program must perform a task that requires
longer than
this amount of time, it is best to break up the task into smaller tasks. Depending
on the
situation, the task could be done as a concurrent background task, or some other
means could
be used to do the job.
The last point requires some elaboration. As many user interface researchers have
pointed out,
the amount of time a user thinks is “reasonable” varies with the task. If the user
simply wants to
delete a character in some text, anything other than instantaneous response will be
perceived as
slow. But if the user requests all names that match a certain pattern from a
database containing the
equivalent of a large phone book, the user may be willing to wait somewhat longer.
However, the requirements of an X application change this scenario considerably.
Imagine an
X-based database program. The user may still be willing to wait a long time (even
several minutes)
for the results of a complex query. But the user will probably be dismayed if he or
she moves or
uncovers the application's window while waiting for the result, only to find that
the window is not
redrawn correctly. This can easily happen if the program is busy and does not
handle Expose
events that accumulate in the event queue. When a program is too busy to handle
events, the user
may think the program is dead, when it is actually busy and not handling events.
The issue of response time can show up in more subtle ways as well. If a program
does not
perform a task quickly and return to the event loop, the user may start to wonder
what is happening
and try to press other buttons. Discovering that the button does not respond, he or
she tries others.
These events are queued by the X server for the application to handle when it can.
When the
program finally returns, it processes all the mouse events generated by the user
while it was busy.
The Motif widget set contains many components, including scrollbars, menus,
buttons, and so on
that can be combined to create user interfaces. The most noticeable characteristic
of the Motif widget
set is its three-dimensional, bevelled appearance. Most Motif widgets contain a
border whose top
and left sides can be set to a different color than the bottom and right sides. By
setting these areas to
the appropriate colors, a three-dimensional shading effect can be achieved, as
shown in Figure 1.4.
If a widget’s top shadow is lighter than the background and the bottom shadow is
darker, the
widget appears to protrude from the screen, as seen in the widget on the left in
Figure 1.4.
Reversing these colors makes the widget appear to be recessed into the screen, as
demonstrated by
the widget on the right side of Figure 1.4. The colors that create this three-
dimensional effect can be
set by the user or the programmer. By default, Motif automatically generates the
appropriate top
and bottom shadow colors, based on each widget’s background color.
We can divide the widget classes provided by Motif into several categories based on
the
general functionality they offer. For example, some widgets display information,
and others allow
the user to select from a set of choices. Still others allow other widgets to be
grouped in various
combinations. The following sections describe the Motif widgets, divided into the
following
categories:
1. Display widgets. These are simple buttons, labels, text editors, and so on.
2. Manager or Container widgets. These are used to contain, or group, other
widgets.
4. Menus. Motif supports popup, pulldown, and option menus that allow the user to
select
from a set of choices.
5. Gadgets. Gadgets are meant to be more efficient versions of the Motif display
widgets.
Using gadgets can greatly improve a program’s performance in certain situations,
although
they have some restrictions that can limit their use.
Display Widgets
One of the simplest Motif display widgets is the XmLabel widget used in the “Hello
World”
example. The XmLabel widget displays a string or a graphical image (in the form of
a pixmap) ina
window. As in nearly all Motif widgets, the string displayed by the XmLabel widget
must be a
compound string. The XmLabel widget does not support any callbacks (other than the
XmNde-
stroyCallback supported by all Xt widgets and the XmNhelpCallback list supported by
all
Motif widgets). Figure 1.5 shows a simple XmLabel widget displaying some text.
A Label
All Motif button widgets provide callbacks to allow a program to perform an action
when the
button’s state changes. The XmPushButton, XmArrowButton, XmCascadeButton, and
XmDrawn-
Button widget classes support similar callback lists, which include the
XmNarmCallback,
XmNdisarmCallback, and XmNactivateCallback callback lists. Functions registered as
these types of callbacks are called when the button is pressed (armed), released
(disarmed), or
activated. A button is normally activated when it is armed and then disarmed while
the mouse
cursor is within the button widget. Motif buttons can usually be activated by the
<RETURN> key
as well.
The XmToggleButton widget supports a different set of callbacks than the other
button
widgets. Functions registered with the XmNvalueChangedCal lback list are invoked
when the
toggle button changes state. The call data structure supplied with this callback
reports the current
state of the toggle.
The XmText widget allows the user to edit single or multiple lines of text. In
single-line mode, the
XmText widget allows the user to enter a short string. In multiline mode, it
functions as a complete
text editor. The XmText widget supplies many convenience functions that allow the
programmer to
scroll the text, toggle the widget between edit and read-only mode, retrieve text
from the widget,
programmatically add text to the widget, and so on. The XmText widget is the only
Motif widget
that uses ASCII character strings instead of compound strings. Motif 2.0 contains
an additional text
widget that uses compound strings.
Motif provides two versions of the basic text widget: the XmText widget and the
XmTextField
widget. The XmTextField widget can display only a single line of text and is
intended to be more
efficient than the XmText widget when only a single-line text entry area is needed.
Figure 1.7 shows a Motif XmText widget with a pair of vertical and horizontal
scrollbars.
The XmText widget does not include scrollbars itself, but programmers can use the
XmScrollBar widget to scroll the contents of an XmText widget or they can use the
XmScrolled-
Window widget, as shown here. There is also a convenience function,
XmCreateScrolledText () that creates an XmText widget as a child of an XmScrolled-
Window widget and handles all the scrolling automatically.
AtToolkitInitialize() ;
Figure 1.7 A Motif scrolled text widget.
The XmScrollBar widget displays a slider that moves in a trough. The user can move
the slider, using
the mouse, to scroll other windows. The XmScrollBar widget also provides arrows at
either end of
the trough, which can be used to move the slider in the direction of the arrow.
Applications can
register callback functions to be called when the user moves the slider. The
callback lists supported
by the XmScrollBar widget include XmNvalueChangedCallback, called whenever the
slider
moves, and XmNdragCallback, called when the user continuously drags the slider. In
addition,
the XmScrollBar widget supports many special-purpose callbacks, such as XmN
toTopCallback
and XmNtoBottomCallback, called when the user moves to the top or bottom of the
scrollbar’s
range, and XmNpageIncrementCallback and XmNpageDecrementCal lback, called
when the user scrolls one page.
Figure 1.8 shows a Motif XmScrollBar widget.
The XmSeparator widget displays a straight line, which can be drawn vertically or
horizontally, The
XmSeparator widget is primarily a decorative widget, used to visually separate
different sections of
a window. Figure 1.9 demonstrates the visual styles supported by the XmSeparator
widget, which
are controlled by the XmNseparatorType resource.
XmSHADOW_ETCHED_IN
XmSHADOW_ETCHED_OUT
XmSINGLE_LINE
XmDOUBLE_LINE
XmSINGLE_DASHED_LINE
XmDOUBLE_DASHED_LINE
The XmList widget displays a list of text items and allows the user to select
entries on the list. The
items displayed by the XmList widget are specified as an array of compound strings.
The XmList
widget supports several selection styles and selection callback lists. For example,
the XmList widget
can be configured to allow the user to select single or multiple lines. Multiple
selections can be either
contiguous or noncontiguous. The programmer can register callback functions to be
called when the
user selects items from the list, when the user double-clicks on an item in the
list, and so on.
The XmList widget can also be used in conjunction with the XmScrolledWindow widget
to
create scrollable lists. Motif provides a convenience function XmCreateScrolledList
() that
can be used to create and automatically configure an XmScrolledWindow and an XmList
widget.
eit t GFI -
p ari nhri
Container Widgets
The Motif widget set provides many widgets that can be used to combine other
widgets into
composite panels. Composite widgets allow endless combinations of buttons,
scrollbars, text panes,
and so on, to be grouped in an application. Complex interfaces are constructed by
combining display
widgets or collections of display widgets in different arrangements. The layout of
each collection of
widgets is determined by the container widget that manages that collection.
The following sections discuss several typical Motif container widget classes.
The XmBulletinBoard widget class is a container widget with a very simple layout
policy. The
XmBulletinBoard widget allows children to be placed at absolute (x, y) coordinates
within the
widget. If no coordinates are provided for the XmBulletinBoard widget’s children,
the children are
placed at (0, 0).
The XmBulletinBoard widget is often used when the programmer wants to maintain
complete
control over the layout of a collection of widgets. Because children can be placed
anywhere within
the parent, complex layouts can be achieved relatively easily. However, the
XmBulletinBoard
widget provides no automatic way to dynamically adjust the layout when the user
resizes a
window. In addition, widget layouts based on the XmBulletinBoard and hard-coded
positions often
exhibit problems if the user changes the font sizes used by the children. Most
widgets resize in
response to changes in font size, which means that a layout based on one font size
may not work for
another.
The XmRowColumn widget is a subclass of the XmManager widget class that organizes
its children
as a grid of rows and columns. A large set of resources control the XmRowColumn
widget’s
behavior. For example, the XmNorientation resource determines whether children are
displayed
in row-major or column-major order. The XmNnumColumns resource controls the number
of
columns when the orientation is vertical or the number of rows when the orientation
is horizontal.
The XmNpacking resource can be set to XmPACK_TIGHT to pack all children as tightly
as
possible or to XmPACK_COLUMN to force all children to be aligned in evenly spaced
rows and
columns.
Figure 1.12 shows an XmRowColumn widget with six XmPushButton widgets as children.
The six children would ordinarily have different sizes, based on the length of
their labels. The
layout in Figure 1.12 is achieved by setting the XmNnumColumns resource to 3 and
the
XmNpacking resource to XmPACK_COLUMN. In this mode, the XmRowColumn widget forces
all
children to have the same width and to line up in evenly spaced columns.
Figure 1.12 The Motif XmRowColumn widget, configured to display three columns.
The XmRowColumn widget’s children do not normally change size when the XmRowColumn
widget is resized. However, the last widget in each row stretches and shrinks with
its parent if the
XmNadjustLast resource is set to TRUE.
If the XmNnumColumns resource is set to 1, the XmRowColumn widget's layout changes
to
that shown in Figure 1.13.
Figure 1.13 The Motif XmRowColumn widget, configured to display a single column.
Here, the XmRowColumn widget still forces all children to be the same size, but the
children are
placed in a single column.
Figure 1.14 The Motif XmRowColumn widget, with XmNpacking set to pack_tight.
The XmRowColumn widget can be used to provide general layouts but more often serves
other, more specialized, purposes. For example, the XmRowColumn widget can be used
as a
RadioBox widget that allows only one child out of a set of children to be selected
at one time. In
this mode, the children are generally some type of button widget. The XmRowColumn
widget is
most often used as a menu pane or menubar. The XmRowColumn widget is less useful as
a general
container widget because it imposes several restrictions on its children. For
example, the XmRow-
Column widget always forces all children to have the same height.
The XmForm widget is one of the most flexible of the Motif manager widgets and is
often used as
the topmost container in an application (excluding the shell widget). The primary
advantage offered
by the XmForm widget is that it is possible to create a collection of widgets that
dynamically resize
themselves according to a layout description that describes relationships between
widgets. The
XmForm widget offers more flexibility than the XmRowColumn widget and more support
for
dynamically resizable layouts than does the XmBulletinBoard widget.
The XmForm widget manages its children according to constraints that specify the
position of
each widget relative to another widget, known as a reference widget. Each child can
have a top,
bottom, left, and right reference widget. Each child of an XmForm widget can also
Support an
“attachment” for each of its top, bottom, left, and right sides. The attachment
specifies how the
child’s position relates to the reference widget’s position. Programmers can also
specify that any
side of a child of an XmForm should be attached to a particular position, relative
to its parent
XmForm widget.
{
// Create a form and four children
widgetA
// Attach widgetA to the left, top, and bottom of the form
XmNbottomAttachment, XmATTACH_WIDGET,
XmNbottomWidget, widgetC,
XmNleftAttachment, XmATTACH_WIDGET,
XmNleftWidget, widgetA,
XmNrightAttachment, XmATTACH_WIDGET,
XmNrightWidget, widgetD,
NULL );
XmNleftWidget, widgetA,
XmNrightAttachment, XmATTACH_WIDGET,
XmNrightWidget, widgetD,
NULL );
Figure 1.15 illustrates the attachments and approximate initial positions of the
XmForm
widget's children, as specified by the function above. The arrows in the figure
indicate an
attachment to a reference widget.
widgetB
: i widgetD
widgetA | 8
widgetC
The primary reason to use an XmForm widget is to allow the layout of the form's
children to
change dynamically when the form widget changes size. Therefore, the attachments
shown above
should be viewed as a behavior specification, not just as a static layout
description. For example,
let's see what happens to the layout described above when the parent XmForm widget
changes size.
Figure 1.16 shows the initial layout when the widgets created by the createForm ()
function are
first managed. Notice that the layout is almost exactly as drawn in Figure 1.15.
Each widget's width
is determined primarily by the width of its label. The two widgets on each end are
twice as tall as
the middle widgets because they are attached to the top and bottom of the XmForm.
Figure 1.17 shows the same window after it has been resized to be much taller.
Notice that widgetC maintains the same height, but widgetB has grown significantly.
This
is because widgetC has no top attachment and therefore is not stretched vertically.
However,
widgetB is attached to the top of widgetC and the top of the form and must grow to
maintain
this relationship. If we detach the bottom of widgetB and attach the top of widgetC
to the
bottom of widgetB, widgetC will grow instead. Both widgetA and widgetD have
stretched
to match the height of the form because these widgets are attached to both the top
and bottom of the
form.
Figure 1.18 shows what happens if the form widget is resized to be wider. Here,
widgetA and
widgetD still maintain the same width because they each have only a single
horizontal
attachment. However, widgetB and widgetC are both attached to the right side of
widgetA
and the left side of widgetD and must grow horizontally to maintain that
relationship.
The XmForm widget also supports the ability to attach a widget to a particular
position. For
example, in the above example, we could attach widgetA to the left side of the form
and to a
position equal to 33 percent of the form’s width. Then, widgetB and widgetC could
be attached
on the left to the 33 percent position, and on the right to the 66 percent
position. The top and bottom
positions of widgetB and widgetC could be set such that each widget occupies 50
percent of the
height. Finally, widgetD could occupy the remaining 33 percent of the width. This
type of layout
would initially look the same as Figure 1.16. However, the windows behavior when
resized would
be quite different from that shown in Figure 1.17 and Figure 1.18. When position
attachments are
used, each widget maintains the same relative size and position regardless of the
size of the
XmPForm.
Motif provides an XmDrawingArea widget for those applications that simply want to
display their
own text or graphics in a window. The XmDrawingArea widget is a container widget
and can
support children, but it is generally used as a simple drawing canvas. The
XmDrawingArea widget
supports callbacks that notify the application when the window is resized or
exposed. The widget
also supports an input callback for handling keyboard or mouse input within the
drawing area.
The XmPanedWindow widget places all children in a vertical stack. The user can
dynamically alter
the size of each pane by using the mouse to move a small “sash” associated with
each pane. Figure
1.19 shows an XmPanedWindow widget with three XmScrolled Window widgets as
children.
Using Containers
Let’s look at a simple program that demonstrates how to use several Motif container
widgets to build
a more complex interface than the hello and pushme examples discussed earlier. The
following
program creates a window layout that has two command buttons in a single row along
the top of the
application. A scrolled work space area occupies most of the application’s window
below the
command buttons. Figure 1.20 illustrates this window arrangement.
To create the window layout in Figure 1.20, we must combine several container
widgets, a
Shell widget, an XmForm widget, an XmRowColumn widget, and an XmScrolledWindow
widget,
with the simpler XmPushButton and XmDrawingArea widgets. The widgets form a
hierarchical
structure, known as the application’s widget tree. Figure 1.21 shows the hierarchy
corresponding to
the window layout in Figure 1.20. Each node of the tree shows the name of a widget,
with the
widget's class name shown below in italics. The hierarchy does not include the
scrollbars, which
are considered to be part of the XmScrolled Window widget.
widgettree
ApplicationShell
form
XmForm
|
Sais cies ahh ays eer ene
rowcolumn SW
XmRowColumn XmScrolledWindow
eas ees
runButton quitButton canvas
Figure 1.21 The widget tree for the window in Figure 1.20.
The following program creates the widget tree shown in Figure 1.21 and configures
the various
widgets to achieve the layout shown in Figure 1.20. As written, the program doesn’t
do anything,
but some actions or behavior could be added simply by assigning callbacks to the
XmPushButton
and XmDrawingArea widgets. Look at the code below, and then we will discuss it.
ON ADO PW Db Pp
Vow U U U WN DO ee ND DN N D em ee a Es ES $ pl et G
AUP WNnNF OW OAKDUMU ES WHR OW WAI HU RP WHY FO
37
38
39
40
41
42
43
44
45
46
47
48
49
MIAMI A AAA ANDA AAA AAA LAT AMAT ATA ALAAT AAT TITE
// Widgettree.C, Demonstrate a typical widget hierarchy
FELELLISTTTTT AAA AAA AAN AAA IA AAA AA AAA III 00
#include <Xm/Xm.h>
#include <Xm/RowColumn.h>
#include <Xm/Form.h>
#include <Xm/Scrolledw.h>
#include <Xm/DrawingA.h>
#include <Xm/PushB.h>
XtAppContext app;
// Attach a RowColumn widget top along the top edge of the Form
re = XtVaCreateManagedWidget ( "rowcolumn",
xmRowColumnWidgetClass,
form,
XmNleftAttachment, XmATTACH_FORM,
XmNrightAttachment, XmATTACH_ FORM,
XmNtopAttachment, XmATTACH_FORM,
XmNbottomAttachment, XmATTACH_NONE,
NULL );
sw = XtVaCreateManagedWidget ( "sw",
xmScrolledWindowwWidgetClass,
form,
XmNleftAttachment, XmATTACH_FORM,
XmNrightAttachment, XmATTACH_FORM,
XmNbottomAttachment, XmATTACH_FORM,
XmNtopWidget , YO,
XmNtopAttachment, XmMATTACH_ WIDGET,
NULL );
OO), EEE
CSS ESS ae
a O
ee ee
NARA AT
53 xmDrawingAreaWidgetClass,
54 sw,
55 NULL, O );
56
60 xmPushButtonWidgetClass,
61 TO,
62 NULL, 0);
63
65 xmPushButtonWidgetClass,
66 re,
67 NULL, O );
68
69 //
ae //
72
74
75 XtRealizeWidget( shell );
76 XtAppMainLoop( app );
TT }
This program uses a Motif XmForm widget to set up the basic layout of the scrolled
work area
and the row of buttons. The layout shown in Figure 1.20 is achieved by attaching
the XmRow-
Column container widget to the top, left, and right sides of the XmForm widget. The
XmScrolledWindow widget must be attached to the left, right, and bottom of the
XmForm, and to
the bottom of the XmRowColumn widget. These attachments are specified by sets of
resources
passed to XtVaCreateManagedWidget () when each widget is created. The XmScrolled-
Window widget manages an XmDrawingArea widget and the XmRowColumn widget manages
two XmPushButton widgets that allow the user to issue commands.
This example provides just one simple example of how different types of widgets can
be used
to form an application’s interface. The various Motif container widgets can be
combined in nearly
endless ways to achieve many complex layouts. Learning how to combine the available
widgets to
create different panels can require some investment of time. It is very helpful to
develop a feel for
the basic behavior of each type of widget, as well as a general understanding of
the many options
supported by each widget class.
Menu Widgets
The Motif widget set provides a versatile collection of widgets that allow
applications to create
popup, pulldown, and option menus. A menu pane consists of a popup shell widget
that manages an
XmRowColumn widget containing buttons, labels and, occasionally, other types of
widgets. The
buttons are the selectable entries in the menu. Actions can be associated with each
menu entry by
registering a callback function with each button in the menu. By combining
different types of
widgets, the programmer can create many different types of menus: pulldowns,
popups, cascading
pulldowns, cascading popups, option menus, and so on.
The following widgets can be used to construct menus:
* The XmRowColumn widget class functions as a menubar as well as both popup and
pulldown menu panes. When used as a menu pane, the XmRowColumn widget must be
created as a child of an XmMenuShell widget. Motif provides convenience functions
to create
both the XmMenuShell and the XmRowColumn widget at once. These functions are:
* The XmToggleButton widget can be used in a menu to toggle between two states.
* The XmLabel widget class is often used to add titles or subtitles to a menu.
* The XmSeparator widget can be used to delineate different sections of a menu. For
example,
an XmSeparator widget can be used to set off a title or subtitle from other menu
items.
Figure 1.22 shows an example of a Motif menubar, with a pulldown menu pane. The
menu
pane also has a second menu pane that cascades from the main menu pane to the
right.
The Motif widget set builds on the underlying popup facilities provided by Xt to
provide a versatile
set of dialog widgets. Motif uses a subclass of the Shell widget class, the
XmDialogShell widget
class, as the basis of most dialogs. However, the programmer seldom needs to deal
with the XmDia-
logShell widget class directly because Motif provides convenience functions that
create different
types of dialogs and popups. Most Motif widgets know when they are children of the
XmDia-
logShell widget class and automatically pop up and pop down their parent shell when
managed and
unmanaged.
The following are the Motif widget classes designed to be used as dialog widgets.
XmBulletinBoard. This widget is the base for many dialogs. It does not impose any
ordering
or geometry constraints on its children. The XmBulletinBoard widget provides
special
support for automatically popping up or down dialogs when an “OK” or “Cancel”
button is
activated.
+ XmCommand. This widget provides a command input region and a command history
region
that lists previously entered commands.
XmFileSelectionBox. This widget class allows the user to select from a list of
files and
navigate through the file system.
XmForm: This widget uses constraints attached to each managed child to determine
the
position of each child.
e XmMessageBox. This widget class displays a message to the user. The XmMessageBox
widget provides a message area, a symbol area, and three buttons labeled “OK,”
“Cancel,”
and “Help.”
XmSelectionBox. This widget class allows the user to select from a list of choices
displayed
in a scrolled list. The XmSelectionBox widget class also provides “OK,” “Cancel,”
and
“Help” buttons.
By setting appropriate resources, many types of dialogs can be created from these
basic widget
classes. Motif provides a large set of convenience routines to make dialogs easier
to create and use.
These convenience functions create the following types of dialogs:
The corresponding convenience functions add “XmCreate” before these names. These
dialogs
are not really new widget classes but are combinations of an XmDialogShell widget
and one of the
manager widgets described earlier in this chapter. These convenience functions each
return the
corresponding manager widget, not the XmDialogShell widget created as the parent of
the manager
widget. Motif tries to hide the XmDialogShell widget as much as possible,
Question Gia
TTT tert ec
For the most part, gadgets can be used just like other display widgets, although
there are a few
restrictions, primarily because the gadgets do not have associated windows. In
Motif, a gadget can
only have the same foreground and background colors as its parent widget.
Gadgets can support callback functions and have the same appearance as their
corresponding
widgets. Motif provides both widget and gadget versions of several interface
components. The
Motif gadget classes include:
XmArrowButtonGadget XmLabelGadget
XmSeparatorGadget XmToggleButtonGadget
XmCascadeButtonGadget XmPushButtonGadget
Motif also provides convenience functions that can be used to create gadgets. For
example, an
XmLabelGadget can also be created with the function:
e A A e
Nearly all widgets can be customized by the user or the programmer. Widgets are
customized by
specifying values for various resources supported by the widget. These resources
affect how widgets
behave, as well as how they appear. For example, the XmRowColumn widget supports a
resource
that determines how many rows and columns it has. All widgets support a resource
that controls their
background colors.
We saw how programmers can set resources programmatically on page 20, in the “Hello
World” example. There, the program controlled the label displayed by the XmLabel
widget by
specifying a value for the widget's XmN labelString resource. Often it is useful to
specify the
values of various resources in a file instead of in the program. Motif (and all Xt-
based widgets) use
a facility known as the resource manager to load resources from various
customization files. The
resource manager is just a collection of related functions in Xlib and Xt.
Resource Files
The resource manager creates an internal resource database and loads resources from
several files
when a program starts. Resource files are simply text files that contain
specifications of the form:
resourceName: value
If multiple values are specified for the same resource, the last specification
overrides earlier ones.
The resource manager loads four distinct resource files. These are:
* The application defaults file. This file is most often found in /ust/lib/X 1
1/app-defaults/
classname, where classname indicates the class of the application as specified by
an argument
to XtAppInitialize() or XtOpenDi Splay() (see page 17). The exact location of
this file is site-dependent and depends on the language being used and a search
path deter-
mined by the value of an XFILESEARCHPATH environment variable. The application
defaults file usually contains resources provided by the programmer(s) who wrote
the appli-
cation. These resources may be required to enable the application to run properly.
* The per-user application defaults file. This file allows the user to add to, or
selectively
override, some of the resources in the application’s defaults file. The per-user
application
defaults file is loaded according to a complex path-resolution scheme that involves
several
environment variables and search paths. A file with the same name as the
application’s class
is loaded from a directory found by searching a path designated by the
XUSERFILESEARCHPATH environment variable. By default, if the
XUSERFTILESEARCHPATH variable is not set, the per-user application defaults file is
loaded
from a directory specified by the environment variable XAPPLRESDIR. If that
environment
variable is not defined, Xt searches for a file whose name is the same as the
application’s class
name in the user’s home directory, as specified by the value of SHOME.
* The user's defaults file. These resources are loaded from a file named .Xdefaults
in the user’s
home directory or from the RESOURCE_MANAGER property on the root window. (See
[Scheifler90] for an explanation of window properties.) Users can use the
programxrdb to
store resources in the RESOURCE_MANAGER property. To initialize this property, many
systems are set up to invoke xrdb automatically when the user logs in. The user's
defaults
generally contain user-preferences such as color and font specifications.
* The user's per-host defaults file. The location of this file is determined by the
value of the
XENVIRONMENT environment variable. If the environment variable does not exist, the
file
$HOME/.Xdefaults-host is loaded, where host is the name of the system on which the
appli-
cation is executing.
the resource database. These are loaded after the resource files and therefore take
precedence over
similar resources specified in the files discussed above.
52 Chapter 1 An X/Motif Tutorial Using C++
The exact algorithm used to find and load these files is very complex. See
[Asente90] for
details on the loading sequence and the format of the various search paths and
environment
variables.
Resource Specifications
In Xt, all widgets, applications, and resources must have both a name and a class.
A resource name
can be specified relative to a program and a specific widget by combining the name
and class of a
resource with the widget names and classes in the program. A complete resource name
consists of
the name of a program, the name of each widget in a path through the widget tree,
and the name of
the desired resource. Let's look at the widgets created by the widgettree.C program
on page 45.
Suppose we want to change the label on the quit button in this example. If we name
the program
widgettree, we can identify the resource name of this label as:
The “*” character serves as a wildcard and can be used in place of one or more
names in a
resource specification. So, we can also specify the label of the quit button like
this:
When more than one pattern applies to a given resource, the resource manager uses a
complex
matching algorithm to determine which specification to apply. See [Scheifler90] for
a complete
discussion of the resource manager’s matching algorithm.
Summary 53
Widgettree*runButton*labelString: Run
Widgettree*quitButton*labelString: Quit
Widgettree*XmRowColumn*packing: pack_column
Widgettree*XmRowColumn*‘numColumns: 2
Widgettree*XmRowColumn*adjustLast: FALSE
Any resource file can contain any resource specifications. However, some resources,
such as
color choices, are usually left up to the user and should not be specified in an
application resource
file. Different users may have different color preferences and may also use
different displays with
varying color capabilities and characteristics.
It is also important to note that any resources set in the program itself (such as
the XmLabel
string resource in the “Hello World” program) cannot be changed in a resource file.
Programmers
must use good judgement when setting resources programmatically. Resources that are
clearly the
domain of user preference should not be overridden in the program. On the other
hand,
programmers should not allow resources that are essential to the operation of the
program to be
accidently overridden by the user.
1.6 Summary
E AAA IA A AAA ae
This chapter introduced the X Window System and programming with Motif, using C++.
We saw
how to initialize Xt, create and manage widgets, and register callbacks to handle
user input. One of
the first things programmers must learn when using Motif is how to assemble the
various widgets to
form the complex panels needed for an application. Programmers can choose from an
assortment of
containers that support many different layout policies. Learning to assemble these
components to
achieve the desired result takes practice and experimentation.
This chapter used C++ for all examples, although the programs are not much
different than if
we had written them in C. The primary difference is in the introduction of function
prototypes and
stronger type-checking. Chapter 2 introduces some techniques for using Motif and C+
+ with an
object-oriented style.
Chapter 2
C++ Classes and Widgets
There are two principal issues that must be resolved to use Motif effectively with
C++. The first is a
simple question of how to call functions in C libraries, such as Motif, from
programs written in C++.
This is straightforward and is discussed in the Motif tutorial in Chapter 1. The
second issue is a
matter of style, particularly when using the object-oriented features of C++. This
chapter begins to
address the style issue by discussing ways to use Motif with C++ classes.
After mastering the mechanics of mixing C++ and C, the next difficulty C++
programmers
usually encounter is that, like C++, Motif supports an object-oriented
architecture. In spite of some
surface similarities, the two object systems are different and incompatible. C++
programmers often
work with object-oriented libraries by creating new subclasses of various classes
in the library,
specializing them to suit the needs of their applications. Programmers who have
used this approach
may wonder how they can create a C++ class that is a subclass of a Motif widget.
The obvious
answer is that they cannot (although Motif 2.0 provides a limited ability to write
widgets in C++.)
While the inability to subclass from an arbitrary widget class may appear to be a
major obstacle at
first, it is not really a problem, as we will see.
Chapter 1 discusses one way to mix Motif and C++. That approach is simply not to
use the
object-oriented features of C++ and to write applications in a style similar to
that used for C
programs, with the addition of strong type-checking. While this approach may be
acceptable in
some cases and is certainly no worse than programming in C, it does not take full
advantage of the
C++ language nor of the many benefits of object-oriented programming techniques.
A second approach involves wrapping each Motif widget class in a C++ class, with
the goal of
mimicking the behavior of each widget class, but using C++ syntax. This method
offers the
advantage that all Motif widgets appear to be C++ classes to the application.
Wrappers allow a
programmer to maintain the illusion of using only C++ libraries throughout an
application by
55
hiding the normal programmatic interface to Motif. This approach often appeals to
programmers
who prefer C++ syntax and find the C syntax used by Motif to be unaesthetic within
a C++ appli-
cation. The consistency offered by widget wrappers can also be appealing. For
example, Motif
requires that compound strings be freed with xmStringFree(), widgets be destroyed
with
XtDestroyWidget (), pixmaps be destroyed with XFreePixmap (), and so on. Using
widget
wrappers, all these entities appear to be C++ objects which can be freed with the
delete operator.
We can summarize these three approaches by observing that the first approach
addresses
programming with Motif in C++ but ignores the object-oriented capabilities of C++.
The second
approach addresses programming with C++ and Motif using an object-oriented style
but assumes
that widgets are interesting objects within the application or that it is important
for Motif widgets to
look like C++ objects for some reason. The third approach, which is used throughout
this book,
addresses object-oriented programming with C++ and assumes that the classes in a
program
represent the interesting architectural elements of the application. Classes that
have a user interface
component use Motif widgets as primitive elements from which to construct that
interface. Not
everything in a C++ program has to be a class, and it is possible, and sometimes
useful, to take a
functional approach in portions of a program even when using C++. For example, this
book uses
the original C-based Motif programmatic interface directly, choosing to focus on
architectural
issues and application-level objects.
The remainder of this chapter focuses on techniques for creating user interface
components by
encapsulating collections of widgets in C++ classes. Section 2.1 begins by
exploring the basic
concept of a user interface component and introduces a base class that implements
some basic
features used by all user interface classes in this book. Section 2.2 explores some
design issues
related to user interface classes, and Section 2.3 describes a class that supports
the component
e e — —_—_ AA
Nearly all Motif applications create collections of widgets that are related in
some way. For example,
many applications support a system of menus. The user sees a menubar as a single
logical
component of the user interface. However, the Motif programmer must construct menus
from many
individual widgets. Usually, the programmer uses functions to build up higher
abstractions that let
applications deal with a “menu” rather than the individual pieces of the menu.
iii AN
i
2 * dummy.c: skeleton of a typical Motif C application
8
9
void main ( argc, argv )
10 int argc;
11 char *argv[];
12 {
15
16 /* Initialize Xt */
cle’
22
29
30 XtRealizewidget ( shell );
31 XtMainLoop ( app );
32 )
The above example uses individual functions to separate the task of creating menus
from the
task of creating a command panel. Each function creates a portion of the
application’s complete
widget tree. With some care, these functions can even be used to create multiple,
similar collections
of widgets. For example, the above program calls the function createCommandPanel ()
twice,
presumably instantiating a new collection of widgets each time. (Notice that this
example is over-
simplified. In a realistic implementation, it would be necessary to supply names,
callbacks, and so
on.) The function createCommandPanel () returns the root of the widget subtree it
creates for
further manipulation by the program. This approach allows the programmer to deal
with large
numbers of widgets by viewing them as a collection of widgets within a single
subtree.
C offers the programmer only limited facilities for organizing and managing such
collections
of widgets. In C++, we can do much better by encapsulating a collection of widgets
within an
object. The mechanics of creating widgets, specifying widget locations and other
resources,
assigning callbacks, and so on can all be captured in a C++ class.
This simple idea is the basis of all examples in the remainder of this book. We
will refer to
such classes as user interface components. A component not only encapsulates a
collection of
widgets but defines the behavior of the overall component as well. Widget sets like
Motif provide
simple, low-level building blocks, like buttons, scrollbars, and text fields.
Components are C++
classes that use these simple building blocks to create higher-level objects like
file browsers,
command panels, menus, and so on.
For example, consider the following simple C++ class, which combines an XmLabel
widget
and an XmText widget to create a labeled text area:
E FILEELL TIS IIL PES ES PATELLA AMIA ERIN AE EUA AAA VA ES FEAR
4 #ifndef LABELEDTEXT_H
5 +#define LABELEDTEXT_H
6 #include <Xm/Xm.h>
8 class LabeledText {
10 public:
A le L
14 private:
p
ul
19, $
20 #endif
#include
#include
#include
#include
#include
0 3] HAN UV FP UNP
ETE TILES EEL EEL LT TELE E EEE TAL LTA AAT ALTA IIIA LAAT LEG EES
// LabeledText.C: A simple C++ component class
ESTER ERRERATA AT TELLTALE PAL TELAT ATL ES FAT EAL EA IA AA
"LabeledText.h"
<Xm/Xm.h>
<Xm/RowColumn.h>
<Xm/Label .h>
<Xm/TextF.h>
pa
|
{
NNP PRP RP RPP RP RB
FOW ATA WH un
w
_text
parent, NULL, O );
_rowColumn, NULL, O );
XtCreateManagedWidget ( "text", xmTextFieldWidgetClass,
_rowColumn, NULL, O );
The following simple program shows how this class can be instantiated to create a
component
that appears on the screen.
#include
Oo WON HA MN FP WD P
PRR RB
UNEO
void main
"LabeledText.h"
// Initialize Xt
18
21 XtRealizeWidget ( shell );
22 XtAppMainLoop ( app );
23 }
This example uses a class to combine several widgets in a very simple and limited
way. The
class has several obvious shortcomings. For example, how can the rest of the
program interact with
any of these widgets? Widgets are seldom displayed on the screen for no reason. The
application
must be able to access and manipulate these widgets, to change the information
displayed by labels
or text, or to receive and act on user input.
C++ classes provide a natural way to associate such actions and behaviors with
logical collec-
tions of widgets. To show how this is done, we must first discuss how to use
callbacks and other
similar functions within C++.
The previous section introduced a simple technique for using a C++ class to combine
groups of
widgets to create more complex user interface components. Such a class not only
creates a collection
of widgets but also defines the overall behavior of the user interface component.
The intent is not
just to wrap up a collection of widgets in a C++ class. Instead, the goal is to
create the primary classes
needed by an application, some of which use widgets to implement a user interface
component.
For example, we might wish to write a class similar to the LabeledText class shown
above, but
which can be used to request a password from the user and to perform some action,
depending on
the validity of the password. To handle such a situation, we must be able to handle
user input,
which is normally done by registering a callback function with one or more widgets.
Ideally, we
could combine the functions that perform such tasks with the widgets by
encapsulating both as
members of a class.
However, callbacks pose a minor problem for C++ classes. C++ member functions have
a
hidden argument, which is used to pass the this pointer to the member function.
This hidden
argument, which is typically passed as the first parameter, makes ordinary member
functions
unusable as callbacks for Xt-based widgets. If a member function were to be called
from C (as a
callback), the this pointer would not be supplied, and the remaining arguments
would be
incorrect. If we want to use member functions to define the behavior of a C++/Motif
user interface
component, we have to arrange another way for a member function to be called as a
result of a
widget callback.
Fortunately, there is a simple way to handle this problem, although it requires the
overhead of
one additional function call. The approach is simply to declare a member function
to perform the
desired task and then arrange for a function that does not have the hiddenthis
argument to call
that member function. There are two basic ways to do this.
The first method is to use a normal function, just as we did in Chapter 1, which
can invoke the
appropriate member function. The only catch is that the callback function needs a
way to access the
appropriate instance whose member function is to be called. This can be provided by
specifying a
pointer to the object as client data when registering the callback.
For example, let’s create a Password class that is similar to the LabeledText class
previously
discussed, but which calls a checkPassword() member function when the user types
<RETURN> in the text area. The header file for the Password class can be written
like this:
TILTGLELALPATIASAL LAAT ALATA EEA AT ISAT ALAA AA AE
// Password.h: Retrieve and check a password
#define PASSWORD_H
#include <Xm/Xm.h>
class Password {
w 0 JO Us UNEP
H|
©
public:
e He
N He
erep
U pe Ww
private:
e re
1 M
ep
O © o
J?
#endif
NO
=)
#include <Xm/Xm.h>
#include <Xm/RowColumn.h>
#include <Xm/Label.h>
#include <Xm/TextF.h>
—
ho
14 {
i7
21 _rowColumn, NULL, O );
22 _text = XtCreateManagedWidget ( "text", xmTextFieldWidgetClass,
23 _rowColumn, NULL, O );
24
at
30 }
32 XtPointer clientData,
33 XtPointer callData)
34 {
36
38
41 )
The checkPassword () member function has full access to the other members of the
Password class and uses XmTextGetString() to retrieve the contents of the _text
widget.
The function shown here is incomplete. A complete implementation would need to
check the
retrieved password and take appropriate action if the password was right or wrong.
45
49 }
The only problem with the approach demonstrated here is that it requires an outside
function to
have access to all member functions that could be involved in actions related to
callbacks and user
input. Sometimes this might be satisfactory, but it would often require classes to
expose details that
are more correctly kept private.
Let's rewrite the above example using the static member function approach:
E FELL ERPS ESS LIT ALI EET TAA AA TSISMIS SA AAA IA AAA RAS
3- OSLILT EEA AAI EELEL ETAT ALT TL TAL ELT LAS TA IV EPA ELLI
4 #ifndef PASSWORD_H
5 define PASSWORD_H
6 #include <Xm/Xm.h>
7
8 class Password {
10 public:
11
13
14 private:
15
18 Widget _label;
19
25
28 #endif
Notice that both the checkPassword () member function and the checkPassword-
Callback () static member function are now declared in the private portion of the
class. These
members could be declared in any part of the class. However, if a member function
is not part of
the class’s external protocol, there is no reason for the function to be made
visible. The static
member function is called only by the widget with which it is registered, so there
is seldom any
reason for such callback functions to be declared publicly.
3 IANAAAAAA ANA A AAA DAI AAA NAAA ATTA ATTA TATA AAA
4 #include "Password.h"
5 #include <Xm/Xm.h>
6 #include <Xm/RowColumn.h>
8 #include <Xm/TextF.h>
11 ff
13
15 xmRowColumnWidgetClass,
16 parent, NULL, 0 );
17
19 xmLabelWidgetClass,
20 _rowColumn, NULL, O );
2A
23 xmTextFieldWidgetClass,
24 _rowColumn, NULL, O );
25
31 ( XtPointer ) this );
32 }
34 XtPointer clientData,
35 XtPointer callData)
co - DES 1
38
40
43 }
The static member function and the friend function approaches are similar in
spirit. The static
member offers better encapsulation and is the approach demonstrated in the
remainder of this book.
However, this use of static member functions is based on the assumption that the
calling conven-
tions between C and C++ are the same. This is not guaranteed to be true, and some
C++ compilers
may use different conventions. Friend functions can be used even with C and C++
compilers that
have incompatible calling conventions because they can be declared as extern "C".
However,
this book uses static member functions because they offer the simplest approach and
should rarely
be a problem in practice.
ISD ALE ALLA LILA TATA EDEL TALE RRA RAS LAAT RIE E
#ifndef PASSWORD_H
#define PASSWORD_H
#include <Xm/Xm.h>
class Password {
woanauw FP WN FP
public:
ph op
h o
=
NO
13 protected:
14
iv
21
26 #endif
FEEDEL ETAL EURE NINA AIA LTA LT ATARI ATL ALIS EI ITALIA LAE
// StrictPassword.h: Check a password more closely than usual
PELEELILAEL AE LAAT LTE ELT AL EAT PAR TAT LD ED TAL TITAS LILIA TS
#ifndef STRICTPASSWORD_H
#define STRICTPASSWORD_H
#include <Xm/Xm.h>
0 JJ AN MN 4 UY yn P
No)
10 public:
EL
14 protected:
15
12 $3
18 #endif
One concern many programmers have about the technique described in the previous
sections is that
the client data argument to XtAddCallback () is used for the instance pointer, and
therefore not
available for other uses. In practice this is a nonissue. Typically, in C
programming, the client data
argument is used to provide some data to a callback function that would not
otherwise be available.
When using the object-oriented approach discussed in this book, the callback
functions, and the
associated normal member functions are used entirely as internal connections
between the class and
the widgets contained within that class. Any data that a member function should
ever need is
contained within the class structure. Allowing access to any other data would be a
violation of the
class’s encapsulation.
There are other ways to look at this issue as well. For example, it is sometimes
necessary to
pass more than one piece of data as client data when programming in C. When this
situation occurs,
the usual approach is to create a structure that contains all the required data and
pass the address of
an instance of the structure. By passing an object’s instance pointer in C++, we
are doing exactly
the same thing. So, rather than losing the client data argument when using C++, you
are actually
gaining an entire structure in which to keep data that can be accessed within a
callback.
It is also worth noting that the client data argument is merely a convenient way to
provide the
instance pointer, and not the only way. For example, one could associate the client
data with a
widget by storing the pointer in the XmNuserData resource supported by all Motif
widgets. The
resource could be set at the point the callback is registered and retrieved in the
callback function.
We could also store the association in a hash table (such as the XContext mechanism
supported by
Xlib). Both of these approaches are more awkward and less efficient than simply
passing the
instance pointer as client data.
Another concern some programmers have is simply the amount of typing required to
write two
functions instead of one. This issue is easily solved. First, there are many
commercial tools that can
support the process of constructing a user interface, using the approach described
here. Because
most of these “user interface builders” generate code automatically, the extra
callback functions do
not require any additional work from the programmer, and, most of the time, their
existence is
hidden completely.
When writing code by hand, it is still simple to automate the process to a large
extent. For
example, the following macro could be written to easily produce the class header
declarations
needed for both functions:
2 // CallbackMacros.h
4 +#include <generic.h>
This macro uses the name2 () macro found in generic.h to concatenate two symbols,
the
name provided to the macro and the name “Callback.” For example, the following
declaration:
private:
static void testCallback) ( Widget,
XtPointer,
XtPointer) ;
protected:
A similar macro can be used to produce the implementation of both the static member
function
and the declaration of the normal member function. The following macro requires a
class name and
the name of a member function:
This macro also uses the name2 () macro and is meant to be used in a class’s
implementation
file. For example, the following code segment:
LISI LELLI LIT LAGI AAA TAAL ELA ALT LA ARMAR AAA BASA EAS
#ifndef PASSWORD_H
#define PASSWORD_H
tinclude <Xm/Xm.h>
#include "CallbackMacros.h"
Class Password {
RbEeRvw o Jan; WD FP
fa O
public:
hop
WwW N
(=
>
15 private:
16
19 Widget _label;
20
21 DECL_CALLBACK (checkPassword) ;
22 );
23 #endif
The Password implementation would include the following lines, which handle both
the static
member function and the normal member function.
There are still other possible approaches, including using a class to represent
callbacks and to
hide all the details. This book will continue to explicitly declare and implement
the static member
functions in the interest of clarity. However, you may prefer to hide these
details, using one of the
mechanisms discussed in this section.
One of the most important issues to deal with when creating a C++ class concerns
the interface
provided by the class. Most objects must interact with other objects or other parts
of a program to be
useful. The classes described so far are deficient in this respect. For example, to
be useful, the
LabeledText class would need to support ways to retrieve and set the text,
manipulate the label, and
so on. There are many ways to implement this support, some better than others.
Let’s consider the
LabeledText class again and identify a few of the features that would be needed to
make this a
genuinely useful class. The class needs to provide the following features, at a
minimum:
e Some way to retrieve the text that the user enters into the input area.
e A way to set the label that appears in the label area.
e A way to detect when the user has completed an entry.
Other features might be useful as well. For example, the following are not strictly
necessary,
but could be useful:
All of these features are supported in one way or another by the individual widgets
contained
within the class. So, one way to allow other parts of a program to manipulate this
component is to
provide direct access to the individual widgets supported by the class. Other parts
of the program
can set resources on these widgets, just as they would do if no classes were used.
However, this
approach violates the very spirit of object-oriented programming. By exposing the
widgets, the
class would be reduced to a mere structure that holds a collection of widgets.
It is important to realize that any C++ class presents two interfaces, the public
interface and the
protected interface. The public interface is the one that can be used by other
parts of the program,
while the protected interface is available to derived classes. We need to consider
each one
separately. The following sections discuss each interface.
Let's extend the LabeledText class to meet some of the requirements listed in the
previous section.
The following implementation provides member functions that allow programs to set
the string
displayed in the label area as well as the input area. The class also allows the
contents of the input
area to be retrieved and allows the entire component to be set to a read-only mode.
The class could
be declared as follows:
5 define LABELEDTEXT_H
6 #include <Xm/Xm.h>
>
8 class LabeledText {
10 public:
dd
17
18 private:
19
23 «Se
24 #endif
Notice that the public interface reveals only minimal information about the actual
implemen-
tation. It is clear that there is a label area and a text area whose text can be
set separately, but that
was implied by the original idea of the LabeledText area. The interface does not
necessarily imply
that there is a Motif XmLabel widget nor a Motif XmText widget within the
component, nor are
there any clues about how they are combined. This component could be reimplemented
to use other
widgets without changing the interface in any way.
Contrast this approach with an implementation that provides direct access to the
widgets used
in the component:
#define LABELEDTEXT_H
#include <Xm/Xm.h>
TAHA 0 PWD PF
8 class LabeledText {
9
10 public:
LL
17 private:
18
22 1;
23 #endif
In this implementation, the program can directly manipulate any of the widgets.
There is no
need for functions like set Text () because programs can simply retrieve the text
widget and use
XtSetValues () to manipulate it. This is, however, a very bad design, because it
completely
exposes the internal structure and implementation of the class. This class cannot
be easily changed
because programs that use it will depend on internal details that should be kept
hidden.
One objection some programmers may have to hiding the implementation details of a
class has
to do with resources like color or fonts. How, you may ask, can a program change
the color of the
text widget if the very existence of the widget is supposed to remain hidden”? The
answer lies in
make the interface look pleasant. Typically, this sort of color choice should be
left to the user,
whose taste in color may differ from the application programmer. Users typically
use resources to
control an applications color, although a programmer may set up default colors in
application
resource files.
The second reason is to convey information. For example, if the text widget is set
to be non
editable, the programmer might wish to change the color of the text widget. The
programmer might
also like to change the color of the text widget to indicate an error condition, or
perhaps to highlight
the label to indicate that the data to be input in this field is required. In this
case, the action of
setting a color is actually incidental to the real intention. The programmer really
wants to indicate
some state information, which would be better thought of in abstract terms. For
example, the class
could be implemented so that when a program calls the readOnly () member function,
the colors
are changed appropriately. The color to be used to represent read-only mode could
be specified as a
resource by the user. Similarly, the class could support a highlight () member
function for use
in other situations, and perhaps a showError() member function for others. This
approach
provides the needed functionality without compromising the integrity or
maintainability of the
class.
The protected, or derived class interface is generally more open than the public
interface to allow
derived classes more flexibility. In the simplest case, it may be reasonable to
declare any members
that are not part of the public interface to be part of the derived class
interface. For example, the
following implementation of the LabeledText class simply moves all widgets into the
protected area
of the class. This allows derived classes to manipulate the widgets in any way at
all.
LFEPA ELSES EIT ATI LETS EES EES ETE EATS CIA RENA EL IGS I TTT
2 // LabeledText.h: A simple C++ component class
3 ALLA AAA AL EEFI TENITI EET ATT TAT PATIL ELE ELLELE
4 #ifndef LABELEDTEXT_H
5 #define LABELEDTEXT_H
7 #include <Xm/Xm.h>
9 class LabeledText {
10
11 public:
LZ
18
19 protected:
20
24 };
25 #endif
ON AU BP WD P
10
Lt
12
13
14
15
16
T7
18
19
20
21
22
23
24
25
26
27
28
29
30
3d
o WNDU BR yn sp
#define LABELEDTEXT H
#include <Xm/Xm.h>
class LabeledText {
public:
protected:
private:
y;
tendi f
73
In between these extremes are other possibilities. For example, we might decide
that derived
classes need to have access to the text and label widgets. Such access would be
needed to extend
the class in any reasonable way, such as adding ahighlight () member function,
providing user
feedback, or installing callbacks to check the text being entered dynamically.
However, the base
class might reserve the right to alter the layout for itself. The following
implementation allows
access to the label and text widgets, and the virtual textEntered () function, but
not to the
XmRowColumn manager widget.
PEI NATA ARA ABR A T ARA ANDAR SES ERÓ RAR ARA
#ifndef LABELEDTEXT_H
#define LABELEDTEXT_H
tinclude <Xm/Xm.h>
9 class LabeledText {
10
11 public:
E2
18
19 protected:
20
24
25 private:
26
31 #endif
Yet another variation on this last approach is to maintain all data members in the
private
section of the class, and to provide protected access functions to expose certain
members to derived
classes. This allows derived classes to access these members but not to assign new
values to the
data members.
In any case, it is important to consider both the public and protected interfaces
so that they are
chosen deliberately rather than accidentally. The best design provides exactly the
abstraction that is
intended with only minimal exposure of incidental details.
E Ha
The classes discussed so far use an ad hoc approach. Each class merely supports the
widgets,
callbacks, and members it requires. While this simple approach suffices for many
situations, it is
often more useful to define some guidelines or common features that should be
provided by all
component classes.
For example, because each component creates a widget subtree, each component must
have a
parent widget. It would be a good idea for all components to deal with their
parents in the same
way. Also, it would be convenient if each component supported simple operations
such as
managing and unmanaging the root of the widget tree it represents. To work well
with the resource
manager, widgets should have unique names. Therefore, all C++ user interface
classes should allow
e Components assign the root of the widget subtree created by the component to a
protected
instance variable. Using the same variable name in each class helps programmers
understand
the implementation of a class.
e Each component class provides an access method that can be used to retrieve the
root widget
of the component's subtree. While the goal of a component class is to encapsulate
the
behavior of a single logical collection of widgets, classes occasionally need to
expose at least
the root widget of the widget tree. For example, if an application needs to
position the entire
component in a Motif XmForm widget, it needs to be able to set the base widget's
constraint
resources. Normally, other widgets inside a component should not be exposed.
e Components handle the destruction of widgets within the component's widget tree.
The
widgets encapsulated by an object should be destroyed when the object is destroyed.
Some of these features must be implemented by each individual user interface class.
However,
some can be captured in an abstract class, from which other classes can be derived.
Let’s look at a very simple class that supports some of the features outlined in
the previous section.
4 LISIIL LIDIA AADI TARA RARA DRA MARA RADA TARA RANA RAT ELL ATA LAT ALE
5 #ifndef BASICCOMPONENT_H
6 #define BASICCOMPONENT_H
8 #include <Xm/Xm.h>
10 class BasicComponent {
LE
12 public:
13
14 virtual ~BasicComponent () ;
18
19 protected:
20
22 Widget _w;
23
25
oi 33
28 +#endif
purposes as well.
The BasicComponent constructor is declared in the protected portion of the class to
prevent the
class from being instantiated directly. The class does not contain any pure virtual
members but is
intended to be an abstract class. Declaring the constructor to be protected is one
way to prevent
direct instantiation. Derived classes can still call the BasicComponent constructor
from their own
constructors, but applications cannot instantiate the BasicComponent class
directly.
The BasicComponent class also defines two member functions, manage() and
unmanage (), which manage and unmanage the component’s base widget. These functions
are
part of the class’s public protocol and allow other classes or parts of the
application to display or
hide the widget collection represented by any component class. These member
functions are
declared as virtual to allow derived classes to alter this behavior, if needed.
Some classes might
need to perform other actions when a component is added or removed from the screen
in addition to
managing or unmanaging the base widget. Therefore, themanage () and unmanage ()
functions
should not be viewed as just a C++ wrapper around the corresponding Xt calls.
Instead, they should
be thought of as a way to make an entire component visible or to remove it from the
display.
Unlike other classes described so far in this book, the BasicComponent class is
intended
specifically to serve as a base class for other classes. As such, it is important
to pay particular
attention to the interface provided for derived classes and also to the class’s
role in defining the
protocol to be followed by all derived classes. In addition, base classes, or
potential base classes,
need to help programmers detect errors that could be introduced by derived classes
that do not use
the base class correctly.
Toward that end, BasicComponent makes frequent use of the assert () macro defined
in the
header file assert.h. The assert () macro prints an error message and aborts,
producing a core
dump, if a specified condition is not true. The core file allows a programmer to
use a debugger to
trace the sequence of steps that led to the problem and correct it. The assertion
can be completely
removed from code, once the code has been tested, by compiling with the symbol
NDEBUG defined.
Therefore, this macro provides a way to add checks that are useful to a developer
when deriving
new classes from a class like BasicComponent, without forcing finished applications
that use the
class to incur unnecessary overhead at runtime.
The BasicComponent constructor initializes _w to NULL and also makes a copy of the
name
parameter. The function XtNewString() allocates enough space for a copy of the
given string
and then copies its argument into the new string. The BasicComponent constructor
uses the
assert () macro to check for the existence of the name parameter. This serves two
purposes.
First, the XtNewString() function may fail if given a NULL string. Second, using
assert ()
provides a way to enforce the rule that all components must have a name. The
BasicComponent
class cannot determine if the name is really unique, but at least it can ensure
that it is non NULL.
1 EP ELIPLAL EL EAL PIPL EAT EEIEIEE TELLETT TAPE LIT AAAR I RAA ES
2 // BasicComponent.C: Initial version of a class to define
3 // a protocol for all components
4 PASARAN TALE CEISTE APT LEAT AT IIRI ILA ETT ET EEE EE TITEL
5 tinclude "BasicComponent.h"
6 tinclude <assert.h>
7 tinclude <stdio.h>
10 {
id _w = NULL;
H|
D
we
78 Chapter 2 C++ Classes and Widgets
Constructors for classes derived from BasicComponent are expected to accept a name
as an
argument and pass this argument on to BasicComponent. References to an instance’s
name within
the derived class should use the _name member. The BasicComponent constructor
simply
initializes the _w member to NULL. The BasicComponent constructor cannot determine
what type
of base widget to create; that task is left for derived classes.
15 BasicComponent : :~BasicComponent ()
LG
17 LE of. ow’)
18 XtDestroyWidget ( w );
19 XtFree ( _name );
20 23
The manage() member function calls XtManageChild() to manage the root of the
widget tree. The manage () function also uses the assert () macro to make sure the
widget
actually exists before calling XtManageChild(). This serves two purposes. First, it
prevents
manage () from calling XtManageChild() with a NULL widget, which would cause an
error
that might be hard to trace. Second, it attempts to enforce the BasicComponent
subclass protocol,
which expects derived classes to create a base widget. It would be better if the
BasicComponent
class could detect errors immediately after all constructors are called, but C++
does not provide any
way for base classes to know when all derived class constructors have been called.
As implemented
here, the BasicComponent class at least catches any derived class that does not
create a base widget
before the manage () method is called.
21 void BasicComponent : :manage ()
24 [
43 assert ( _w != NULL );
24 XtManageChild ( w );
25 }
The unmanage() member function is similar. It also uses the assert() macro to
guarantee that a widget exists before calling XtUnmanageChild(). This serves the
same
purpose as the assertion in the manage () function.
27 -f
28 assert ( _w != NULL );
29 XtUnmanageChild ( w );
30-4
The BasicComponent class supports only a minimal feature set and does not address
every
issue listed at the beginning of this section. Several topics must be introduced
before a more
complete class can be presented. However, the following section uses the
BasicComponent class as
the basis of several components, to demonstrate how C++ user interface components
based on such
a class can be used in an application.
An Example
the stopwatch, and the other stops the clock, leaving the final time displayed in
the text area.
This is a simple application, and the interface could be constructed in many
different ways. As
implemented here, the buttons are Motif XmPushButton widgets, which are managed by
an
XmRowColumn widget. The labeled text area consists of two XmLabel widgets, grouped
within an
XmRowColumn widget. One XmLabel widget is used as a label: the other serves as an
output area
whose contents can be changed. The second XmLabel widget is a child of an XmFrame
widget,
which displays a Motif-style shadow around the widget to offset it from the label.
These two logical
collections of widgets (the buttons and the output area) are managed by yet another
XmRow-
Column widget. Figure 2.1 shows the widget tree created by the stopwatch program.
The gray boxes in Figure 2.1 indicate logical groupings, or components, within this
appli-
cation. These collections of widgets are related not only by their positions on the
screen but also
button and directs the user’s commands to the rest of the application.
The box on the right represents the “face” of the stopwatch and displays the
current elapsed
time. This subtree can be implemented as a Face class that handles the widget
creation and layout
and also provides an interface for changing the contents of the display.
stopwatch
ApplicationShell
stopwatch
XmRowColumn
controls face
XmRowColumn XmRowColumn
start stop label frame
XmPushButton XmPushButton XmLabel XmFrame
- XmLabel
|
|
|
|
3
;
i
fis
f
Y
E
53
ath
The Timer class handles the task of updating the elapsed time displayed by the Face
component. The Timer class is not a user interface component but interacts with the
other classes to
display elapsed time. Finally, the entire stopwatch can be viewed as a logical
component, as
indicated by the larger box around the larger subtree whose root is “stopwatch.”
This component is
implemented as a Stopwatch class that collects the Control, Face, and Timer classes
and binds them
together into a distinct, self-contained component.
The Control class, which is derived from BasicComponent, creates the start and stop
buttons used
by the stopwatch program. The Control class supports start () and stop () member
functions,
which forward corresponding messages to a Timer object. The Timer object associated
with this
object is specified in the Control constructor and is maintained in the private
portion of the class.
Figure 2.2 shows an instance of the Control class as it might appear on the screen.
The user
interface portion of the Control class is trivial and just displays two buttons.
[Start [Stop
5 #define CONTROL_H
6 #include "BasicComponent.h"
7 class Timer;
8 class Stopwatch;
Ti
12 public:
13
15
16 protected:
17
20
26
30
xi // Static member functions that interface member
33
36 Ji
37 #endif
The Control class declares only the constructor in the public portion of the class.
The protected
portion contains pointers to several widgets used within the component, as well as
some pointers to
associated objects. These might be useful to any class derived from Control. All
other members are
declared to be private to the Control class.
The Control constructor takes four arguments: a parent widget, a name for the top
widget in the
class’s widget tree, and pointers to Stopwatch and Timer objects. The constructor
stores pointers to
the Timer and Stopwatch objects for later use and then creates the widgets needed
by this
component.
1 AE ELIT EARLIEST TAT EL LEED TS TELA LEPTIN TAL FELT ILIA ETAL I LALIT ETAL ITIL)
2 // Control.C: A start/stop pair of buttons for the stopwatch program
3 PEREELTLE LA EEALL EL ELE ED ELLA TLE ALL ALT ALATA EAA TAL tA A AIT AAAI EET
4 #include "Control.h"
5 #include <Xm/Xm.h>
6 #include <Xm/RowColumn.h>
7 #include <Xm/PushB.h>
8 #include "Timer.h"
9 #include "Stopwatch.h"
10
12 Widget parent,
13 Stopwatch *stopwatch,
15 ¢
16
19
2l
23
25 xmPushButtonWidgetClass,
26 we. MLL, “0-33
28 xmPushButtonWidgetClass,
29 -W NU 0.)
30
33
The top of the Control widget hierarchy is an XmRowColumn widget, which is assigned
to the
_w member inherited from the BasicComponent class. The _name member, also inherited
from the
BasicComponent class, specifies the name of the widget. Next, the start and stop
buttons are
created as children of the XmRowColumn widget. Finally, the constructor registers
the two static
member functions, startCallback() and stopCallback(), as XmNactivate-
Callback callback functions for the two buttons. Notice that the XmRowColumn widget
is not
managed within the constructor; this is left under the control of the code that
instantiates the
Control class.
The startCallback() static member function retrieves the instance pointer from the
client data and calls the object’s start () member function, The Control::start()
function, in turn, calls the start () member function supported by the Timer object
with which
this object is associated. Control::start() also calls the Stopwatch object's
timer-
Started () member function, which will be discussed shortly.
41 XtPointer clientData,
42 XtPointer callData )
43. {
45 obj->start( w, callData );
46 }
47
49 (
50 _timer->start ();
51 _stopwatch->timerStarted () ;
52 }
The Control class’s stop () member function is handled in the same way as the start
()
member function. The static member function, stopCallback(), retrieves the instance
pointer
from the client data and calls the Control::stop() member function, which calls the
_timer
object's stop () function and the Stopwatch: :timerStopped () member function as
well.
54 XtPointer clientData,
55 XtPointer callData)
56 {
57 Control *obj = ( Control * ) clientData;
58 obj->stop( w, callData 13
59 }
60
62 {
63 _timer->stop() ;
64 —Stopwatch->timerStopped () ;
65 }
The Face class is also derived from BasicComponent. The Face class encapsulates an
output area
and a label, along with an interface for displaying a floating-point number in the
text area. This
component supports no callbacks, but simply displays the data it is given. Figure
2.3 shows how an
instance of the Face class might appear on the screen.
4) MENA AAA AAA IA AA AAN AA NARA AAA NOA AAA AAA NAAA III ONO
2 // Face.h: A simple digital clock face for a stop watch
3 IMAN IA ANA ADA NAAA NAAA DANA AAA ADA NANA AAA A AEEA TII)
4 #ifndef FACE H
5 define FACE _H
6 #include "BasicComponent.h"
9
10 public:
Fk
15 protected:
16
21 #endif
The Face constructor requires a widget to be used as the parent of the object's
base widget,
plus a name. The constructor creates the label and output areas, managed by an
XmRowColumn
widget. The second XmLabel widget, which serves as the text output area, is managed
by an
XmErame widget to provide some visual separation between the elapsed time and the
label. N otice
that the text displayed by the label is not specified here; the label can be set in
a resource file.
1 ETET RAR AAA ATA AA AAA LATTA NAAA AAA AAA AAA
4 tinclude "Face.h"
5 #include <Xm/Xm.h>
6 #include <Xm/RowColumn.h>
8 #include <Xm/Frame.h>
9 #include <stdio.h>
10
11 Face::Face ( const char *name, Widget parent ) : BasicComponent ( name )
T2 {
14
16
23 }
The only other member function supported by the Face class is setTime(). This
function
accepts a floating-point number as its only argument. The value of this argument is
displayed in the
time widget as a six-digit number, with three decimal point precision. The
floating-point number
is displayed in the XmLabel widget by first using sprintf() to convert the float to
a
character string. Once the string is formatted, the Motif function
XmStringCreateLo-
calized() converts the character string to the compound string form expected by the
XmLabel
widget.
Note that the decision to use two XmLabel widgets in the Face class is completely
arbitrary.
There are several other ways to achieve similar results. For example, we could
replace both the
output text label and the XmFrame widget that surrounds it with an XmTextField
widget. The
XmTextField widget expects ASCII text instead of compound strings, which could be
an advantage
or disadvantage, depending on how the widget is being used. If we switched to an
XmTextField
widget, it would be best to configure the widget to be read-only and to hide the
text insertion cursor,
to make it clear that the Face object is an output-only region. Alternatively, the
Face class could be
redesigned to present an analog display. In any case, such decisions do not alter
the external
interface to the Face class. By providing an external interface based on the
semantics of the class
and not the widgets used to implement it, such changes can be made easily, without
affecting the
rest of the program.
25 1
27 XmString xmstr;
28
30
32
34
36
38
43 XmStringFree ( xmstr );
44 }
The Timer class is the only class in the stopwatch program not derived from
BasicComponent. The
Timer class uses the Xt function XtAppAddTimeOut () to supply a clock. The Timer
class
supports a start () member function and a stop () member function. When the clock
is started,
the Timer calls the setTime () member for the associated Face object at regular
intervals. The
interval between calls is determined by a parameter passed to the Timer constructor
and is
00
ON
WO Ian Mm 4 WD P
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
3
38
#define TIMER_H
#include <Xm/Xm.h>
class Timer {
+?
public:
#Hendif
Timer.C contains the member function implementations for the Timer class. The
constructor
requires three arguments. The first is an XtAppContext, which is required for the
XtAppAddTimeOut () function used to implement the clock. The second is a pointer to
the Face
object to be updated by the Timer. The third argument indicates the length of time,
in milliseconds,
between updates. Because the Timer class is not a user interface component, the
constructor simply
initializes all data members.
5 #include "Face.h"
6 #include <Xm/Xm.h>
10 _face = face;
TA _id = NULL;
12 —_app = app;
13 _counter = 0;
14 _interval = interval;
15 2
The Timer class uses the Xt timeout callback mechanism to implement a simple clock.
The
function
The same techniques used for the widget callbacks discussed earlier can be used to
handle
timeout callbacks from C++. The actual callback function can be a static member
function that calls
a member function in turn. Normally, the object’s instance pointer is passed as
client data to
XtAppAddTimeout (), just as in previous widget callbacks.
The start () member function uses XtAppAddTimeOut () to register the static member
function tickCallback () to be called in _interval milliseconds.
o ty E f
18 // Reset the elapsed time
19 _counter = 0;
20
22
28 }
Before registering a timeout callback, the start () function resets the _counter
member
to zero and calls stop () to remove the previous timeout, if any. This allows the
start button to be
pressed at any time. Even if the stopwatch is already running, starting the
stopwatch a second time
30 {
33 iE € 104
34 XtRemoveTime0ut ( _id );
35 _1d = NULL;
36 }
39 // Get the object pointer and call the corresponding tick function
40
42 obj->tick();
43 )
The tick () member function updates the _counter member each time it is called. It
then
calls the el apsedTime () member function, which uses the value of _counter to
compute the
approximate elapsed time in seconds. The resulting value is sent to the Face object
as a floating-
point number. The time calculated by elapsedTime () is approximate because the
timeout
callback mechanism may not be accurate. The function is not necessarily called
exactly when the
specified time elapses. If greater accuracy is required, theelapsedTime () function
can be
changed to make a system call to determine the time more accurately.
45 {
46 // Increment a counter for each tick
47
48 _counter++;
49
52 _face->setTime ( elapsedTime() );
53
55
The elapsedTime () member function computes the elapsed time, based on the number
of
times the _counter member has been incremented and the Timer’s clock rate.
60 {
61 return ( ( float ) _counter * _interval / 1000.0 );
62 }
It is often useful to create component classes that contain not only widgets, but
also other compo-
nents. Because other components are just classes that encapsulate a widget
hierarchy, this is easy to
do. The Stopwatch class is one example of a class that encapsulates other
components. The primary
purpose of the Stopwatch class is to connect the Face, Control, and Timer classes
discussed in
previous sections into one entity.
The Stopwatch class creates an XmRowColumn widget, seen as a root of part of the
widget
tree in Figure 2.1. The XmRowColumn widget manages the Face and Control components’
base
widgets.
The file Stopwatch.h contains the declaration of the Stopwatch class. The class
supports three
protected data members, one for each of the objects that compose the stopwatch
component. The
Stopwatch also allows the Control class to access its private and protected members
by declaring
Control to be a friend. This allows the Control class to call the Stopwatch class’s
timer-
Stopped () and timerStarted () member functions without making these functions part
of
the Stopwatch's public interface.
1 FIAR AI aL EAL AL EAT PLA TAA TAA ATI LITAI ERI ETA ELTA A AST
2 // Stopwatch.h: Group subcomponents into one stopwatch component
3 PEEILELTAI EL CATA TLA ELT TAAL AT TAA DAT TAA TAD EI AITINA AIT III IAEI
4 #ifndef STOPWATCH_H
5 #define STOPWATCH_H
6 #include "BasicComponent.h"
3 #include "Timer.h"
11 class Face;
12 class Control;
13
15
18 public:
19
22
23 protected:
24
28
29 private:
30
35
36 tendif
Only the Stopwatch constructor and destructor are declared in the public portion of
the
Stopwatch class. The elapsedTime(), timerStarted(), and timerStopped()
functions are specifically provided to allow derived classes to perform additional
actions when the
timer is started and stopped. Therefore, these functions are defined in the
protected portion of the
class. All other members are kept private to the Stopwatch class.
1 PEA LETEELALTLTL TT LEAP SUT AENA AAAAA AS AAA AAA AAA ILA AALS ATA ASI IIT
2 // Stopwatch.C: Group subcomponents into one stopwatch component
3 EPELLTIL ELA TALI EAT POLAT IIL ERAS ATI ATRAI TREAT TITAS ATA TIA
4 #include "Stopwatch.h"
5 #include <Xm/Xm.h>
6 #include <Xm/RowColumn.h>
7 #include "Timer.h"
8 #include "Face.h"
9 #include "Control.h"
10
12 BasicComponent ( name )
13 {
15
19
22 _face,
23 1000 I;
25
ai
28 _face->manage() ;
29 _control->manage () ;
30: }
31 Stopwatch::~Stopwatch ( )
32 {
33 delete _face;
34 delete _timer;
35 delete _control;
36 )
The Stopwatch class defines two virtual functions that serve as hooks for derived
classes. The
Control object encapsulated by a Stopwatch object calls these functions when the
user starts or
stops the timer. These hooks, along with the elapsedTime() member function, allow
programmers to derive classes that perform some additional task when the user
starts and stops the
stopwatch. Without these functions, the Stopwatch class may be useful to the user
in a stand-alone
program, but it cannot be connected to, or used to control, other objects in a
system.
3H. |
39 // Empty
40 }
af. %
43 // Empty
44 }
Figure 2.4 shows the inheritance relationships between the classes used in the
stopwatch
program. Face, Stopwatch, and Control are derived from the BasicComponent class,
whereas Timer
has no base class.
Timer BasicComponent
The driver for the stopwatch program is quite simple. The program initializes the
Xt Intrinsics and
then creates an instance of the Stopwatch class, specifying the top-level shell
widget as the
component’s parent. The program then manages the Stopwatch component, realizes the
top-level
shell, and enters the Xt main event loop.
3 IRA AAA NAAA SARA RINA RA SATA TAE RAMAS ARRE RES
4 +#include <Xm/Xm.h>
5 #include "Stopwatch.h"
8 {
9 XtAppContext app;
10 Widget shell;
11
14
19
20 XtRealizeWidget ( shell );
ot. XtAppMainLoop ( app );
22 }
The program can be built by compiling the files Face.C, Timer.C, Control.C,
Stopwatch.C, and
stopwatch.C and then linking the resulting object files with the Motif, Xt, and X
libraries. Figure
2.5 shows the initial appearance of the final program.
Figure 2.5 Initial layout of the stopwatch program.
A oe ee woe A Cw MC A A AA E E SI A E E Te
Stopwatch*face.orientation: horizontal
Stopwatch*face*label*labelString: Elapsed Time:
Stopwatch*face*time*labelString: 000.000
Stopwatch* face* frame* shadowType: shadow_in
Stopwatch*control.orientation: horizontal
Stopwatch*control.start.labelString: Start
Stopwatch*control.stop.labelString: stop
=a Tr TF
Elapsed Time:
The choice of how to split a system into objects is a design decision, of course,
and no two
programmers are likely to make the same decisions. For an extremely simple example
like the
stopwatch program, it would be reasonable to implement the functionality as a
single class.
However, in real applications, the type of division demonstrated here becomes more
useful. Object-
oriented programs often trade the quickest, easiest way to implement a given piece
of functionality
for a more robust, modular, potentially general solution that may save time and
effort later on.
stopwatch iM
The ability to change, improve, or even redesign one portion of a program without
affecting
the others is one of the main reasons for using object-oriented techniques. If, in
addition, some of
the smaller pieces of a particular program could be implemented as self-contained
objects, the
chances that these objects may be useful in some other context becomes an important
factor. For
example, classes similar to the Timer class and the Face class may be useful in
other applications
that are quite different from the Stopwatch component.
eee
The previous section demonstrates how the BasicComponent class can be used as a
basis for other
component classes. The BasicComponent class implements some basic features useful
to all
component classes. However, there are several additional facilities that may be
useful to many user
interface classes as well. Some of these can be handled in a base class so that all
classes are not
forced to duplicate code.
The following sections examine issues related to widget destruction and the X
resource
manager and show how these issues can be addressed by an enhanced base class,
UlComponent.
The UIComponent class is derived from the BasicComponent class discussed earlier in
this
chapter; the new class adds support for handling widget destruction and uses the
resource manager
to customize and initialize classes.
1 PEPIESTELTL TL ELT ALAR ELIT IA RI AI AIMAR EAT ESTA TALS ATT OL LATTA TAY
2 // UIComponent.h: Base class for all C++/Motif UI components
3 PELLAZS TL TIPE TT ELSE EL ELL ERTIES IIR LIL TAL ET EL TAT IS TAL OETA IES E
4 #ifndef UICOMPONENT_H
5 #define UICOMPONENT_H
6 #include <Xm/Xm.h>
7 #include "BasicComponent.h"
10
YA public:
T2
14
16
18
20
21 virtual const char *const className() { return ( "UlComponent" ); }
22
23 protected:
24
28
32
34
36
38
42
43 private:
44
46
49 #endif
The UlComponent class defines several new member functions that implement new
features
that may be useful to derived classes. Each of these new features is described
individually in the
following sections.
Like the BasicComponent class, the UIComponent class defines a basic protocol for
all derived
classes and also to support programmers who create new derived classes. Therefore,
the UICom-
ponent class pays particular attention to error handling and frequently uses
theassert () macro.
The UlComponent class also makes frequent use of const declarations for the same
reason.
The UlComponent constructor is located in the file UIComponent.C, along with the
other
UIComponent member functions. It is written as:
WD 0 J awn Ff WN P
FL ELE LEIAL ADE CESTA AA PEALE TELIA ELLA LAA IAT EPPA LANE FABIAN TAS
// UIComponent.C: Base class for all C++/Motif UI components
EEL LEIA MEA IATA AA ST PTA LAT TALL ELIS AT ARLE LSAT A ELIT AA GTS ELT ETAL SS
#include "UIComponent.h"
tinclude <assert.h>
tinclude <stdio.h>
UIComponent: :UIComponent ( const char *name ) : BasicComponent ( name )
10 {
e 4 // Empty
12 }
Widget Destruction
Some simple programs create all widgets at startup, and never destroy widgets while
the program is
running. However, more complex programs often create and destroy various parts of
the interface
dynamically. When widgets are destroyed, it is easy to leave dangling references —
pointers to
memory that once represented a widget but are no longer valid. Dangling references
are a problem
in any program but can be particularly troublesome in Xt-based applications for two
reasons. First,
when a widget is destroyed, its children are also destroyed. It is often very
difficult to keep track of
the references to these children. Second, Xt uses a two-phase destroy mechanism.
The first pass
simply marks the widget to be destroyed; the second pass does the actual
destruction. The second
pass may not occur immediately, depending on how and when the widget is destroyed.
Consider how this behavior affects the stopwatch example described earlier in this
chapter. If
the application were to destroy the program’s top-level shell widget before
deleting the Stopwatch
object, the integrity of every object in the Stopwatch subsystem would be
compromised. When a
widget is destroyed, Xt automatically destroys all the widget’s children. So,
destroying a widget
that serves as the parent of a Stopwatch object ultimately destroys the widgets
created by the
Stopwatch object, as well as the widgets created by the Face and Control objects.
Therefore,
destroying a widget can potentially free data members that are supposedly
accessible only from
within an object. This represents a fairly serious breach in the encapsulation of
the component.
Although this scenario may seem contrived, similar situations can easily occur in
more complex
programs.
The situation described above can lead to several problems, depending on how
objects are
deleted and how widgets are destroyed. The least serious problem is that an
application may have a
“memory leak.” The widgets that form the interface of a component may be destroyed,
but the
object that formerly contained these widgets may still exist. The interface is no
longer visible on the
screen and as long as no other part of the program interacts with the object, no
problems will occur.
However, the object still occupies space within the program, even though its user
interface portion
does not appear on the screen.
The second problem is more serious. It is fairly easy to write a program that
accidently refer-
ences the widgets in a class after the widgets have already been destroyed through
a “back door.” In
some cases, applications may try to delete a widget twice, which usually causes the
program to
crash. Calling XtSetValues () or other Xt functions with a widget that has been
destroyed is
also an error that can occur easily in this situation. For example, in the
stopwatch program, the
Timer class would continue to update the Face class even after the widgets in the
Face class were
destroyed, unless specific steps were taken to avoid the problem.
The UIComponent class provides some support for handling these problems. The
widget-
DestroyedCallback() function is a static member function that can be installed as
an
XmNdestroyCallback function by all subclasses of UlComponent. The widgetDe-
stroyedCallback () function retrieves a pointer to the UlComponent object from the
client
data passed to the callback. Then, instead of taking any specific action itself,
the callback calls the
virtual member function widgetDestroyed(). No other class needs to access this
static
member function, so it is declared as private to the UlComponent class.
The following function implementation continues where we left off in the previous
section, on
line 13 of the file UlComponent.C.
14 XtPointer clientData,
15 XtPointer )
> de NA
18 obj->widgetDestroyed ();
19:98
The default widgetDestroyed () function just sets the _w member to NULL, so that
the
UIComponent class, as well as derived classes, can catch any references to a
component's base
widget after it has been destroyed. However, subclasses can override
thewidgetDestroyed ()
member function if needed. Derived classes might need to perform additional cleanup
when the
widget tree is destroyed. Notice that the goal here is to allow a class to be
written in such a way that
it does not cause a fatal error even when misused. An alternate approach is to
signal an error if the
widgetDestroyed() function is ever called, to help programmers who use a class to
catch
mistakes.
21 {
22 _w = NULL;
aa. f
25 {
26 assert ( _w != NULL );
28 &ULComponent : :widgetDestroyedCallback,
29 ( XtPointer ) this );
SH. 3
Ideally, the UlComponent base class could force derived classes to install the
widgetDe-
stroyedCallback() function, or install it automatically. However, this is difficult
to do
because of the way C++ constructors work. As a partial solution, the UIComponent
manage ()
member function checks to see if an XmNdestroyCallback has been installed and
attempts to
enforce this mechanism. Checking for a registered XmNdestroyCallback function is
not a
foolproof test but it catches most cases. The test is not foolproof because Xt does
not provide a way
to determine if a particular function has been registered as a callback, only
whether or not some
function has been registered. This approach also does not provide any warning for
objects whose
32.1
33 assert ( _w != NULL );
58 XtCallbackHasSome );
36 XtManageChild ( w );
37 3}
Notice that the manage () member function uses the assert () macro to test for the
presence of an XmNdestroyCal lback. This test is intended to help programmers make
proper
use of the UIComponent class, but the test should not be necessary once a program
has been tested.
The assert () macro allows programmers to remove this test from production-quality
code or
38 UIComponent: :~UIComponent ()
39 {
46 &UIComponent : :widgetDestroyedCallback,
47 ( XtPointer ) this );
48 }
49 }
Customizing Components
For example, it might be useful to allow users to specify the precision displayed
by the
stopwatch program described earlier in this chapter or to control the rate at which
the component
updates the elapsed time. These parameters cannot be set using existing widget
resources because
this type of information is application-specific.
The usual way to allow the user to select options like these is to support command-
line
arguments or to use the Xt function XtGetApplicationResources() to retrieve appli-
cation-specific resources specified by the user. These resources are retrieved
relative to the name
and class of the application and are generally maintained within an application as
global variables
or members of a global structure. This approach works fine in many situations, but
in an object-
oriented system, forcing a class to rely on global application-level parameters
compromises the
encapsulation of the class.
Fortunately, Xt provides a way to use the resource manager that works nicely with
C++ classes
used as user interface “components.” Because each C++ component can be viewed as a
subtree
within an application’s widget hierarchy, we can use the Xt function
XtGetSubresources ()
to initialize member data in a class from the resource database. Before showing how
to use this
technique with a C++ class, the following section briefly discusses what the
XtGetSubre-
sources () function does.
XtPointer base,
String subpartName,
String subpartClass,
XtResourceList resources,
Cardinal numResources,
ArgList args,
Cardinal numArgs );
typedef struct (
String resource_name;
String resource_class;
String resource_type;
Cardinal resource_size;
Cardinal resource_offset;
String default_type;
XtPointer default_addr;
} XtResource, *XtResourceList;
This may look complex, but XtGetSubresources () is simple to use. Let's assume that
we have an XmLabel widget named “text” that, with its parents, corresponds to the
following class
and name resource lists:
The first list represents the class resource list for the widget, and the second is
the assumed
name resource list. These lists correspond to a path through the widget tree of the
stopwatch
program described earlier in this chapter. (See the widget hierarchy in Figure
2.1.)
Now, suppose we want to retrieve a resource named “precision”, whose class is
“Precision”,
In addition, assume that we want to retrieve the value of the “precision” resource
as if the resource
belonged to the label widget supported by the Face class. In other words, we would
like to allow the
user to specify values like
*text*precision: 5
XtResource resources[] {
: (
: "precision", // Name of resource
A "Precision", // Class of resource
XmRInt, // Required type
3 sizeof ( int ), // Size of expected type
E XtOffset ( aStructure *, decimalPlaces ), // destination addr
E XmRString, // Type of default value
E ( XtPointer ) "2" // Default value
4 ),
Si
$ .
: y;
2. Create the various widgets and define a variable of type aStructure in which to
store
the resource.
aStructure myData;
E rarer
// Create widget tree
relative to the XmLabel widget from the resource database and store it in the
myData
structure:
Notice the “subpartname” and “SubpartClass” arguments in this function call. These
are
strings that XtGetSubresources () considers to be “subparts” of the specified
widget. This
function attempts to retrieve a resource whose full specification (assuming the
widget tree listed on
the previous page) is given as:
stopwatch*precision: 5
Or
stopwatch*text*precision: 3
or
stopwatch*SubpartClass*precision: 4
The UlComponent class defines a virtual member function, className (), that returns
the
name of the class. Each class derived from UIComponent is expected to override this
method,
which provides a rudimentary way to identify the class to which an object belongs.
The getRe-
sources () member function uses this information to load resources relative to the
class name of
the class to which an object belongs.
The member function getResources () takes an XtResourceList that describes the set
of resources to be retrieved and takes a second argument that specifies the size of
the resource list.
After checking for error conditions, getResources() calls XtGetSubresources () to
retrieve values for all resources in the specified list.
52 1
54
55 assert ( _w != NULL );
57
60
61 XtGetSubresources ( XtParent( _w ), ( XtPointer ) this,
62 _name, className (),
63 resources, numResources,
64 NULL, 0);
65 -]
The third parameter, which is supposed to identify a subpart, is set to the name of
the object.
The fourth argument specifies the class name of the C++ class to which this object
belongs. These
arguments effectively identify this object as a subpart of the parent widget
specified when the
component is instantiated. The final two arguments can provide a list of resources
and values to
override values found in the resource database.
Let's look at a new version of the Stopwatch class that uses this mechanism. The
new version
of Stopwatch is derived from UlComponent and allows the user to specify the rate at
which the
stopwatch updates its displayed time. The new Stopwatch class is declared as
follows:
PIDEELEDELE RRA RA TATA ARA AAA E AEREA TELE ATLL TATIANA TA IES
// Stopwatch.h: Group subcomponents into one stopwatch component
PETRA TITTET IITA AAAAN IAN ET EL ELT ALERT LAITIITI AEREA ARA
tifndef STOPWATCH_H
#define STOPWATCH_H
#include "UIComponent.h"
#include "Timer.h"
protected:
virtual void timerStarted(); // Subclass hooks called when
virtual void timerStopped () ; // timer starts and stops
float elapsedTime() { return ( _timer->elapsedTime () a3
int _interval;
private:
// Encapsulate a resource specification used to init this class
static XtResource _resources[];
This version of Stopwatch has two new members. The _Yesources member is an
XtResourceList that specifies how resources should be retrieved and loaded into the
Stopwatch class. It is declared as a static member to encapsulate the resource list
within the class.
The array is declared in the private section of the class; it is used only at
initialization time to load
resources, and does not need to be accessed from any other class.
The second new member is the _ interval data member. The new version of the
Stopwatch
class uses this value to indicate how often the value displayed by the stopwatch
component should
be updated. The original stopwatch program passed a hard-coded value of 1000
milliseconds to the
Timer class. The new version of Stopwatch passes the _interval value instead.
Furthermore,
end users can use the X resource manager to specify the value of this data member.
The new version of the file Stopwatch.C begins by initializing the _resources
member. The
first member of this array specifies a resource whose name is “interval” and whose
class is
“Interval.” The value is to be retrieved as an integer and stored in the_interval
member of a
structure (an object) belonging to the Stopwatch class. Finally, the last two
parameters specify a
default value for the resource. If no corresponding value is found in the resource
data base, the
default value is used. Xt handles the conversion between the specified character
string and the
expected integer type.
MAMARIA NAAA NAAA TALL TATA TATA TAAL AT AAA ATA AAA AAA TAL ittia
// Stopwatch.C: Group subcomponents into one stopwatch component
// Customizable, UIComponent version
PELTTTTTLLTTATITT TTA TAT AT ATTA NAAA AA AAA NA AAA AA AAA MAN ANA NINO ANI III
#include "Stopwatch.h"
#include <Xm/Xm.h>
#include "Timer.h"
#include "Face.h"
#include "Control.h"
"interval",
"Interval",
XmRint ,
sizeof ( int ),
xtoffset ( Stopwatch *, _interval ),
XmRString,
( XtPointer ) "1000",
fe
y;
The new Stopwatch constructor is similar to the earlier version, but with several
additions.
After the component's base widget is created, the constructor calls both the
installDestroy-
Handler () function discussed in the previous section and the getResources ()
function. The
_ resources array is specified as the first argument to this function, and the
second argument
uses the Xt macro XtNumber () to determine the size of the resources array. Once
the
getResources () function is called, the _interval member will be set to the value
found in
the resource database or to 1000 if no value is found. This value is then passed to
the Timer
constructor and used to determine the clock rate at which the Face component is
updated.
_face->manage () ;
_control->manage () ;
The new version of the Stopwatch class allows users to specify the update rate in
resource files
with statements such as:
*interval: 500
Or
*Stopwatch*interval : 100
Notice that the “Stopwatch” name used in these resource specifications refers to
the Stopwatch
class, not to the stopwatch demo program described earlier in this chapter. These
resources apply to
any instance of the Stopwatch class, regardless of the program in which the
component is used.
Resource files provide a way for end users to customize applications to fit their
personal tastes and
preferences; but that is not the only purpose of resource files. It is customary
for X applications to
specify as much as possible in a resource file to allow applications to be
integrated into new environ-
ments without modifying source code. Resources that properly belong in a resource
file include
labels and perhaps colors and fonts. Some applications even define bindings between
application
functions and keys or mouse buttons in resource files so that these can be changed
easily.
For example, the stopwatch program provides a set of application defaults, listed
on page 93,
that could be regarded as important for the proper layout of the component. These
resources were
originally specified as application defaults for the stopwatch program. However,
that description iS
not entirely accurate. This file really specifies resources needed by the Stopwatch
component.
Ideally, any application that needs a component like the Stopwatch component should
be able to
instantiate one or more instances of the Stopwatch class. Because the component is
self-contained,
it has the potential to be used as a reusable component in many applications.
The fact that this component relies on a set of externally defined resources
reduces the degree
to which the component is really self-contained. There are several ways to address
this problem.
One is to use XtSetValues (), or an equivalent function, to hard-code all resources
needed by
the component within the class. However, when a program sets a widget resource
programmati-
cally, it overrides user specifications for that resource. In some situations, this
might be an adequate
solution. However, in other cases, taking such a drastic step would make the
component unusable
to some programs. If the defaults cannot be changed, some programmers who could
potentially
reuse the class may have to write a nearly duplicate class just to change an
unsuitable resource
setting.
Notice that many widget resources can be changed programmatically after the widget
has been
created. Therefore, derived classes can sometimes change the behavior of widgets
created by base
classes by setting new resource values in the derived class constructor. However,
this does not
allow the user to customize a component, nor does it allow applications to alter a
component’s
behavior without deriving a new class. Also, some widget resources cannot be
changed after the
widget has been created.
Another approach is to set most resources in resource files rather than in the code
and to make
the resource settings needed by a class available to any application that might use
the component.
For example, imagine a library that contains a collection of component classes. In
addition to the
binary library file and the various class header files, the library could also have
an associated set of
resource files required by various components. Programmers who use this library
would then be
expected to copy the resources for any components used in their programs into the
application
defaults file used by their program. This approach can be confusing and error-prone
because, once a
programmer copies the resources into another file, there is no way to manage
changes to the
original or even track the origin of the resources.
String Stopwatch::_defaults[] = {
"*face.orientation: horizontal",
"*face*label*labelString: Elapsed Time:",
"*face*time*labelString: 000.000",
"*face*frame*shadowType: shadow_in",
"*control .orientation: horizontal",
"*control.start.labelString: Start",
"*control.stop.labelString: stop",
NULL,
y;
This is the same set of default resources listed on page 93, but in the form of a
NULL-termi-
nated array of character strings. Notice that if we want to encapsulate this set of
resources in the
class, as shown here, we must also add a line to the class declaration, declaring
the array as a static
member of the Stopwatch class. This is similar to the way the XtResource array was
handled in
the previous section.
CO
69 int 1;
70 Display *dpy
71 XrmDatabase rdb
v2
74
75 rdb = XrmGetStringDatabase ( "" )
76
79 i. = Oe
81 {
82 char buf[1000];
83
86 }
87
89
90 if ( rdb )
91 {
94 }
95 3
Notice that before each resource is added, the name of the specific instance is
prepended to the
resource. Prepending the object's name partially solves one potential problem with
this technique.
We need to define these resources on a per-component basis. Because many components
may be
mixed within a single application, there is a reasonable chance for collisions to
occur between the
resource settings of different components unless the resources are qualified on a
per-component
basis. Failing to qualify the resources in any way would mean that an object might
load resource
specifications like
*orientation: vertical
which would almost certainly coincide with some other part of the program and cause
difficulties.
Although the class of the component is not useful in this situation, the resources
can be
qualified by the name of the instance, which is also used by convention as the name
of the
component’s base widget. However, an object’s name is not known until runtime. So,
setDe-
faultResources() constructs a new resource specification, qualifying each entry by
the
instance name of the component, before loading the resources into the resource
database. For this
approach to work, resources must be specified relative to the root of the
component’s base widget
but must not include the name of the base widget. Resources that apply to the base
widget itself can
be specified using the name of the resource, preceded by a “*”.
Notice that this scheme does not guarantee that collisions will be prevented;
however, colli-
sions are common in the X resource scheme. The probability of a collision is, at
worst, no more
likely than any other collisions that occur between resources specified in the
various resource files.
This approach may also seem somewhat inefficient. However, it should be no less
efficient than
reading resources from various files. In most cases, it should be much faster.
Once all entries are loaded into the new resource database, the function
xrmCombineData-
bases () merges the new database with the existing application database, placing
the items in the
new database at the beginning of the combined database. This allows resources read
from a
resource file to override the defaults specified within the class definition.
Using this mechanism appropriately requires some judgment on the part of the
programmer.
The component defaults should be specific enough that they cannot be overridden
accidentally, but
not so specific that the user, or application, cannot override them intentionally.
The precedence
order used in arbitrating resource specifications is complex. See [Scheifler90] for
more information
on the precedence rules used to arbitrate collisions in resource files.
To see how this mechanism works, we can rewrite the Stopwatch constructor one last
time, as
follows:
_face->manage () ;
_control->manage () ;
}
stand on their own. Programmers can specify the correct defaults for a component
without resorting
to hard-coding the resources in the program.
OO
The approach used in this book encapsulates relatively large-grained user interface
components
within C++ classes. These classes construct collections of widgets that provide
some cohesive
function within an application. This function may be strictly related to the user
interface (for
example, the LabeledText class described earlier in the chapter) or may embody some
higher-level
operation in addition to supporting a user interface (for example, the Password
class described in this
chapter).
In this book, all such classes are derived from the UIComponent class. It is not
strictly
necessary to use the UlComponent class. However, the UlComponent class defines a
common
protocol and provides some features that can be shared by all derived classes.
Classes derived from
UIComponent will be referred to as components in the remainder of this book.
Earlier in this chapter, on page 75, we discussed some basic guidelines for using
C++ classes
that serves as user interface components. The following list expands those original
guidelines to
apply specifically to components based on the UlIComponent class:
Ad
PAE
dee Fe Be
AS
a aaa a aa O AS
* Components should usually create the base widget and all other widgets in the
class
constructor. The constructor should manage all widgets except the base widget,
which should
be left unmanaged. The entire subtree represented by a component can be managed or
unmanaged, using the member functions supported by UIComponent.
* All constructors should take at least two arguments: a widget to be used as the
parent of the
component’s base widget and a string to be used as the name of the base widget. The
name
argument should be passed on to the UIComponent constructor, which makes a copy of
the
string. All references to a component's name should use the _name member inherited
from
UIComponent.
* All component classes should override the virtua] className () member function,
which
is expected to return a string that identifies the name of the class.
e Components should define any callbacks required by the class as static member
functions.
These functions are normally declared in the private section of the class because
they are
seldom useful to derived classes. All callback functions should be passed the this
pointer
as Client data. Callback functions are expected to retrieve this pointer, cast it
to the expected
object type, and call a corresponding member function. By convention, static member
functions used as callbacks have the same name as the member function they call,
with the
word “Callback” appended. For example, the static member function startCallback ()
calls the member function start (). This convention is only meant to make the code
easier
to read and understand.
e Member functions called by static member functions are often private but may also
be part of
the public or subclass protocol of the class. Occasionally, it is useful to declare
one of these
functions to be virtual, allowing derived classes to change the function ultimately
called as a
result of a callback.
* Derived classes that need to initialize data members from values in the resource
database
should define an appropriate resource specification and call the
functiongetResources ()
immediately after the installDestroyHandler () function.
FP a aaa a a REES S S A A
One decision that must be made once the need for a new user interface component has
been
identified is whether to implement the type of C++ component described in this
chapter or whether
to write a new C-based widget, using the model supported by Xt and Motif. There is
no clear-cut
rs
PERET ee
e So
SE RA En
C++ Tricks and Techniques 113
answer to this question. The best course depends on the programmer’s experience,
the amount of
time available, and what the component is supposed to do.
For example, all manager widgets can control the sizes and positions of their
children. Xt
notifies each manager widget when its set of managed children changes, making it
easy to
implement interesting and dynamically changing layouts. This is not possible to do
reliably from
outside the widget architecture. Therefore, if a component must move or manipulate
other widgets,
it may be better to write the component as a real widget, using the C-based Xt
architecture.
Widgets are relatively hard to write because they require the programmer to follow
a complex
set of conventions that are not enforced by a compiler. For programmers who have
never written a
widget before, creating a new widget often proves to be a daunting task. In
general, C++ classes
such as those described in this book are easier to write, partly because C++
provides language
support for object-oriented techniques, and partly because it is easier to combine
existing widgets
than to write completely new ones.
The ability to customize existing widgets and combine them in new and novel ways is
both the
strength and limitation of writing C++ components as described here. If you can
implement the
behavior you need by combining existing widgets, a C++ class is the easiest
approach. Because
Motif widgets are extremely customizable, it is often possible to create a
surprising variety of inter-
faces using existing widgets. On the other hand, there are user interfaces that
cannot be created by
combining or customizing existing widgets. In this case, writing a new Motif
widget, using the Xt
architecture, is the most reasonable choice.
C++ is a complex language that provides a large set of facilities that can be used
in many clever
ways. Used correctly, these facilities can help to provide clear and useful
interfaces for C++ classes.
However, these features are easily overused or misused. For the most part, the
examples in this book
are kept deliberately simple and avoid using many of the more novel features of C+
+. The primary
focus of this book is on the architectural issues of using Motif in an object-
oriented way, which has
little to do with exploiting features of C++ syntax. However, many of the features
of C++ can be
useful, and may be interesting to use in your own classes. This section explores
just a few of these,
and also discusses some of the potential problems with using these features. A book
devoted to the
C++ language would be very helpful in understanding how and when to exploit these
and other
similar techniques more fully.
Function Overloading
One of the most useful features of C++ is function overloading, which is the
ability to define multiple
functions with the same name. As long as functions have a unique type signature, it
is possible to
reuse the same name. This feature can be used effectively whenever an operation
could performed
on different argument types or different numbers of arguments.
For example, the primary difference between the LabeledText and Face classes
discussed in
this chapter is that the LabeledText widget displays a character string as given to
its set Text ()
member function, whereas the Face class displays a floating-point number. If we
used function
overloading to generalize the LabeledText class so that it could display various
types of data, it
could be used instead of the Face class. All that would be required is additional
versions of
setText () that accept other types of values, including floating-point numbers. For
example, we
could implement LabeledText as follows:
5 #define LABELEDTEXT_H
6 #include <Xm/Xm.h>
8 class LabeledText {
9 public:
10
18
19 protected:
20
24
25 private:
26
30 #endif
C++ Tricks and Techniques 115
This version of LabeledText supports three different set Text () member functions.
The first
version takes a character string as an argument and displays the string in the text
field:
32 {
33 XtVaSetValues ( _text, XmNvalue, str, NULL );
34 }
The two remaining functions format their arguments and call the previous version of
setText (), as follows:
36 q
38
40 setText ( buf );
41 }
42
45 char buf[10];
46
49 }
When the set Text () function is used in a program, C++ determines which version of
the
function to call, based on the types of the arguments provided. Careful use of
overloaded functions
can produce a clean and more flexible interface and can make a class useful in more
situations. For
example, with this addition, there is little need for the Face class. The
LabeledText class could be
reused, rather than requiring a new class to be defined.
Operator Overloading
possible to redefine the “=” and “+” operators so that one can write code like
this:
String a ( "hello" );
String p { " * 33
String c ( "world" );
String d =a + b + C;
Consider how we might apply operator overloading to classes that use Motif, such as
the Face
or LabeledText class. Instead of calling the set Text () member function, a program
might use
the C++ “<<” operator, which is normally used for input and output. The LabeledText
class can be
rewritten to provide an overloaded set of operators that allow us to write
statements like:
These operators can be added as inline member functions, which produces a class
declaration
that looks like this:
3 FILETATA EIA EL ELE AAA IA DAD ALT ESTAR EVA TRA AAA AAA ANNA
4 #ifndef LABELEDTEXT_H
5 define LABELEDTEXT_H
6 #include <Xm/Xm.h>
7
8 class LabeledText {
10 public:
2.
19
26
23 protected:
28
32
33 private:
34
There are many other possibilities for using operators with components, although we
must be
careful to choose operators that make sense. Used carefully, operators can make
code easier to
maintain. Overloaded operators can also obscure what a program is really doing,
unless used
carefully. Abstractions are good if they help a programmer understand or maintain
code, but they
can be bad if they are misleading.
Most typical C++ textbooks contain examples that create objects automatically
rather than using the
new operator to create them dynamically. For example, it is common to see code that
looks like this:
void aFunction ()
{
SomeClass obj;
obj .print();
} // obj destructor called automatically
{
ErrorDialogClass dialog ( errorMsg );
dialog.post();
{
ErrorDialogClass dialog ( str );
ee
ro
ae
e al:
——
Se
a
Ste a
pee
ie, St a at oa eS E
dialog.post();
return ( dialog );
This example assumes the existence of a StringClass that can be used to encapsulate
the
behavior of a string. The function expects a string object to be passed by value.
When this happens,
a copy of the string object is created, using the copy constructor supported by C+
+. The object used
inside errorFunction() is created when errorFunction () is called and deleted when
the
function returns. While this can be rather inefficient, it works reasonably well
for data structure
classes like strings.
However, look at what happens with the return value of this function. The function
returns a
copy of an ErrorDialogClass object. C++ automatically destroys the object created
inside the
function, just as before, but arranges for a copy of the object to be created and
returned. If this
object is a user interface component, like those described in this book, one dialog
class, along with
an associated set of widgets, would be created when errorFunction () is executed.
Then,
those widgets would be destroyed when errorFunction() returns, and, assuming the
class
provides a copy constructor, an entirely new set of widgets would be created for
the copy of the
object.
This is clearly not desirable behavior for user interface components. Again, there
are ways to
work around this issue, but the simplest way to address the problem is to stick to
a simple style that
creates objects dynamically. In this book, all components are created with the new
operator and
destroyed with the delete operator. All objects are passed as pointers instead of
by value.
2.8 Summary
Fe 5 5 5 5 5 5 5 5 5 5 5 55 5 5
This chapter introduces several techniques for using Motif widgets with the object-
oriented features
of C++. The basic mechanism for using callback functions and other indirect C
function-calling
techniques with C++ classes is straightforward. Techniques for handling widget
destruction, default
resources, and so on, are more complex. However, supporting mechanisms can be
provided by a base
class for all derived classes to use.
This chapter also suggests a few style conventions for using widgets with C++
classes. These
style conventions are not required to use Motif with C++ but they can help
collections of user
interface components to behave more consistently. The UlComponent class also
defines a simple
protocol for all C++ classes that create widget-based user interfaces. The
UIComponent class
provides support for interfacing C++ classes to Motif widgets and the Xt
Intrinsics. The remainder
of this book derives all user interface classes from the UlComponent class.
Chapter 3
Designing with Objects
E cn
There are many well-known approaches for designing applications, based on more
traditional
programming styles. However, object-oriented programming is relatively new, and
techniques for
designing object-oriented systems are not as well defined. Many design strategies
have been
proposed, but few are widely accepted or practiced. Some proposed approaches
emphasize compre-
hensive, theoretical techniques for developing object-oriented systems, while
others are based on
rapid prototyping in interpretive environments. In spite of their differences, the
goal of each of
these techniques is to allow the programmer or designer to start from a problem and
develop a set
of classes that can be used to implement a solution.
This chapter discusses one technique that helps programmers develop and maintain an
object-
oriented point of view when designing applications and discusses some issues
related to object-
oriented design. Section 3.1 discusses some basic steps found in most object-
oriented development
processes. Section 3.2 presents a technique known as “Classes, Responsibilities,
Collaborators”
(CRC), which can help developers identify and characterize the classes in a system.
Section 3.3
describes a notation based on CRC, which is used to document and present object-
oriented designs
throughout the rest of the book. Section 3.4 discusses some practical issues
related to designing
classes to be reused in more general situations.
A A el
In spite of many differences between various approaches to object-oriented design,
many people
agree that the process of developing an object-oriented application can be broken
down into a few
basic steps, at least at a very high level. The process of object-oriented
development is often
described as consisting of some variation of the following four stages:
Reusable classes generally fall into one of two categories. The first type of
reusable class can
be instantiated directly, while the second is used indirectly by deriving new
classes from an abstract
base class. If at all possible, programmers should recognize the existence of such
classes at the
earliest possible stages of design. Part of the design process involves making
trade-offs between
implementing new classes and molding the system to take advantage of existing
classes.
of reusable classes. Programmers often start from simple existing classes and
derive new classes
that add additional features or alter the behavior of the original base class.
These new classes can
Specializations start from a general-purpose base class and move toward a specific
purpose. For
example, Chapter 2 starts with a general Password class and then derives a
StrictPassword class
that adds additional features and alters the behavior of the base class to handle a
more restrictive set
of requirements. Sometimes the derived class adds additional features and is
intended to be used in
a wider range of situations than the base class. For example, the UlComponent class
extends the
developed as part of a derived class are propagated upward to base classes to allow
other classes to
opment process involves a seemingly endless stream of small details, which cannot
be completely
captured in the simple steps described here. However, these steps provide a useful
framework for
understanding the object-oriented approach.The following sections briefly discuss
each of the steps
in the object-oriented development process.
Sometimes it is clear that a task needs to be performed, but it is less clear which
object should
do it. It is useful to consider all the activities in a system and be sure some
object is responsible for
each action. Sometimes, the responsibility for a particular task can be assigned to
an object that has
already been identified. At other times, the action may suggest that a new type of
object should be
added to the system. For example, in the Password class discussed in Chapter 2, it
is apparent that
there is a need to maintain a list of valid passwords somewhere. We might decide
that this responsi-
bility does not really belong in the user interface component that accepts the
password from the
user. Instead, we could create a second class, a PasswordDatabase class, to perform
that function.
Many possible types of relationships can exist between objects (or classes).
Dependency
relationships occur when one object depends on another to perform a task. Objects
may be
mutually dependent and cooperate closely to fill a particular function, or they may
have unilateral
dependencies. Other classes may exhibit container relationships. The Stopwatch
class described in
Chapter 2 serves mainly as a container for the other objects. And of course,
inheritance relation-
ships are common in object-oriented programming. Here, the relationship includes
not only
elements that a derived class inherits from a base class but also any protocol
between the two
classes (in C++, the protected part of the base class).
This brief discussion is not meant to minimize the amount of effort required to
actually
implement the classes in a system. Most of this book is devoted to the
implementation details of
using C++ classes and Motif widgets. However, at the early stages of design, it is
often more appro-
priate to emphasize the more abstract qualities of various objects, as well as the
structure of the
overall program. A good understanding of the environment in which an object will
exist can make
the task of implementing an appropriate class somewhat less difficult.
Ward Cunningham and Kent Beck have developed a method for designing object-oriented
systems
that can help programmers follow the steps outlined in the previous section. This
method is known
as Classes/Responsibilities/Collaborators, or CRC [Beck89]. The approach uses
abstract principles
that apply to any object-oriented system, regardless of the implementation
language. CRC helps the
designers identify objects and relationships between objects, while building a
common under-
standing of a system. Beck and Cunningham have referred to CRC as “a laboratory for
teaching
object-oriented thinking,” because the approach helps designers who are new to the
object-oriented
approach think about problems in object-oriented terms.
CRC is most useful at the earliest stages of design. The primary goal of CRC is to
help the
programmer or designer identify classes that can be used to construct a system. The
method
concentrates on identifying the key responsibilities of each class and also
identifying relationships
between classes. Other classes that are involved in fulfilling a responsibility are
known as collabo-
rators. Classes may depend on collaborators to provide information or to perform
operations.
Although CRC can be used by an individual, it seems most effective when used by a
team.
Typically, a group gathers around a table, equipped with a package of 3x5 note
cards. A vertical line
is drawn near the middle of each card. Starting from some initial statement of the
problem, the
members of the design team suggest names of objects and classes that might be in
the system.
As each new class is identified, its name is written across the top of a note card.
Each card will
eventually contain a list of the class’s responsibilities and a list of other
classes that collaborate with
the class. Figure 3.1 shows the format of a typical CRC card.
Class Name
Responsibilities Collaborators
As the design proceeds, the group continues to discuss the architecture of the
system, seeking
to understand how the system might be divided into objects and classes and the role
each part
plays. Often this process takes the form of asking “what if?” As various scenarios
are examined,
new classes may be identified and assorted tasks that must be performed begin to
emerge. In an
object-oriented system, all tasks must be the responsibility of some class.
Each newly identified responsibility is recorded along the left side of the card
that represents a
Often, identifying one responsibility leads to other questions. For example, if the
Password
class is to validate a password when it is entered, the question naturally arises
“Where are valid
passwords kept?” The answer to this question is a design decision, which may lead
to added
responsibilities for some existing object or may introduce other objects. One
possibility in this
example is to define an additional responsibility for the Password class, which
could be stated as
“maintains valid passwords.” Another obvious question is “How does the system get a
password
from the user?” In response to this question we could add the responsibility,
“requests password
Password
One interesting characteristic of the CRC approach is that the distinction between
objects and
classes is deliberately blurred. At early stages of design, it is not always
necessary to distinguish
between instances of a class and the class itself. Generally, the focus is on
objects that exist in the
system, although the information of a card generally indicates a class. In general,
it should be clear
that the entities that interact in a program are objects, but the behavior of these
objects is defined by
classes.
The CRC approach seems to draw much of its strength from the group dynamics of the
design
team. The team may start with little or no common understanding of the problem. The
cards help
focus the group’s attention on the behavior and characteristics of the system. The
design team
develops a common understanding of the problem as new classes are discovered.
One key benefit of CRC is that it keeps the attention of the design team focused on
architec-
tural issues at the early stages of design. Instead of getting bogged down in
extraneous detail, the
team is encouraged to think about major components in a system and to determine
what these
components do and how they interact. CRC naturally keeps the team focused on
identifying
objects, rather than functions, data, algorithms, or other aspects of the problem
that might normally
be considered in a non-object-oriented design.
The fact that cards can easily be thrown away, erased, or modified contributes to
an overall
feeling of free-wheeling brainstorming. CRC encourages designers to think out loud
and breaks
down inhibitions that might arise if a design team begins to focus on more detailed
decisions too
early in the process. For example, suppose we were to decide that the Password
class should
maintain the list of valid passwords itself. It would be quick and easy to cross
out the PasswordDa-
tabase collaborator and add the new responsibility to the Password class card.
Beck and Cunningham have used CRC to teach object-oriented concepts as well as to
design
systems. Class cards can also be useful for explaining or documenting the design of
an overall
system. They often provide an appropriate level of detail for a first introduction
to a class. For
example, the Password class card in Figure 3.2 contains enough information to
convey the primary
function of the class. Ideally, someone who did not participate in the design
should be able to look
at the Password card and deduce that a Password object accepts a password from a
user and checks
the password against a list of valid passwords maintained by another object. In
practice however,
Beck and Cunningham report that the cards are often meaningless to those who do not
participate in
the initial design. It seems that much of the information about the behavior of the
system remains in
the heads of the design team. The cards act as a catalyst for the design process
and record an
important part of design but do not capture the entirety.
Responsibility-driven Design
The responsibility-driven approach varies in many subtle ways from the simple CRC
technique
described in the previous section. CRC cards simply list all collaborators along
the right side of the
class card. Wirfs-Brock et al. introduce the idea that collaborators should be
closely associated with
a specific responsibility. For example, Figure 3.3 shows a slightly revised class
card for the
Password class. This class card lists the collaborator, PasswordDatabase, directly
to the right of the
Password
1. Requests password from user
PasswordDatabase
Figure 3.5 shows a very simple collaboration graph containing the Password and
PasswordDa-
tabase classes. The rectangular boxes represent individual objects (or classes).
The semicircle along
the side of the box on the right represents a responsibility handled by the class.
A vector connecting
two classes represents a collaboration between those classes. The arrow points to a
relevant respon-
sibility handled by the collaborating class. For example, the arrow in Figure 3.5
indicates a
collaboration, or dependency, between the Password and PasswordDatabase classes
that involves
the PasswordDatabase class’s first responsibility, as listed on its class card.
Password i PasswordDatabase
This book uses a simple notation to describe the design of various examples
throughout the
remainder of the book. The notation is based largely on the CRC and responsibility-
driven
approaches described in the previous section, although with some custom features
that differ from
those approaches. This book uses four types of diagrams as aids when discussing
classes and appli-
cations built using C++ classes. These are: class cards, collaboration graphs,
message diagrams, and
inheritance graphs. The following sections introduce the diagrams used in the
remainder of this book
and demonstrates them, using the stopwatch example from Chapter 2.
Class Cards
This book uses CRC cards similar to those described in the previous sections to
record the respon-
sibilities and interdependencies between classes during the early design stages.
The class cards used
here are primarily inspired by the original Cunningham-style CRC cards, but borrow
a few ideas
from Wirfs-Brock. For example, collaborators are listed beside the responsibility
with which they
are associated. Responsibilities do not necessarily need to be associated with a
collaborator, but
when a collaborator exists, the card indicates this relationship.
The upper left corner of the class card lists the name of the class the card
represents. If a class
is known to be derived from some other class, this fact can be indicated on the
class card by listing
the base class after the class name, separated by a colon, in a style similar to
that used by C++. The
upper-right corner of a class card contains a word that indicates whether a class
is an abstract class
or a concrete (nonabstract) class. This region of the card can also identify a
class as a subsystem. A
subsystem is an object that serves as a module that loosely binds a set of related
objects.
Subsystems can be either concrete or abstract.
Finally, all responsibilities are numbered, beginning with 1. Each card starts its
own
numbering sequence, which allows a responsibility to be referenced without having
to repeat the
entire phrase. This feature is particularly useful in conjunction with
collaboration graphs.
Figure 3.6 shows a class card for the UlComponent class developed in Chapter 2, The
UICom-
ponent class has no collaborators, but lists five responsibilities. Notice that the
responsibilities
specify what the class does, but not how. The card does not contain sufficient
information to allow
a programmer to use the class. It provides enough information, however, to explain
why the class
exists and to remind the designer of what the class does.
Also notice that the class card does not list every operation supported by the
UIComponent
class. For example, the UlIComponent class implemented in Chapter 2 supports a
baseWidget ()
member function that returns the root of a component's widget tree. This
information could be
included, but this seems like a relatively inconsequential feature (or perhaps an
implementation
detail) of the class. A good class card conveys the basic flavor of a class, not
every detail.
UlComponent : BasicComponent Abstract
Figure 3.7 demonstrates a class card that includes a collaborator. This card
describes the Timer
class implemented in the previous chapter. The class has three responsibilities,
all of which involve
maintaining and reporting the elapsed time. The Timer class collaborates with the
Face class, which
actually displays the time.
Timer Concrete
Figure 3.8 shows a class card for the Face class. The Face class is responsible for
displaying a
floating-point number, used here to represent elapsed time in a digital format. The
Face class has no
collaborators because it simply displays a value in response to messages, which can
come from any
part of a program.
Face : BasicComponent Concrete
allows the user to issue a stop or start command. The Control class passes stop and
start commands
on to a Timer object, which is listed as a collaborator on the Control class card.
Pee eae E
ES
AN
wa
Ste a ÍA
PERA A EI aoe ae
MN ik A, AA E ls TO A
> A ir Li T e
CA E A AN AA
r — EULA
Figure 3.10 shows the Stopwatch class card. As implemented in Chapter 2, the
Stopwatch class
does very little. Its primary purpose is to instantiate and connect a Control
object, a Face object,
and a Timer object. However, this is an implementation-oriented point of view. When
deciding
what should go on the Stopwatch class card, we must consider the external view of
the class. The
Stopwatch class completely hides the other classes. To a programmer who needs the
functionality
of a class like Stopwatch, the fact that the class ties three other objects
together is not particularly
important.
It would be useful, however, to know that the Stopwatch class measures and displays
continuous elapsed time and that the class provides an interface that allows the
user to start or stop
the time. From an external viewpoint, these are the responsibilities of a Stopwatch
object, as noted
on the Stopwatch class card. Notice that the card identifies Stopwatch as a
subsystem, which
indicates that it encapsulates other objects. The Stopwatch class has three
collaborators, on which
the Stopwatch class depends to fulfill its responsibilities.
Nia A a ee
ST Te
Collaboration Graphs
As classes begin to be identified and class cards are being developed, it is often
useful to visualize
how various objects and classes in a system are related. The collaborations listed
on the class card
provide some indication, but it is often helpful to view this information more
graphically. Beck and
Cunningham mention that designers often find it helpful to position the class cards
to indicate
various relationships. For example, all classes that collaborate may be placed
together to indicate a
close relationship.
Figure 3.11 shows the Stopwatch class card and its various collaborators in a
single group. In
addition to showing that the classes collaborate by overlapping the cards, the
position of the cards
may imply other relationships. For example, the Stopwatch class card is on top,
which might
indicate a supervisory role. Note that such meanings are not well defined, and
teams may develop
different conventions as new needs arise.
Timer
Concrete
Timer
1. Starts and stops a clock
clock started
Figure 3.12 shows a simple diagram that represents each collaboration as a vector
leaving a
class and pointing to a collaborator. Like collaborations, this connecting line can
be unidirectional
or bidirectional. For example, Control collaborates with Timer, but not vice versa.
Classes that
collaborate with each other are shown with an arrow at both ends of the line. The
rectangular boxes
in Figure 3.12 represent concrete classes, and the box with rounded corners
indicates that
Stopwatch is a subsystem, which contains the other classes.
The collaboration diagram in Figure 3.12 simply shows that some relationships exist
between
various classes, without providing additional information. This diagram can be
thought of as an
entity-relationship diagram in which the only relationship is “collaborates,” where
“collaborates”
can be loosely interpreted as a dependency. In most cases, a collaboration
indicates that one or
more messages are exchanged between the objects. It may be useful to start with
simple diagrams
such as this and add more detailed information later.
Figure 3.13 shows a revised collaboration graph for the Stopwatch subsystem.
Stopwatch Subsystem
Control
The collaboration involves the Timer’s first responsibility, which is to start and
stop the clock.
Similarly, the Timer class depends on the Face class to display the time. The Face
class is listed as a
collaborator on the Timer class card, and the collaboration graph shows that the
Timer class
depends on the Face class’s first responsibility, as listed on its class card
The collaboration graph handles subsystems a little differently than other classes
because the
subsystem’s primary purpose is to encapsulate the other objects and provide a
single external
interface. For example, the first responsibility of the Stopwatch subsystem is to
measure elapsed
time. This responsibility is delegated to the Timer class, as shown by the arrow
pointing to the
second Timer responsibility, which, from the Timer class card in Figure 3.7, is to
maintain elapsed
time.
Stopwatch Subsystem
Timer q Control
LR w A
NY i
Face Stopwatch
CIS ESA
The subsystem box indicates the external responsibilities and shows that the
Stopwatch
subsystem contains the other classes, while the internal Stopwatch class shows the
collaborations
with the other classes. Notice that this does not mean that there are two distinct
Stopwatch objects.
The diagram in Figure 3.14 simply differentiates between the Stopwatch class’s role
as an encapsu-
lating subsystem that provides an external interface and its internal relationships
with other objects.
Collaboration graphs contain little information that is not already present on the
class cards.
However, the graphical format provides a useful view of the system. Often,
attempting to draw a
collaboration graph reveals structural flaws in the system that would not otherwise
be seen.
Drawing a collaboration graph may uncover missing responsibilities or unusual
collaborations. The
class cards may be revised many times while drawing the collaboration graphs.
Therefore, it is
useful to create collaboration graphs as class cards are being developed. Correctly
identifying
Message Diagrams
manage
elapsedTime
elapsedTime
manage
timerStopped
timerStarted
Stopwatch
manage
Message diagrams are similar to collaboration graphs, but there are significant
differences as
well. Complete message diagrams of real systems are often too complex to be useful
for large
systems. However, message diagrams of restricted subsets of a system may be useful
for
documenting or visualizing the structure of that part of the system. In systems
that exhibit some
hierarchy, it may be feasible to diagram the messages between subsystems at various
levels of
abstraction. For example, programs that use the Stopwatch component can show
messages to and
from the Stopwatch class but need not include the Timer, Face, and Control classes
in an appli-
cation-level message diagram.
Note that, unlike the other notations discussed in this chapter, message diagrams
are more
suitable for documenting and examining completed systems than as vehicles for
discussing early
designs. By the time a programmer can specify the precise messages sent between
each object in a
system, the initial design is most likely complete, and the system is close to the
implementation
phase.
Inheritance Graphs
BasicComponent Timer
However, simply packaging code into a class does not automatically make the code
reusable.
Writing a truly reusable component can be a surprisingly difficult task. Object-
oriented
programming languages and techniques can provide a mechanism to support the
construction of
reusable components, but that is only the beginning. In general, programmers must
plan for reuse
from the beginning; it seldom happens by accident. This section discusses a few of
the design
issues that must be addressed to produce reusable classes.
It is important to realize that not all classes must be, or even can be, reusable.
There is nothing
wrong with writing a class that serves only one specific purpose. A trade-off must
always be made
between the time required to write the class required in a given application and
the additional time
required to implement a more general solution that might be useful at some later
time. Sometimes,
the additional effort is not worthwhile. Often, however, a small initial effort is
repaid many times.
The first, perhaps somewhat obvious, requirement of any reusable component is that
it must
solve a general problem. Good candidates for reusability are usually applicable to
many situations.
This is one reason most books that discuss reusable classes present examples such
as arrays, hash
tables, linked lists, and other common data structures. Such objects are so general
that they are
usually needed over and over again by nearly everyone. However, the requirement
that a class be
generally useful is relative. While lists, stacks, and hash tables may be
applicable to most programs,
many applications may have domain-specific or application-specific needs that can
take advantage
of a reusable class.
The task of creating reusable classes begins at the earliest stages of design, when
objects and
classes are being identified. There is almost always more than one way to decompose
a problem
into objects. In particular, there may be at least one set of objects that solves
only the specific
problem under consideration and perhaps another set of objects that solves the
problem in a more
general way. The most direct, and least reusable, solution is usually the easiest
to identify and to
implement. The more general solution requires more care and more effort, but may
produce a class
or classes that can be reused on other situations, providing greater productivity
later.
Programmers who have learned to think in terms of reusable classes are always
looking for a
more general solution to any problem. For example, consider the Password and
PasswordDataBase
classes discussed earlier in this chapter. If fully and carefully implemented,
these would both be
good examples of reusable classes. They perform a single, isolated task that is
needed by many
different programs.
However, we should ask whether these classes could be made even more useful. For
example,
the Password class displays a prompt and allows the user to type into a text field,
performing some
action when the user types the <RETURN> key. This functionality is needed in many
situations.
Perhaps we should implement a general PromptForInput class that captures as much
general
Similarly, the PasswordDatabase class is just one example of a class that looks up
a string in
some type of table. The fact that it is a table of passwords may impose some
security constraints,
but otherwise this class provides a general facility. It would be unfortunate if it
were restricted to
just handling passwords instead of handling a more general case.
Good reusable classes often require several rounds of redesign specifically aimed
at improving
reusability before they evolve into truly reusable classes. Reusable classes must
be designed to
minimize dependencies on other classes and provide a consistent, well-thought-out
protocol. The
implementation must be robust as well. Not all classes are worth this additional
effort.
Another requirement for a reusable component is that it must be easier to reuse the
component
than to write one from scratch. This may seem like an obvious statement, but it is
easier said than
done. For example, for a class to be easy to reuse, programmers must be able to
find it easily. They
must know of its existence at the appropriate time. More code has probably been
rewritten because
the programmer didn’t know that a routine already existed than for any other
reason.
Assuming the programmer can find a promising piece of code to reuse, the component
must be
easy for the programmer to understand. At a minimum, the component should be well-
documented.
Unfortunately, documentation is usually the weakest part of any software library.
Sometimes classes are not reused because they provide more functionality than is
needed for a
particular application. A programmer might decide not to reuse a class because it
has features that
will not be used in the targeted application but that are costly in terms of space
or efficiency. Inher-
itance provides a useful way to address this issue. It is often useful to begin
with a simple class that
implements the absolute minimum functionality required for a given situation. Then,
additional
functionality can be added in derived classes, creating a hierarchy of classes with
continually
increasing functionality. Programmers who want to reuse such classes can choose the
point in the
inheritance hierarchy that best fits their needs and do not need to pay for
features they don’t need.
Programmers who need a simple class can create new subclasses from a class near the
top of the
hierarchy, while those who need more features can choose from derived classes.
Suppose a programmer needs a simple timer module that displays elapsed time. After
spending
some time looking for such a class, he or she discovers the Stopwatch class
described in Chapter 2.
However, suppose that the programmer only needs a programmatic interface for
starting and
stopping time. In that case, the Stopwatch provides user interface components that
incur some
additional cost, which make the class unusable. The programmer may be able to use
some of the
other classes in the Stopwatch subsystem. More likely, because he or she will have
to spend the
time to figure out how the Stopwatch works before even attempting to use the other
classes, the
Stopwatch will just be thrown away and a new class will be written.
Unfortunately, most C++ classes must have outside references. Few objects can
exist, or be
useful, in complete isolation. Any object that calls a member function belonging to
another object
must have a pointer to that object. Furthermore, because of the strong type-
checking provided by
C++, the exact type declaration of such objects must be known at compile time.
Objects can receive input (messages) without being aware of the class of the
sending object.
Classes that only receive messages, such as the Face class discussed in Chapter 2,
are often the
easiest to reuse. The Face class, for example, contains no references to other
classes. It could be
used in many situations in addition to the Stopwatch class for which it was built.
(Notice that the
Face class has other limitations that may limit its potential reusability. It
displays only floating
point numbers, assumes a certain precision, and so on.)
The Timer class, on the other hand, produces output by sending messages to a Face
object. In
fact, the way the Timer class is written, it can send messages only to a Face
object (or an object
instantiated from a class derived from Face). This is unfortunate because it
seriously limits the
reusability of the Timer class. There should be many applications for a class that
produces a clock
tick at any specified interval and that can be started and stopped
programmatically. However, the
fact that the Timer class has a hard-coded dependency on the Face class prevents it
from being
useful in many situations.
There are several solutions to this problem. One solution is to make the Timer
class a parame-
terized type, or template, as described in [Stroustrup91]. Parameterized types
allow classes to be
treated as type-independent templates that separate the algorithms implemented by
the class from
the data types, including classes, upon which the algorithms operate. For example,
if the Timer
class were implemented as a parameterized type, the type of the object to which it
is to be
connected could be specified when the class is instantiated. This allows a
programmer to create a
StopwatchTimer class for the Stopwatch example and to use the Timer to interact
with completely
different classes in other applications.
Besides templates, there are some other solutions available for programmers who
want to
make classes as reusable as possible. The issue is how to connect the “output” of
one class to
another without tying the two classes together. Basically, there are three ways to
implement a class
that needs to send messages to another class.
The first way is to call the other object’s member function directly. This requires
that the decla-
ration of the other class be known at compile time and binds the class tightly to
the other class. The
Timer class in Chapter 2 uses this approach, and the template mechanism described
above is a more
flexible extension of this basic approach. Calling member functions directly is the
most expedient
way to connect different objects but results in the least reusable code, unless
parameterization is
used.
The second approach is to defer the connections to derived classes. This can be
done by
designing classes to call virtual member functions, which can or must be
implemented by derived
classes, instead of referring to outside classes directly. For example, let’s
rewrite the Timer class to
1 PLAI EPEAT CAA LET L AIN AAA RIA AA LEELA AL ELSA AAA AAA TLS E
4 PPEUFELI STL EFL EEL ETL ET ALTA LEAT ES OLE ELIT ATL ELT AL EIT ETAL
5 #ifndef TIMER_H
6 #define TIMER_H
7 #include <Xm/Xm.h>
9 class Timer {
10
11 public:
17 protected:
2d
22 private:
24
27
32 );
33 #endif
All member functions are the same as previous versions, except for the constructor
and the
tick() member function. The new constructor requires only an application context
and the
update interval and no longer expects a Face object.
id = NULL;
_app = app;
_counter = 0;
_interval = interval;
The new version of tick() calls the reportTime () virtual function instead of
updating
the Face directly. The tick () member function can be written as follows:
_interval,
&Timer::tickCallback,
( XtPointer ) this ):
This approach eliminates the dependency on any outside class and produces a more
reusable
Timer class. However, to use the class, the programmer must create a new derived
class that
overrides the reportTime () member function. To use the new Timer class in the
stopwatch
example, we must create a derived class that provides an interface to the Face
class. We can write
such a derived class as follows:
on nm FP WD P
ILA ELE EL EL ARA NAAA AASA A ERA EEL EL EAL TAL ALIS EL TEAL EAS
// StopwatchTimer.h: A clock class for the stopwatch
TEEEL ETL ALL EAA ELA ATID AT EAL LAL AAS AMAIA VITARA ERA AAR SI
#ifndef STOPWATCHTIMER_H
#define STOPWATCHTIMER_H
#include "Timer.h"
class Face;
i?
#tendif
The implementation file contains only a constructor and the reportTime() member
function, both of which are very simple. The constructor simply calls the Timer
constructor and
saves the pointer to the Face object. The reportTime () member function calls the
Face object’s
setTime () member function.
LET IESE AE LTTE LEIS ELLA ELE EE ITA ELIE AA EEL EELS IATA EAN
2 // StopwatchTimer.C: A clock class for the stopwatch
3 ELALAAAALA NA CELE LEELA EEL ALE ET ETT LF EET TATE TAG ATA AD ETT ET
4 #include "StopwatchTimer.h"
5 #include "Face.h"
8 Face *face,
9 int interval )
10 Timer ( app, interval )
134
12 _face = face;
13 }
14
16 (
19 _face->setTime ( time );
18 )
This approach allows the Timer class to be used in more situations by eliminating
the depen-
dency on the Face class. However, it requires some additional programming to create
the derived
class. For a simple class like the Timer class, it may be almost as easy to
reimplement the class each
time it is needed. However, for more significant classes, this approach can be very
effective.
The third way to allow a class to communicate with another is to use a callback
technique
similar to that used by Xt and Motif. With this approach, the Timer class must be
given a function
to be called with each clock tick. Applications that use the Timer class can define
the function to
make the actual connection to another class.
IERIAVARA TIA EA TESTE TEE TELA ESE ETAT EELS LEE ARRIBA E AAA IA IATA
// Timer.h: A generic clock class, "callback" version
FEL EE REL ELIE E RANA IL EES IL EAE LT EEL IP ITT TI AAA MARIAN AAA TUI AA
#ifndef TIMER_H
#define TIMER_H
#include <Xm/Xm.h>
wo 0 3 HA UN FP WD FE
10 class Timer {
12 public:
13
19 private:
20
22
24
26
27 TimerCallback _func;
28 void * data;
33 37
34
35 #endif
This version of Timer is similar to the original, except that instead of requiring
a pointer to a
Face object as an argument to the constructor, the Timer class requires a pointer
to a function of
_ type TimerCallback and some optional client data. The TimerCallback function is
defined
as a void function that expects two arguments, a float and an untyped pointer (void
*).
The Timer constructor maintains a pointer to the specified function, to allow it to
be called
later.
1 ESETAAAAAA RES AAAIA AAA RANA A ANAA TIELT TIAI ELIA IACI
2 // Timer.C: A clock class for the stopwatch
5 tinclude <Xm/Xm.h>
8 TimerCallback callback,
9 int interval,
10 void *data )
11 (
12 _func = callback;
13 _data = data;
14 _id = NULL;
15 app = app;
16 _counter = 0;
17 _interval = interval;
H
00
w
20 f
25
27
All remaining functions are the same as the original Timer class in Chapter 2. Now,
let’s see
how the Stopwatch class can use this new Timer class to display the elapsed time in
a Face object.
First, the file Stopwatch.C implements a function, updateFace (), that expects a
floating point
number and an untyped pointer. In this case, the pointer is expected to be a Face
object. Once the
pointer is cast to the proper type, updateFace () calls the Face class’s set Time
() function for
the object.
g PLEA TAAL TATTLE ELE EL LETT TAAL LA TAL SARA AAA LAAT ALERT ERUIT tI
2 // Stopwatch.C: Group subcomponents into one stopwatch component
3 EXRAAARIAAN A TASA ATA ARANA AAA TMT ETAT AEA TAPIA NADA RITA ERAS
4 #include "Stopwatch.h"
5 #include <Xm/Xm.h>
6 #include <Xm/RowColumn.h>
7 tinclude "Timer.h"
8 tinclude "Face.h"
9 tinclude "Control.h"
10
EM
1s ¢
The Stopwatch constructor must specify the updateFace () callback function when the
Timer is instantiated. It must also pass a pointer to the appropriate Face object
as the client data
argument to the Timer constructor. The client data will be passed back to the
updateFace ()
function when it is called.
ae
25
31
g3
34 // Manage the two user interface components
35
36 _face->manage() ;
37 _control->manage () ;
eN: TE.
All other Stopwatch member functions are the same as in the previous version. This
approach
allows the Timer object to be reused in a variety of situations and, other than
templates, is the
simplest way to make the Timer class reusable because it requires the programmer to
write the least
amount of additional code. Anyone who wants to use the Timer class simply has to
write a single
function that performs the appropriate action at each tick of the clock.
For very simple classes, such as those described here and in Chapter 2, the
techniques
described can help reduce dependencies on external objects and produce more
reusable classes.
Unfortunately, the solutions presented here cannot always be applied to all
situations. Most classes
are much more complex than those discussed so far. Not all classes are simple
combinations of
unidirectional input and output. For example, if two objects must interact closely,
the techniques
just described may not be adequate. Some classes are simply not able to stand alone
and are not
likely candidates for future reuse.
Summary 145
3.5 Summary
Object-oriented programming offers the ability to package code in such a way that
it can be
reused many times. Reusability is a worthwhile goal, because it can improve
programmer produc-
tivity, reduce maintenance costs, and improve software quality. However, truly
reusable classes are
often surprisingly hard to write. This chapter examined a few pragmatic issues
related to reuse by
trying to redesign the Timer class from Chapter 2 to be more reusable. Classes can
be designed to
be reusable, but it takes extra care and attention at all stages of development.
One of the goals of the UlComponent class and its corresponding model was to
provide a base
for general-purpose user interface components based on C++ and Motif. C++ user
interface compo-
nents are meant to be higher-level than Motif widgets and may be developed for a
specific
application. However, with some care, it is feasible to create more general-purpose
components
based on the Ul[Component class.
This chapter represents a very simple, brief, and incomplete overview of a complex
topic. The
main intent is to provide some basic tools that can be used to explore and explain
the design of the
examples in following chapters. Readers can turn to many sources for more
comprehensive infor-
mation on object-oriented design, as well as alternative approaches and viewpoints.
The
Bibliography on page 434 lists several sources of additional information. Designing
Object-
Oriented Software, by Rebecca Wirfs-Brock, Brian Wilkerson, and Lauren Wiener
describes a
simple approach to object-oriented design that inspired much of the material in the
first part of this
chapter. Their technique, which focuses on identifying objects and interactions
between objects,
can help programmers develop a clean, object-oriented architecture for a system
before embarking
on an implementation. For those looking for more pragmatic tips and techniques for
designing with
C++ classes, The C++ Programming Language, by Bjarne Stroustrup [Stroustrup91]
devotes
several chapters to object-oriented design.
The next two chapters apply the basic design techniques discussed in this chapter
to a more
realistic example. Chapter 4 develops a high-level design for a simple program,
based on the CRC
approach described in this chapter. Chapter 5 uses the component approach for
mixing Motif and
C++ to implement the design described in Chapter 4.
Chapter 4
TicTacToe: Design
This chapter begins an extended case study that exercises some of the techniques
discussed in
Chapter 3 and also provides further examples that use Motif with C++. The case
study discusses the
design and implementation of a simple computer version of the well-known tic-tac-
toe game. We
will call the computer version “TicTacToe.” The emphasis in this chapter is on
developing the early
design of the program. The exercise starts with a simple statement of the problem,
and then begins
to identify a set of classes with which to model the system. The design proceeds
until a complete set
of class cards is developed, along with a collaboration graph that details the
architecture of the
program. Chapter 5 presents an implementation based on the design developed in this
chapter.
The simple design process described in the previous chapter addresses only the
internal archi-
tecture of a system. In interactive programs, designing an adequate user interface
is equally
important. After discussing the internal design, Section 4.5 turns to the issue of
designing the user
interface portion of an application like TicTacToe.
This chapter and the next also present the first realistic use of the complete
UlComponent
class, along with the idea of user interface components based on C++ classes, as
introduced in
Chapter 2. The discussion attempts to begin the design process with as few
assumptions as
possible. However, there is an underlying assumption that the program will be
implemented in
C++, using Motif as the user interface platform.
The exercise in this chapter tries to trace a realistic design process, complete
with occasional
false turns, missteps, and second guessing. Good designs are seldom achieved in a
single pass, and
the design discussion that follows does not always take a straight path to the
eventual solution. The
goal is to demonstrate and evaluate a design process based on the CRC approach in
the context of a
typical application based on C++ and Motif, not to simply spell out the final
solution.
The first step when developing any application is to identify or clarify the goals
of the project. A
reasonable place to start is with a written description of the program to be
developed. This
description can be used as the first step in finding the objects and classes in the
program. If the project
has not already been defined by someone else, the programmer or project team can
always write a
Real projects will often have a more complete and formal description, but this is a
simple
program. The brief description above leaves a lot to the imagination, but it should
be enough to get
us started designing the game.
Let’s begin the design process by trying to determine what objects will be needed
by TicTacToe. As
discussed in Chapter 3, one way to get started is to go through the description
given above, and
underline all the nouns or noun phrases. We can also mark the verbs at the same
time. In the
following description, all nouns are underlined and all verbs are italicized. With
some experience,
| In a book like this, it would be very easy to contrive a description of
requirements such that all the nouns magically map
to the objects we need in the system. All that would be needed is to do the design
(or the implementation!) first and then
write the requirements to fit the design. However, I promise that this was not done
here. I did have some idea of where I
wanted to go before writing the description, but hopefully that will always be the
case - it would be silly not to use all your
experience at all stages of the design. However, the problem description remains as
it was first written. It is perhaps not
even a very good description. That is all right. The point is to use the
description as a starting point for thinking about the
objects and classes in the system, not to get it perfect the first time.
The description contains the following nouns, which, according to the approach
presented in
Chapter 3, may suggest objects and classes that could be used to implement the
TicTacToe game:
The description also contains the following verbs, which should indicate actions
that might be taken
by the objects in the system.
display mark select respond wins
prompt report clear start quit
begins issue is make marking
gets get displaying
These lists contain many verbs and nouns that do not seem very useful. Others might
or might not
be useful as objects and classes, and it seems likely that several radically
different designs could be
developed, all based on this list of nouns and verbs. Going through the entire list
is a tedious task
(and this is a small example!), but fortunately we can let intuition and experience
weed out most of
the list quickly.
For example, the noun TicTacToe is the name of the game, and it doesn’t seem useful
to model
the entire game as an object. Modeling each X and O as an object is a possibility
but seems like
overkill for a project of this size, so we can remove those. Similarly, modeling
moves and turns as
objects would require more work than we need to do for a simple game, although such
objects
noun’, and mouse as being an unnecessary entity to model, given X’s event-driven
model.
Continuing to consider each noun, we can narrow the list of potential objects to
the following:
e grid
e square(s)
* commands
e board
This doesn’t seem like an adequate list, but let’s leave it for now and move on to
the actions.
Looking at the list of verbs, we see several items that indicate user actions and
others that indicate
actions performed by the game. It appears that the user should be able to quit the
game, and start or
begin a new game. These are types of commands and may be related to the command
object
already tentatively identified as an object. Let’s assume for now that we need such
an object and
We also see that the user needs to be able to se/ect squares and that an
appropriate mark must
be displayed on the game board. It seems likely that the tic-tac-toe grid, listed
as an object above
could be modeled as an object that supports methods for displaying Xs or Os in the
squares in the
grid. The grid also serves as an input area that allows the user to select and mark
a square. We also
see from the description that we need a way for the user to clear the board when a
new game is
started. That could also be a responsibility of the grid object.
There are two other interesting verbs in our list. The first one is the verb
responds. Who
responds? Because TicTacToe is to be a computer game in which the user plays the
computer, there
must be a “brain” somewhere that plays against the user. Perhaps the brain is
indicated by the game
and TicTacToe nouns we threw away so quickly. Who responds? The game responds. But
it isn’t
really the entire game, it is the other player in the game, the computer. So, let’s
pick a new name
and refer to the intelligent part of the program as the Engine object. The Engine
is the part of the
program that drives everything, determines the computer’s next move, and so on.
The other interesting verb is reports. Who reports the winner? The Engine object
could do it,
or perhaps a separate object watches the game to determine the winner. Another
question is “How
is the winner reported to the user?” Related to this question, we see another verb,
prompt. The
program is supposed to prompt the user for the next move. How? Perhaps we need an
object to
handle messages to the user, popping up dialogs or whatever is appropriate to
convey information
to the user. We could tentatively identify this object as the Message object, which
is responsible for
communication with the user.
At this point, let’s start to put together a few CRC-style class cards and see what
we have. Like the
lists of verbs and nouns, these class cards need not be perfect. They serve as a
way of getting started
2 “Version” actually seems like a very good candidate object in a real system,
although not in the context in which the word
is used in this description. A Version object that records information about the
current release of a system can be useful in
supporting a piece of software. The Version object could be responsible for
maintaining an identifying “stamp” of some
kind and also for checking versions of anything the program depends on, such as the
operating system. The Version object
could check all relevant dependencies at startup time and report any problems to
the user: “Sorry, you must upgrade your
system to at least OS 17.2 before running this program,” and so on. We don’t need
anything this sophisticated for the sim-
ple example discussed here. Such decisions represent a typical part of the design
process, of course. Anything could be
represented as an object. The goal is to split the problem into slices that make
sense, that work together, and that meet the
needs of the project.
quickly and focusing attention on the kinds of objects that could be used to model
the problem. It is
more important to get started than for everything to be correct the first time.
First, we know we have a tic-tac-toe grid. In fact, three of the four nouns in the
list of potential
objects seem to indicate a grid (grid, squares, and board). Let’s call this object
the GameBoard, and
create a class card for it. We can assume that GameBoard is a concrete class.
Because the TicTacToe
grid is a visible portion of the game, we can also anticipate that the GameBoard
class will be derived
from the UlComponent class, although such details are relatively unimportant at
this point in the
design.
The previous section listed several responsibilities for the GameBoard class. The
GameBoard
displays the familiar tic-tac-toe grid and is also responsible for displaying Xs
and Os. For now, we
can assume that, like the pencil and paper version of tic-tac-toe, moves are
entered directly in the
playing area. This implies that the GameBoard class is responsible for handling
input and for
accepting the user’s moves.
It is reasonable to expect the GameBoard to interact with the Engine object (also
identified
during the examination of verbs and nouns) in some way, because the Engine needs to
be able to
communicate the computer’s moves to the user and to be notified of the user’s
moves. The first of
these interactions represents a one-way collaboration in which the Engine uses the
GameBoard as
an output device. This indicates that the GameBoard is a collaborator of the Engine
class, which
can be listed on the Engine class card.
The second interaction involves passing information from the GameBoard to the
Engine for
further handling. This means the GameBoard must collaborate with the Engine class
to record each
of the user’s moves, which should be indicated on the GameBoard class card.
These initial ideas produce the GameBoard class card in Figure 4.1.
1. Displays a Grid
2. Displays an X or O in each square
3. Clears the playing board
Next, we can look at the Engine class. So far, we know that the Engine is
responsible for picking the
computer’s move. It seems likely that the Engine will need to maintain some
information about the
internal state of the game to accomplish this task, so we can add “maintains state
of game” as another
responsibility. Because the Engine must keep track of the moves and the progress of
the game, it also
seems reasonable to make the Engine class responsible for prompting the user for
the next move.
The Engine class should also notify the user when either side wins the game.
The Engine object needs to collaborate with the GameBoard object to specify and
display
moves. The Engine object also needs to communicate with the user, but we have not
firmly estab-
lished how that is to be done. For example, we stated above that the Engine class
should prompt the
user for the next move. However, it does not seem right for the Engine to be
involved in creating or
displaying messages in windows. More likely, the Engine should only initiate or
control these
actions; some other object should handle the actual communication. For now, we can
say that the
Engine object collaborates with the Message object, tentatively identified earlier,
to accomplish
these goals.
These decisions result in the Engine class card shown in Figure 4.2.
Engine Concrete
Now we can turn our attention to the Command class. We know that the user must be
able to exit the
game and that the Command class should provide some way for the user to issue an
exit command.
Exiting an application is a simple thing, so perhaps we should make the Command
class responsible
for accepting the command from the user and actually exiting as well.
Next, the Command class must accept a “new game” command from the user. Carrying
out
this command involves resetting the state of the game board, which is maintained by
the Engine
class. Therefore, the Engine class is a collaborator.
When a new game begins, the GameBoard also needs to be cleared, so the Command
object
might also collaborate with the GameBoard. However, this implies that two separate
messages, one
to the GameBoard and one to the Engine, must be sent to accomplish one logical
operation. This is
an example of a “wide interface,” which is best avoided. Such situations often lead
to code that is
difficult to maintain. It seems better for the Command object to simply notify the
Engine object that
a new game should be started. The Engine object can then reset its own state and,
in so doing,
request the GameBoard to clear itself. This identifies yet another collaboration
between the Engine
class and the GameBoard class and further establishes the Engine class’s role as
the central
controller or “traffic cop” of the system. Making the Engine class responsible for
resetting the game
also simplifies the design and implementation of the Command class, which now
interacts with
only one other class instead of two.
Earlier, we identified the need for a Message object that somehow displays messages
to the user. The
Message object fulfills the function indicated by the verb reports in the initial
description. We
decided earlier that the Engine was responsible for initiating various messages to
the user, but that
the Message class would provide the interface for the communication.
Let’s create a card for the Message class as well. It apparently has only one
responsibility: to
display messages. Only the Engine class interacts with the Message class, because
that is the only
object we have identified as needing to report information to the user. So, the
Message class does
We have now identified four objects in the TicTacToe program and have a rough idea
of the respon-
sibilities of each object, or class. We also have identified some collaborations
between objects and
have speculated a bit about how the objects fit together to form the system. We can
now begin to
examine this information in more detail.
The primary goal at this stage is to make sure the responsibilities have been
assigned correctly
and that the objects identified so far are adequate and represent a workable
design. Before imple-
menting the system we should also be comfortable with the overall structure of the
application. It
may be useful at this point to develop a collaboration graph that shows how all the
pieces fit
together to define the architecture of the program. Although we are not yet ready
to begin imple-
menting these classes, we can begin to consider some implementation issues as well.
For example,
we can begin to look for opportunities for inheritance and other forms of reuse.
A Collaboration Graph
Let’s begin by examining a collaboration graph based on the class cards identified
in the previous
section. Figure 4.5 shows a simple collaboration graph created by simply connecting
all collabo-
rating classes in the system. We see that the Engine plays a central role and is
connected in one way
or another to the other three objects, which are all user interface components. The
Command class
collaborates with the Engine class to allow the user to start the game. The Engine
uses the Message
class to provide information to the user, while the GameBoard class and the Engine
class collaborate
with each other to provide input and to display both the user’s moves and the
computer-generated
moves.
With this vague view of how the system fits together, let’s revisit each of the
classes we have
specified so far. The goal is to make sure the responsibilities assigned to each
class make sense,
according to our current understanding of the system. In some cases,
responsibilities may need to
be reassigned or reorganized. In other cases, some minor changes in wording may
help clarify the
design.
Earlier, we decided that the Command class is a user interface component that
allows the user to
issue a quit command and a new game command. So far, we have not decided the exact
form of the
interface used to issue these commands. The class could be implemented as popup or
pulldown
menus or perhaps a set of command buttons. The class might not even be a visible
component. It
could just be an object that responds to commands typed in by the user.
According to the original class card shown in Figure 4.3, the Command class handles
each of
its commands differently. In one case, it assumes the responsibility of fulfilling
the quit command
itself, by exiting the game. In the other case, the new game command must be
handled elsewhere,
and the Command object simply forwards the request to the Engine class. In
retrospect, this seems
like a poor division of responsibilities. The Command class’s responsibility should
be to accept
commands from the user and to route them to the appropriate entity in the program,
not to fulfill
commands itself. For example, other objects might need to perform some cleanup
before the
program exits. If the Command class handles exiting the application by itself,
other objects will not
have an opportunity to perform any required actions before the program exits.
It seems more appropriate for the Command class to collaborate with the Engine
class to exit
the application. The Command class simply passes the command from the user on to
the Engine
class. This is more suitable for the Engine class’s role as the central coordinator
for the game.
Figure 4.6 shows the revised Command class card.
Command Concrete
The GameBoard class seems to have a reasonable set of responsibilities. Like the
Command class,
we are not sure exactly how the GameBoard functions or precisely how it appears on
the screen.
However, we expect the GameBoard to display a tic-tac-toe grid, accept input, and
display output.
As an input device, the GameBoard passes the user’s moves on to the Engine class,
which records
and responds to them. As an output device, the GameBoard object is completely under
the control
of the Engine, which displays the computer-generated moves and clears the board
when a new game
is started.
The Message class is the only other user interface component. This class simply
provides a way for
the Engine to communicate with the user. There is some potential for overlap with
the GameBoard
class because both function as output devices. It seems possible that the
functionality supported by
the Message class could be assumed by the GameBoard at some point. However, because
we have
not yet considered the user interface to TicTacToe, it seems better to abstract the
facility used to
display messages for the user in the Message class for now.
The Engine class is a very important part of the TicTacToe program because the
Engine controls the
action of the game. It warrants closer examination because it has many
responsibilities and plays a
crucial role in the game. In fact, it seems that we may have overloaded the Engine
class on the first
pass, simply throwing everything that wasn’t part of the user interface into the
Engine class.
As defined on page 151, the Engine class is responsible not only for the flow of
the game,
(keeping track of who moves when), but also maintaining an internal version of the
state of the
board and choosing the computer’s responses to the user’s moves. It might make more
sense to
reassign some of these responsibilities and redesign the Engine to play a more
coordinating role.
Maintaining the state of the board separately from the GameBoard object seems
redundant,
and perhaps we could just use the GameBoard object to keep the state of the game.
However, this
might prove to be cumbersome in some situations. For example, we might want to
experiment with
the board, trying out future moves and so on. One way to determine the game’s moves
would be to
create a game tree and use a min-max approach to find the best possible move
[Charniak85]. This
approach involves generating a version of the board for nearly every permutation of
moves and
evaluating each position. Because the GameBoard creates a visible user interface
component, it
does not seem likely that we would want to make this process visible by
instantiating many
GameBoard objects, complete with a tic-tac-toe grid for each possible move.
However, it does seem useful to define a lightweight Board class that can be
instantiated as
many times as necessary to create off-screen representations of the game. One
instance of the
Board class can be used to track the current state of the visible board, while
additional instances
might be used to try out future moves. So, let’s remove the responsibility of
maintaining the state of
the board from the Engine class, but add the Board class as a collaborator. Even if
TicTacToe
creates only a single Board object, the design will be more modular.
Next, we need to consider the way the game chooses moves. There are several
strategies for
choosing moves in a tic-tac-toe game. In fact, we might at some point want to
change the algorithm
used to determine moves or even to change the rules of the game itself. Rather than
grouping the
decision-making process with the mechanics of playing the game, it might be best to
pull this
component from the Engine object and create a new MoveGenerator object.
What is left for the Engine class? It fills a coordinating role. For example, it
must remember
whose move is next so that it can arrange the appropriate action at each step. This
seems to be an
internal responsibility because it does not directly involve any other class. Also,
in the discussion
on page 154 we removed the responsibility of exiting the game from the Command
class and
moved it to the Engine class. The Engine class should now perform the steps needed
to close down
the game.
The two newly identified collaborators, Board and MoveGenerator, as well as a set
of revised
responsibilities, are shown on the final Engine class card in Figure 4.7. In many
cases, the responsi-
bilities listed on the original Engine class card have been reworded to indicate
that the Engine relies
on other objects to accomplish its goals. The class card in Figure 4.7 attempts to
convey the
external responsibilities of the Engine class without implying that it performs
these actions itself.
Engine Concrete
As discussed in the previous section, the Board class represents the current state
of a game. One
Board object maintains the current state of the game being played. The Board class
must support
some representation of the game board, but the choice of representation is an
implementation issue
and can be kept private. The Board class must allow both the user and the TicTacToe
game to record
moves. The user enters moves via the GameBoard. The GameBoard passes each move to
the Engine
class, which records the move with a Board object. The computer-generated moves are
chosen by
the MoveGenerator class, although the Engine is responsible for seeing that a move
is generated and
recorded with a Board.
Figure 4.8 shows the Board class card with the responsibilities discussed so far.
158 Chapter 4 TicTacToe: Design
Board Concrete
The Board class is used by the Engine and MoveGenerator classes, but Board does not
refer to
any other classes itself. Therefore, the Board class card lists no collaborators.
The MoveGenerator object has a single, simple responsibility. Given a Board object
that represents
any particular state, the MoveGenerator should choose the computer’s next move. To
select a move,
the MoveGenerator needs access to a Board object. In addition, the MoveGenerator
needs to be able
to determine what moves are available and who, if anyone, has won, given any
particular board
configuration.
There are several algorithms that could be chosen to determine the next move. The
game tree
approach mentioned earlier works well for simple games like tic-tac-toe. This
technique evaluates
all possible moves and chooses only moves that lead to a win or draw for the
computer, which leads
to a game that is hard to beat. At the other end of the spectrum, the MoveGenerator
could simply
choose a square at random. Or, the MoveGenerator could choose moves at random
during early
phases of the game and switch to a more aggressive strategy later in the game.
It even seems possible that the game could support several different types of move
generators.
Perhaps the user could choose which type of object is used by selecting between
novice, interme-
diate, and expert modes. We could support this capability by designing a single
MoveGenerator
class that supports each of these modes or by allowing the Engine to install
different move gener-
ators to accommodate each level.
In any event, these are lower-level details that do not need to be fully considered
at this point.
The only thing we need to specify right now is the external behavior of a GameBoard
object. We
know that the Engine is in charge of the progress of the game, and that the Engine
object relies on a
MoveGenerator to compute the next move. The MoveGenerator depends on a Board object
to
determine the current state of the board, which allows the MoveGenerator to pick a
“good” move.
MoveGenerator Concrete
Figure 4.10 shows an inheritance graph for classes identified as part of the
TicTacToe program.
Three of these classes are clearly user interface components and, as such, we
expect them to be
derived from the UIComponent class described in Chapter 2. The remaining classes
have no
particular inheritance relationship.
UlComponent
We have now identified and described the objects in the system in a fair amount of
detail. Once the
main classes and the various relationships between them have been solidified, we
can draw a more
detailed collaboration graph. The complete graph provides more information about
how these pieces
fit together than the simpler collaboration graph shown in Figure 4.5.
For the collaboration graph, we can consider the Engine, the Board, and the
MoveGenerator to
be grouped into an “Engine Subsystem.” Recall that we originally identified the
functionality of
these three objects as belonging to a single Engine object. Although these
responsibilities have now
been distributed throughout multiple objects, it is still useful to think of a
single “brain” as doing
the work. In fact, the Board and MoveGenerator have no interactions
(collaborations) except
between themselves and the Engine class, which reinforces the idea that these
classes are really a
single, cohesive subsystem.
It is also useful to think of the entire game as a subsystem that contains all the
other objects.
This certainly matches the user’s view. To the user, there are only two objects:
the user and the
game. Choosing to show the entire game as a composite object, or a subsystem, can
help us to see
how cohesive the structure is (or isn’t).
Figure 4.11 shows a collaboration graph for the objects discussed in the previous
sections. The
numbers in the semicircles in each object refer to the responsibilities identified
on each class card.
TicTacToe Subsystem
Message
CSS
GameBoard Command
LAA on Es Ree o E
E User c
The collaboration graph in Figure 4.11 makes the overall architecture of the system
quite clear.
For example, we can see exactly how the user interacts with the system. The Engine
Subsystem is
the most cluttered looking area of the graph, but this is to be expected because
the Engine is the
heart of the system.
The design exercise has now provided a set of classes that implement the basic
features of the
program, an inheritance hierarchy for these classes, and a fairly detailed picture
of the overall archi-
tecture of the system. The design may need to be adjusted as we proceed, but the
design exercise
has provided a firm foundation from which the game can evolve.
So far, the design process described in this chapter has considered only the
internal structure of the
TicTacToe program. However, designing the user interface is an equally important
part of devel-
oping any interactive program. Of course, we have already assumed several things
about the
interface. We assumed from the beginning that TicTacToe is mouse-driven and that
the user has
some way to issue simple commands.
There is also a lot we do not know. Exactly what does the game look like? How are
the “quit”
and “new game” commands issued? What feedback does the game provide and how is this
infor-
mation communicated to the user? Before we begin implementing, we need to answer
some of
these questions.
The answers to these questions will certainly have an impact on the implementation
of the
program, but they may also influence the design stage. Although this case study
discusses the user
interface after the internal design, we could have started the user interface
design before even
thinking about the internal architecture. The interface design generally occurs in
parallel with the
other parts of the design.
The TicTacToe user interface presents a surprisingly interesting challenge for such
a simple
game. Many computer programs have no parallels outside the computer environment in
which they
run. However, tic-tac-toe is a well-known game, and users are sure to approach an
electronic
version with certain expectations. It seems important for the game to make the
transition from a
simple pencil and paper game to the computer in a natural way.
The traditional tic-tac-toe game is very easy to play. It requires no special setup
and can be
played almost anywhere. All that is necessary is that the players be able to make
simple marks on
some surface. It has virtually no overhead of any kind, which may be one reason the
game has
remained popular for so long. It would be very easy to destroy the game’s
simplicity by developing
a program that is too “computer-like,” and thereby lose the essential appeal of the
game.
On the other hand, literally trying to duplicate the original game on the computer
is likely to
fail. The computer screen is a different medium than pencil and paper. Often,
techniques that work
well on paper do not seem natural when transferred unchanged to the computer
screen. Besides, if
With these concerns in mind, let’s start by specifying some overall design goals
for the
interface:
e TicTacToe should be designed so that a player does not have to learn new ways to
perform
simple functions. The player should be able to apply his or her experience with the
original
tic-tac-toe game to this version.
e While drawing on the heritage of the original game, the game should take
advantage of the
fact that it is running on a computer.
e Because we have already decided that the game will be implemented in Motif, the
visual
design and interaction style should be consistent with Motif guidelines.
* The visual design should exploit the capabilities of Motif and provide a pleasing
appearance
for the program.
* The game should incorporate transition effects and otherwise respond in a way
that provides
an enjoyable experience for the user. The game should be fun to play.
Handling Input
The biggest difference between playing tic-tac-toe on paper and on a computer lies
in the way input
must be handled. On paper, the players simply make moves by drawing Xs and Os with
a pencil. A
new game can be started by simply drawing a new grid, and the players can stop
playing by
crumpling the paper and throwing it in the trash. So, the first major issue to be
resolved is to
determine how these actions, or their equivalents, should be handled in the
computerized version.
The original problem statement suggested that the user should be able to make a
move by
clicking on a square. While this approach seems reasonable, there are other
possibilities that are
worth considering. For example, we could place two buttons below each square in the
tic-tac-toe
grid, one to mark the square as an X and the other to mark the square as an O.
Allowing the user to
select the type of mark to make would allow the program to support a multiplayer
game, something
we have not considered so far.
Figure 4.12 shows a proposed visual layout based on this idea, using a storyboard
format. A
storyboard is a sequence of drawings that shows different states of the program,
much like the
panels of a cartoon. Storyboards attempt to convey the actions that take place
between the program
and the user and can be thought of as a simple movie script. The figure on the left
shows the game
as the user would first see it. The picture on the right shows how the user can
make a move by
pushing a button below the desired square.
The interface shown in Figure 4.12 is inadequate in many ways. The screen looks
very
cluttered, with many small duplicate buttons. The large number of buttons detracts
from the overall
look. In fact, this layout seems to have lost the familiar look of the traditional
tic-tac-toe game. The
board is not immediately recognizable as tic-tac-toe. The Xs and Os are also very
close together,
and the user could accidently choose the wrong one. Worse, it is difficult to be
sure whether the
middle row of buttons applies to the squares above or below.
Figure 4.12 An early interface design that uses buttons to choose moves.
Another possible approach that addresses some of these problems is to use a “drag
and drop”
user interface model. The rows of buttons in the previous interface could be
replaced by a pair of
icons along the bottom of the playing area. The user could make a move by pressing
the mouse
over the icon that represents the desired mark, dragging the object to the desired
square and
releasing.
Figure 4.13 shows an artist’s conception of this interface, using another simple
storyboard.In
the figure on the left, the user selects an X from a “bin” of Xs. The user then
drags the X to the
desired square, as shown on the right, causing an “X” to be displayed in that
square (upper left).
This interface has some potential. Drag and drop interfaces can be fun to use,
which meets one
of the original goals. If one of the players is the computer itself, the game could
display a simple
animation to allow the user to watch the opposing move taking place. The drag and
drop interface
also solves several of the problems found in the previous design. For example,
there is less
ambiguity about which square is being marked. The layout of the game board is also
more recog-
nizable as tic-tac-toe.
However, this approach has several problems, too. Perhaps the most fundamental
problem is
that the model is quite different from the way the traditional game is played. Many
games involve
picking up pieces to be placed on a board, but tic-tac-toe is not normally one of
them. A first-time
user would probably not know how to make the first move. Furthermore, although the
version of
Motif used in this book does support drag and drop, this feature is not widely used
outside of
desktop managers. An application that uses drag and drop as its primary user model
would be a
novelty in most environments.
AR OPIRATA CRIADA S G uA 10904 AOP ALPERRA, IPIE VALAMA EN D AMR KALEO RA NTT
Another common user model is an explicit selection model. When using an explicit
selection
interface, the user is expected to select an object and then choose an operation to
be performed on
that object. This can be done in one of two ways. If we choose the noun-verb model,
the user
chooses the object (the “noun”) first and then chooses the action (the “verb”) to
be performed on
that object. The other model is the verb-noun approach. Here, the user chooses the
action by using
buttons, a menu, or some other mechanism, then chooses the object to which that
action is to apply.
Motif, as well as most other popular user interface styles, tends to favor the
noun-verb model.
Applying the explicit selection model to TicTacToe, the user could select a square
with the
mouse, and then mark that square by selecting a menu item or clicking on a button.
The noun-verb
explicit selection interface model works well in many situations. However, after
some consider-
ation, it does not seem right for tic-tac-toe. The process seems time-consuming and
clumsy
all seem too clumsy for TicTacToe. The most straightforward interface would allow
the user to
simply click on a square to make a move. In this model, selecting a square would be
similar to the
action of pressing a button. Motif allows users to abort a command by moving the
sprite outside an
armed button before releasing the mouse button. The action of selecting a square
should follow the
same model. Providing feedback that a square is “armed” and about to be selected
would be useful
as well. In this respect, the squares should mimic Motif buttons and could perhaps
be implemented
as Motif pushbutton widgets.
One question involves feedback about which squares are still available for moves.
What
should happen if the user tries to select a square that has already been marked?
One simple
approach is to just ignore the move and issue a warning. The warning could be a
printed message,
an error dialog, or maybe just a “beep.” Another approach is to disable input in
squares that have
already been marked. If input is disallowed, the game should provide some
indication that the
squares cannot be chosen. Some interfaces highlight available input areas in some
way as the
mouse cursor moves over the command area. Notice the difference between these two
approaches.
The first allows incorrect moves but tries to recover afterwards. The second tries
to prevent the user
from making an illegal move from the beginning.
There are several reasons why additional feedback might be desirable. First, all
programs need
to anticipate deliberate or accidental illegal moves. If a programmer ignores the
possibility of
incorrect input, the application may become confused and crash. At a minimum, user-
friendly
programs should detect an error, inform the user, and proceed. Second, it is often
useful to give the
user redundant feedback to reinforce the fact that only unmarked squares can be
marked. Providing
feedback about available moves allows the program to guide and advise about the
rules of the
game. Finally, providing this type of feedback helps fultill one of the original
goals: the program
should be fun to use. A program that is responsive and provides constant feedback
to the user is
usually more enjoyable to use than a more passive program.
We also need to decide how to provide feedback when one player wins the game. In
traditional
tic-tac-toe, the winner draws a line through the winning squares. Should the
computer version do
the same? It seems reasonable, but there are alternatives. One approach would be to
change the
background color of the winning squares. This would only work correctly on color
displays,
although inverse video could be used on monochrome displays. An alternative would
be to
highlight the squares by drawing a border around each square. All Motif widgets
support
XmNborderColor and XmNborderWidth resources that could be used to indicate the
winning
squares. This approach could be used with both color and monochrome displays.
Displaying the
winning squares automatically, using some type of highlight, seems like a simple
enhancement to
the pencil and paper version of the game that could be more visually appealing.
Conclusions
Let's recap the decisions made in the previous sections. We decided that the
interface should be
mouse-driven and that the user indicates a move by clicking on a square. The
squares should offer
the ability to abort a move, in a manner consistent with Motif button widgets. We
also decided that
the game should provide some form of additional feedback about which squares are
still available
for future moves. The game should either not allow illegal moves or report an error
after the illegal
move occurs. Finally, the game should use Motif’s ability to highlight widget
borders to indicate the
appropriate sequence of squares when a player wins the game. An audible indication
might be used
to announce a winner as well.
Figure 4.14 shows an artist’s conceptual drawing of the final TicTacToe interface
in a story-
board format.
Summary 167
In this design, the TicTacToe grid consists of raised buttons that depress when
selected. The
“undo” capability, suggested by the Cancel button shown in earlier designs has been
eliminated.
The two command buttons are now labeled “Clear” and “Quit.” The first command
clears the board
and starts a new game, while the second exits the game. Finally, the area between
the two command
buttons seems like a reasonable place to display messages to the user, if needed.
The upper left panel in Figure 4.14 shows the game as it first appears on the
screen. All squares
are selectable, and the user gets to make the first move. As the user moves the
mouse cursor over
the available squares, a highlighted border appears to indicate that the square is
available. The
image in the upper right shows a square as it is being selected and marked with an
X. The square
appears to be pushed into the screen as the user selects the square. The lower left
image shows the
game as an O is being marked. This storyboard assumes that the game can support two
players,
although a single-player game would be similar. According to the original plan for
a single-player
game, the O squares would be marked by the game itself. Finally, the figure in the
lower right
shows the game after X has won. The row of Xs is highlighted by a white border
around the
squares.
The discussion in this section is far from being a complete analysis and design of
the visual
4.6 Summary
This chapter presented a design for a simple computer version of the well known
tic-tac-toe game.
The exercise began with a simple problem statement and used the simple object-
oriented design
Chapter 5
TicTacToe: Implementation
Following the approach introduced in Chapter 2, each user interface object in the
game is
derived from the UIComponent class and forms a widget subtree. The question of how
to tie these
various subtrees together naturally arises. Each component manages its own
collection of widgets,
but what manages all the components? The main body of the game could create the
necessary
container widgets to manage all the various components. But an equally attractive
idea is to create
a single component that creates the widget infrastructure that ties all the
components together.
We also need to decide how the various objects communicate with each other. For
example, the
GameBoard object needs to be able to call the Engine object’s member functions. To
do this, the
GameBoard object needs a pointer to an Engine object. In the Stopwatch example
described in
Chapter 2, we connected the various components by passing pointers to objects in
the various
constructors. We could use the same approach for TicTacToe as well. Another
approach is to make
the objects in TicTacToe globally accessible. The main body of the program could
instantiate an
Engine object that is globally available as theEngine, a GameBoard object referred
to as
theGameBoard, and so on. However, in some systems, we might wish to have two or
more
collections of objects that work independently. For example, consider the program
with two
Stopwatch panels shown in Chapter 2. In such a system, we cannot connect both
subsystems using
global variables.
The approach used here is to define a TicTacToe class that encapsulates all the
major objects in
the system and establishes the various connections between objects. The TicTacToe
class provides
access functions that allow the various objects to retrieve other objects
indirectly. For example,
when the GameBoard object needs to call an Engine member function, it refers to the
Engine object
contained by the TicTactoe object. This approach does not solve all problems, as
there are still
dependencies between classes. However, this approach at least defines a central
location through
which all connections are made.
We can now look at the implementation of each class in TicTacToe. The number of
classes has
already grown by at least one, the TicTacToe class, and responsibilities or
additional classes that
were not anticipated during the design phase may also be discovered during the
implementation.
The previous section suggested a new TicTacToe class that serves two purposes: to
provide a widget
framework that groups and manages all the other components, and to serve as a
central connection
point between all objects in the system. The TicTacToe class can be declared as
follows:
1 PINTA AAA AIR TAT IAA ATA LATTA TAL LAT GIT ETULIITE LE
2 // TicTacToe.h: TicTacToe subsystem that encapsulates all
3 // major components of the game
6 #define TICTACTOE_H
7 #include "UIComponent.h"
9 class GameBoard;
10 class Message;
11 class Command;
12 class Engine;
13
15
16 friend GameBoard;
17 friend Command;
18 friend Engine;
19
20 public:
22 virtual ~TicTacToe() ;
25 protected:
26
28
29 GameBoard *gameBoard ( ) const { return ( _gameBoard ); }
30 Message *messageArea() const { return ( _msgArea ); }
St Command *commandArea() const { return ( _commandArea ); }
32 Engine *engine () const { return ( _engine ); }
33
34 private:
35
38 GameBoard * gameBoard;
39 Message *_msgArea;
40 Command *_ commandArea;
41 Engine * engine;
42 );
43
44 +#endif
The file TicTacToe.C contains the TicTacToe member function definitions. The
constructor
instantiates each of the objects used in the game. It also creates an XmForm widget
that serves as a
parent widget for all other components and manages the overall layout of the game.
The constructor
is somewhat lengthy because of the many XmForm constraint resources that must be
specified.
1 LEE AA ERA LARA AMA AAT ELTA TATA TAS EAI AAAS AMIA TRAER
2 // TicTacToe.C: TicTacToe subsystem that encapsulates all
6 tinclude "GameBoard.h"
7 tinclude "Engine.h"
8 tinclude "Command.h"
9 tinclude "Message.h"
10 tinclude <Xm/Form.h>
12
14 UIComponent ( name )
LS
16
1?
18
19
20
21
22
23
24
25
26
27
28
29
30
32
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
SL
52
53
54
55
56
57
58
59
60
61
62
63
installDestroyHandler () ;
// Separate the commands from the message area
Widget sep = XtCreateManagedWidget ( "commandSeparator",
xmSeparatorWidgetClass,
w, NULL; 0 73
XmNbottomWidget , sep,
XmNbottomAttachment, XmATTACH_WIDGET,
NULL );
XtVaSetValues ( sep,
XmNtopAttachment, XmATTACH_NONE,
XmNleftAttachment, XmATTACH_FORM,
XmNrightAttachment, XmATTACH_FORM,
64
67 XmNleftAttachment, XmATTACH_FORM,
68 XmNrightAttachment, XmATTACH_FORM,
71 NULL );
72
‘eo
76 XtVaSetValues ( _commandArea->baseWidget () ,
i XmNtopAttachment, XmATTACH_NONE,
78 XmNleftAttachment, XmATTACH_FORM,
79 XmNrightAttachment, XmATTACH_FORM,
80 XmNbottomAttachment, XmATTACH_FORM,
81 NULL );
82
85
86 _commandArea->manage () ;
87 _gameBoard->manage () ;
88 _msgArea->manage () ;
89 _gameBoard->clear () ;
90 }
The TicTacToe constructor creates and connects four major objects used in
TicTacToe, along
with several supporting widgets. The first object to be created is an Engine. The
Engine object does
not have a user interface but needs a pointer to a TicTacToe object to allow it to
access the other
objects in the game. The this pointer for the TicTacToe object is passed to the
Engine constructor
to provide a single connection point between the Engine object and the user
interface components
in the game.
The TicTacToe constructor creates an XmForm widget to contain and position all
other compo-
nents of the interface. These components, which are instances of the Message,
Command, and
GameBoard classes identified in the previous chapter, support independent widget
trees. The
TicTacToe constructor simply positions each component’s base widgets to determine
the overall
layout of the game. Each of these components expect a pointer to the TicTacToe
object as a way to
tie them all together and bind them to the other objects in the game.
In Chapter 4, we suggested several layouts for the TicTacToe interface. The final
interface
design placed the command buttons and message area below the playing area, set off
by a separator.
The TicTacToe constructor tries to follow the suggested layout as closely as
possible. The
constraints specified in the constructor attach the GameBoard to the top, left, and
right sides of the
form and attach an XmSeparator widget below the game board. This is exactly as
decided in
Chapter 4.
The proposed layout of the command and message areas poses a slight problem. There
is a
basic conflict between the visual layout specified in Chapter 4 and the
implementation strategy
based on the UIComponent class. To place the message area (implemented by the
Message class)
between the two buttons in the command panel (implemented by the Command class),
the Message
object would have to be part of the Command widget tree. Classes derived from
UlComponent, as
described in Chapter 2, represent subtrees within the widget hierarchy. It isn’t
possible to have
UIComponent objects that cross widget trees.
So, how can the Message component exist in the middle of the Command component?
There
are several solutions. One approach would be to attach the Message object to the
bottom of the
Form and attempt to float it in the middle of the two buttons. This layout could be
made to work by
carefully specifying the fonts and controlling the length of the messages displayed
in the message
area. But this is a very questionable approach. Depending on various user-definable
resources, the
message area could overlap the buttons, or vice versa.
Another solution is to encapsulate the Message object inside the Command object.
This would
tie these two classes together, which seems like a debatable decision. Currently,
the Message class
is completely self-sufficient, making it a highly reusable class. Establishing a
link between the
Command and Message classes would reduce the modularity of the design, which seems
like a
great price to pay just to achieve a specific screen layout. On the other hand,
many applications
need to issue commands and display status messages. We could reconsider the
original design and
create a CommandPanel class that supports not only buttons, but also an area for
messages.
However, there are other simple alternatives. We can attach the Command object to
the bottom
of the TicTacToe form widget and place the Message object between the Command
object and the
XmSeparator widget. This differs only slightly from the final layout specified in
Chapter 4 and
maintains the separate identity of the Command and Message objects. This layout
also provides
more room for messages.
Figure 5.1 shows the layout of TicTacToe at the component level. An XmForm widget
serves
as a container for the other components of the game. The GameBoard occupies the
upper portion of
the window. The Message and Command components are located at the bottom of the
XmForm,
separated from the GameBoard by an XmSeparator widget.
GameBoard
Message
The TicTacToe destructor simply deletes the objects created by the constructor. The
UICom-
ponent destructor destroys the XmForm widget, which destroys the XmSeparator widget
created by
the TicTacToe constructor as well.
91 TicTacToe: :~TicTacToe ()
o x
93 delete _gameBoard;
94 delete _msgArea;
95 delete _commandArea;
96 delete _engine;
974 3
The GameBoard class is derived from the UIComponent class. It creates the familiar
tic-tac-toe grid
and implements the responsibilities listed on the GameBoard class card in Figure
4.1. The
GameBoard component is the most complex part of the TicTacToe user interface. It
acts as both an
input and an output device. It not only displays Xs and Os but, according to the
interface design in
LPI AAA ARANA RAAAARAIAIA NAAA AAA SAAA LTA ETA AAG ITI
5 +#define GAMEBOARD_H
6 #include <Xm/Xm.h>
7 #include "UIComponent.h"
8 class TicTacToe;
11
19 public:
13
15 virtual ~GameBoard() ;
24 protected:
25
29 TicTacToe *_game;
39
40 private:
41
43
45
50 #endif
The GameBoard class’ public interface includes functions for marking a square as an
X or an
O, highlighting an individual square, and clearing the entire board. It also
supports member
functions that activate and deactivate squares to indicate whether or not they can
accept input. The
protected and private portions of the class define various callbacks and member
functions that
display and maintain the Xs and Os on the screen and also handle input from the
user.
One of the first decisions that must be made before implementing the GameBoard
class is what
widgets to use to implement a grid. There are several possible choices. For
example, we could draw
the tic-tac-toe grid on a single window. Because a tic-tac-toe grid consists of
only four lines,
drawing the grid would be very easy. However, input handling would be more complex
because we
would have to compute which square corresponds to various x,y positions. In
addition, the visual
design in Chapter 4 described a more visually complex grid than just four crossing
lines. It is
usually easier to implement different input areas with individual widgets. Using
individual widgets
for each square in the tic-tac-toe grid also makes the output easier, because each
square can just
display a single X or an O.
Possible widget choices include the various button widgets, the XmLabel widget, or
even the
XmDrawingArea widget. The XmLabel widget supports an XmNlabelPixmap resource, which
could be used to display an X or O pixmap. However, the XmLabel widget does not
support input
except through event handlers. The XmPushButton widget can also be used the same
way, and it
supports various callbacks that could be used for input. The XmDrawingArea also
supports various
Ia earl
Re eh te PI ay
input callbacks and provides callbacks that allow programs to redraw the contents
of the XmDraw-
ingArea widget when the widget receives an Expose event.
All of these choices seem like reasonable possibilities. However, the visual design
discussion
in Chapter 4 indicates the need for a very flexible display. The squares need to be
able to respond
like buttons, display symbols like an XmDrawingArea, and also provide support for
different visual
effects. One of the most flexible Motif widgets that fits this description is the
XmDrawnButton
widget. The XmDrawnButton widget behaves much like the XmPushButton widget but
allows the
program to display arbitrary images in the widget. The XmDrawnButton widget also
supports
several different types of shadows, which can be used to provide different visual
effects.
Next, we need to choose a manager widget to arrange the XmDrawnButton widgets into
the
tic-tac-toe grid. The XmRowColumn widget seems like a natural choice because the
tic-tac-toe
game board consists of rows and columns. The XmRowColumn widget is a good choice
for this
type of layout whenever all children are the same height.
Other possibilities include the XmForm widget and the XmBulletinBoard widget. The
XmForm widget would allow the board to be resized dynamically - something the
XmRowColumn
widget does not support. However, setting up various constraint resources to create
a tic-tac-toe
grid would be complicated. The XmBulletinBoard forces the program to specify the
precise
position of each child and does not handle resizing. The XmRowColumn widget seems
to be the
simplest choice.
The GameBoard constructor takes three arguments, a name that is passed to the
UIComponent
constructor: a widget to be used as the parent of this component's base widget, and
a pointer to a
TicTacToe object. The constructor creates an XmRowColumn widget and nine
XmDrawnButton
widgets. The XmRowColumn widget is configured to manage the XmDrawnButton widgets
in a
square 3 by 3 grid. The constructor is written as:
1 FELIS ASIII PELLET IAL LIA AA TA RIAS AAN EIA IAARAAIERI REEF AI LE i
2 // GameBoard.C: A tic-tac-toe board
3 LEELA ALAS CECELIA ALIA TAA RA TA ETT AT ATA IATA ITAA TTA AA
4 #include "TicTacToe.h"
5 #include "GameBoard.h"
6 #include "Engine.h"
7 #include <Xm/RowColumn.h>
8 #include <Xm/DrawnB.h>
Tt Widget parent,
14 int i;
15 XGCValues values;
16
a Be _game = game;
18 _gridSize = 100;
19
21 // grid of widgets
hb
NO
rm
ote 6 AO AIDA AI AMA E
23
24
25
26
27
28
29
30
31
32
33
34
E
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
TE
XmNpacking, XmPACK_COLUMN,
XmNorientation, XmHORIZONTAL,
XmNadjustLast, FALSE,
NULL );
installDestroyHandler () ;
xmDrawnButtonWidgetClass, _w,
XmNuserData, sE
XmNrecomputeSize, FALSE,
XmNpushButtonEnabled, TRUE,
XmNshadowType, XmSHADOW_OUT,
XmNwidth, _gridSize,
XmNheight, _gridSize,
NULL ) ;
XtManageChildren ( _grid, 9 );
ee
|
|
The GameBoard constructor initializes a variable that determines the size of each
square to 100
pixels and stores a pointer to the TicTacToe object that encapsulates the GameBoard
object. The
constructor then creates an XmRowColumn widget configured to support a rectangular
grid
consisting of three columns and three rows. Setting the XmNadjustLast resource to
FALSE
prevents the XmRowColumn widget from stretching the last row of children when the
widget is
resized, The U[Component: :installDestroyHandler () function sets up the XmNde-
stroyCallback function for the XmRowColumn widget.
A for loop on line 35 creates each of the nine squares in the tic-tac-toe grid.
Each square is
implemented as an XmDrawnButton widget, set to be_gridSize pixels wide and
_gridSize
pixels high. The constructor also configures the given widget as a selectable
square in the game
board.
The constructor concludes by managing the widgets in the grid and creating a
graphics context
(GC) that can be used when drawing the Xs and Os. A graphics context is a resource,
maintained by
the server, that describes various attributes to be used by X graphics functions. A
graphics context
describes the foreground and background colors, line width, font, and so on. (See
[Scheifler90] for
information about graphics contexts.)
The function
creates a cached graphics context, using the given widget to determine the screen
with which the GC
will be used. The mask argument specifies which members of the \GCValues structure
contain
valid data. Cached GCs should be freed when they are no longer needed, using the
function:
The graphics context created by the GameBoard constructor uses the foreground of
the first
button in the grid, on the assumption that all squares in the grid will have the
same color. The
constructor also retrieves the background color of the GameBoard’s base widget, as
well as the
shadow thickness and highlight color of the first widget in the grid. These values
will be used to
turn on and off the highlight-on-enter feature and to provide other visual effects,
as explained later.
The GameBoard destructor releases the graphics context if the widget still exists.
72 GameBoard: :~GameBoard ()
73 (
74 1f ( ow l= NULL })
The markX () and markO() methods provide a programmatic interface that allows other
objects to mark an X or an O in a square in the GameBoard grid. These methods are
very similar.
Both must be given a position in the grid (0 - 8) in which to display an X or an O.
The appropriate
mark is recorded by registering one of drawOCallback() or drawXCallback() as an
XmNexposeCallback for the specified widget. Functions on this callback list are
called
whenever the XmDrawnButton widget’s window needs to be redrawn, so once a callback
is regis-
tered, that square will continue to display the corresponding mark.
Notice that before adding the callback, both methods remove any previously
registered
XmNexposeCallback functions. It is important to be very careful when using
XtRemoveAllCallbacks(). Removing all callbacks may remove callbacks installed
elsewhere, perhaps even internally by the widget. In this case, the XmDrawnButton
does not
register any XmNexposeCal lback functions. In fact, it would be rare for any widget
to register
a callback to handle exposures. However, in other cases, carelessly removing all
callback functions
could lead to unexpected results.
If the widget is realized, the appropriate figure must be displayed in the window
immediately.
The safest way to accomplish this task is to call XClearArea () to clear the
widget’s window and
generate an Expose event. Calling the Xlib function XClearArea () with zero width
and height
(fifth and sixth arguments) clears the entire window. Setting the last argument to
TRUE requests the
X server to generate an Expose event for that window, which will cause the newly
registered
callback to be invoked.
The markX () and marko () functions also change the visual style of the
XmDrawnButton
widget that represents the given square. Unmarked squares look and act like Motif
XmPushButton
widgets to indicate that they can be selected. When a square is marked as an X or
an O, the
markX() or markO() function disables pushbutton behavior by calling
deactivateSquare (). This implements one of the goals established in Chapter 4.
According
to the user interface design, the game should provide additional feedback to the
user about what
squares are selectable in addition to displaying an X or an O. Deactivated squares
are no longer
selectable, and therefore do not look like pushbuttons. The change in the visual
appearance also
adds some additional sense of animation to the interface.
78 {
82
84
85
86
87
88
89
90
91
92
93
94
95
96
deactivateSquare ( position );
XtAddCallback ( _grid[position],
XmNexposeCallback,
&GameBoard: :drawXCallback,
( XtPointer ) this );
if ( XtIsRealized ( _grid[position] ) )
XtAddCallback ( _grid[position],
XmNexposeCallback,
&GameBoard: :drawOCallback,
t XtPointer ) this );
if ( XtIsRealized ( _grid[position] ) )
XClearArea ( XtDisplay ( _grid[position] ),
XtWindow ( _grid[position] ),
000505 Dy SPRUE 3:
Expose events occur in the widgets within the tic-tac-toe grid. These static member
functions
retrieve the object pointer from the client data and call the correspondingdrawX ()
and draw0 ()
member functions.
1147
118
219
120
121
122
123
124
125
XtPointer clientData,
XtPointer )
129 {
131 obj->drawO ( w );
132 3)
The drawX () member function computes a bounding box that spans the region from 20
to 80
percent of the size of a single widget in the grid and draws lines between opposite
corners of the
bounding box. This member function is virtual to allow subclasses to change the
appearance of the
X.
134. 1
136
141
146 )
The draw0 () member function computes a bounding box appropriate for a circle and
calls
XDrawArc () to display a circle (an “O”) in the specified widget. Like drawX (),
drawO() is a
virtual member function that allows subclasses to redefine how the O is drawn.
148 {
155
158 —9gC,
160 }
LEE ——— EE
EE EE q O
The clear () member function removes all Xs and Os from the GameBoard. The function
must remove all XmNexposeCallback functions registered for the widgets in the grid
and must
also call XC1learArea () to erase any current contents of the windows.
We could also call the Xlib function XClearWindow() to erase the contents of each
square.
However, the Motif XmDrawnButton widget draws a shadow inside its window. If we
clear the
entire window, the shadow will also be erased. If we useXClearArea (), the widget
will receive
an Expose event and redraw the shadow after the window is cleared.
162 1.74
163 int: 17
164
167
169 {
Liz
LTS
180 }
181 }
188
192 )
When used as an input mechanism, the GameBoard does not interpret the input itself.
The
mark () member function just passes the user’s request to mark a square onto the
Engine object.
The GameBoard cannot mark the square itself because it does not know the current
state of the
game. The function accesses the appropriate Engine object through the TicTacToe
object associated
with a particular GameBoard object. The index of the square that the user wants to
mark is sent to
the Engine object as an argument to the Engine: : recordMove () function.
194 {
195 _game->engine()->recordMove ( index );
196 }
Chapter 4 discussed the need to make the interface feel responsive and to provide
feedback
about which squares are selectable and which are not. The functions activateSquare
() and
deactivateSquare() provide feedback by changing the shadow style and other
attributes of a
button. The activateSquare () function enables pushbutton behavior for the
XmDrawnButton
widget used by the GameBoard component. If the XmDrawnButton widget’s XmNpushBut-
tonEnabled resource is set to TRUE, the XmDrawnButton widget behaves like a Motif
XmPushButton widget.
198 {
209 }
Recall from Chapter 4 that the user interface design calls for squares to provide
some feedback
when they are selectable. We speculated that this feature could be implemented by
highlighting the
border of a widget when the pointer entered a square or when the square received
input focus. The
XmDrawnButton widget supports an XmNhighlightOnEnter resource, which, if set to
TRUE,
automatically highlights a widget when it receives input focus. So, the most
straightforward way to
implement the desired behavior is to set XmNhighlightOnEnter to TRUE in the
activateSquare() function and set it to FALSE when the square is deactivated.
Eee Eee
ee eee
Unfortunately, things are not that simple, and there are two problems with this
solution. First,
some users may dislike the highlighting effect. This behavior causes a great deal
of flashing on the
screen that some users like and others find annoying. It seems better to allow
users to control the
XmNhighlightOnEnter resource. On the other hand, we could insist that we are
justified in
forcing the use of highlight-on-enter because the game uses this feature to
communicate important
information.
However, there is another, slightly more complex problem. The deactivateSquare ()
function is called when the user selects a button while the pointer is in a square.
Therefore, if
XmNhighlightOnEnter is TRUE (which it would be if the square was active), the
square is ina
highlighted state when deactivateSquare() is called. The deactivateSquare()
function would then use XtSet Values () to set XmNhighlightOnEnter to FALSE.
This appears to be the correct approach, except for one small problem. The logic
used in the
Motif code that controls the highlighting works like this: If XmNhighlightOnEnter
is TRUE
when the pointer enters a widget, the widget is highlighted. If XmNhighlightOnEnter
is TRUE
when the pointer leaves the widget, the border is unhighlighted. In the approach we
have been
discussing, XmNhighlightOnEnter will be set to FALSE before the pointer can leave
the
widget. So a widget that has been marked, and therefore deactivated, can never be
unhighlighted!
One could debate about whether this is a bug or a feature in Motif, but the answer
would be
irrelevant. To continue to make progress with TicTacToe, we must find some other
way to achieve
the desired effect. One approach would be to work around the problem by registering
a callback or
an event handler to detect when the pointer leaves the widget’s window and turn
highlight-on-enter
off at that time. However, this is a lot of work, and when combined with the
usability issue raised
earlier, it seems as if a better and simpler approach is called for.
ALI {
220 NULL );
221 )
The Gameboard also needs to provide some way to indicate that a player has won the
game. In
Chapter 4, we decided that a winning sequence could be shown by highlighting the
winning set of
widgets. To support this feature, we can add ahighlightSquare () member function,
which
highlights a single square.
There are several ways to highlight a widget. For example, we could set all borders
to some
initial thickness and simply change the color to highlight the widget. Another
approach would be
to use the same highlight mechanism Motif uses to implement highlight-on-enter. We
could
programmatically highlight the winning squares, even though they don’t have input
focus.
However, the only programmatic interface for highlighting widgets is a set of
undocumented,
internal functions. Calling these functions directly seems unwise.
223 {
225
230 }
To further enhance the effect, the GameBoard supports a second member function,
deemphasizeSquare (), that sets a widget's shadow thickness to zero, effectively
flattening
the widget. When used in conjunction with the highlightSquare() member function,
the
deemphasizeSquare() function allows the Engine to make all squares fade into the
background and accent the winning moves with a raised border. The final
implementation differs
slightly from the plan in Chapter 4, but the result is still visually effective.
23% 3
233 // Make a square fade into the background by shutting
230
This completes the implementation of the GameBoard class. Figure 5.2 shows two
views of
the completed GameBoard panel. On the left, all buttons are in their activated
state, and the board is
clear. The figure on the right shows the gameboard after the user has recorded an X
in the middle
square. In addition to displaying the X, the center square is deactivated and
appears to be depressed
into the screen, indicating that it can no longer be selected.
The Message class provides a way for messages to be posted to the user. This could
be a nontrivial
task. As discussed during the visual design of TicTacToe, there are many different
types of messages
that need to be displayed. Some need to be subtle and nonobtrusive, while others
are important
enough that we need to make sure the user sees and responds to them. However, for
this implemen-
tation, the Message class is very simple. It creates a single XmLabel widget and
displays strings in
the widget upon request. Although the implementation is simple, we can replace the
Message class
with a more sophisticated mechanism later, if needed, because the implementation
details of the
functions: postMessage (), which displays a string, and postAlert (), which
displays a
string and also sounds a bell. The class structure is declared as:
E PELTAAP TET AT ATLA L AEE AL ILA EL LAAT TA CLT IETT DAA A ARIAS AA
3 PELEEST ELT LEAL AL EL ELL I LAT TEE LAD EAL ALLE ALL ALITA LTA PETS TELAT i
4 #ifndef MESSAGE_H
5 #define MESSAGE_H
6 #include "UIComponent.h"
10 public:
11
17 +#endif
The Message constructor creates an XmLabel widget and installs the UlComponent's
destruction handler function. It also calls its own postMessage () member function
to initialize
the message area to display an empty string. Without this step, the XmLabel widget
would display
the name of the widget when it first appears.
4 FLITE T TALLILLE FETE ELTA EL LIA TAS EET GLI ETL EPL EEL ET EET
5 #include "Message.h"
6 #include <Xm/Xm.h>
7 #include <Xm/Label.h>
8 #include <assert.h>
9
10 Message: :Message ( const char *name, Widget parent )
11 UIComponent( name )
1a. A
14 installDestroyHandler () ;
—
o
a
The postMessage() member function creates a compound string from a character string
passed as an argument and displays the string in the XmLabel widget.
18-53
19 assert ( w );
20
24
26
28
Ja 3
When discussing the user interface in Chapter 4, we identified a need to notify the
user that
some messages were particularly important. One simple way to do this is to add a
member function
to the Message class that sounds a bell when displaying a message. The postAlert ()
member
function calls the Xlib function XBe11 () to sound a short bell and calls
postMessage () to
display the accompanying message, if one exists.
EY. NS |
37 if ( msg )
39
42 }
The last user interface component to be implemented is the Command component. The
Command
class is a very simple component that creates two Motif XmPushButton widgets
managed by an
XmForm widget. Like the Message widget, the Command class has very few external
dependencies
and could easily be reimplemented to present a different interface or to add other
commands.
The Command class defines the callback functions assigned to the pushbutton in the
protected
portion of the class and also defines the member functions quit () and newGame ( ).
The class is
declared as follows:
VD 0 JO 0 FP WD P
PRP PP PP
Aun nO
11
18
L9
20
21
22
23
24
25
26
27
28
29
30
PASIFIST LUITE ALRITE ELTA LT EAT EE ELI RAS ERES PAGANA RATA ATINA
// Command.h
GEVTISTIS TLD ATI LAAT AL ALA LE ELAS TEL AL TORE TAT ALATA CEP AL OTL AAT Ea
#ifndef COMMAND_H
#define COMMAND_H
#include "UIComponent.h"
class TicTacToe;
class Command: public UIComponent {
public:
Widget _newGame;
Widget _quit;
TicTacToe *_game;
virtual void newGame( Widget, XtPointer );
virtual void quit( Widget, XtPointer );
private:
Hendi f
The Command constructor creates an XmForm base widget and two XmPushButton widgets.
The function registers the appropriate callbacks with each XmPushButton widget and
manages
both buttons.
wW 0 JO U$S WD P
EAAEIAAAIA RAEE RA AAA RARA REA AMARE RARA ERAS VAR INT OES
// Command.C: Manage a set of command buttons
FEGLELEEELLS ERICA AAA FEET EET IST EL ELIA LT EF TLE ELS CL A NL EA
#include "stdlib.h"
#include "TicTacToe.h"
#include "Command.h"
#include "Engine.h"
#include <Xm/Form.h>
#include <Xm/PushB.h>
10
11
12
13
14
15
16
LY
18
19
20
21
22
23
24
25
26
27
28
29
30
a4
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
54
52
53
54
55
56
XtVaCreateManagedWidget ( "newGame",
xmPushButtonWidgetClass, _w,
XmNtopOffset, Ss
XmNbottomOffset, e
XmNleftOffset, Di
XmNtopAttachment, XmATTACH_FORM,
XmNleftAttachment, XmATTACH_FORM,
XmNrightAttachment, XmATTACH_NONE,
XmNbottomAttachment, XmATTACH_FORM,
NULL );
XmNtopOffset, 5;
XmNbottomOffset, 34
XmNrightOffset, Sy
XmNtopAttachment, XmATTACH_FORM,
XmNleftAttachment, XmATTACH_NONE,
XmNrightAttachment, XmATTACH_FORM,
XmNbottomAttachment, XmATTACH_FORM,
NULL );
XtAddCallback ( _newGame,
XmNactivateCallback,
&Command: :newGameCallback,
( XtPointer ) this );
XtAddCallback ( _quit,
XmNactivateCallback,
£Command : :quitCallback,
( XtPointer ) this );
The XmRowColumn widget would provide a simpler way to manage the buttons created in
this component, but the XmRowColumn widget does not allow us to achieve the layout
determined
in Chapter 4. The constraints specified in the Command constructor attach the “New
Game” button
to the left side of the panel and the Quit button to the right side.
The newGameCallback() function extracts the Command object pointer from the client
data, and calls the newGame () member function.
58 XtPointer clientData,
59 XtPointer callData)
60 {
62 obj->newGame ( w, callData );
63 }
The newGame () function retrieves the Engine object from the TicTacToe object and
sends it
the reset () message.
65 {
66 _game->engine ()->reset () ;
67 }
Finally, the quitCallback () member function calls the quit () member function,
which
forwards the request to the Engine object.
69 XtPointer clientData,
70 XtPointer callData)
TL {
T3 obj->quit ( w, callData );
74 3
76 {
77 _game->engine () ->quit ();
78 4}
This completes the user interface portion of TicTacToe. Figure 5.3 shows the widget
tree
created by the classes in TicTacToe. The light gray box outlines the widgets
contained within the
entire TicTacToe subsystem. The darker boxes outline each of the three user
interface subcompo-
nents, the Message, Command, and GameBoard classes. The root of the tree is an
ApplicationShell
widget, created by the main body of the program, which has not yet been discussed.
tictactoe
ApplicationShell
game
XmForm
aes de di commandSeparator e a
omman ameboar
XmLabel XmForm a Amor le
quit - newGame xo (9)
XmPushButton = XmPushButton XmDrawnButton
The Board class represents the current state of a TicTacToe game. As shown in the
class card in
Figure 4.8, the Board class must provide methods to record moves, report the
current status of the
board, and provide a list of free squares. These are fairly high-level requirements
and the Board class
could be implemented many different ways.
The user interface design in Chapter 4 identifies the need to highlight the winning
squares in
some way. The Engine object must be able to determine what squares represent the
winning pattern
so this information can be passed along to the GameBoard. This responsibility was
not identified in
the initial class design. Because the Board class maintains the state of the game
board, the infor-
mation must come from the Board object. The winningSquares () member function
provides a
pointer to an array that indicates which squares contain the winning moves.
VD 0 3J0U FF WYN F
38
39
40
41
42
43
44
45
46
47
FLIEL ALIATE FIILE ELI LACE EAL ECT TLE EEEE LEVL EIN AFA
// Board.h: Represent a TicTacToe board
PULA TAAL ET ELTA LAL EEL AL ACE EDL AAA AMARE RAMAS AN AAA IED RADA DA
#ifndef BOARD_H
#define BOARD_H
// Convenient values
class Board {
public:
Board () ;
MoveStatus recordMove ( int, markType ); // Record an X or an O
// Return the first move that could win for a given player
int winningMove ( markType );
J3
#endif
There are various ways to determine what player, if any, has won a game at any
point. Because
there are relatively few winning patterns in tic-tac-toe, this implementation
defines a two-dimen-
sional array of winning patterns in the file Board.C, as shown below. Each of the
eight patterns in
the array can be thought of as a mask. If all squares set to a | in the mask are Xs
or all are Os in the
main Board object, that player has won.
The patterns are easier to see if written in rows of three. For example, the
pattern
Ls De 0
Li 2
Ly Da Y
LIE AAARAAA ERA IA AAA ITAL EATS ELE LEDER SET ITT ARAN AA
2 Jf Board.tC
DS LEARN ACERO EA ARA ELA TIE ATA SAN AMABA ARA ET LIAI TEL TALL EI
4 #include "Board.h"
9
8 int Board::_winningBits[8][9] = {
9 E i, e Serb Oy. Us dy Ba O de
14 CR A E. oy Oe. Le
15 Pike is e ls e a
16 Le e de ds e da De e Er
17, E
The Board constructor simply calls clear () to initialize the internal board.
18 Board: :Board()
19 {
20 clear(); // Initialize the board
a E }
The clear() member function initializes the _state array to contain the value
NOBODYYET, effectively clearing the board. The actual initial value is not
particularly important so
long as it is not an X or an O. The name NOBODYYET was chosen because it makes
sense in the
context of a yet-to-be-described member function,
23 4
26
29 }
The recordMove () member function takes two parameters, a position on the board and
an
argument that indicates whether an X or an O should be recorded in that position.
This function first
checks to see if the specified square on the board is unmarked. If the square
already has an X or an
O in the specified position, the function returns ILLEGALMOVE, indicating an error.
Otherwise
recordMove() marks the move in the _state array and returns a value of VALIDMOVE,
indicating success.
34
36
37 _state[position] = mark;
38 return ( VALIDMOVE );
or ae
The freeSquares () member function loops through the _state array testing for
squares
that contain neither an X nor an O. The function stores the positions of all
unmarked squares in a
second array. The_freeList array is then returned to the caller, along with a
reference to the
number of unmarked squares in the list. The return value of the freeSquares ()
function is
declared to be an array whose contents are declared to be const, to prevent the
caller from
modifying the contents of the _freeList array.
41 (
42 ine iy Te
43
44 // Build up a list of the indexes (0-8) of free squares
45
49
50 numFree = j;
51 return ( _freeList );
52 }
54 (
55 int i, count;
56
61 count++;
62
63 return ( count );
64 }
The whoHasWon () member function is the most complex function supported by the
Board
class. It uses a double loop that tests each square of the board against each
element in the
_winningBits mask. For each winning combination, the function counts the number of
Xs and
Os that occur on squares that have a value of 1 in the _winningBits array. For each
winning
scenario, if there are three Xs or three Os counted, that player has won, and the
function returns an
XX or OO value, as appropriate.
If neither X nor O has won, the function checks to see if any squares are still
free. If all squares
are marked, the game must be a tie, and whoHasWon() returns the value TIE.
Otherwise the
function returns NOBODYYET. Notice that the _winningPattern member is set to the
current
pattern tested in each pass. If either player has won the game, this member will
point to the winning
bit pattern in the _winningBits array.
66 {
67 int. 4. 34
68
70
72 {
74 int ocount = 0;
TS
80 if ( _state[j] == 00 )
82 else if ( _state[j] == XX )
87 if ( xcount == 3 )
88 return ( XX);
89 }
90
94 return ( TIE );
95 }
The Board class supports a simple mechanism that can allow the Engine class to play
a
reasonably smart game. The function winningMove () checks to see if a player can
make a
single move that would win the game. The function takes a player as an argument and
returns the
winning move, if it exists. With this information, the MoveGenerator class can
implement a simple
one-move look-ahead approach that results in a surprisingly strong game. The
winningMove ( )
function simply looks for situations where the given side has two entries in a row
and the other
player has none.
98 E: de
99
101 {
104
110 opponent++;
111
115
117 {
119
w
‘
ee ee eee Owe
SAFE A eS ee
mr RA O eee
121 {
125 }
126 }
Lev }
128 return ( -1 );
139. 3
The MoveGenerator class determines the computer’s next move, based on the current
state of the
board. The MoveGenerator class has only one interesting member function,
getNextMove ().
This function takes a Board object as an argument and computes and returns the
position of the next,
presumably “best,” move, based on the current state of that Board.
1 ies ke SERN ERER EMER ELE BOGE CME AGE AL RASA EES ORR GAM SAARES
3 LET EEALA AER AMAN AAA AAA AAA ATACAN LED ETAL IAAT AT U
4 #ifndef MOVEGENERATOR_H
5 #define MOVEGENERATOR_H
6 class Board;
8 class MoveGenerator {
10 public:
u e e
12 MoveGenerator () ;
16
17 #endif
There are many ways that we could compute the “best” move for any given state of
the game board.
For example, the game tree approach mentioned earlier traces the possible paths the
game could
take. Based on this information, one can determine which moves can lead to a win
and which can
lead to a loss. TicTacToe is a reasonably deterministic game, which can only result
in a tie between
two players who have a reasonable strategy, so we may want to make the game easier
for the user to
win.
For this implementation, the MoveGenerator class uses the winningMove() function
supported by the Board class to implement a one-move, look-ahead approach. The
strategy is
simple: 1f there is a move that would win the game for the computer, MoveGenerator
returns that
move. If there is a move that would win the game for the user, MoveGenerator blocks
that move. If
there are no winning moves and the center square is open, MoveGenerator takes that
square.
PALF ETAT TELT LEELA ATL CEAI ACLIS ATAI LT PLETE EL ETA ETL EE t
// MoveGenerator.C
PEII ELTA LLL EL EET TIT ALA AA FILA LILI EATS TELA TEP EEL SL
#include "MoveGenerator.h"
#include "Board.h"
tinclude "unistd.h"
tinclude "math.h"
#define CENTER 4
MoveGenerator : :MoveGenerator ()
W -A
12 }
WO J3J]004uNn P
The getNextMove () member function obtains the list of remaining free squares from
the
given Board object, then checks for possible winning moves on either side. If no
such moves exist,
getNextMove () tries to take the center square. Failing that, getNextMove ()
generates a
random number between zero and the number of squares remaining. It then returns the
index of the
chosen square as the new move.
14 (
16
18
21 if ( movesLeft == 0 )
22 return ( -1 );
23
25
27 return nextMove;
28
32 return nextMove;
33
36 return CENTER;
aT
41 return ( freeSquares[randomIndex] );
42 }
The Engine class plays a coordinating role and acts as the “traffic cop” of the
TicTacToe game. The
final design of the Engine class pulled most of the knowledge about the game out of
the Engine class.
We placed the representation of the game board in a separate class and the
responsibility for
choosing a move in yet another class. The goal was to make the Engine simpler and
more general,
handling only the mechanism of switching between the user’s moves and the moves
generated by
the game.
The final design also uses an Engine subsystem object to encapsulate the Engine,
MoveGen-
erator, and Board classes, as shown in Figure 4.11. While this is a useful way to
visualize the
architecture of the system, notice that all interactions between other components
in the game and
the Engine subsystem actually go through the Engine object. As a matter of
implementation, it
seems easier to just encapsulate the other components of the Engine subsystem in
the Engine itself.
So, the Engine class, as implemented here, supports pointers to a Board object and
a MoveGen-
erator object. The class defines a protected function, checkForWin (), which is
used internally,
and the public functions recordMove (), reset (), and quit () which implement the
corre-
sponding responsibilities defined on the Engine class card in Chapter 4.
In addition to the class declaration, the file Engine.h defines several string
constants used to
report the state of the game to the user.
3 ALPLELILAT ST LEIS ELA TATI AA TARA ESA ITS TLS GE CIA AR ARRE
4 #ifndef ENGINE_H
5 #define ENGINE_H
6 #include "Board.h"
7 class TicTacToe;
8 class MoveGenerator;
10 class Engine (
Lk
12 public:
13 Engine ( TicTacToe * );
14 virtual ~Engine();
17 void quit();
19
20
21
22
23
24
25
26
21
28
29
30
31
32
x3
34
33
36
a
38
protected:
TicTacToe * game;
markType _whoseMove; // Remember whose turn it is
Board * board; // Internal game state
MoveGenerator *_moveGenerator; // Pick next move
int _gameOver; // True if game has ended
void checkForWin(); // Check and report the winner
$:
#tendif
The Engine class constructor sets the initial state of several internal instance
variables,
including setting the _whoseMove member to indicate that X (the user) gets the
first move. This
instance variable always indicates the current move, X or O. The constructor also
instantiates a
Board and a MoveGenerator object.
wW 0 JO UU FP WD FE
PbhepbrrRrR A COC, PP
Wo JOUAYnRrO
PAPE ATLL
EVITA AE
tinclude
include
include
#include
#include
#include
#include
LELTULI LLP ELIS PA A ELILT ETUE FA
"TicTacToe.h"
"Engine.h"
"GameBoard.h"
"MoveGenerator.h"
"Message.h"
"Board.h"
game = game;
_gameOver = FALSE;
The Engine destructor simply deletes the Board and MoveGenerator objects when the
Engine
object is destroyed.
20 Engine: :~Engine()
E 2
22 delete _board;
23 delete _moveGenerator;
24 3
The reset () member function resets the _whoseMove and _gameOver variables to their
initial state, and sends a clear () message to the GameBoard object associated with
this Engine
object. The reset () member function also requests the Message object to display a
“new game”
message.
26 4
27 _whoseMove = XX;
28 _gameOver = FALSE;
29 _board->clear () ;
31 _game->messageArea()->postMessage ( NEWGAMEMSG );
32 3
The recordMove () function is the heart of the Engine class. This function is
called each
time the user or the game makes a move. If the current game is over, the member
function posts a
message to warn the user and returns. Otherwise, it tries to register the move with
the internal
Board object by sending it a recordMove () message. If the Board recordMove ()
member
function returns the value VALIDMOVE, the Engine calls the GameBoard's markX () or
markO() function to display the move in the appropriate square. Otherwise, the
function returns,
after posting an illegal move message for the user.
Before proceedingrecordMove () checks to see if the current move wins the game. If
so,
the game is over, and the function simply returns after issuing a warning. Finally,
depending on
whose move itis, recordMove () prompts the user for the next move or calls the
MoveGenerator
object to choose a new move. If it is TicTacToe’s turn to choose a move, the
function simply calls
itself recursively after toggling the _whoseMove data member.
34 {
38 return;
39 }
40
43
46 if ( _whoseMove == XX )
47
48
49
50
51
52
53
54
59
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
ne
72
73
74
T9
76
77
78
79
_game->gameBoard()->markX ( position );
else
_game->gameBoard()->markO ( position );
}
else
{
_game->messageArea()->postAlert ( ILLEGALMOVEMSG ) ;
return;
checkForwWin() ;
if ( _gameOver )
return;
// If this is the game’s move, change to X’s move and ask the
if ( _whoseMove == OO )
{
_whoseMove = XX;
_game->messageArea()->postMessage ( USERSMOVEMSG );
}
else
{
_whoseMove = OO;
recordMove ( _moveGenerator->getNextMove ( _board ) );
The checkForWin() member function calls the Board member function, whoHasWon (),
to determine the current state of the board. The function posts a message to the
user and sets the
_gameOver flag to TRUE if the game is over for any reason.
80
81
82
83
84
85
86
87
88
89
E i, *winningSquares;
markType winner;
90 {
93 _gameOver = TRUE;
94
96 _game->gameBoard()->deemphasizeSquare ( i );
97
99 }
101 {
103
105
107
113 _game->gameBoard()->deactivateSquare ( i );
114
115 if ( winningSquares[i] )
116 _game->gameBoard()->highlightSquare ( i );
137 else
120
123 if ( winner == XX )
1283
The quit () member function simply exits the applica :on when called.
131 exit ( 0 );
132 }
To complete the TicTacToe program, we simply need to write a driver that creates an
instance of the
TicTacToe class. The TicTacToe subsystem instantiates and ties together the other
components of the
game. Figure 5.4 shows how all these classes fit together in the TicTacToe
subsystem.
MIN
reset
freeSquares
MoveGenerator
numFreeSquares
postMessage
; postMessage
Command pp Engine Message
postAlert
recordMove
whoHasWon
recordMove
clear
getNextMove
clear
recordMove
deactivateSquare
;
3
a
Y
e
E
Ed
E
ob
=
El
deemphasizeSquare
3
oh
Y
z4
A
GameBoard F:
o
oD o
3 EN 3
4
Sl aj >
Ss]
«O
engine
TicTacToe
manage
Let's complete the game by writing a driver that instantiates a single TicTacToe
subsystem.
Because the TicTacToe class handles the task of connecting the game components and
defining the
widget layout, the main body of the game is very simple. The program initializes Xt
and then
creates a TicTacToe object to construct the rest of the game.
1 TIS ELAFARG AULA LAAT RANA TAT AT EPET AAT ATS AAA NAAA RAMA AAA LANA AS
2 // tictactoe.C: A simple tic-tac-toe game using Motif and C++
3 EET IATL ILL ATT IT LE LE TART ATT ELA LALLA AAT ITAA TALIS AL ALT ET Et
4 #include <Xm/Xm.h>
5 #include "TicTacToe.h"
8 {
9 XtAppContext app;
10 Widget toplevel;
LL
16
20
21 game->manage () ;
22
24
25 XtRealizeWidget ( toplevel );
26 XtAppMainLoop ( app );
27 9
We can build the TicTacToe program by compiling all seven classes, plus the main
routine,
then linking the results with the X and Motif libraries as well as the binaries for
the UIComponent
and BasicComponent classes.
The first figure, at the upper left shows the game as it initially appears. The
board is clear, all
squares look like pushbuttons, and the message area displays a message that prompts
the user to
make the first move. The second scene, at the upper right of Figure 5.5, shows a
square as the user
positions the mouse cursor to make a move. The square under the mouse cursor is
highlighted with
a light border to indicate that it is active and available to be selected.
The third scene, at the lower left, shows the game after several moves have been
made. The
squares that have already been selected are depressed into the screen and are not
highlighted when
the mouse cursor enters the square. Those squares that still represent valid moves
maintain the
appearance of a Motif XmPushButton widget and highlight when the user moves the
mouse cursor
into the square.
The last scene, at the lower right corner of Figure 5.5, shows a game after a
player has won. All
squares have been faded into the background by removing all shadows. All squares
are also
inactive and do not highlight when the mouse cursor enters a square. The winning
squares are
accented, using the etched-out shadow style supported by the XmDrawnButton.
Finally, the
message area confirms the game status by reporting the winner.
5.7 Summary
This chapter completes an exercise that began in the previous chapter. Chapter 4
stated the basic
problem and developed a high-level design. This chapter presented the
implementation of that
design, using C++ and Motif. Now that TicTacToe is finished, it should be
worthwhile to review the
entire process and decide what worked well and what didn’t. For the most part, the
implementation
adheres closely to the design detailed in Chapter 4. The classes identified in the
initial design phase
are all present, and for the most part they perform the functions identified during
the design. The
overall structure of the game has changed very little, and the architecture visible
in the message
diagram in Figure 5.4 resembles the organization in the collaboration graph in
Chapter 4.
However, not everything went smoothly and there were a few surprises. For example,
imple-
menting a mechanism to highlight the winning squares was not as straightforward as
originally
anticipated. The command buttons and message area could not be laid out exactly as
planned. A
few new responsibilities were identified during the development phase and several
additional
member functions were required.
One of the claims made for object-oriented programming is that classes tend to be
reusable. We
paid some attention to reusability both in the design and the implementation by
trying to provide
clean interfaces and minimize outside references. In spite of this, few of these
classes are really
reusable in a general sense. The Message class could be reused easily, because it
has no depen-
dencies. But the Message class is very simple and is little more than a wrapper
around a Motif
XmLabel widget. The others are closely tied to their role within the TicTacToe
program. The
classes are modular and self-contained but not really reusable.
Of course, the primary goal of the TicTacToe exercise was not to create a set of
general
purpose classes. Another evaluation criteria is “How easy is it to maintain or add
enhancements to
this program?” We can't really know the answer to this question without attempting
to modify the
Summary 209
program. However, it seems that the program is well-modularized and that, at least,
minor changes
could be made easily. For example, the MoveGenerator class could be replaced by a
class that
supports a better algorithm for selecting moves without affecting other parts of
the program. The
internal representation used by the Board class could easily be altered. The
interaction style and
presentation supported by the GameBoard class could also be modified in some ways
without
requiring changes to the rest of the program.
One test of the value of an object-oriented approach is to see how easy it would be
to add some
new features. The main questions here are “To what degree does a change to one part
of the
program affect other parts of the program? Does changing the internal details of
one class require
all classes to change? Can the program be extended without changing the external
protocol of one
or more classes? If not, what effect does that have on other classes?”
For example, how hard would it be to add a score-keeping facility? Aside from
individual
implementation challenges presented by each new class or classes, the primary issue
is “How hard
would it be to make this new feature work with the other existing classes?” Does
the existing
protocol support the new feature, or will one or more existing classes need to be
altered? If existing
classes must be changed to support enhancements to the game, can the changes be
made by
creating new derived classes, or must the original classes be modified? These
questions are harder
to answer without proof.
We need to evaluate the user interface as well. User interface development tends to
be iterative
by nature. We can’t really know if the interface proposed in the design phase is
effective, until the
program has been built. The “feel” of a program is so difficult to predict
beforehand and so subject
to personal taste, that the results can only be judged by having the intended
audience try the
program.
Several of the features provided in TicTacToe need to be tested and verified with
users. For
example, we tried to make the program very responsive and implemented several types
of
feedback. But will the users understand the various ways shadows are used with
squares? Will they
find this feedback helpful, essential, distracting, or annoying? The game uses
highlight-on-enter to
reinforce the squares that are selectable and to give the game a lively, responsive
feel. This
mechanism seems particularly appropriate for a game but may annoy some users.
The TicTacToe interface has one particularly interesting problem that was not
anticipated until
the game was played in its final form. The computer picks its moves so quickly that
the game’s O
move seems to show up at almost exactly the same time as the corresponding X square
chosen by
the user. In fact, the “O” appears so quickly that the user may not even notice
that the game has
chosen its move. The speed of the game’s moves keeps the user from feeling as if he
or she is
playing against a real opponent. Possible solutions to this problem include adding
a delay before
the game responds or adding some type of transition effect to simulate the
computer’s “thinking”
time. Also, because the user has no indication that the game is picking a square
until the O appears,
perhaps the most recently picked square should be indicated in some way.
Although we do not yet have enough information to judge the success of the user
interface
design, it seems that this effort was also worthwhile. For a simple problem, a
surprising number of
user interface-related issues arose during both the design and the implementation
phases.
Regardless of the final quality of the interface and how it is received by users,
it seems fair to say
that the program was improved by the attention to the user interface at an early
stage.
PATT
Application Frameworks
The first part of this book concentrates on the mechanics of using C++ and Motif.
Chapters 1 through
5 also focus on ways to identify useful objects and construct corresponding classes
that can be used
to implement an application. Part II continues to develop the ideas begun in Part
I, but with a slightly
different emphasis. Instead of focusing on the process of developing classes
suitable for an
individual application, the remaining chapters explore ways to create a framework
of reusable
classes designed to support many applications.
Although earlier chapters discussed ways to make classes more general, most of the
classes
examined so far have been designed to support a specific application. Reusability
was a secondary
consideration, and very few of the classes discussed in earlier chapters are useful
in situations other
than those for which they were designed. The UlComponent and BasicComponent classes
are
notable exceptions. These abstract classes were designed from the beginning to
provide founda-
tions for other classes.
In contrast with earlier examples, most of the classes discussed in Part II are
designed
primarily to provide a foundation for other classes. Developing classes that can be
reused in many
different situations is somewhat different from designing classes to be used only
in a specific appli-
cation. When designing a single application, the emphasis is on identifying objects
that perform a
specific function or fulfill a particular role in that application. When
reusability is the foremost
goal, the programmer has to identify classes that address the needs of many
applications, often
without knowing in advance what these applications do.
211
provide programmers with a body of preexisting, pretested code that can be combined
to form a
complete application. There are at least two ways to reduce the amount of code a
programmer must
write to complete an application.
More importantly, the generic application defines the flow of control used by all
similar appli-
cations. The programmer doesn’t have to worry about how to connect the various
components of
the program — the framework makes the connections automatically. To create a new
application, the
programmer implements only those parts of the program that are unique. Application-
specific
elements can be provided by adding a few new components, defining a few member
functions, or
by deriving new classes from those provided by the framework. In this sense, basing
an application
ona framework is very similar to deriving a new class from an existing class. In
both cases, a new
entity can be created by specifying only how the new entity is different from the
existing one.
212
Applications that share a similar purpose or interact in some way may have even
more in
common. For example, imagine a collection of programs that provides a complete
environment for
a medical facility. Perhaps each program has access to various databases containing
doctors’
schedules, patients’ medical histories, inventories, medical diagnostics, case
studies, and so on.
Some of the programs might need to communicate with each other to work together
effectively.
Such programs might also coordinate the way they present information throughout the
system. For
example, color could be used consistently across the set of related tools. An
application framework
provides a way to support such a family of related programs and to provide
consistency throughout
the environment.
Most frameworks do not support all applications equally well. One challenge when
designing
an application framework is to find a balance between power and flexibility. In
general, the power
of an application framework is inversely proportional to its flexibility.
Frameworks provide power
by correctly anticipating the needs of a particular type of application and
performing various
functions automatically. Flexibility, in this context, means that the framework
must allow applica-
tions to define or alter the framework’s default behavior as needed. Object-
oriented programming
makes it possible to achieve both these goals to some extent, because (at least in
theory) applica-
tions can use inheritance to override features of the framework, when necessary. In
practice, the
more powerful the framework, the harder it is to write applications that don’t fit
into the
framework’s predefined structure. However, an application framework provides many
advantages
over the toolkit approach for applications that fit the model supported by the
framework.
213
The framework developed in the following chapters is very simple and falls far
short of
capturing all the elements common to all Motif applications. The primary focus is
on demon-
strating how to create a basic architecture that can be enhanced and expanded with
additional
classes to meet future needs. MotifApp represents one collection of classes that
provide support for
some Motif-based applications. Of course, not all applications fit into the mold
assumed by this
particular framework, and not all programmers would choose the same set of classes
to form the
base framework. The classes described here are meant to serve as examples that will
hopefully
inspire your own classes, and perhaps your own complete frameworks, designed to fit
your needs.
Part II continues to use the graphical notations introduced in Chapter 3, when they
are useful.
However, the following chapters emphasize the structure supported by an example
application
framework, and the design decisions associated with the development of individual
classes are of
less interest than they were in earlier chapters. Class cards continue to provide a
useful way to
summarize the key responsibilities of various classes. Inheritance and message
diagrams are also
used when they provide useful information about the framework, but collaboration
graphs are not
used. A collaboration graph is a useful tool during early stages of design. In most
cases, message
diagrams provide a better way to visualize the relationships between classes once
they are
completed.
Chapter 6
The MotifApp
Application Framework
This chapter introduces a simple application framework that supports typical Motif
applications
written in C++. The framework, which we will call MotifApp, supports features
common to many
Motif applications, including menus, dialogs, multiple top-level windows, and so
on. Most of the
classes in MotifApp are based on the UlComponent class described in Chapter 2 and
use the
techniques introduced in Part I of this book. Although the MotifApp classes include
some common
user interface components, the main purpose of this framework is to define the
overall structure of
an application. As with most frameworks, the way classes fit together is more
important than the
functionality of any one class.
Applications based on MotifApp must be written in a very stylized way to allow the
framework
to handle most of the connections between major components. To the extent possible,
applications
based on MotifApp need only implement application-specific parts of the user
interface. The
framework provides those parts that are common between typical Motif applications
and defines
how the major elements of a MotifApp program fit together.
This chapter introduces two key classes, Application and MainWindow, which form the
core of
the MotifApp framework. Section 6.1 provides an overview of the MotifApp
application
framework. Section 6.2 describes a simple reusable List class that is used several
times in classes in
the framework. Section 6.3 presents the Application class, and Section 6.4
describes the
MainWindow class. Sections 6.4 and 6.5 show how these classes fit together to form
the foundation
of a MotifApp library. Section 6.7 reimplements the “Hello World” example
originally described in
Chapter 1, using the classes described in this chapter. This example demonstrates
the differences
between applications based on the MotifApp framework and those written using a more
traditional
approach. Later chapters develop additional classes for the framework and discuss
individual
components in more detail.
An Overview 215
6.1 An Overview
The primary purpose of the MotifApp framework is to simplify the task of writing a
Motif appli-
cation by capturing structural elements common to most Motif applications. Although
Xt and Motif
provide a much higher-level interface than Xlib provides, most Motif and Xt
applications contain a
surprising amount of duplicate code. For example, all Motif applications must
initialize the Xt
Intrinsics, open a connection to the X server, enter an event loop, and so on.
There is very little reason
for each application to duplicate the code that implements these steps. Instead, we
can capture these
steps in a class that all applications can reuse.
This list provides a good start toward defining the features of a framework for
Motif applica-
tions. In fact, the steps above are common to nearly every Xt program, regardless
of the widget set.
Looking back at the examples in earlier chapters, it is easy to see that the body
of main () in most
Motif programs is remarkably similar. Most programs perform all the steps listed
above with very
little variation. In fact, if we ignore small differences between various
applications, we can see that
nearly every Xt application contains the same statements and follows the same form,
which could
be written as follows:
PEELEL EEL LATA L ILI AL ELS AI ALE EST LEE L LOLA EARLE RAMA VIA NAS
// Generic form of a typical Motif application
{
0 Widget toplevel;
ua bC OoJ AU FP uN P
yO i XtAppContext app;
12
14
E7
19 // APPLICATION-SPECIFIC CODE
20 VILE FIAT LIL EIT TAT ASI AL EET LE LETT ELS FELT I ETL EA
2d
22 // Realize the shell widget and enter the main event loop
43
24 XtRealizeWidget ( toplevel );
25 XtAppMainLoop ( app );
26 3
The code shown above varies so little from program to program that it seems a waste
of time to
type these statements repeatedly for each program. Programmers often start new
programs by
copying an existing program, and one way to start writing a new Motif application
quickly is to save
this template in a file and use a copy as the basis of each new program. If these
few lines were the
only elements common to most Motif applications, copying this template might be a
reasonable
approach. However, we will be identifying many features in this and the following
chapters that are
not so easy to capture in a simple text template.
Applications based on the object-oriented approach suggested here can modify the
behavior of
the generic code by deriving a new class. Because changes are made only to a
derived class and not
to the original, it is easier to revert to the original behavior. It is also easier
to be sure that any new
errors introduced during development are completely contained within new code.
Programmers are
An Overview 217
unlikely to introduce accidental errors in the existing code because the original
code is encapsulated
in a class and changes are made only to a derived class.
The template shown on the previous page is so simple that it may not seem
worthwhile to
capture it in a class. The power of this approach becomes more evident when we
begin to identify
more complex features common to the types of applications we wish to support.
Although the
characteristics listed earlier represent the minimum feature set common across all
Xt applications,
most Motif applications share many more characteristics. For example:
e Most Motif applications support menus. Many Motif applications have a menubar
that
spans the top of the application’s main window.
e Most Motif applications are interactive and allow the user to execute commands.
These
commands might be issued by selecting an item from a menu, pushing a button,
selecting an
item on a list, or simply by typing or clicking a mouse button.
e Many Motif applications consist of more than one window. All have at least one
window
that can be considered to be the application’s main window. Many support multiple,
independent top-level windows, or collections of secondary windows.
Most use popup dialogs to display information to the user, request input, report
errors,
and so on.
Using just the two lists of features discussed so far, it is possible to define a
collection of classes
that support applications that fit within this model. Not every application
requires all the features
listed, but many do. It would be possible to identify more features if we could
pick a specific appli-
cation domain. For example, if we chose to support only drawing editors and paint
programs, we
could say that all such programs allow the user to select from palettes of colors
and patterns, are able
to read and save files in various formats, and much more. However, the features
listed above are
sufficient to demonstrate the benefits of an aBpuCeNes framework for a relatively
wide range of
typical Motif applications.
The MotifApp framework described in the rest of this book includes the following
core classes:
e Application. This class captures the initialization and general overhead required
of every
Motif or Xt application. The Application class provides the equivalent of the
application
template just discussed, plus some additional features.
e MenuBar. The MenuBar class constructs menu panes from lists of Cmd objects. The
MenuWindow class creates a MenuBar object automatically, but the MenuBar class can
also
be used independently.
Ahead Be he oe
ME IAE PA
SES NEES AR AI i PO
al ST Ie Vy ES
es Ed a |
* DialogManager. Most Motif applications use one or more dialogs. The DialogManager
class, along with several derived classes, provides an easy-to-use central facility
that
supplies dialogs for the entire application.
Figure 6.1 shows a class hierarchy of the MotifApp library, formed by the
collection of classes
described in this and the following chapters.
CmdInterface ButtonInterface
ColorChooser HSV View
RGBController
ColorView RGBView
Clock Swatch View
Application
InfoDialogManager
BasicComponent UlComponent DialogManager + QuestionDialog Manager
PTAS WorkingDialogManager
MainWindow -—————— Menu Window
Message
AskFirstCmd —— WarnNoUndoCmd —— QuitCmd
UndoCmd
Cmd
InterruptibleCmd
NoUndoCmd SelectFileCmd
CmdList IconifyCmd
ColorModel ManageCmd
Although we have not needed any so far, most class libraries benefit from having a
small set of basic
data structure classes available. Before starting the implementation of the simple
MotifApp
framework, there is one basic class that is indispensable, a generic list class.
Because there is no
standard List class available to everyone, this section implements a simple one for
use throughout
the remainder of this book. If a more complete List class is available, it can be
used instead.
The following class uses templates to define a SimpleList class that can be used to
contain an
arbitrary data type. The SimpleList class allows items to be added or removed from
the list. The size
of the list can be determined, and an overloaded [] operator provides access to
individual elements
of the list. Using C++’s template support, the SimpleList class is written to
contain elements of type
<T>, where T is replaced by an actual data type when the class is instantiated. The
class declaration
is written as follows:
5 #define SIMPLE_LIST_H
8 class SimpleList {
9 public:
10 SimpleList ();
11 ~SimpleList();
12 void add(T);
16
17 private:
18 T * data;
19 int _size;
20 $;
21 +#endif
|
|
|
|
The SimpleList implementation file defines the constructor, destructor, and add()
and
remove () member functions. For this simple list, we can justuse XtRealloc () to
alter the size
of the internal list when elements are added or removed. Notice that the type T can
be used
throughout, whenever there is a reference to the data type contained by the list.
41
42
43
VITARA TRIANA NAAA RA GAA AE ETAT TA AAA TT
#include "SimpleList.h"
#include <X11/Intrinsic.h>
_data[_size++] = t;
44 if ( _data[i] == £ )
45 {
49 _size--;
50
52 _data[j] = _data[j+1];
53
a5
58 return;
59 }
60 }
The SimpleList class will be useful many times in this and the following chapters.
It is easy to
create lists to contain any type of data. For example, we might create a list to
hold pointers to
arbitrary UIComponent objects like this:
SimpleList<UIComponent*> _componentList;
The Application class handles the initialization and event-handling steps that are
common to all Xt-
based applications. Instead of calling Xt functions like XtAppInitialize() or
XtApp-
MainLoop(), MotifApp applications simply instantiate an Application object.
Programs can
derive new classes from Application, if necessary, but most of the time, the
Application class can be
instantiated directly. The Application class is derived from the UIComponent class,
although it is
somewhat unique and does not follow the UlComponent model precisely. “The most
apparent
difference is that the Application class does not create any widgets in the
constructor, as we will see
shortly.
The Application class described in this chapter is very simple. However, most
programmers
will quickly find that other facilities can be supported in the Application class.
For example, if all
applications of interest must connect to a server or a database, the connection
could be supported in
the framework. The Application class provides the essential services required by a
Motif appli-
cation. Additional features can be added easily by creating subclasses. Figure 6.2
shows a class card
that summarizes the responsibilities of the Application class.
Ea le BAL. IE ey
A A A A AA EEE NBEO
is EA S tl Be
mins Chr.s
ON a St TE OEE ES
e et ls pee rl weed 2
The Application class provides a place to store some data that may be needed
throughout an
application. This includes a pointer to the X Display structure associated with the
application's
connection to the server, the XtAppContext required by many Xt functions, the name
of the
application, and the class name of the application. This information is maintained
in the protected
portion of the class and is available to other classes via public access functions.
The protected part
of the class also includes two member functions, initialize() andhandleEvents ().
These
member functions are called from main (), which is declared as a friend of the
Application class.
Because only main () calls these functions, they do not need to be public. These
functions are
protected, instead of private, to allow derived classes to override them.
The public portion of the Application class includes the constructor and destructor
and various
access methods that return the values of private data members. The public protocol
also includes the
manage () and unmanage () member functions, which override those inherited from
UlCom-
ponent, and an iconi fy () member function. These member functions provide a way to
manage,
unmanage, or iconify all windows in a multiwindow application with a single
command.
The private portion of the class includes two member functions that can be used to
register and
unregister MainWindow objects with an Application object. The Application uses this
information
to manipulate all the windows in an application as a group. These member functions
are completely
hidden by declaring them in the private section of the class. The MainWindow class
can call these
functions to register a new MainWindow object, because MainWindow is declared to be
a friend.
The Application class is declared as follows:
wo oOo onauwnFr WN P
Pp
o
12
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
AEII EEE ELIE AREA AAA LILLE ET CERF RARAS AAA AMAT ELA AD EOE ELT ELT A
// Application.h:
PIPETTE EGA LETITIA MASIA AAA AAA AA EEL AEREA RIMA IA ATI EEL TAT
#ifndef APPLICATION_H
#define APPLICATION_H
#include "UIComponent.h"
#include "SimpleList.h"
class MainWidnow;
class Application : public UIComponent {
// Allow main and MainWindow to access protected member functions
friend void main ( int, char ** );
friend MainWindow; // MainWindow needs to call
// private functions for registration
public:
void iconify();
Display * display;
XtAppContext _appContext;
ak
53 private:
54
528 E
60
62
64
65 #endif
The file Application.C contains the implementation of the Application class member
functions.
This file includes the required header files and then defines a pointer to an
instance of the Appli-
cation class, initialized to NULL.
Rese CRA OEE AAA NARA IVA AA IATA RARA ABRIR RINA AMI RARA
2 // Application.C:
5 #include "MainWindow.h"
6 #include <assert.h>
7 #include <stdlib.h>
10
12 UIComponent ( appClassName )
13 4
15
16 theApplication = this;
17
19
20 _display = NULL;
21 _appContext = NULL;
23 3
Notice that the constructor also passes the appClassName argument to the
UlComponent
constructor as the name of the instance. Ideally, the argument passed to
UlComponent should be the
name of the program, as indicated by argv [0].As we will see shortly, the
Application class is used
in such a way that the program name is not available when the Application object is
instantiated. The
value of appClassName serves as a temporary instance name, because the UlComponent
constructor requires a name argument, until the correct name of the application can
be determined.
25 {
28
30
38 _display = XtDisplay ( _w );
32
36
37 installDestroyHandler () ;
38
40
41 XtVaSetValues ( _w,
42 XmNmappedWhenManaged, FALSE,
45 XmNwidth, 1,
46 XmNheight, 1,
47 NULL );
48
52
53 XtFree ( _name );
55
58
59 XtRealizeWidget ( w );
60
63
65 {
66 _windows [i]->initialize();
68 }
69 }
The MotifApp framework supports applications with multiple top-level windows. There
are
several possible models for multiwindow applications in Xt. One approach is to
create a single top-
level window used as the main window of the application. All other windows should
then be popup
shells whose parent is the main window. Another approach is to create a single
shell, which never
appears on the screen. All other windows must then be popup children of the main
shell. In this
model, all top-level windows are treated equally, as siblings. One window may
logically be the top-
level window of the application, but as far as Xt is concerned, all windows are
equal. (See
[Asente90] for more information about various ways to use multiple top-level
shells.)
The Application class supports the second model and creates a single widget that
serves as the
parent of all top-level windows created by the program. The Application’s base
widget does not
appear on the screen. All other shells in the application must be created by
calling XtCreatePop-
upShell() and displayed by calling XtPopup(). These shell widgets are created by
the
MainWindow class, discussed in the next section.
The resources specified on line 41 are necessary to support some classes, discussed
in Chapter
7, that cache dialogs. One problem with caching dialogs is that every dialog must
have a parent. If
every dialog to be displayed can potentially have a different parent, caching can
become very
complex. MotifApp avoids this problem by using the Application class’s base widget
as the parent
of all cached dialogs.
The Motif window manager centers each dialog over its parent widget, if that parent
has a
window. Normally, the Application class’s base widget would not need to be realized
to serve as the
parent of popup shells. However, it is necessary to realize this widget to support
the dialog mecha-
nisms described in Chapter 7 because Motif dialogs must be associated with a window
to work
correctly with window managers. To prevent the Application class’s base widget from
appearing on
the screen when it is realized, initialize() sets the shell’s XmNmappedWhenManaged
resource to FALSE. With this resource set, realizing the shell creates an X window
for the widget
but does not map the window. The initialize () function also positions the shell
widget in the
center of the screen, so dialogs created as children of this widget appear centered
on the screen by
default.
The Application class sets the value of the _name member to the name of the
program, as
indicated by argv [0]. By the time initialize() is called, the BasicComponent
constructor
has already allocated a string and assigned it to _name. Therefore, this string is
deleted on line 53,
before a copy of argv [0] is assigned to _name.
Once the application’s main shell has been created and realized, the initialize()
function
loops through a list of MainWindow objects that have been registered with the
Application, calling
each object'sinitialize() andmanage() member functions. The MainWindow class, which
is discussed in Section 6.3, also creates its widgets inan initialize () member
function instead
of creating them in the constructor.
The Application destructor frees the string allocated in either the constructor or
the
initialize() member function. The BasicComponent destructor destroys both the
string
assigned to _name and the base widget.
70 Application: :~Application ()
TE A
12 XtFree ( _applicationClass );
73 }
75 (
TT?
78 XtAppMainLoop ( _appContext );
79 }
The Application class supports several operations that can be performed on all top-
level
windows in a multiwindow application. To perform operations on all windows, the
Application class
must maintain a list of all top-level windows in the application. In the MotifApp
framework, top-
level windows are implemented as instances of the MainWindow class, discussed later
in this
chapter. The Application member functions, registerWindow() and unregister-
Window() allow each instance of the MainWindow class to register itself with the
Application
class when it is created and to unregister itself when the MainWindow object is
destroyed.
Sl of
82 _windows.add ( window );
e:
The function unregisterWindow() removes a MainWindow object from the list of top-
level windows supported by an Application object.
|
86 _windows .remove ( window );
it Se
The Application class uses its list of MainWindow objects to apply operations to
all windows in
an application. These operations might include popping up and down all windows,
iconifying all
windows, or perhaps setting a busy cursor in each window when the application
performs some
lengthy task. The Application class supports manage(), unmanage(), and iconify()
member functions. Derived classes can also implement additional operations. The
manage () and
unmanage () member functions override the corresponding UIComponent members.
94 >)
96 (
98
101 1
103 (
105
Section 6.7 demonstrates how to use the Application class. But first, we must
discuss the
MainWindow class, which works closely with the Application class to display a top-
level window.
Applications that use the MainWindow class need to create only the widgets that
appear in the
work area. All other widgets and all connections between other objects are handled
by the
MainWindow class. Figure 6.3 shows a class card that outlines the MainWindow
class’s
responsibilities.
The MainWindow class is an abstract class derived from UlComponent. The MainWindow
class’s public protocol consists of a constructor, destructor, and the manage (),
unmanage (),
and iconify () member functions. The public portion also includes an initialize ()
member
function. Like the Application class, the MainWindow class does not create widgets
in its
constructor, but uses the initialize () member function instead. This allows the
MainWindow
class to be instantiated before the application is ready to begin creating widgets.
The protected portion of the class defines the protocol between the MainWindow
class and
derived classes. This protocol includes a pure virtual function that must be
overridden by derived
230 Chapter 6 The MotifApp Application Framework
4 #ifndef MAINWINDOW_H
5 #define MAINWINDOW_H
10 public:
ai
14
L7
19
aa
24 protected:
25
ay
a3. Fi
34 +#endif
The MainWindow constructor calls the UIComponent class constructor and then
registers the
new object with the program’s Application object. An assert () statement checks
that
theApplication exists.
E III RAR AAA DA AAA NAAA ARES AAA AAIIARINARARIDARAAA AAA A IRA AI LS
4 #include "Application.h"
5 include "MainWindow.h"
6 #include <Xm/MainWw.h>
7 #include <assert.h>
11 _workArea = NULL;
12
15
16 theApplication->registerWindow ( this );
E7 }
The initialize() member function creates the widgets used by the MainWindow object.
Recall from Section 6.2 that the Application class's initialize () member function
calls the
initialize() function for each registered MainWindow object. If a program creates a
MainWindow object after the Application’s initialize () member function has been
executed,
the MainWindow: :initialize() member function can also be called directly.
19 {
23
24 _w = XtCreatePopupShell ( _name,
25 applicationShellWidgetClass,
26 theApplication->baseWidget (),
27 NULL, O );
28 installDestroyHandler () ;
29
30 // Use a Motif XmMainWindow widget to handle window layout
31
33 xmMainWindowWidgetClass,
34 We SRG, O ja
35
37
39 assert ( _workArea );
40
43
44 XtVaSetValues ( _main,
45 XmNworkWindow, _workArea,
46 NULL );
47
50 if ( !XtIsManaged ( _workArea ) )
51 XtManageChild ( _workArea );
52 }
The MainWindow destructor removes the deleted object from the Application object’s
list of
windows. The base class destructor destroys the shell widget, which triggers the
destruction of the
other widgets.
53 MainWindow: :~MainWindow( )
54. {
57 theApplication->unregisterWindow ( this );
58 }
popup shells must be made visible with Xt Popup () instead of with XtManageChild().
Like
the manage () member function defined by UlComponent, this function uses the assert
()
macro to be sure the component's base widget exists when this member function is
called. The
manage () member function also calls the Xlib function XMapRai sed () to raise the
window to
The MainWindow class overrides the UIComponent manage () member function because
the top. This function also opens the window if it is currently in an iconic state.
|
60 (
61 assert ( w );
63
65
66 if ( XtIsRealized ( w ) )
The MainWindow class also overrides the unmanage () member function, because the
MainWindow class needs to call Xt Popdown () to remove the popup shell from the
screen instead
of calling XtUnmanageChi 1d () as the inherited member function does. The new
unmanage ()
function is written as:
zo 4
71 assert ( w );
72 XtPopdown ( w );
TS 3
75 {
76 assert ( w );
ET
82
85 if ( XtIsRealized ( w) )
This completes the implementation of the MainWindow class. The MainWindow class
provides
the minimum set of features needed to allow an application to create only the “work
area” portion
of the user interface. The MainWindow class works closely with the Application
class to provide
some basic window manipulation functions. MotifApp applications are expected to use
subclasses
of MainWindow as the basis of each top-level window in an application.
AA
|
|
|
|
|
|
|
|
|
|
|
|
One of the most interesting aspects of MotifApp is that applications based on this
framework do not
need to define main (). The function main () is defined as part of the framework
and handles
calling the Application: :initialize() member function and entering the event loop.
Hiding main () allows the framework to handle even more of the routine
initialization all applica-
tions must perform and encourages the view that each program is an instance of the
Application
class. MotifApp applications are created by simply instantiating an Application
object.
MotifApp defines main () in the file Main.C. This function is written as follows:
L FEEIAF AA ASA NAAA AAA PELLET ATL ETAT LAS PTA LET AA RARA AA LEA GT
2 // Main.C: Generic main program usec by all applications
Y LAL EFTISI TLL I LILA ATA EA LILIA ETT LAA DHEA TAAL ET TAA AT ITA EEL LIES
4 #include "Application.h"
5 #include <assert.h>
10
12 {
LS
16 assert ( theApplication );
17
21
22 theApplication->handleEvents () ;
oe }
Notice that main () does not create an instance of the Application class but
expects it to exist
when the program begins. Each program must instantiate an Application object as a
global variable.
If the application does this correctly, theApplication will be initialized before
the first
statement in main () is executed. The function main () uses the assert () macro to
make sure
applications actually instantiate a global Application object.
Figure 6.4 shows a message diagram that documents the relationships between the
MainWindow class, the Application class, and main () . It also shows the role of a
class derived
from MainWindow.
Derived Classes
initialize
Application handleEvents
m
iconify
base Widget
anage
unmanage
YV
N
. =
T
5
|
|
unregisterWindow
registerWindow
Main Window
create WorkArea
The MainWindow and Application classes work together to form a simple, but somewhat
unusual, way to write programs. After briefly discussing how to build the MotifApp
library, Section
6.6 demonstrates how these classes are used to write a MotifApp program.
The examples in the remainder of this book assume the existence of a MotifApp
library. This library
includes the BasicComponent and UIComponent classes discussed earlier in this book,
in addition
to the Application and MainWindow classes described in this chapter. The library
also includes the
function main (), which is defined by the framework instead of by individual
applications. Most of
It is, of course, unusual to have the function main () builtinto a library. Linking
main () into
the MotifApp library means that all applications that use classes from this library
must follow
certain conventions. In particular, programs must not define main (), because main
() already
exists in libMotifApp.a, and only one function named main () can exist in any
program.
The following section shows how this library is used in conjunction with
application-specific
Although we have not yet discussed many other classes in the MotifApp framework,
the Application
and MainWindow classes provide an adequate base for simple applications. This
section demon-
strates how to use these classes to implement a version of the “Hello World”
example, first presented
in Chapter 1.
ISTRIA RAR LTL LLL IEI ITTIA ETLE IA CEA EIA ARA BA RANA AA AA
// HelloWindow.h: "Hello World" main window
PEF FLEET ALSLA LTD TPIS AT AT AAA TALIA EEDA AAA AAA TIAA ANNA NASA TAA ASAT ALA
#ifndef HELLOWINDOW_H
#define HELLOWINDOW_H
#include "MainWindow.h"
oO ON HD FP WD P
e He
H O
public:
e He
w N
hop
un a
Rh op
~~] O
protected:
e He
io 00
N
O
hi
21 +#endif
AMAT TA ELELTL NAAA A EAGT A ALIAS DANA MAA NAAA ARIADNA A A AMA
// HelloWindow.C: "Hello World" main window
PLRRAAAAAA AA NATA AAA VARAS EA NAAA ANAAA EPPA LEE L RARA ANA E.
tinclude "HelloWindow.h"
Nu FW ny F
$ {
14
17 XmNlabelString, xmstr,
18 NULL );
19 XmStringFree ( xmstr );
20 return ( label );
21 }
Once a class derived from MainWindow has been defined, we can complete the
application by
creating the body of the application. MotifApp’s notion of a “main application
body” is very
different from that of more traditional programs. The file HelloApp.C is considered
to be the main
body of the “Hello World” example. This file includes the Application.h and
HelloWindow.h header
files and then declares two objects, an Application object and a HelloWindow
object. This is the
entire program.
E APIALARIA DENIA AAA SIR EE RAF DES RERELAA AA ANAA ALA TT TAT AE ARE ALTE
5 #include "Application.h"
6 #include "HelloWindow.h"
Let’s look at the sequence of events that occur when this program runs. First, the
program
evaluates the global statements contained in HelloApp.C. These statements create an
instance of the
Application class, followed by an instance of the HelloWindow class. The MainWindow
constructor
registers the new HelloWindow object with the Application object, which can now be
referenced
though the variable theApplication. After these objects have been instantiated, the
program
enters main (), which calls the Application object’s initialize() member function.
Appli-
cation::initialize() opens a connection to the X server, creates a shell widget,
and then
calls the HelloWindow objects initialize() member function.
MainWindow: :initialize() creates a shell and an XmMainWindow widget, and then
calls
the HelloWindow class's createWorkArea () member function to create the label
widget.
Finally, the Application object manages the HelloWindow object, causing it to
appear on the screen.
Notice that most of this process is hidden from applications based on MotifApp.
MotifApp
applications need only derive a new class from MainWindow. This derived class
provides any appli-
cation-specific widgets that appear in the window. Once this class has been
written, the body of the
application simply creates an instance of the Application class, along with
whatever MainWindow
objects the application wishes to display when the application runs.
To build the HelloApp program, the file HelloApp.C must be compiled and linked with
the
HelloWindow class and the MotifApp library. For example, the executable “hello” can
be built like
this:
CC -c HelloApp.C HelloWindow.C
CC -o hello HelloApp.o HelloWindow.o libMotifApp.a -1Xm -1Xt -1X11
Although each application must create a single Application object, programs can
create as many
MainWindow objects as needed. For example, the following program creates two
instances of the
HelloWindow class, which creates two windows that display the “Hello World”
message.
LUE ESEL AREA ET AP FIL TL LLL ETI SIT IA IPE CET ASA IAI d t
2 // HelloApp.C: "Hello World" program,
DIETA ERAERR IRA NAAA AAA LEII AA AA ARA ETI TTA PPT RT
5 #include "Application.h"
6 #include "HelloWindow.h"
Fi
8
9
1
Figure 6.5 shows the widget tree created by this multiwindow example. The
Application class
creates the root of the tree, but this window does not appear on the screen. The
only widget directly
created by the program is the XmLabel widget shown in each window. All other
widgets are created
by the application framework.
hello
ApplicationShell
Window 1 Window2
ApplicationShell ee
mainWindow mainWindow
XmMainWindow XmMainWindow
label label
XmLabel XmLabel
Figure 6.6 shows the multiwindow version of “Hello World” as it appears on the
screen. Each
window can be manipulated independently.
Figure 6.6 “Hello World” with multiple MainWindow objects
6.8 Summary
This chapter introduces the foundation of a very simple application framework that
defines the basic
architecture of a typical Motif application. The heart of the framework is the
Application class. This
simple class encapsulates the Xt initialization code required by all applications.
Applications based
on the MotifApp framework must create an instance of the Application class as a
nonlocal variable
at the beginning of a file.
The Application class works closely with the MainWindow class. This abstract class
creates a
shell and a Motif XmMainWindow widget. Concrete classes derived from MainWindow
must
define a single member function that creates the application-specific widgets that
form the
program's interface. Immediately after creating an instance of the Application
class, every
MotifApp application is expected to create an instance of a class derived from
MainWindow. In
addition, the MotifApp library defines main () instead of expecting applications to
define this
function.
The simple Application and MainWindow classes presented here provide only the
minimum set
of features that could be supported by such classes. Application frameworks are
particularly
effective when working with a family of related applications. In such cases, it is
usually easy to
identify many common features that can be supported in the framework rather than
being imple-
mented in each program. It is usually best to add such features to the framework by
creating new
classes derived from the simpler base classes, like Application. By keeping the
Application class as
simple as possible, this class can meet the needs of a broad range of applications.
Applications that
require additional features and enhancements can define new classes derived from
Application.
The following chapters examine other parts of the MotifApp framework, building on
the simple
architecture introduced in this chapter.
Chapter 7
Dialogs
Dialog windows that appear on the screen for a relatively short amount of time for
some special
purpose are a common feature of many interactive applications. Dialogs allow
applications to ask
questions, display important information, or provide access to functionality that
does not always
need to be accessible from the application’s main window. Because most applications
contain a
significant amount of code dedicated to handling such dialogs, dialog-related
facilities make good
candidates for C++ classes. Although Motif provides many ready-to-use dialog
widgets, it is still
useful to consider some C++ classes that capture some higher-level mechanisms that
help manage
dialogs.
Most interactive applications support many ways to get input from users. For many
applications, the
normal mode of operation requires continuous real-time input. For example, the
TicTacToe game
described in Chapter 5 allows the user to interact directly with the program
through its main window.
This type of input needs to be quick and simple for the user.
Consider the window in Figure 7.1. This hypothetical application provides a graph
of some
information that changes over time. The user can control the range of time over
which the graph is
displayed by changing the “From” or “To” fields. Presumably, the graph changes
immediately to
display the new range. The user can also use the toggle button in the lower left
corner of the
window to alter the way the information is displayed.
This window allows the user to see and manipulate all available parameters
directly. For a
simple program, it is reasonable to place all commands in the main window. However,
as a
program’s complexity grows, so does the number of commands the program supports.
Displaying
all available operations in a single window quickly produces an application with an
unmanageable
number of buttons, menus, and other input areas. Such an interface can be
overwhelming to the
user because it is difficult to tell which input areas and controls are essential
and which are merely
options. Such interfaces are often large and clumsy looking because of the large
number of controls
in the main window.
$ Shipped
One way to handle applications with many inputs, controls, or options, is to place
seldom-used
portions of the user interface in separate dialog windows that are not displayed
unless needed.
Dialog windows can be used in several different ways. For example, applications
often use dialogs
to convey information to the user. Once the user has seen this type of dialog,
either the user or the
application usually removes it from the screen. Dialogs can also be used to request
specific infor-
mation from the user. Such dialogs usually disappear once the user has supplied the
needed
information.
Applications can also use dialogs as optional control panels that can stay on the
screen as long
as the user needs them. For example, Figure 7.2 shows a variation of the interface
shown in Figure
7.1. In Figure 7.2, the controls for manipulating the range and format of the data
displayed in the
graph have been removed from the main window. These controls are not visible unless
the user
needs them. Clicking on the “Select Range” button displays a dialog that contains
the controls
previously located in the main window.
There are several ways this dialog could be used. One approach is to allow the
controls to
affect the graph display in real time, just as in the earlier version of the
program. When used this
way, the dialog usually stays on the screen as long as the user needs it. Once the
user has finished
using the dialog, he or she can dismiss the dialog or leave it on the screen for
later use. The dialog
could also be designed to allow the user to change the values in the dialog without
immediately
affecting the data displayed in the main window. When the user presses an OK button
on the dialog,
the dialog usually disappears and the graph is updated.
Units Shipped
ee
PIR RT ES PT ER re
The example shown in Figure 7.2 demonstrates a dialog that allows the user to input
data at
any time. While the dialog is displayed, the user can usually continue to interact
with the appli-
cation in other ways. This dialog simply provides a way to reduce the clutter in
the main window
by putting the controls in a separate optional window.
Applications can also use dialogs to request information from the user that the
application
requires before proceeding. Figure 7.3 shows a dialog that asks the user to confirm
a “quit”
command. This type of question normally requires an immediate answer. The
application cannot
carry out the current command, “quit,” without an answer.
244 Chapter 7 Dialogs
Units Shipped
up
Cuestion Dialog
Consider the example shown in Figure 7.3. Here, the user selects a “Quit” button to
exit an
application. Attempting to be user friendly, the application posts a dialog asking
the user to confirm
the command. A naive way to write the callback routine for the quit button might
look something
like this:
if ( confirmExitCommand() )
exit (.0 );
can be used safely, but only if the programmer is aware that the program is not
really blocked but is
actually continuing to handle events. (See [Young94] for a discussion of this
approach to dialogs.)
{
Widget dialog;
XmString xmstr;
XtVaSetValues ( dialog,
XmNmessageString, xmstr,
XmNdialogStyle, XmDIALOG_FULL_APPLICATION_MODAL,
NULL );
XtAddCallback ( dialog,
XmNokCallback,
reallyQuitCallback,
NULL );
XtAddCallback ( dialog,
XmNcancelCallback,
destroyDialogCallback,
NULL );
Figure 7.4 shows the sequence of events that would occur when the user tries to
exit an appli-
cation that implements this approach.
as cancel callback
Eventually, the user will select one of the buttons on the confirmation dialog,
either the OK or
the Cancel button. Depending on which button is selected, the reallyQuitCallback ()
(vector E) will be called, or the destroyDialogCallback() (vector F) will be
called. In the
first case, the application exits. In the second case, the destroyDialogCallback ()
simply
destroys the dialog, which removes it from the screen, and returns control (vector
G) to the event
loop, which waits for the next user input.
Notice that during this entire scenario the application is never actually blocked
while waiting
for a response. The application is continually handling events, including Expose
events, window
configuration events, and so on. There is one potential problem with this approach.
The application
can also handle device events, which means that the user can push buttons, pop up
menus and so
on, to issue additional commands. This can lead to many problems and is not what
the user expects.
Fortunately, there is a simple way to force the user to answer the question before
moving on.
Notice that the quitCallback() function shown on the previous page sets the
dialog’s
XmNdialogStyle resource to XmDIALOG_FULL_APPLICATION_MODAL. This resource
configures the dialog in such a way that Xt ignores any device events for the
application except
those that occur in the posted dialog. The remainder of the application can still
receive and process
exposure and configuration events, but mouse or keyboard input is ignored. The user
can choose
the OK or Cancel buttons on the dialog, but nothing else. Therefore the application
appears to the
user to be waiting for a reply to a question, even though the program is not
blocked in the tradi-
tional sense.
There are several possible scenarios in this example. Each sequence starts when the
user
initiates an event (vector A) that causes the program to invoke the quitCallback()
function
(vector B). The quitCallback () function posts a dialog and registers callbacks for
the OK and
Cancel buttons, and then returns (vector C). Eventually the user selects an OK or
Cancel button on
the confirmation dialog (vector D), which results in either the reallyQuitCallback
()
function (vector E) or the destroyDialogCallback () function (vector G) being
called.
ButtonRelease event
as OK callback
4, Return
J Button press on
saveAndExitCallback() “OK” or “Cancel”
1. Save files of confirmation dialog
2. exit() ula "ER
I reallyQuitCallback()
if unsaved files
2. Register justExitCallback()
as “no” callback
3. Register saveAndQuitCallback()
as “yes” callback
4. Return
else
Exit()
EEO Uhh
SSS ESS a ae ee ee ee A
PA a
any files need to be saved. If unsaved files exist, the function does not exit, but
simply posts another
dialog and registers two callbacks for the OK and Cancel buttons on this dialog.
The new dialog
displays a question: “Do you wish to save files before exiting?” and then returns
control to the
dispatch loop (vector F).
Eventually, the user will select one of the OK or Cancel buttons on the new dialog
(vector 1).
When this happens, the dispatch mechanism will call either justExitCallback()
(vector K),
or saveAndExitCallback() (vector J), as appropriate. Both of these functions exit,
ending
the sequence, although saveAndExitCallback () saves all files first.
It is easy to imagine extending this sequence even further before exiting. For
example, the
routine responsible for saving the files might encounter an error while saving a
file (the disk could
be full, or the application might not have permission to write the file).
Additional dialogs might be
required to ask the user to make additional choices or to allow the user to abort
the exit sequence.
Motif provides many dialog widgets that can be used to implement mechanisms such as
those
described above. However, while Motif provides the raw pieces, most applications
need to write a
considerable amount of code, much of which is redundant, to manage these dialogs
effectively.
This makes a class or classes that manage dialogs an attractive idea. For example,
the sequence of
events described above could be encapsulated in a QuitHandler class, so that every
application
would not have to implement this mechanism. The following section looks at a
general class that
supports applications that use dialogs. The remainder of this chapter, together
with Chapters 8 and
9, describes additional classes that make it easier to implement commands that
involve complex
sequences of user interaction, such as the scenarios discussed in this section.
There are several issues that must be resolved in any substantial application that
uses dialogs. For
example, how does the application deal with many distinct messages to the user?
Does each separate
message use its own dialog? If so, are all dialogs created at startup or only when
needed? If an appli-
cation creates all dialogs at startup, the program will take a longer time to
display the first window.
These dialogs, which may never be used, consume memory and tie up server resources.
If dialogs
are not created until they are used, programs will be smaller and have a faster
initial startup time.
However, depending on the performance of the system being used, there may be an
unacceptable
delay in posting each dialog because the application must create a new dialog for
each message.
One reasonable compromise is to create only a single dialog widget and reuse it for
all
messages. This is an efficient approach because only one dialog widget must be
created. Once the
dialog has been created, later uses are very fast. However, there are some problems
to solve when
using this approach. For example, what happens if the dialog is already in use and
the application
needs to display a second message? The new message could replace the previous
message in the
displayed dialog, but this might make the first message flash by so quickly that
the user cannot read
it.
One effective approach is to create a dialog cache. If the cache contains an unused
dialog when
the application needs to post some information, an existing dialog can be posted
quickly. Otherwise
the application can create additional dialogs as needed. These dialogs can be added
to the cache or
destroyed when they are no longer needed. This mechanism is an ideal candidate for
a C++ class
that encapsulates the details and provides an easy-to-use interface.
i SALTATILI IRA RARA AAA REA AAAAA ARA AT ATA LA EA EET OD GT ELL TS
2 // DialogCallbackData.h: Auxiliary class used by DialogManager
S ARMAS TA MAIAIAAAAMARAR TALA AI ADA PANT ALTER AL ALLL ALLL STA ET
4 #ifndef DIALOGCALLBACKDATA
5 #define DIALOGCALLBACKDATA
9 class DialogCallbackData {
10 public:
LL
13 DialogCallback ok,
14 DialogCallback cancel,
15 DialogCallback help)
16 {
17 _ok = OK;
18 _help = help;
19 _cancel = cancel;
20 _clientData = clientData;
21 }
27 private:
28
29 DialogCallback _ok;
30 DialogCallback _help;
31 DialogCallback _cancel;
32 void *_clientData;
33
3@ 3}
35 #endif
TIRAN
250 Chapter 7 Dialogs
The DialogManager class handles the details of displaying and caching arbitrary
types of
dialogs. The DialogManager is an abstract class that provides a supporting
infrastructure for
derived classes, which must create the actual dialog widgets. For example, Section
7.3 describes an
InfoDialogManager class used to display simple messages, and Section 7.4 describes
a QuestionDi-
alogManager class that can be used to ask the user a question. The MotifApp
framework supports a
single instance of each of these classes. Applications that need to display dialogs
can simply call a
member function of the appropriate DialogManager object.
The DialogManager class defines the external protocol followed by all dialog
manager classes.
Applications can display a dialog by calling the post () member function for the
appropriate
dialog manager object. This member function allows the caller to specify a message
to be displayed
and also allows the caller to provide some optional callback functions to be called
if the user selects
one of the various buttons supported by Motif dialogs. For example, all dialogs
support an OK
button and a help button; many also support a cancel button. Applications can
simply provide a
function of type DialogCallback, as defined in DialogCallbackData.h, to be called
when the user
clicks on one of these buttons.
Figure 7.6 shows a class card that outlines the primary responsibilities of the
DialogManager
class.
The DialogManager class relies on the Application class because Motif dialogs
require a parent
widget. We could force applications to pass a widget to the post () method to be
used as a parent,
but allowing different parents would complicate the caching mechanisms. Also, Motif
dialogs tend
to center over their parent, and applications may not always have an appropriate
widget available at
the point in the program at which the dialog is posted. For example, some dialogs
may be posted
from callbacks called from a menu item where the only available widget is the
button widget in the
menu. Centering the dialog over a button in a menu does not normally provide the
best position.
The DialogManager class uses the unmapped shell supported by the Application class
as the
parent of all dialogs. Using the Application class’s shell widget ensures that all
dialogs have the
same parent, which simplifies the caching and centers the dialog on the screen. The
center of the
screen is not always the best place for a dialog, but it is a reasonable compromise
between usability
as:
3 SIJETLU UTAT TA TAT ALA EL EAT ETA ETAL AT ATL ALT PLETAL CELT
4 #ifndef DIALOGMANAGER_H
5 #define DIALOGMANAGER_H
7 #include "UIComponent.h"
8 #include "DialogCallbackData.h"
E ls!
12 public:
13
iS
22
24
26
a7 private:
28
29 Widget getDialog();
30
33 #endif
The DialogManager class has a very simple public protocol. The constructor expects
a single
argument, which is used as the name of all dialogs. Applications that need to
display a dialog simply
call the post () member function, providing a string to be displayed and various
optional callback
functions to be called when the user selects the OK, Cancel, or Help buttons on the
dialog. Notice
that not all Motif dialog widgets have all of these buttons, but the DialogManager
class supports all
three for those cases in which they apply.
The protected section of the class declaration defines the subclass protocol. All
derived classes
must implement a createDialog() member function. This member function must create
and
return a dialog widget of the type supported by the derived class. This widget must
be a Motif
XmMessageBox widget or a subclass of XmMessageBox. The private portion of the class
contains
a callback and an auxiliary function used to manage the dialogs.
MAA LATA RAR AAA TAP LAA ULIA LAITA UAII LEIE A
// DialogManager.C: Support cached dialog widgets
TEAEPA ELV LL IT AAA TALL AAA AT ATTA ARAATA MAATTI LARA GT TA
#include "DialogManager.h"
#include "Application.h"
#include <Xm/MessageB.h>
#include <assert.h>
wo Ory nO PP WD P
11 // Empty
12 3
Help buttons. The test on line 27 prevents the callbacks from being installed
repeatedly with the
cached dialog widget.
14 {
16
19
20 if ( w & !XtIsManaged ( w) )
21 return ( _w );
22
24
27 if ( newDialog != w )
28 {
30 &DialogManager: :actionCallback,
31 ( XtPointer ) this );
32
33 XtAddCallback ( newDialog, XmNcancelCallback,
34 &DialogManager: :actionCallback,
35 ( XtPointer ) this );
36
38 &DialogManager: :actionCallback,
39 ( XtPointer ) this );
40 }
41
45 return ( newDialog );
46 }
The post () member function displays a dialog widget on the screen. The post ()
function
begins by calling the private member function getDialog(), which returns a dialog
widget.
This widget may be a cached dialog that already exists, or it may be a new dialog
if the cached
dialog is not available. The post () member function requires a string to be
displayed in the
dialog and may also take an optional clientData parameter and several optional
callback
functions, which are treated in much the same way as Motif callback functions. The
clientData
parameter is an untyped pointer that can be used to pass data back to the caller
when callbacks are
invoked.
48 void *clientData,
49 DialogCallback ok,
50 DialogCallback cancel,
51 DialogCallback help)
52 {
54
55 Widget dialog = getDialog();
56
60 assert ( dialog );
62
65
68 XmStringFree ( xmstr );
69
172
76
77 if ( thelp )
78 {
81 }
82
83 // Post the dialog
84
85 XtManageChild ( dialog );
86 return ( dialog );
87 }
The post() function does not install the provided callback functions directly.
Instead,
post() instantiates a DialogCallbackData object to store pointers to the caller’s
callback
functions along with any client data. This object is stored in the dialog’s
XmNuserData resource,
where it can be retrieved inact ionCallback().
After all callbacks have been installed, post () displays the dialog and returns
the widget to
the calling application. This allows applications to manipulate the dialog widget
directly, if desired.
Notice that although the DialogManager class is derived from UlComponent,
applications cannot
normally use the baseWidget () member function to access the dialog widget
encapsulated by
the class because baseWidget () always returns the cached dialog. There is no way
to access a
particular dialog widget, other than retaining the widget returned by post ().
The function actionCallback() is called as a result of any user action. This static
member function retrieves the DialogCallbackData object from the dialog’s
XmNuserData
resource argument to gain access to the application-defined callback functions. The
appropriate
application’s callback function, if it exists, is then called based on the reason
the callback was
invoked. Before returning, the callback deletes the DialogCallback object. In
addition, if the widget
is a temporary dialog, the widget is destroyed.
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
105
106
4:07
108
109
LLa:
112
113
114
TS
116
ELY
118
XtPointer clientData,
XtPointer callData )
DialogCallbackData *dcd;
switch ( cbs->reason) {
case XmCR_OK:
callback = dcd->ok() ;
break;
case XmCR_CANCEL:
break;
case XmCR_HELP:
callback = dcd->help();
break;
y;
if ( callback != NULL )
( *callback )( dcd->clientData () Ke
delete dcd;
if ( obj->baseWidget() != w )
XtDestroyWidget ( w );
Figure 7.7 shows a message diagram of the DialogManager and related classes. The
Dialog-
Manager and all derived classes use the global Application object’s shell widget as
a parent. Notice
that, unlike many other message diagrams in this book, this diagram includes both
the abstract
DialogManager class and two derived classes. This diagram documents the interface
between the
base class and derived classes and also illustrates the different role each of
these classes play. The
interface between the DialogManager and its subclasses consists of the single
member function,
createDialog(). Because the InfoDialogManager and QuestionDialogManager classes
inherit
the characteristics of the DialogManager class, they also inherit many of the other
messages shown
in this diagram.
DialogCallbackData
ok
clientData
cancel
Aviles base Widget
pplication DialogManager
getDialog
createDialog
createDialog
InfoDialogManager QuestionDialogManager
Figure 7.7 A message diagram for the DialogManager and related classes.
Programmers often need to display simple messages to the user. A message might be a
warning, such
as “Disk almost full,” or simply a status report, such as “Compilation completed.”
This type of
dialog is one of the simplest to use, and provides a first test case for the
DialogManager class
described in the previous chapter. The InfoDialogManager class is derived from
DialogManager and
displays a Motif InformationDialog when a post () member function is called. The
MotifApp
framework includes a single instance of the InfoDialogManager class, declared athe
InfoDi-
alogManager. Any part of the application can access this object by simply including
the file
InfoDialogManager.h.
The InfoDialogManager class defines only a constructor and a createDialog() member,
5 #define INFODIALOGMANAGER_H
6 #include "DialogManager.h"
10 DULL
LE
14 protected:
LS
17 );
18
20
21 +#endif
IVAR A AAA AAA AAA AAA ARA AAA AAA AAA ATA TATA TL
// InfoDialogManager.C:
#include <Xm/Xm.h>
#include <Xm/MessageB.h>
DialogManager *theInfoDialogManager =
new InfoDialogManager ( "InformationDialog" );
wow ð ~w A U e un Pp
Pp
o
16 )
Notice that this class does not have to deal with the cache mechanism. It merely
provides a
particular type of dialog. The base class handles all the details of caching
dialogs, posting dialogs,
setting up callbacks, and so on.
This completes the InfoDialogManager class. Now let’s look at a very simple program
that
tests the InfoDialogManager class. The program uses the MotifApp framework and
displays a
pushbutton. The button has a single callback, which posts a dialog each time it is
called.
First we need to create a subclass of the MainWindow class to contain the button
widget. This
class can be declared as follows:
1 EMMA ANNA AA DADA DAA AAA A AAA ANNA AAA AA ANA NAAA AAA AAA ONIS
2 // DialogTestWindow.h: Test the InfoDialogManager class
3 ESIAABAAA AAN ADAN A AAA ANNA AAA NAAA AAAAAA TATA ATA TL
4 #ifndef DIALOGTESTWINDOW_H
5 ttdefine DIALOGTESTWINDOW_H
6 #include "MainWindow.h"
7
10 public:
13 protected:
a da
16 #endif
ESAARA NADIA NADA DIANA ANNAN AAA AAA NAAA AAA AAA EIT
// DialogTestWindow.C: Test the InfoDialogManager class
DMA REA RRA TARDA IIIT URIINI ATT AAT A TATA RITOS)
#include "DialogTestWindow.h"
#include "InfoDialogManager.h"
#include <stdio.h>
#include <Xm/PushB.h>
ON DM FP WD F&F
10
he a :
14 xmPushButtonWidgetClass,
15 parent, NULL, O0 );
16 XtAddCallback ( button, XmNactivateCallback,
17 | postDialogCallback, NULL );
18 return ( button ):
ye . A
23 char buf[100];
24
30
31 theInfoDialogManager->post ( buf );
3 )
PAFFI AtA ttt ttti titit AAA AAA AA TAA ATA TATA AAA AD
// InfoDialogTestApp.C: test the InfoDialogManager class
AAA AAA AAA AAA AA AAA RARA AAT AAT AAA AAT AAA AAT TED
#include "Application.h"
#include "DialogTestWindow.h"
Application *myApp = new Application ( "DialogTest" yz
MainWindow *window = new DialogTestWindow ( "DialogTest" );
DI DM PWN P
To build this test program, we must compile the files InfoDialogTestApp.C and
DialogTestWindow.C and link the results with the MotifApp library. (Both the
DialogManager
class and the InfoDialogManager class need to be added to the MotifApp library
first.) Figure 7.8
shows the test program after posting several dialogs.
Dialogye
MotifApp provides several services in the form of objects that can be accessed
throughout an appli-
cation. These objects are instantiated as nonlocal, statically initialized objects
and can be used by
any part of a program by simply including the appropriate header file. For example,
MotifApp
supports an instance of the InfoDialogManager class, identified as
theInfoDialogManager.
This object can be used as a central resource by including InfoDialogManager.h. The
object itself is
instantiated in the file InfoDialogManager.C as a global object.
file whose constructor references a global object in another file. The globally
instantiated classes
described in this book are carefully constructed in such a way that the
constructors of these classes
only initialize each object and do not depend on any other objects. The one
exception is the
MainWindow class; this class must be instantiated after an Application object, and
in the same file,
to ensure the proper initialization order.
Applications often use dialogs to ask questions that must be answered before the
program can
proceed. For example, an application may need to ask a user to confirm an exit
command or to ask
if the user really wants to perform a potentially destructive operation. Section
7.1 describes the
programming model that must be used to handle such questions. This section
describes a simple
QuestionDialogManager class that supports this model within MotifApp.
The QuestionDialogManager class defines only a constructor and then overrides the
Dialog-
Manager class’s pure virtual createDialog() member function. The
QuestionDialogManager
class is declared as follows:
PIERIT UTIEL TELI EL EI ETETA ETTA REII AT TET TIA AAN IL EEES CET
// QuestionDialogManager.h
PELEPELIFT ETT L TAT ALI LT IIT ASAT TAT TTA PRL EL LTE TELAT ELI T
#ifndef QuestionDIALOGMANAGER_H
#define QuestionDIALOGMANAGER_H
#include "DialogManager.h"
O JO U0 4 WD P
kr wo
o
public:
QuestionDialogManager ( const char *name ) : DialogManager (name) ()
Rh op
DN He
13 protected:
1503023
16
LID ATLL EPIL I CLT RETAMA AAA ASAT TL ETAL ETA ET TAA TTL AAS I TT
#include "QuestionDialogManager.h"
#include <Xm/Xm.h>
#include <Xm/MessageB.h>
O WAN HA MN FWD H
10 QuestionDialogManager *theQuestionDialogManager =
oie new QuestionDialogManager ( "QuestionDialog" );
16 XtVaSetValues ( dialog,
L7 XmNdialogStyle, XmDIALOG_FULL_APPLICATION_MODAL,
18 NULL );
19
20 return ( dialog );
h 3
Now we can write a short program to test the QuestionDialogManager. This program is
very
similar to the test program described in the previous section. The
QuestionTestWindow class is
declared as:
EAN AA AAA ERA AAIA RL ALAA ETA ETEA AAAI EELT IAAI TETA
// QuestionTestWindow.h: Test the QuestionDialogManager class
AMM INNATIA AAA RANIA AA AT LSAT ATT
#ifndef QUESTIONTESTWINDOW_H
#define QUESTIONTESTWINDOW_H
#include "MainWindow.h"
#include <stream.h>
oO O N nA 0 FWD EF
10
am
12
i3
14
15
16
T7
18
19
20
#endif
a callback function that posts a question dialog when the user clicks on the
button.
O 0 JO 0 Ff WD PP
e pp
e O
PRP PPP
IHW & WD
18
19
20
21
PEILILA LEI TEL LILLIA LALA EILERT ETILE AL ALP ARANA ERTL EEE TEARS
// QuestionTestWindow.C: Test the QuestionDialogManager class
RIELLET AAA TS LETITIA TE EPA LTA TAT AAT EEE ELT AA EL ELTA AE OL OES TLE ETEO
#include "QuestionTestWindow.h"
tinclude "QuestionDialogManager.h"
#include <Xm/PushB.h>
22
23
24
25
26
27
28
void postDialogCallback ( Widget, XtPointer, XtPointer )
{
theQuestionDialogManager->post ( "Can you answer the question?",
(void *) NULL,
okCallback,
cancelCallback );
CARR
OS |
34 {
The file QuestionTestApp.C completes the application. This file just instantiates
the Appli-
cation object and a QuestionTestWindow object.
MAGE RE Eee STEALER EAE ELSES BEL ERES RARER MESSE LALA EUARAEAS:
// QuestionTestApp.C:
LEI LATE LIL AGL TIAL I IAAL LEIA IAEI TIITII PET ETI ET?
#include "Application.h"
#include "QuestionTestWindow.h"
© y HM PWD P
Summary 265
7.5 Summary
This chapter presents a DialogManager class that supports cached dialogs. The
implementation is
very simple but defines a protocol that can support more complex implementations.
For example,
the DialogManager class could be altered to cache more than one dialog, if desired,
without
requiring any changes to derived classes or to applications that use the MotifApp
dialog classes. One
value of the DialogManager, along with its related classes, is that applications
can display a dialog
with a single call. The DialogManager interface does not expose the widget-based
implementation,
which allows portions of a program that have no user interface component to post
messages or ask
questions. Because the DialogManager caches widgets, dialogs will often appear on
the screen more
quickly than if the program has to create a new Motif widget each time a dialog is
needed.
HE ANEMIA A a a AE
SEE ROD A CS US A OE ES ld ls
eA Ne de rn ty A NA A A re er Pe ee a
REA AN A Sel aaah le eae ae
ROSS
mAh
AA
Chapter 8
Command Classes
Representing commands as objects has many advantages. Many commands have some state
or
data associated with the command, and others may involve a set of related
functions. In both cases,
modeling a command as an object allows the data and functions associated with a
single logical
operation to be encapsulated in one place. Because command objects are complete and
self-
contained, they can be queued for later execution, stored in “history” lists,
reexecuted, and so on.
This chapter presents several classes that can be used to represent commands. The
primary
responsibility of each command class is to provide a way to perform some action and
also to undo,
or reverse the effects of, that action. Section 8.1 describes an abstract Cmd class
that serves as the
basis of all other command classes. This section also describes some auxiliary
classes, including a
All commands in the MotifApp framework are represented by classes derived from an
abstract Cmd
class. The Cmd class defines the primary interface to all derived classes and
provides a supporting
also a way to reverse, or undo, that action. The Cmd class provides a public
interface for each of
these operations. The execute () member function initiates an action, and an undo
() member
function reverses that action. The Cmd class also supports member functions that
can be used to
enable and disable a command, along with any user interface associated with the
command. Each
command object also supports a list of other commands that need to be enabled or
disabled when
this command is executed. This feature makes it easy to set up dependencies between
commands.
The Cmd class provides support for a central undo facility that allows applications
based on
the MotifApp framework to undo the most recent command. The Cmd class supports an
undo
facility by maintaining a pointer to the most recently executed command in a static
data member.
This member is shared by all classes derived from Cmd but is not visible to other
classes.
Figure 8.1 shows a class card that summarizes the Cmd class’s primary
responsibilities.
Cmd Abstract
l. Initiates an action
2. Undoes the action
iiss
268
The
Cmd Implementation
Now we can examine the implementation of the Cmd class. The Cmd class is an
abstract class that
supports six essential public member functions. These member functions correspond
to the respon-
sibilities listed on the class card in Figure 8.1.
The execute () member function initiates the action or actions supported by the
command.
This function provides the public interface through which any command is executed.
However, execute() relies on other methods to perform the action supported by the
command.
The undo() member function causes the action initiated by the execute() member
function to be reversed. Like execute (), this function provides the public
interface but
The activate () member function enables the command and enables any user interface
component associated with the object. Command objects can only be executed if they
are
currently enabled.
The deactivate() member function disables the command and disables any user
interface component associated with the object.
The addToAct ivationList () member function adds a Cmd object to a list of objects
The add () member function adds a Cmd object to a list of other commands to be
executed.
The Cmd class also provides a way to register interface components with a command
object
and provides several access member functions. These include a member function that
reports
whether or not an object is active and a member function that can be used to
determine if a
particular object supports an undo () member function. The Cmd class uses the
SimpleList class
to implement the list of CmdInterface objects. The CmdInterface class is described
in Section 8.2.
The Cmd class also uses a CmdList class to maintain other lists of Cmd objects. The
CmdList class
is described in the following section.
FPwouowmodI HA UW BP WD P
#define CMD_H
#include "SimpleList.h"
#include "CmdInterface.h"
class CmdList;
MI A IND e dt RÓS a
ee ie Came
12
13
14
13
16
1
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
a3
54
95
56
57
58
59
60
class Cmd (
friend CmdInterface;
friend CmdList;
public:
// Access functions
protected:
int _hasUndo; // True if this object supports undo
static Cmd * lastCmd; // Pointer to last Cmd executed
0; // Specific actions must be defined
0; // by derived classes
E eer eee
63
64 CmdList * activationList;
65 CmdList * deactivationList;
70. 3
71 =#endif
Classes derived from Cmd must implement the pure virtual doit () and undoit ()
member
functions defined in the protected portion of the class. In addition to the doit ()
and undoit ()
member functions, the protected section of the Cmd class declares a static member,
_lastCma,
which points to the most recently executed Cmd object. Because it is static, this
member is shared
by all objects belonging to classes derived from Cmd. The _1astCmd member always
points to
the most recently executed command to allow the command to be undone easily. The
Cmd class
also supports a Boolean member, _hasUndo. This flag specifies whether or not a
command can be
successfully reversed. Derived classes must set this flag to FALSE if they don’t
support undo.
Notice that the Cmd class defines an enumerated type, CmdType, and supports a
public access
function that returns the type of a Cmd object. This feature supports various uses
of the
CmdInterface class, including menus and toolbars. The default type of Cmd is
Action, but derived
classes might choose to override this value. For example, some Cmd classes might
represent a state
that can be on or off. Such a class might declare its type to be Toggle. When an
interface is
associated with such a Cmd, a Motif XmToggleButton widget might be used. The
following
chapter explores this idea further.
The private portion of the Cmd class maintains a list of objects to be activated
and deactivated
when a command is executed. Because the supporting mechanism is implemented
entirely by the
Cmd class, these CmdList objects do not need to be accessible to derived classes.
Similarly, the
Cmd class supports two private flags that indicate the current and previous active
state of the
command. These, along with a name and a function for reverting the state, should
not be needed by
derived classes. The private portion of the Cmd class also defines a pointer to a
list of CmdInterface
objects. This list allows command objects to activate and deactivate any associated
user interface
according to whether the object is currently enabled or disabled.
The file Cmd.C declares an external command object known as theUndoCmd. This object
provides a convenient way to undo the most recent command. Applications that need
to undo a
command can simply execute theUndoCmd. Section 8.4 describes theUndoCmd in more
detail.
The Cmd constructor initializes the data members, setting the _activationList and
_deactivationList members to NULL, and _name and _active to values specified by the
constructor’s arguments. The constructor initializes the _hasUndo flag to TRUE,
because all
derived classes are expected to implement undo by default.
1 ESTA AVIAR AAA ANI DAA L TAS ATTIRE AL I TIT TAT ERAS
3 FEL ELATL ESA AAA ET TAL AAI AA ATL ASIP IS AS ADIT TEAL ASL TA
4 tinclude "Cmd.h"
5 tinclude "CmdList.h"
6 tinclude <X11/Intrinsic.h>
7
13 {
15 _active = active;
16 _hasUndo = TRUE;
17 _activationList = NULL;
18 _deactivationList = NULL;
19 }
The Cmd destructor just frees the name and deletes the two CmdList objects.
20 Cmd: :~Cmd()
aa A
22 XtFree ( _name );
23 delete _activationList;
24 delete _deactivationList;
25 }
27 |
28 _ci.add(ci);
29
30 ae (017
31 if ( _active )
32 ci->activate();
33 else
34 ci->deactivate();
a6}
The activate() member function enables a command object. This member function sets
the protected data member, _active to TRUE after saving the current value in the
_previouslyActive variable. If there are interface components associated with this
command, this function calls each CmdInterface object’s activate () member
function.
x MS i
41 _ci[i]->activate();
42
43 // Save the current value of active before setting the new state
44
45 _previouslyActive = _active;
46 _active = TRUE;
a? 3
The deactivate() member function disables a command. This function saves the
previous state of the objects and then sets the protected data member, _active to
FALSE. The
function also deactivates all associated interface components.
53 _ci[il->deactivate ();
54
55 // Save the current value of active before setting the new state
56
57 _previouslyActive = _active;
58 _active = FALSE;
The revert () member function is a private member function that returns a Cmd
object to its
previous active or inactive state, as stored in the _previouslyActive member.
e 74
64
65 if ( _previouslyActive )
66 activate ();
67 else
68 deactivate () ;
69 >)
The Cmd class supports two CmdList objects: a list of Cmd objects that should be
activated
when a command is executed; and a list of Cmd objects that should be deactivated
when a
command is executed. A Cmd object can automatically activate or deactivate all
objects on these
lists when it is executed. This feature can be useful when there are dependencies
between certain
commands. For example, in a text editor, it may be reasonable to assume that a
command to paste
some text is not valid until a cut or copy command has been issued. In a system
that Supports an
undo command, the undo command can be disabled until a command that can be undone
is
executed.
The function addToActivationList () takes a pointer to another Cmd object and adds
it
to the list of objects to be activated when the command is executed.
Ta 1
72 if ( !_activationList )
75 _activationList->add ( cmd );
Th: F
78 Y
79 if ( !_deactivationList )
82 _deactivationList->add ( cmd );
83 }
The public member function execute () provides the external interface used to
execute any
command. Unless a command object has been disabled, execute () calls the object’s
doit ()
member function, which must be defined by derived classes, to actually perform the
action the
command supports. Next, if this object supports undo, execute () activates the
theUndoCmd
object and saves the current command object in the _lastCmd variable. If a command
does not
support undo, execute () deactivates theUndoCmd and sets __lastCmd to NULL.
Finally, execute () calls activate () for each object on the _activationList and
the deactivate() member function for each object on the _deactivationList. The
execute () function is written as:
85 (
88 if ( !_active )
89 return;
93 doit ();
94
97
98 if ( _hasUndo )
99 {
101 theUndoCmd->activate () ;
102 }
103 else
104 {
106 theUndoCmd->deactivate () ;
107 }
108
109 // Process the commands that depend on this one
110
111 if ( _activationList )
£12 _activationList->activate () ;
113
114 if ( _deactivationList )
115 _deactivationList->deactivate () ;
116 }
The undo () member function provides the external interface for reversing the
effects of the
execute () member function. First, undo () calls undoit (), which must be
implemented by a
derived class. It then deactivates the theUndoCmd object, because the Cmd
architecture only
supports a single level of undo. Finally, the undo() member function invokes the
revert ()
member function for each dependent object to return these objects to their previous
state:
218 .¢
119 int i;
120
123 undoit () ;
124
125 // The system only supports one level of undo, and this is it,
126 // so deactivate the undo facility
127
129
133 if ( _activationList )
134 _activationList->revert();
135
136 if ( _deactivationList )
t37 _deactivationList->revert();
138 }
The following sections describe the CmdList class, which is used to maintain lists
of Cmd
objects, and the CmdInterface class, which provides a connection between a Cmd
object and a user
interface component. Later sections describe specialized derived classes that
provide additional
functionality. The Cmd class does the bulk of the work required to support command
objects, and
most derived classes are far simpler.
The Cmd class and related classes often need to deal with lists of Cmd objects. It
is useful to create
a simple class that makes it easier to deal with these lists of objects. The
CmdList class uses the
SimpleList class discussed in Chapter 6 but is more than just a list of Cmd
objects. The CmdList
class is derived from Cmd and supports most of the same operations as any Cmd. For
example, a
CmdList can be executed, which is the same as executing every item on the list. The
CmdList class
is used internally by the Cmd class and can also be useful to other classes that
use Cmd objects.
Figure 8.2 shows the CmdList class card.
1 EELLEVETLASI SILLA LAT ALLE ETL TAA LAA TATED TAL ATTA ET ATS
2 // CmdList.h: Maintain a list of Cmd objects
3 CATA AARRS VARLAATA ARMA AAA EL EEL TES LIAL ELL LCE ELT TI EET
4 #ifndef CMDLIST_H
5 #define CMDLIST_H
6 #include "Cmd.h"
El #include "SimpleList.h"
10
11 public:
12
25
26 protected:
27
30 SimpleList<Cmd*> _contents;
SL
Ja 33
33
34 #endif
The CmdList implementation overrides most of the virtual functions declared by Cmd
and
applies each operation to each of the entries on the internal list. The destructor,
doit () and
undoit () member functions are empty.
FELALARSATITL ERTL ELA CLL ECL TAS TS ATID AA AT STILL CLL ILS ERAS
// CmdList.C: Maintain a list of Cmd objects
ESE ESE CUE e eee EERO ee Ree eee Baa SELENA EI AAN AA TAS
#include "CmdList.h"
CmdList: :~CmdList ()
{
// Empty
wo oaoaryt aur WN EF
}
The Cmd Class 277
EL $
14 }
16 {
19 )
20
a |
25 }
26
28 {
31 )
32
33 void CmdList::revert ()
34 {
37 }
38
39 void CmdList::doit() { }
40 void CmdList::undoit() { }
Cmd objects can be added one by one to the CmdList, using the inline add() member
function. A CmdList object can be treated much like an array, just like the
SimpleList class.
However, most of the operations supported by a Cmd object can be applied to all
objects in a
CmdList by calling the appropriate member function. For example, the following code
segment
creates a list of Cmd objects and then executes each one in sequence:
execList->add ( two );
execList->execute();
Further uses of the CmdList class will be described later in this book.
The Cmd class described in the previous section is completely independent of any
user interface.
Command objects can be instantiated and executed programmatically and do not
necessarily require
a user interface component. However, it is common for a command object to represent
an action
initiated by the user through some user interface component, such as a button
widget or an entry in
a menu. This section describes a CmdInterface class that supports interaction
between Cmd objects
and widgets or other user interface components.
The CmdInterface class is an abstract class derived from UlComponent. The class
does not
create any widgets itself but just adds some features to those provided by
UlComponent to support
interactions with Cmd objects. For example, the CmdInterface class accepts
activate() and
deactivate () messages from Cmd objects. These member functions change the
sensitivity of
the component's base widget to match the state of the associated Cmd object. Each
CmdInterface
object also automatically registers itself with a Cmd object, which must be
specified in the
CmdInterface constructor. Finally, the CmdInterface class supports a callback
function that can be
used to execute the associated Cmd object. The CmdInterface class does not create a
widget, so it
cannot register the callback, but derived classes can register this callback
function with the appro-
priate widget.
Figure 8.1 shows a class card for the CmdInterface class, which summarizes its
responsibilities.
The CmdInterface class is declared in the file CmdInterface.h. The class supports a
protected
_active flag that maintains the current active status of the component. This
information is needed
because a CmdInterface object can be deactivated before any widgets are created. By
maintaining
this state, derived classes can activate or deactivate widgets, as needed, when
they are created. The
constructor is declared in the protected portion of the class to force the
CmdInterface class to be
treated as an abstract class.
The CmdInterface class also supports two virtual member functions, activate () and
deactivate(). These functions activate and deactivate the widget or widgets
supported by a
CmdInterface object. Only a Cmd object should activate or deactivate a CmdInterface
object.
Therefore, these member functions are declared in the protected portion of the
class. In fact, the
CmdInterface class has no public protocol. The entire CmdInterface protocol is
protected and
accessible only to derived classes and the Cmd class (because the Cmd class is
declared as a
friend). Applications that need to disable a command should deactivate the
appropriate Cmd object,
not the user interface component associated with the Cmd.
1 EEFLIILEP ALITA ELE AT ITAL EES EET IAI AT ARIAL ELTETT EIE
2 // CmdInterface.h
3 PDI ETLIL TILT ETAL A ALERT AT LAAT LEAT ALATA TATTLE AER IA TTA aT
4 #ifndef CMDINTERFACE
5 #define CMDINTERFACE
6 #include "UIComponent.h"
8 class Cmd;
IL
12 friend Cmd;
t3
14 protected:
15
16 Cmd *_cmd;
ck
20
21 CmdInterface ( Cmd * );
25 tendif
The CmdInterface constructor expects a Cmd object as its only argument. It passes
the name of
the Cmd object to the UIComponent constructor, to be used as the name of the
object’s widget. The
CmdInterface constructor initializes the active member to TRUE, saves a pointer to
the
specified Cmd object, and then registers itself with that Cmd object.
#include "Cmd.h"
O Onn UW fF WD P
_active = TRUE;
_cmd = cmd;
PRP PR
NF O
cmd->registerInterface ( this );
pa
Ww
15 XtPointer clientData,
16 XtPointer )
a.
19
20 obj ->_cmd->execute () ;
SL” Y
Notice that this callback does not follow the convention used in many other
examples in this
book because it performs a task directly, rather than calling a CmdInterface
virtual function. This
function is so simple that it does not seem necessary to call yet another function.
Furthermore,
derived classes have the option of registering or not registering the callback, so
there is little reason
has already been created, XtSetSensitive() is used to set the widget's sensitivity
accord-
ingly. Each function also updates the value of the _active flag. If these functions
are called
before the object’s widget has been created, the derived class constructor can use
this value to
initialize the widget appropriately.
23 4
24 O Ey
27 _active = TRUE;
28 1
The NoUndoCmd Class 281
30 4
31 if ( _w )
34 }
Some commands are difficult, or even impossible to undo, for one reason or another.
For example,
an action that deletes a file cannot be undone (unless the deletion does not really
take place until a
later time). The Cmd class forces all derived classes to support undo by declaring
the undoit ()
member function as a pure virtual. However, derived classes can implement an empty
undoit ()
member function that does not really provide undo, but satisfies the C++ compiler.
The Cmd class
depends on the value of the _hasUndo member to specify whether a class really has
the capability
to undo its actions or just defines an empty member function.
The NoUndoCmd class is a very simple abstract class that defines an empty undoit ()
member function and ensures that the _hasUndo flag is set correctly for all classes
that cannot
support undo. Therefore, the easiest way to implement a class that cannot support
undo is to derive
the class from NoUndoCmd.
The file NoUndoCmd.h contains the class declaration. The NoUndoCmd class supports
only
an empty undoit () member function and a constructor. The constructor expects a
name and a
flag that specifies the initial state of the object.
1 FAEZTICEP LIT ER LAL EEL EET TT RIA TELAT ARTA ETIRI ER AREA TARTA
2 // NoUndoCmd.h: Base class for all commands without undo
3 FEL EELAL CITES ELT ESET AT ASIA ET LT LATA ISTE LL ETT IIT ATI LEETE
4 #ifndef NOUNDOCMD_H
5 #define NOUNDOCMD_H
6 #include "Cmd.h"
10 public:
LZ
13 protected:
15 F3
16 #endif
The NoUndoCmd constructor calls the Cmd class constructor and then overrides the
value of
_hasUndo.
1 IAEFTAAAR ABRA AA RAMA AAA AAA ALAA EGTA TTT EP LEE ES CAE EP EL ET TI
2 // NoUndoCmd.C: Base class for all commands without undo
3 PIF PISI FIT IIIA TELAT EEL ITED LAAT EEL FELELET ETT PLAT EL ATL
4 #include "NoUndoCmd.h"
6 #define FALSE 0
12 )
The undoit () member function is an empty stub, defined because the Cmd class
requires
derived classes to implement this function. Classes derived from NoUndoCmd do not
need to
implement an undo () member function.
14 -{
15 // Empty
16 )
When any command is executed, the object activates and deactivates a central
command object
named theUndoCmd, depending on whether or not the executed object supports undo.
This object
is an instance of UndoCmd, a class whose only responsibility is to call the undo()
member
function for the most recently executed command object. Because theUndoCmd itself
cannot be
undone, the UndoCmd class is derived from the NoUndoCmd class.
The file UndoCmd.h contains the declaration of the UndoCmd class. This class
supports a
constructor and a single doit () member function. The header file declares a
pointer to an external
object named theUndoCmd. The MotifApp framework contains only a single instance of
UndoCmd, which is available to any part of the program that includes UndoCmd.h.
TUNA IAIADAAA AA AT ALA LAL ATL TAA FIA IAEA TTS ETAT SATA TALL
// UndoCmd.h: An interface for undoing the last command
PETES TITLED LE EEA LATL AL ATT ADL EATS RATATAT ALT ITT AAA EEL ALE
#ifndef UNDOCMD_H
#define UNDOCMD_H
#include "NoUndoCmd.h"
nm FP WN FP
9 public:
10
13 protected:
14
16. F}
17
The file UndoCmd.C creates an instance of UndoCmd that can be used throughout any
MotifApp program. The UndoCmd constructor is empty and just calls the base class
constructor.
6 #define NULL 0
7 #define FALSE 0
11
14 // Empty
15 )
The UndoCmd class's doit () member function is responsible for undoing the most
recent
command. If the _1astCmd member inherited from the Cmd class is non-NULL, doit ()
calls
the undo () member function for the most recently executed command object. Because
a
command can only be undone once, the function then sets _lastCmd to NULL.
16 void UndoCmd: :doit ()
17 í
18 if ( _lastCmd != NULL )
19 {
as
22 _lastCmd->undo () ;
25. 3
Some commands may have serious consequences that cannot be reversed (for example,
deleting a
file or exiting a program). Many programs ask the user for confirmation before
actually executing
such commands. This functionality is often difficult to provide, or at least
tedious to implement, and
is sometimes neglected.
The Cmd structure described in this chapter provides an easy way to capture a
mechanism for
checking with the user before executing a command. We can implement the basic
facility once and
for all in a base class. Cmd classes that represent commands that should be
confirmed before being
AskFirstCmd class posts a dialog to ask the user to confirm a command before
actually executing
it. Figure 8.4 shows an AskFirstCmd class card that outlines this class’s
additional responsibilities.
AskFirstCmd : Cmd Abstract
The AskFirstCmd class defines an execute() member function that overrides the
Cmd::execute() member function. Following the approach described in Section 7.1,
AskFirstCmd::execute () just displays a dialog instead of invoking the doit ()
member
function. AskFirstCmd uses the MotifApp framework's QuestionDialogManager class to
display
and manage the dialog. A single callback, to be called if the user confirms the
command, is
provided.
Programmers may want to change the message displayed in the dialog, depending on
the
nature of the specific command. AskFirstCmd supports different messages, or
questions, by
providing a setQuestion() member function, which sets a protected _question member.
Derived classes or applications that use an AskFirstCmd object can call the
setQuestion ()
LEE ERE
i marl paced th Boner
AIN
PEPA
PATRIOTA
EAT FEN
ss a Speake ican
A del
> SSS
5 #define ASKFIRSTCMD_H
6 #include "Cmd.h"
10 public:
Ll
17 private:
18
20
pe
25
26 char *_question;
O Y
28 #endif
The AskFirstCmd constructor calls the Cmd constructor and initializes _question to
a
default string. The setQuestion() function makes a copy of the string given as its
argument
and deletes the previous value. Therefore, the constructor sets _question to NULL
before
calling the setQuestion () function with the default question.
2 // AskFirstCmd.C
TAIANA ECIPOIVA ARANA AAA EAS ETI TS AE RANIA IAN EESTI DANI
4 #include "AskFirstCmd.h"
5 #include "QuestionDialogManager.h"
12 _question = NULL;
13 setQuestion ( DEFAULTQUESTION ) ;
E
D
ww
The setQuestion() member function deletes any previous string and creates a copy of
its
argument.
rl
17 XtFree ( _question );
18 _question = XtNewString ( str );
19 }
The AskFirstCmd class overrides the virtual execute () member function implemented
by
Cmd. Instead of calling the doit () member function directly, this function uses
the Motif App
framework’s theQuestionDialogManager object to post a dialog. The current value of
the
_question member is used as the string to be displayed in the dialog. The action
supported by
this command is delayed until the user answers the question in the dialog.
2d {
If the user confirms the command, the yesCallback () function is invoked. This
function
calls the Cmd: :execute() member function to execute the doit () member function
and
handle the other details involved in executing the command.
wu. A
If the user selects the cancel button on the dialog, the dialog is dismissed and
the command is
not executed.
One common use for classes like AskFirstCmd is to warn the user that there is no
undo available for
a particular command. This section describes a derived class that provides an
appropriate warning
message and initializes the_hasUndo member to FALSE. The WarnNoUndoCmd class is
similar
to the NoUndoCmd class but provides the benefit of warning the user before it is
too late.
The WarnNoUndoCmd class is simple to implement and contains only a constructor and
an
rFwowWoOA AHA UM FP WN FP
PPP rP PPB
YH OB WD PB
ESPIAMAACAAI IAN EL EE LATA ALATA AG ALLS OF PEEL ELE ALA AA CISA AAN LETT
// WarnNoUndoCmd.h: Warns user before executing a command
FETILELL AR EIEII TILANTEITA TIISTAI TAPE EE ELE ETA IS ES CRE FF
#ifndef WARNNOUNDOCMD_H
#define WARNNOUNDOCMD_H
#include "AskFirstCmd.h"
public:
The WarnNoUndoCmd constructor sets the_hasUndo member to FALSE to indicate that the
class has no undo capability. It also provides a more specific default question to
be displayed by the
QuestionDialogManager when the command is executed. This question can still be
changed by
derived classes or by the application that instantiates a WarnNoUndoCmd object. The
WarnNoUn-
doCmd class's undoit () member function is empty but must be implemented because
Cmd
declares it to be a pure virtual function.
PrP Pe
um a WN
16
17
18
19
LIDS FELT AAA RARA AAA VETA AA RINA EEN ASIII TT ALT ET ETI TAT A EL ELT TAT
// WarnNoUndoCmd.C: Warns user before executing a command
PELEITILID I PEALE CESA GEIL REL AAA AABT EPL CLI IATL SETI EET L LARA
tinclude "WarnNoUndoCmd.h"
// Empty
}
This completes a core set of abstract base classes that support command objects in
the
MotifApp framework. Figure 8.5 provides a view of the connections between many of
these
classes. The Cmd class interacts closely with the CmdList, CmdInterface, and
UndoCmd classes to
provide an infrastructure on which derived classes can be built. Figure 8.5 also
shows some of the
messages supported by the AskFirstCmd class. This class also inherits all the
messages and connec-
tions of the Cmd class and interacts with the QuestionDialogManager class.
operator | execute
registerInterface
execute
undo
a O
3
a
deactivate
Pe |
UndoCmd
setQuestion
Figure 8.5 A message diagram of the Cmd class and related classes.
Figure 8.6 shows the inheritance graph that includes the classes discussed in this
chapter. Most
of the classes are abstract, with the exception of the supporting CmdList class and
the UndoCmd
class.
For the most part, the command classes described in this chapter are independent of
the rest of
the MotifApp framework. None of the command classes have direct dependencies on the
Appli-
cation or MainWindow classes. In fact, the Cmd class is not even dependent on Motif
because the
user interface aspect of each command is abstracted in a CmdInterface class.
However, the
AskFirstCmd and WarnNoUndoCmd classes are indirectly dependent on the Application
class and
the structure of the MotifApp framework because these classes use the DialogManager
class to post
dialogs. The DialogManager class is dependent on the Application class. In any
case, the Cmd class
Summary 289
and derived classes are intended for use in the MotifApp framework and should be
added to the
MotifApp library.
8.7 Summary
Chapter 9 describes a menu system based on the Cmd classes described in this
chapter and
demonstrates how the Cmd classes might be used in a typical program.
Chapter 9
A Simple Menu System
Motif provides a flexible set of widgets and gadgets that can be used to construct
many types of
menus. Although Motif provides all the necessary pieces, putting these pieces
together can be
somewhat tedious. Each item in a Motif menu is a separate widget. The programmer
normally
creates each widget separately and registers callbacks to be invoked when the user
selects an item in
the menu. The construction of even a modest menu along with the various callbacks
needed for each
item can easily require hundreds of lines of code. Motif provides a convenience
function, XmCre-
ateSimpleMenu (), that makes some menus easier to build. However, this function
still does not
address many of the common issues programmers face when implementing menus and the
commands that appear in menus.
More importantly, the interactions between the various actions on a menu are often
very
complex. For example, some entries in a menu may need to enable or disable other
menu items
when they are executed. This requires maintaining relationships between the actions
involved and
the various widgets in a menu, so that the correct menu widgets can be made
sensitive and insen-
sitive. Many programs also provide the ability to “undo” the previous command.
Although Motif
and other widget sets provide the components to build menus, they do not directly
define an archi-
tecture that allows applications to do such things easily.
This chapter presents a menu package based on the Cmd class and derived classes
discussed in
Chapter 8. This menu system supports a menubar with multiple pulldown menu panes,
an
automatic undo facility, and an easy way to activate and deactivate groups of menu
items. Section
9.1 describes a ButtonInterface class, which the MenuBar class uses to provide an
interface
between a menu item and a Cmd object. Section 9.2 describes the MenuBar class and
Section 9.3
The MenuBar class discussed in the following section creates menu panes that
consist of Motif
XmPushButton widgets. Each item in a menu pane corresponds to a Cmd object. The
interface
between each widget in a menu and its associated Cmd object is provided by
instances of the Button-
Interface class. This class, which is derived from the CmdInterface class described
in the previous
chapter, creates a Motif XmPushButton widget and registers the
CmdInterface: :execute-
CmdCallback () function as an XmNactivateCallback function, to be called when the
user
1 PERIIITAI RIIIE RRA ERA AEARAAARAAAR RE RARAS ASARIEIA AER ERIS LLT IIR
2 // ButtonInterface.h: A pushbutton interface to a Cmd object
3 IIIVIVIVI OOO AAA AAT AAT TAA AAA AAA AAT ATTA TULL ELETA
4 #ifndef BUTTONINTERFACE
#define BUTTONINTERFACE
7 #include "CmdInterface.h"
10
11 public:
EZ
14 y;
15 #endif
The ButtonInterface class defines only a constructor. All other facilities are
inherited from
CmdInterface. The constructor expects a parent widget and a pointer to a Cmd
object. The
constructor creates an XmPushButton widget and installs a callback to handle
unexpected widget
destruction, using the convenience function defined by UlComponent. Once the widget
has been
created, the constructor sets the widget's sensitivity according to the current
value of the
_ active flag, which is initialized when the object is registered with a Cmd
object. Finally, the
constructor installs the executeCallback () function, which executes the associated
Cmd
object when the base widget is activated.
“ws Me TUO a ae. Peer ak Oe AAA re a RA eel Ae Dee ecm ee ard sh ry Nos wm PEN A oe
Oe Al ane ba ee Pe, LOU EP y T
Sn 00 a a O nana ana Oi OD O ID xa OO DO NN a ee te es eee es ASA i x
3 AMAIA ARA AA LAL ETA LA EILL LLI ALAA AT AAT AAT PRAT ALT ETS AAA ARIANE
4 #include "ButtonInterface.h"
5 #include <Xm/PushB.h>
10 _w = XtCreateWidget ( _name,
Hi | xmPushButtonWidgetClass,
12 parent, NULL, O );
13
14 installDestroyHandler () ;
15
20
21 if ( _active )
22 activate();
23 else
24 deactivate ();
25
26 XtAddCallback ( _w,
27 XmNactivateCallback,
29 (XtPointer) this );
30 3
The rest of the functionality of the ButtonInterface class is inherited from the
CmdInterface
class described in Chapter 8. The following section shows how the MenuBar class
uses the Button-
Interface class to construct a menu pane.
The MenuBar class encapsulates the process of constructing a Motif menubar with
multiple
pulldown menu panes. The MenuBar class creates a pulldown menu from a list of Cmd
objects,
creating one menu entry for each command in the list.
The MenuBar class supports only one public member function in addition to the
constructor.
The addCommands () member function takes a CmdList object and a string and creates
a menu
pane with an entry for each Cmd object in the list. The second argument specifies a
name for the
pulldown pane.
NNPPRPP CP FP RP PB
P-OvwOoJaAaUuRAsYN
22
then installs the destruction handler provided by the UIComponent class. The
menubar widget is
the base widget of this component.
© HU FWD P
Pre F wo
WN RO
14
LS
16
1y
18
ERAN AAAAA ERE LEE AT ALLE LTA LAL ENE EARAS PETAL ALLIES EES
#include "MenuBar.h"
#include "Cmd.h"
#include "CmdList.h"
#include "ButtonInterface.h"
#include <Xm/RowColumn.h>
#include <Xm/CascadeB.h>
The member function createPulldown() takes a CmdList object and creates a pulldown
menu pane containing a menu entry for each Cmd object on the list. Motif menu panes
consist of a
pulldown menu widget and an associated XmCascadeButton widget. The XmCascadeButton
is the
widget that appears in the menu, and the pulldown menu is a menu pane that cascades
from the
menu.
Once a new pane is created,createPulldown () loops through the CmdList and creates
a
ButtonInterface object for each Cmd object whose type is Action. The pulldown menu
pane serves
as the parent widget of each ButtonInterface component. If the type of a Cmd is
List, then the
object is really a list of commands, and createPulldown () calls itself recursively
to add the
list as a cascading submenu. Other types of Cmd objects could be handled similarly.
For example,
if createPulldown() encounters a Cmd whose type is Toggle, it might create an
interface
object of type ToggleInterface, which would provide a Motif XmToggleButton widget.
This class is
not implemented here.
se CER i
21 int i;
23
SI
39
41 {
46 {
47 CmdInterface *ci;
50 }
51 }
52. }
54 {
57
The MenuBar class has no direct dependencies on the rest of the MotifApp framework
and can
be used wherever a menubar is needed. However, because many applications need
menubars, it is
convenient to provide a top-level window as part of MotifApp that automatically
creates a
MenuBar object. The next section describes a MenuWindow class that automatically
provides a
menubar for applications that require one.
1 IAAPICAA IA DARIA RARA AAA ARA AT ESE AREA EIEI SITE TIL AE E ERE ETT
2 // MenuWindow.h: Add a menubar to the features of MainWindow
3 EXIVELIS ESA RIA ETIL RT EELE ITERE IIA ERA AAA FRI FIFE EEFE
4 #ifndef MENUWINDOW_H
5 #define MENUWINDOW_H
6 #include "MainWindow.h"
7 class MenuBar;
8
10
11 public:
12
14 virtual ~MenuWindow() ;
15 protected:
16
13 MenuBar *_menuBar;
18
24 tendif
The MenuWindow constructor initializes the _menuBar member to NULL and passes the
name argument to the MainWindow constructor.
4 tinclude "MenuWindow.h"
5 tinclude "MenuBar.h"
9 _menuBar = NULL;
10 }
L2 {
19
16 MainWindow: :initialize();
17
20
22
23 XtVaSetValues ( _main,
25 NULL) ;
26
29 createMenuPanes () ;
30 _menuBar->manage () ;
si. 3
The MenuWindow destructor frees the MenuBar object created by the initialize()
member function.
32 MenuWindow: :~MenuWindow( )
33 4
34 delete _menuBar;
35 )
This section presents a simple example that creates a menu, using some of the Cmd
classes from
Chapter 8 along with the classes described in this chapter. MotifApp applications
that need a
menubar do not have to create the menubar widget nor the widgets in the menu. When
using
MotifApp, an application can simply define a new class derived from MenuWindow and
provide a
list of Cmd objects to be included in each menu pane. Rather than worrying about
the details of
constructing a menu, programmers who use MotifApp concentrate on implementing the
commands
supported by the application. The framework handles the details of creating menus
to execute these
commands.
Before discussing an example program that uses a menubar, we need to implement some
command classes used by the example program. The program does very little except
demonstrate
the menu facilities. However, the QuitCmd, ManageCmd, and IconifyCmd classes,
described in the
following sections, can be used by other programs.
The QuitCmd class is derived from the WarnNoUndoCmd class described in Chapter 8.
It provides
a user-friendly way to exit an application, as described in the dialog discussion
in Chapter 7. This
class should be useful to many applications and can be added to the MotifApp
library. The QuitCmd
class supports a constructor and a doit () member function. The class is declared
in the file
QuitCmd.h as shown below.
5 define QUITCMD_H
6 #include "WarnNoUndoCmd.h"
fi
10 protected:
aBa
13
14 public:
15
19
20 #endif
The QuitCmd constructor calls the base class constructor to initialize the object
before setting
the default question to the string “Do you really want to exit?”
4 #include "QuitCmd.h"
5 #include <stdlib.h>
12 }
The doit () member function calls exit (). This member function is called only if
the user
replies affirmatively to a dialog popped up by the AskFirstCimd class. The dialog
and the associated
details are all handled by the base classes provided by the MotifApp framework. The
QuitCmd
class only needs to implement the action to be taken if the command is actually
performed.
¡4.4
15 // Just exit
16
17 exit ( 0 yi
JE 3
The ManageCmd class provides a way for the user to invoke the manage () member
function
supported by the Application class. Recall that this member function manages all
registered
MainWindow objects in an Application. The ManageCmd class and the IconifyCmd class
described
in the next section provide simple ways to open or close all windows in an
application with a single
command. This class should be useful to many applications and can be added to
MotifApp.
The ManageCmd class is derived from the NoUndoCmd class, described in Chapter 8,
and
supports a doit () member function. It would be convenient if this class supported
an undoit ()
member function as well. However, the doit () function relies on the Application
class to manage
all windows. The ManageCmd class cannot determine which windows were previously
managed.
Therefore, it cannot know which windows should be hidden and which should be left
alone when
undoing the manage command. Without extending the Application class, it is not
possible to undo
the action correctly.
1 PEELS ELITE TELA LAT AT TATA ASAI AAA EL TLL EDA EA CLP ALC EF AICTE ETF
2 // ManageCmd.h: Manage all windows in a MotifApp application
3 FABRA RARA MARAMAAI AA IA AAA AAA RAAIAANAAIAAAAAIAASAAAANAA AAA AAA AAA
4 #ifndef MANAGECMD_H
5 #define MANAGECMD_H
6 #include "NoUndoCmd.h"
>
8 class ManageCmd : public NoUndoCmd {
9
10 protected:
11 virtual void doit(); // Manage all windows
12
13 public:
14 ManageCmd ( const char *, int active = TRUE);
15 virtual const char *const className () { return ( "ManageCmd" ); }
16 );
17 #endif
The ManageCmd constructor simply calls the base class constructor to initialize the
object.
1 EATAAAAAAAAAA AL ELT ET ATMEL AAA IRA IAEA AAA ANTAS ETAL ET
2 // ManageCmd.C: Manage all windows in a MotifApp application
3 EPIRIIARAMARIA ASA RAAAA AL TIL ERT LETT EIA ARMA RRA EA LET ALES A ES
4 #include "ManageCmd.h"
5 #include "Application.h"
6
7 ManageCmd: :ManageCmd ( const char *name, int active )
8 NoUndoCmd ( name, active )
Mid
10 // Empty
Tes
The doit () member function calls the manage () member function for the program’s
global Application object.
Re Berek
The IconifyCmd class is derived from the NoUndoCmd class described in Chapter 8 and
supports a doit () member function. Like the ManageCmd class, the IconifyCmd class
has no
way to determine which windows were already iconified, which makes it difficult to
support undo.
ad FAL CLAESES TESTE ELTA AAA NAAA EA RARA ANACRE ELETI EIAI LTI IEIET
2 // IconifyCmd.h: Iconify all windows in a MotifApp application
3 RECT ERATE RELELEA OEREL AAA ATA IMA TA EA AA AAA ARA EIA AAC RAILS]
4 #ifndef ICONIFYCMD_H
5 #define ICONIFYCMD_H
y #include "NoUndoCmd.h"
10
11 protected:
12
14
15 public:
16
20
21 tendif
The IconifyCmd constructor calls its base class constructor to initialize the
object.
1 ESEPIAIRA SAI AAA ARANA AAA AAAIA ELVITTE AAA AA AER LET EA AST t
4 #include "IconifyCmd.h"
5 tinclude "Application.h"
9 // Empty
10 )
The doit () member function calls the iconify() member function for the program's
global Application object.
t2 |
13 theApplication->iconify(); // Close all top-level windows
14 )
The NoOpCmd class is a simple command class that does nothing except report that
its doit () or
undoit () member functions have been called. The example program in the next
section uses this
class to demonstrate the undo facility and to show how dependencies can be
specified between
objects. The NoOpCmd class is derived from the Cmd class described in Chapter 8 and
supports both
doit() and undoit() member functions. The NoOpCmd class is declared in the file
NoOpCmd.h as:
FERII EPIL EAEAT IT ALTA UTERE OLA AL ILA LT ELA ATA EL PEAT TRS I a?
// NoOpCmd.h: Example, dummy command class
LELECFEL ILA ELT ELLA L EPL ALIA LE LETT ELIS ALATA LATTA A aE AS
#ifndef NOOPCMD_H
tdefine NOOPCMD_H
tinclude "Cmd.h"
Pwo WON HD UW FP WD PP
0 protected:
15 public:
20 #endif
The NoOpCmd constructor calls the constructor of its base class to initialize the
object.
3 VELEEELALHVATTAAT AEDT AAA ATAT LAT AT ELT LLL TA TTT AAA TLE |
4 #include "NoOpCmd.h"
5 #include "Application.h"
6 #include <iostream.h>
8 NoOpCmd: :NoOpCmd ( const char *name, int active ) : Cmd ( name, active )
9 {
10 // Empty
BIS
The doit () member function reports the name of the object and the message “doit.”
EF, E3
The undoit () member function is similar but prints the message “undoit.”
19 4
23 }
The menuDemo program uses the command classes described in the previous sections to
demon-
strate the MotifApp menu system, which is based on the command mechanism described
in Chapter
8. This example displays three identical top-level windows. Each window supports a
menubar with
three menu panes. The first menu pane on each window contains commands that apply
to the appli-
cation as a whole. In this example, these commands are labeled “Quit,” “Open,”
“Iconify,” and
“Undo.”
The second menu pane in each window contains three commands that also apply to the
entire
application. These commands are stubs, implemented by the NoOpCmd class described
in the
previous section. These commands are known merely as “X,” “Y,” and “Z.” As
instances of
NoOpCmd, these command objects simply print a message when they are executed and
undone.
Although these commands appear in the second menu pane of all three top-level
windows, there is
only one Cmd object for each command. It makes no difference what menu the user
uses to execute
A MenuBar Example 303
the command; the same NoOpCmd object is always executed. This demonstrates the Cmd
class’s
ability to support multiple CmdInterface objects for a single Cmd object.
Finally, each window’s menubar has a third menu pane, which contains the commands
“A,”
“B,” and “C.” These commands are also implemented as instances of the NoOpCmd
class.
However, each top-level window has its own instance of each of these commands.
L ZEILE ATT ETAL AT ELL LAT TEED ISAT AT PESTA LAAT AT ASTI AIT AAT ALATA IL
2 // MenuDemoApp.h: An application class for the menu demo program
SD SAA ELLA ERTL TEEPE LLIN LAAT AT CELE EP LIST PLAT TAT TALIA LAA ELIA IES
4 #ifndef MENUDEMOAPP_H
5 #define MENUDEMOAPP_H
6 #include "Application.h"
7 class Cmd;
10
11 public:
13 virtual ~MenuDemoApp () ;
14
16
17 Cmd *quitCmd () { return [ aguit jz >
23
26 protected:
29 Cmd *_quit;
30 Cmd *_manage;
31 Cmd *_iconify;
32 Cra. * dt;
a3 Cmd *_y;
34 Cmd *_z;
38 3
39
40 extern MenuDemoApp *theMenuDemoApp ;
41 #endif
5 #include "QuitCmd.h"
6 #include "ManageCmd.h"
F #include "IconifyCmd.h"
8 #include "NoOpCmd.h"
9 #include "MenuDemoWindow.h"
10
The MenuDemoApp constructor creates the Cmd objects for the first two menu panes
that
appear in each window. These Cmd objects cannot be added to a menu at this point;
the menu must
be constructed in the MenuDemoWindow class. However, because there is to be only
one instance
of each of these classes in the entire application, the “Quit,” “Open,” and
“Iconify” objects are
instantiated in the MenuDemoApp constructor.
22
29
33
34 _x->addToActivationList ( _y );
35 _x->addToActivationList ( _z );
36 _x->addToDeactivationList ( _x );
a7
38 _y->addToActivationList ( _x );
39 _y->addToActivationList ( _z );
40 _y->addToDeactivationList ( _y );
41
42 _z->addToActivationList ( _x );
43 _z->addToActivationList ( -y );
44 _z->addToDeactivationList ( _z );
45 }
The MenuDemoApp constructor also creates three NoOpCmd objects to be used in the
menu
panes of all three windows. These commands report when theirdoit () and undoit ()
member
functions are called. Although the menus are constructed in the MenuDemoApp class,
we can
specify relationships between the Cmd objects when they are created. This is an
important feature
of MotifApp’s command architecture. Applications do not need to worry about
activating or deacti-
vating user interface components. The application only determines what commands
need to be
active or inactive. The Cmd objects handle the interface components, regardless of
how many inter-
faces are associated with any given command.
In this example, the “X” and “Y” commands are initially active, and “Z” is
inactive. The
MenuDemoApp constructor specifies relationships between these objects such that
activating any
command disables that command and enables the other two. This arrangement
demonstrates a
fairly complex “two out of three” toggle behavior. The complexity of programming
such behavior
without support from the Cmd architecture is more apparent when we consider that
each can be
undone at any time and that this example coordinates three simultaneous interfaces
to these
commands. This behavior is, of course, totally arbitrary, because the commands do
nothing. The
behavior of these menu items just demonstrates how dependencies between Cmd objects
might be
useful.
The MenuDemoApp destructor destroys the six command objects created in the
constructor.
46 MenuDemoApp: :~MenuDemoApp ( )
47 {
48 delete _quit;
49 delete _manage;
50 delete _iconify;
51 delete _x;
52 delete _y;
53 delete _z;
54 }
AA
A e iy See Meals
A A A A a ee A A te ON A NR A RN A re ALD Oy a
i Ra rea aly aig CRANE EAE DEN edge pee Fae at) ope ee eT A E ERA bas SR ESOS A N
es ED ee aes ain DiS
The top-level window in this example program is implemented as a class derived from
the
MenuWindow class. The MenuDemoWindow class adds three panes to the menubar provided
by
the Menu Window class. The first two panes represent Cmd objects created by the
MenuDemoApp
class. The third menu pane is created by registering three Cmd objects created in
the
MenuDemoWindow constructor. Unlike the Cmd objects created in the MenuDemoApp
class,
these three objects are unique within each instance of the MenuDemoWindow class.
The file MenuDemoWindow.h declares the MenuDemoWindow class. The class contains
pointers to three Cmd objects in the protected portion of the class. The class
maintains a widget that
might be used as a drawing area and implements the two pure virtual member
functions required by
its base classes.
5 #define MENUDEMOWINDOW_H
6 #include "MenuWindow.h"
8 class Cmd;
EL
12 public:
13
ES
16 protected:
17
18 // Support three window-specific command objects
19
20 Cmd *_a;
21 Cmd *_b;
22 O Fc:
a3
24 Widget _canvas;
oF: 34
28
29 #endif
The MenuDemoWindow constructor instantiates three NoOpCmd objects named “A,” “B,”
and “C,” in much the same way as the “X,” “Y,” and “Z” commands are created in the
MenuDe-
moApp constructor. Although these objects behave in the same way as their
counterparts in the
MenuDemoApp class, these objects are not shared among all windows in the program.
Each
MenuDemoWindow object creates three unique NoOpCmd objects.
ANA
i AMM IAN AAA ALA RAR PAIN VANA SA AASASA ADA AAT ALATA AAT A
2 // MenuDemoWindow.C: Demonstrate Cmd and MenuBar classes
3 PSRAARARI AAA AIBR IIA AAA AAA TIA ERE EEL I LAS ITI ALITA RADAR
4 #include "MenuDemoWindow.h"
5 #include "MenuBar.h"
6 #include "MenuDemoApp.h"
7 #include "NoOpCmd.h"
8 #include "UndoCmd.h"
9 #include "CmdList.h"
10 #include <Xm/DrawingA.h>
EI
20
24
25 _a->addToActivationList ( _b );
26 _a->addToActivationList ( _c );
27 _a->addToDeactivationList ( a );
28
29 _b->addToActivationList ( _a );
30 _b->addToActivationList ( _c );
31 _b->addToDeactivationList ( _b );
32
33 _c->addToActivationList ( a );
34 _c->addToActivationList ( _b );
35 _c->addToDeactivationList ( _c );
36 1]
41 return ( _canvas );
42 }
The createMenuPanes () member function is called by the MenuWindow class after the
windows MenuBar object has been created. This function is expected to add menu
panes to the
window's MenuBar object by constructing a list of Cmd objects from which to create
each menu
pane. The MenuDemoWindow class creates three menu panes. The first pane in each
menubar
consists of an “Undo” command, provided by MotifApp's theUndoCmd object, and also
the
“Quit,” “Open,” and “Iconify” objects instantiated by the MenuDemoApp constructor.
The
function createMenuPanes () adds each of these objects to a CmdList object and adds
a menu
“X,” “Y” and “Z” objects provided by the MenuDemoApp object. Finally, the third
menu pane
contains the “A,” “B,” and “C” command objects created by each instance of
MenuDemoWindow.
454 .4
45 CmdList *cmdList;
46
49
51 cmdList->add ( theUndoCmd );
52 cmdList->add ( theMenuDemoApp->manageCmd () );
53 cmdList->add ( theMenuDemoApp->iconifyCmd() );
54 cmdList->add ( theMenuDemoApp->quitCmd() );
55 _menuBar->addCommands ( cmdList );
56 delete cmdList;
57
62 cmdList->add ( theMenuDemoApp->xCmd() );
63 cmdList->add ( theMenuDemoApp->yCmd() );
64 cmdList->add ( theMenuDemoApp->zCmd() );
65 _menuBar->addCommands ( cmdList );
66 delete cmdList;
67
72 cmdList->add ( _a );
73 cmdList->add ( _b );
74 cmdList->add ( _c );
75 _menuBar->addCommands ( cmdList );
76 delete cmdList;
E E:
At this point, the MotifApp library is expected to contain all the reusable classes
discussed so far,
including the MenuWindow, MenuBar, QuitCmd, ManageCmd, and IconifyCmd classes
described
in this chapter.
Window
Windowe
Windows
It is necessary to try the various menus to understand exactly how they work. The
“Appli-
cation” menu pane provides a way to test the manage () and iconify() member
functions
defined by the Application class and a way to experiment with a user-friendly quit
mechanism,
similar to that described in Chapter 7. The “X YZ” menu pane in each window’s
menubar represents
the same commands. Selecting an item from this menu pane in any one of the windows
enables the
other commands, which changes the sensitivities of the corresponding items in all
three menus.
Finally, each ABC menu pane works independently. Selecting one of the active items
from an ABC
menu pane changes the sensitivities of the other items in that pane but does not
affect items in other
menus. Notice that the Undo entry in any “Application” menu pane can be used to
reverse the
effects of any command, regardless of the window from which it was executed.
9.5 Summary
et LLL LL
This chapter describes a simple menu package based on the command classes discussed
in Chapter
8. Although this system has some limitations, it offers an easy way to construct
complex menus with
a surprising number of features. All commands throughout the system can be undone
through a
single interface, which can easily be added to any menu. The command classes work
with the
DialogManager classes described in Chapter 7 to provide a simple way to handle
commands that
need to be confirmed before executing. These classes allow applications to
implement complex
commands, like the “safe quit” techniques described in Chapter 7, with a minimum
amount of effort.
Chapter 10
Lengthy Tasks
However, most significant applications must do something besides process events and
often
must perform operations that take a relatively long time to complete. For an
interactive application,
a “relatively long time” is any length of time that reduces the responsiveness of
the application.
Unresponsiveness is only one of many problems that can occur when a Motif
application is too
busy to handle events for even a brief period. If the contents of a program’s
windows are lost while
the application is too busy to handle events, the program will be unable to restore
the windows’
contents until it is no longer busy. In this case, the user is left with a blank
window; even Motif”s
three-dimensional shadows will not be redrawn. If the user resizes a top-level
window while the
program is busy, the window manager changes the top-level window’s size, but the
window’s
cont:nts do not resize as the user may expect.
More serious problems can occur if the user doesn’t understand what a program is
doing and
begins to click mouse buttons in random parts of the application’s window or type
at the keyboard,
hoping to get the program to respond. If a busy program allows the X server to
queue these events,
the events will be reported to the application when it eventually returns to the
event loop. The
results may be unpredictable and may even be disastrous.
There is also no general way for the user to interrupt an X application that is too
busy to check
the event queue. UNIX programmers know that they can type a Control-C in a terminal
to interrupt
most processes. Using Control-C as an interrupt character is a feature of the UNIX
shell, which has
ERAT A
ESTER
A AE
SN
ATA
A o
E RI A
a
343
JN
AAA
‘4a
RS
4
5
=
na
EE
E
Ky
bat
cay
see
ny
Al
SE
LN
bef
oe
ES
4
2
PENA) anaes
EE
DELAS
Adee
tS
=
URT oe er SABE
AS AS
SSF
SERE
little in common with X and Motif input handling. To the X server, a Control-C is
just another set of
keystrokes, which the server reports to the application as a sequence of KeyPress
and KeyRe-
lease events. If the application is too busy to check the event loop, it will not
detect such a key
sequence until the program is no longer busy.
This chapter discusses some ways to handle interactive applications that must
perform time-
consuming operations. Section 10.1 discusses some guidelines for applications that
must perform
lengthy noninteractive tasks, and presents several techniques for handling such
situations. Sections
10.2 and 10.3 present several C++ classes that implement some of the solutions
discussed in
Section 10.1. Section 10.4 discusses an example program that uses the classes
introduced in this
chapter to handle a potentially lengthy operation.
The following paragraphs list a few guidelines for applications that expect to be
busy for an
extended period of time and present some techniques for implementing these
guidelines. These
guidelines are based on the user’s view of the application and the need to provide
a responsive
interface.
Always let the user know when the application is busy. The easiest way to show that
an appli-
cation is busy is to change the program’s mouse cursor to a special “busy cursor,”
usually an hour-
glass or a watch. The Xlib function XDefineCursor () can be used to specify a
cursor for an
application’s top-level shell. Multiwindow applications should set the cursor in
all top-level
windows. A busy cursor is often adequate if a task is expected to be completed
quickly (within a few
seconds). For more time-consuming operations, it may also be useful to post a
dialog to provide
information about the task’s progress.
Disable user input while the application is busy. There are several ways to prevent
user input from
being queued while an application is busy. An easy way to disable device events is
to post a dialog
whose XmNdialogStyle resource is set to XmDIALOG_FULL_APPLICATION_ MODAL. This
type of dialog disables input to all parts of an application except the dialog
window itself but allows
the application to continue processing Expose and configuration events. Of course,
if the appli-
cation is too busy to check the event queue periodically, these events will not be
processed until the
application returns to the event queue. However, disabling user input prevents the
X server from
queuing device events (user input). If an application disables input, it will not
be faced with a series
of random user input when it completes the lengthy task and returns to the event
loop, even if the
user tries to type or use the mouse while the application is busy.
Another technique for preventing user input involves creating an input-only window
as a child
of an application’s top-level shell. An input-only window is a region of the screen
that accepts input
but is not visible to the user. This window can be mapped and raised to cover all
descendents of the
shell and prevent user input to those windows while the application is busy. Each
input-only
window can prevent user input in the children of one shell widget. Applications
with multiple top-
level windows or dialog windows must create and map an input-only window for each
visible
window or dialog. (See [Scheifler90] for information about input-only windows.)
Ordinarily, handling an input-only window for each top-level window and dialog in a
multi-
window application could be a complex task. However, an application framework like
MotifApp
could support this technique and allow applications to call a busy () method to
cover all visible
windows in the application.
Keep the user informed of progress. Although displaying a busy cursor or posting a
busy dialog
provides a way to show the user that the application is busy, it is often more
effective to let the user
know exactly what is happening at any time. Even when an application displays a
busy cursor, the
user is likely to be concerned if the program remains busy for more than a few
seconds. Informing
the user when various milestones have passed can be very effective. If the
operation has a known
duration, the program can often use a dialog to report progress in terms of
percentages, 10% done,
20% done, and so on. If this information cannot be computed, it can still be
effective to tell the user
what is happening at various stages. For example, an application might display
messages like
“Reading data,” “Sorting,” and so on.
Reporting the current state of a program takes time and can actually increase the
total time
required to perform a given task. However, the user often believes that a task that
provides
continuous feedback is completed more quickly than an equivalent task that provides
no feedback.
In many interactive applications, the user’s perception is more important than the
actual elapsed
time.
| The XmLabel widget class, as well as many other widget classes, relies on Expose
events to update text displayed in the
widget. This is acommon source of problems in busy applications. Often, an
application posts a dialog widget as recom-
mended here and periodically changes the text to report progress. However, when
using XtSetValues () tochangethe
text displayed in a widget, the XmLabel widget simply stores the new text and
triggers an Expose event. When the ap-
plication processes the event, the widget is redrawn, using the new text. If the
application is too busy toreceivean Expose
event, the dialog is not redrawn, and the text doesn’t change.
eID
Et
BF a A ACA E
NE SNE A
A
iy
a i
es
ARA
ES
eh
aan
eek
Seat a
So aE
Pa N ee
PEAS eu ees
=
ARE
AAA A
EU ES
AA
ERA
E la
ae
Vote e ON FPO
aie ae
AIN er,
ps”
¢ Implement the task as a subprocess. Some very lengthy operations are best
performed in a
separate process. Applications can initiate the task as a subprocess, disable user
input using
one of the techniques mentioned above, and then return to the event loop. The
application can
communicate with the subprocess using pipes or sockets. For example, if the
subprocess
produces data that the parent process needs, the functionXtAppAddTnput () can be
used
to watch for data written by the subprocess to a pipe. Because the application
returns to the
event queue between reading data from the subprocess, the contents of the
application’s
windows can be maintained at all times.
e Applications can also use work procedures to simulate background tasks. A work
procedure
is a callback function registered with Xt to be called whenever the event queue is
empty. A
work procedure must do a small amount of work and return quickly to allow the
application
to continue to check the event queue. While not always possible, tasks can
sometimes be
broken into smaller chunks that can be performed in succession, For example, a ray-
tracing
function could be designed to trace only a few rays in each call. A function that
performs an
animation might render one frame.
¢ Some applications can use a timeout callback function to perform a lengthy task.
A timeout
callback is a function called by Xt when a specified amount of time has elapsed. Xt
removes
timeout callbacks once they are called. However, the timeout function can reinstall
itself to
be called again, setting up a cycle of function calls. Timeout callbacks can be
used to
implement steady, periodic tasks while continuing to handle events. Typical
applications for
this approach include clocks, timers such as the Stopwatch example in Chapter 2, or
any
program that needs to perform periodic checks or measurements.
Allow the user to interrupt the application. Lengthy tasks can be very frustrating
for the user if
there is no way to interrupt the program. Unfortunately, there is no general way to
interrupt an X
application that is too busy to return to the event queue. It would be convenient
if X could support
some type of “interrupt event” that can be sent to an application when the user
types an interrupt
character, like Control-C. Unfortunately, X applications cannot be forced to
receive any event. The
X server cari place an event on the application’s event queue, but this does little
good if the appli-
cation is too busy to check the event queue. Interrupts in a UNIX environment are
based on signals.
The X server cannot send signals to an X application, because X applications may be
running
anywhere on a network and may not be running on the same machine as the server.
input to see if the user has tried to interrupt the program. Figure 10.1
demonstrates the flow of
control in an application that combines a busy cursor, a modal “working” dialog,
and a subprocess
to handle a lengthy, interruptible operation. In this scenario, the application
initiates a lengthy task
in the startTaskCallback() function. This function displays a busy cursor and posts
a
dialog. Because the dialog uses the full application modal style, the dialog
continues to accept user
input but locks out input to the rest of the application. Next, a callback,
interrupt-
Callback (), is installed. This function allows the user to interrupt the task by
clicking on a
button on the dialog. Finally, the real task is started by calling popen () to fork
a subprocess. The
function concludes by registering an input handler to be called by the Xt main
event loop when
input is pending from the subprocess. At this point, the callback returns control
to the event loop.
startTaskCallback()
1. Display busy cursor
handleInputCallback()
1. If task is done,
return cursor to normal and
unmanage dialog
interruptCallback()
1. Kill subprocess,
return cursor to normal and
unmanage dialog
2. Return
While the subprocess is running, the main application continues to monitor the
event queue.
The program ignores user input to all windows except the modal dialog, but handles
Expose
events and configuration events as they occur. As the subprocess performs its task,
it occasionally
communicates with the parent process by writing to the pipe created by popen ().
Depending on
the specific situation, the subprocess might provide progress reports or supply
data to be read by the
main application. When input is available from the subprocess, Xt calls the
handleInput-
Callback() function. In addition to handling the input from the subprocess, this
callback
function checks to see if the subprocess has completed its task. If the task has
been completed,
handleInputCallback() removes the dialog and restores the application’s normal
cursor.
Otherwise, it updates the status on the dialog.
With this approach, lengthy operations can be interrupted because the application
continues to
check the event queue for user input between handling Expose events and input from
the
subprocess. The application ignores all device events except those that occur
within the dialog.
However, if the user clicks on the dialog’s interrupt button, the
interruptCallback()
function is called. This callback terminates the subprocess, removes the busy
dialog, and restores
the application’s normal cursor.
316 Chapter 10 Lengthy Tasks
This technique is not appropriate for every busy situation. For example, if an
operation
normally requires only a few seconds, spawning a separate process is a rather
heavyweight
approach. However, a subprocess may be a reasonable way to implement a task that
takes several
minutes. One challenge for the programmer, of course, is knowing in advance whether
a particular
task will take seconds, minutes, or hours. Using subprocesses is also most suitable
for tasks that can
be cleanly separated from the rest of the application. Obviously, some operations
are difficult to
implement as separate processes. For example, a task that requires continuous
access to data
maintained by the program could be difficult to implement as a separate process.
Figure 10.2 shows the sequence of events that might occur when a work procedure is
used to
perform a lengthy background task.
startTaskCallback()
1. Display busy cursor
doTaskCallback()
1. If task is done,
return cursor to normal,
unmanage dialog, return
TRUE
3, Register interruptCallback
for one of the dialog buttons
4. Register doTaskCallback()
as a work procedure
. Return
interruptCallback()
1.Remove work procedure,
return cursor to normal,
and unmanage dialog
2. Return
Like the previous example, this approach allows the user to interrupt the task at
any time and
allows the application to handle Expose and configuration events while the task is
in progress. In
this scenario, an application initiates a task in the startTaskCallback() function.
This
function displays a busy cursor and posts a dialog that supports an interrupt
callback function.
Instead of starting a subprocess, startTaskCallback() registers the function
doTaskCallback() with the Xt Intrinsics as a work procedure. The callback then
returns
control to the event loop (vector B).
The main application continues to monitor the event queue and handles Expose and
configu-
ration events as they occur. The program disregards all user input except events
that occur within
the dialog. Whenever the event queue is empty, Xt calls doTaskCallback() (vector
C). This
function first checks whether the task has finished normally. If the task has
finished, the callback
restores the application’s normal cursor, removes the busy dialog, and returns
TRUE. Xt automati-
cally removes the work procedure. Otherwise, the function does a portion of the
overall task and
returns FALSE to indicate that the work procedure is not finished and that the
callback should be
called again. If the user interrupts the task, the interruptCallback() function
removes the
work procedure, restores the application’s normal cursor, and dismisses the dialog.
This technique can be useful for some situations, but some tasks are difficult to
implement as a
series of repeated calls to a function. Performance may also be an issue, because
the overhead of
checking the event loop and the repeated calls to the work procedure may represent
a significant
portion of the total time required to perform the task. However, as mentioned
earlier, user
perception is often more important than actual elapsed time in interactive
applications. Time seems
to pass more quickly when an application informs the user of its progress regularly
and when the
user knows he or she can interrupt the task at any time.
Applications can also control the amount of overhead added by this technique by
changing the
amount of work performed by each call to the work procedure. For example, if the
work procedure
uses as much as a quarter to a third of a second (a long time in CPU terms) each
time it is called, the
user is unlikely to notice any lack of response to interrupts. In many situations,
each call to the
work procedure can use several seconds without any ill effects.
The following sections present several classes that support lengthy tasks within
the MotifApp
framework, based on the work procedure approach described here. The
WorkingDialogManager
class can be used to display a dialog that informs the user that the application is
busy. This dialog
class allows applications to change the message to notify the user as a task
progresses. The
WorkingDialogManager class also displays a simple animation to reassure the user
that the appli-
cation is busy working. The InterruptibleCmd class is designed to be used with the
WorkingDialogManager. The abstract InterruptibleCmd class implements a general
framework that
supports the work procedure technique just described. Derived classes must
implement a single
function that can be called repeatedly to perform the task. Together, the
WorkingDialogManager
and InterruptibleCmd classes provide a way to perform lengthy operations that can
be interrupted at
any time, while providing continuous feedback to the user and keeping the
application’s windows
up-to-date.
— em
PEA A 21
As mentioned in the previous section, it is important to lock out user input while
the appli-
cation is busy. It is particularly important to prevent user input when using the
approach
implemented in this chapter, because the busy application continues to handle
events. Allowing the
user to issue additional commands while the application is busy is technically
possible but would
add greatly to the complexity of the application. As recommended earlier, the
WorkingDialog-
Manager widget supports a full application modal dialog style that locks out user
input while it is
posted.
TITEL TASS TAREA TT ALATA AAA AAT PEL ESA TAA AGEL AAT ET
// WorkingDialogManager.h
EAU LTT TATTLE LLL ELIT TAIT AA LTT IIMA LTS AEB
tifndef WORKINGDIALOGMANAGER_H
#define WORKINGDIALOGMANAGER_H
#include "DialogManager.h"
Ou Pp Wn F
Bi
e
Gs
rE
E:
asi
t
T
E
La
12 public:
13
15
18 DialogCallback ok = NULL,
Al
26 protected:
ee
37 F
38
41 #endif
wo 0 30 UN FWD FP
20
21
22
23
24
23
26
27
28
29
30
34
32
33
34
35
36
LECLLERT EG II SILESIA ELA RARA ERT EAT AAT AT ELT AAT TAIT ATS
"WorkingDialogManager.h"
"Application.h"
tinclude
tinclude
#include
#include
#include
#include
<Xm/Xm.h>
<Xm/MessageB.h>
"BusyPixmap.h"
<assert.h>
WorkingDialogManager *theWorkingDialogManager =
_intervalld
_busyPixmaps
DialogManager ( name )
NULL;
NULL;
The DialogManager base class calls createDialog(), which must be implemented by a
derived class, when a new dialog widget is needed. The WorkingDialogManager's
create-
Dialog () member function creates a dialog and registers the function
unpostCallback () to
be called when the user clicks on either the dialog’s OK button or Cancel button.
The create-
Dialog() member function also instantiates a BusyPixmap object, passing the newly
created
dialog widget as an argument.
Widget dialog =
XtVaSetValues (
XtAddCallback (
XtAddCallback (
dialog,
XmNdialogStyle, XmDIALOG_FULL_APPLICATION_MODAL,
NULL );
dialog,
XmNokCallback,
&WorkingDialogManager : :unpostCallback,
( XtPointer ) this );
dialog,
XmNcancelCallback,
&WorkingDialogManager: :unpostCallback,
( XtPointer ) this );
37
38 if ( ! busyPixmaps )
39 _busyPixmaps = new BusyPixmap ( dialog );
40
41 return ( dialog );
42 }
44 void *clientData,
45 DialogCallback ok,
46 DialogCallback cancel,
47 DialogCallback help )
48 {
49
53
54 if ( _w && XtIsManaged (
55 return ( _w );
56
58
63 timer () ;
64
65 return ( _w );
66 }
w) )
The timer() member function registers a timeout callback with the Xt Intrinsics.
The
timerCallback () member function is a timeout callback function that uses the same
technique
as other callbacks in this book. It retrieves an instance of the
WorkingDialogManager class from
the client data specified when the callback was registered. Then it calls that
object’s timer ( )
member function to update the image in the dialog and reinstall the callback.
68 XtIntervalld * )
69 (
72 obj->timer () ;
cie E
The timer () member function installs the static member function timerCallback ()
to
be called again in 250 milliseconds. Therefore, Xt calls timer () approximately
every 250 milli-
seconds, once the initial callback is installed. In addition to reinstalling the
timerCallback ()
function, timer () installs the next available pixmap from the BusyPixmap object as
the value of
the displayed dialog’s XmNsymbolPixmap resource.
74 void WorkingDialogManager::timer ()
718. 4
76 if (124)
FA return;
78
80
81 _intervalld =
82 XtAppAddTimeOut ( XtWidgetToApplicationContext ( w ),
83 250,
84 &WorkingDialogManager: :timerCallback,
85 ( XtPointer ) this );
86
89
90 XtVaSetValues ( _w,
91 XmNsymbolPixmap, _busyPixmaps->next () ,
92 NULL ) ;
93 }
The unpostCallback () function is called when the user clicks on the dialog
widget’s OK
or Cancel button. This callback simply calls the unpost () member function.
95 XtPointer clientData,
96 XtPointer )
97 {
100 obj->unpost () ;
Lua. 4
The WorkingDialogManager Class 323
The unpost () member function dismisses the dialog. This function may be called
from the
unpostCallback() function when the user closes the dialog or may be called
programmati-
cally when an application is no longer busy. The function XtUnmanageChild() removes
the
dialog from the screen, in case unpost () is called programmatically. (The dialog
is unmanaged
automatically when the user clicks on one of the dialog’s buttons.) If a current
timeout callback is
installed, it is removed by calling the Xt function XtRemoveTimeOut (). This breaks
the chain
of repeated calls to timer () and stops the animation displayed in the dialog. The
unpost ()
member function uses an assert () statement to detect programmatic calls to unpost
() that
occur before the dialog has been posted for the first time.
103 4
104 assert ( _w );
105
108 XtUnmanageChild ( w );
109
111
112 if ( _intervalld )
117 if (
118 {
119 // Just change the string displayed in the dialog
120
122
126 )
t27 }
The next sections describe the PixmapCycler and BusyPixmap classes used by the
WorkingDialog-
Manager class to display a sequence of animated images.
1, AMADA NADAN AAA AAN A AAA ADAN AAA NAAA ANNAN TTT ATTA AAA AAT AAT TAA TTL
2 // PixmapCycler.h: Abstract class that supports a continuous cycle
E oF of pixmaps for short animation sequences
4 EMMA EAT AS ALICIA AAA RRA ARRIETA ARRA READ TAAL A PATS A
5 #ifndef PIXMAPCYCLER_H
6 #define PIXMAPCYCLER_H
7 #include <Xm/Xm.h>
9 class PixmapCycler {
10
i public:
t2
16
18
19 protected:
20
25
28 );
29
30 #endif
The PixmapCycler constructor takes three arguments. The first indicates the number
of
pixmaps in a single cycle, and the remaining arguments specify the width and height
of the
pixmaps. The constructor allocates an array of pixmaps large enough to hold
numPixmaps and
initializes the _current member to indicate that the cycle has not yet begun. This
constructor is
declared in the protected portion of the class and can only be called from derived
classes.
0 3 UU & WN P
PRP PPR
“PWN PR O
16
Le
18
19
iIIi tIl t ANDA RA SARA AA TARA TAI AAMA TAR IARVAIA REINA ESA ER
// PixmapCycler.C: Abstract class that supports a continuous cycle
fe of pixmaps for short animation sequences
#define INVALID -1
_numPixmaps = numPixmaps;
_current = INVALID;
_height = h;
)
The PixmapCycler destructor frees the list of pixmaps.
PixmapCycler: : -PixmapCycler ()
delete []_pixmapList;
)
The first time it is called, the next () member function calls createPixmaps () to
create a
series of pixmaps. After that, next () increments an index into the array of
pixmaps and returns
the next available pixmap. When all pixmaps have been used, the cycle begins over
at the
beginning.
20 Pixmap PixmapCycler::next ()
21 4
25 if ( _current == INVALID )
26 {
27 createPixmaps () ;
30
33
35 _current = 0;
36
39
LSS TAEL ETSI SLES LA EET ILA DARIA TESTA ETA A PALI TINE
2 // BusyPixmap.h
4 #include "PixmapCycler.h"
8 public:
9 BusyPixmap ( Widget );
10
wi protected:
Le” 39
6 #define NUMPIXMAPS 8
7 #define PIXMAPSIZE 50
1% W = W;
—
W
w
There are several ways to create a pixmap containing a particular image. One simple
way to
produce a sequence of pixmap images is to draw each image interactively in an
editor, such as the
bitmap editor included with the standard X distribution. The bitmap program creates
ASCII files
that can be included directly in a program and converted to a pixmap. Another way
to produce a
pixmap image is to create a blank pixmap and use Xlib graphics functions to draw
images in the
pixmap. |
The BusyPixmap class creates a sequence of bitmaps that are easier to render
programmati-
cally than to draw interactively. Each image consists of a circle with a filled arc
drawn within the
circle. As the sequence proceeds, the filled portion moves clockwise around the
circle. Figure 10.4
shows the initial pixmap pattern provided by the BusyPixmap class.
The createPixmaps () member function creates two graphics contexts. The first
corre-
sponds to the foreground and background colors of the widget passed to the
constructor, and the
second is the inverse of the first. An auxiliary member function, createBusyPixmap
(), uses
these graphics contexts when creating and rendering each individual pixmap. The
function
createPixmaps () calls createBusyPixmap () ina loop that computes a starting angle
for
the “pie slice” drawn by each call to createBusyPixmap (). Finally, the graphics
contexts are
released when they are no longer needed.
is {
17 XGCValues gcv;
18
21
22 XtVaGetValues ( _w,
23 XmNforeground, &gcv.foreground,
24 XmNbackground, &gcv.background,
25 NULL );
26
aa
32 XtVaGetValues ( _w,
33 XmNforeground, &gcv.background,
34 XmNbackground, &gcv.foreground,
35 NULL ) ;
36
42
43 angle = 360;
45
47 {
53 angle -= delta;
54 }
35
57
60 }
The function creat eBusyPixmap () creates a single pixmap and draws an image
similar to
that in Figure 10.4 in the pixmap. This function requires the starting angle of the
filled area and the
size of the filled area, in degrees.
ot an |
63 Pixmap pm;
65
66 // Create a pixmap
70 pm = XCreatePixmap ( XtDisplay ( w ),
72 _width, _height,
73 DefaultDepthOfScreen ( XtScreen ( w) ) );
74
The InterruptibleCmd class is an abstract class that works much like other abstract
classes
derived from Cmd. New commands can be created by deriving a new class that
implements the
doit () member function. Unlike other command classes, the InterruptibleCmd doit ()
member
function is called as often as necessary to complete the task. The doit () function
must indicate
that the task is completed by setting the _done data member supported by
InterruptibleCmd to
TRUE.
SETE TAILS E TL CAE ATG ES EIEN EN TASA NAAA ANTAS BR TIE IET NETS TAR AS
EARL
E A AR E MES
DG AAN AER
A Ena
A _”™S—-—>—( eeketetet—S”
wow 0 JO US WN FP
VNNNNPPRPPP PPP RB
WNPOVwOJaAaUAwYnonRprOo
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
VIVIVINI AAA ANA AAA ARANA AAA AAA AAA AAA AAA AIM IIA AMAN AT ATT TT
// InterruptibleCmd.h: Abstract class that supports lengthy,
// user-interruptible activities
VIVIVIVI VIANA NAAA AAA AAA NAAA MANI AAA NAAA IAAL AAT
#ifndef INTERRUPTIBLECMD_H
#define INTERRUPTIBLECMD_H
#include <Xm/Xm.h>
#include "NoUndoCmd.h"
// Define a type for the callback invoked when the task is finished
protected:
Boolean _done; // TRUE if the task has been completed
virtual void cleanup () ; // Called when task ends
private:
XtWorkProcId _wpld; // The ID of the workproc
TaskDoneCallback _callback; // Application-defined callback
void * clientData; // Provided by application
);
#endif
The InterruptibleCmd class supports both an external interface and an interface for
derived
application to be notified when the task is finished or has been interrupted. The
callback must be a
function of type TaskDoneCallback, defined in InterruptibleCmd.h as a function that
takes a
pointer to an InterruptibleCmd object, a Boolean value, and an untyped pointer.
The protected portion of the class defines the interface provided for derived
classes. Derived
classes must implement the doit () member function, declared as a pure virtual by
the Cmd class.
This member function must set the protected _done member to TRUE when the task is
completed.
The cleanup () function is called when the task ends. This function can be
overridden by
derived classes to perform any cleanup required when the task is interrupted or
completed. Finally,
the protected portion of the InterruptibleCmd class provides a convenience function
for updating
the text in the WorkingDialogManager widget. The InterruptibleCmd class handles
posting the
WorkingDialog and removing it. The derived class’s doit () function can use the
update-
Message () function to inform the user of progress by changing the text of the
dialog, if desired.
1 CEPLIS AAA LAP STA BR RIA DAR AMA TASA AIN AAA ETL ELTA ALIA IATA ERRATA
3 // user-interruptible activities
4 NAAA AA TAA ETAT AT IAT ATA ARA READERS RIA RA RIDER AIDA
5 #include "InterruptibleCmd.h"
6 #include "WorkingDialogManager.h"
7 #include "Application.h"
8 #include <Xm/Xm.h>
9 #include <Xm/MessageB.h>
10 #include <assert.h>
LE
19 }
20 InterruptibleCmd: :~InterruptibleCmd()
mae’ ie
24 XtRemoveWorkProc ( _wpld );
25 }
The InterruptibleCmd class defines two overloaded execute () member functions. The
first
allows the caller to provide a function to be called when the command is completed,
and the second
overrides the member function inherited from Cmd. The callback function passed
toexecut e ( )
must have the form:
After saving a pointer to the provided callback function, the first execute ()
function calls
the second InterruptibleCmd: :execute () member function, which takes no arguments.
27 void *clientData )
28 {
29 _callback = callback;
30 _clientData = clientData;
31 execute () ;
32 J
The second execute () member function, which can be called directly if there is no
callback
to be registered, initializes the _done member to FALSE and calls the Cmd: :execute
()
member function to initiate the command. The Cmd: : execute () member function
eventually
calls doit (), which must be defined by all instantiable classes derived from
InterruptibleCmd.
The doit () member function must perform a small amount of work and return quickly.
If the
function completes the entire task, it must set the _done member to TRUE.
The execute() member function checks the _done flag before posting a message and
installing a work procedure. If the task is already done, the function calls
cleanup () to notify
derived classes that the task is finished and calls the. callback function to
notify the application
that initiated this command. If the task has not been completed, execute() posts a
working
dialog and installs a work procedure to complete the task. Notice that the post ()
function
arguments specify a pointer to the current InterruptibleCmd instance and a pointer
to the inter-
ruptCallback() member function, which will be registered as the BusyDialogManager’s
cancel callback.
34 fí
36
37 // Call the Cmd execute function to handle the Undo, and other
38 // general mechanisms supported by Cmd
40
41 Cmd: :execute () ;
42
47 if ( _done )
48 {
49 cleanup () ;
50
51 if ( _callback )
53 }
54
57 // aS soon as possible
58
59 if ( !_done )
60 {
66 }
67° =")
FA
74
75 return ( obj->workProc() );
va A:
The workProc () member function calls the doit () member function to perform the
next
part of the task. If the task has been completed following this call, workProc ()
removes the
dialog from the screen and calls a virtual cleanup () member function. Finally,
workProc ()
calls the application-defined callback function. The first argument to this
function identifies the
current InterruptibleCmd object, and the second indicates that the task completed
normally.
Finally, workProc () returns _done. If this value is TRUE, Xt will remove the
workProc-
Callback() work procedure. Otherwise, Xt calls wrkProcCallback () again after it
has
cleared the event queue of any pending events.
TR 4
79 doit ();
80
84
85 if ( _done )
86 {
87 theWorkingDialogManager->unpost () ;
88 cleanup () ;
89 1£ ( _callback j
90 ( * callback )( this, FALSE, _clientData );
91 )
92 return ( _done );
93 }
The cleanup () function is empty. This virtual member function may ve overridden by
derived classes if needed. The InterruptibleCmd class calls this function whe: n
operation has
completed successfully or when the user interrupts the operation. Derived classes
can implement
the cleanup () member function to perform any operations required to clean up after
the task.
95 {
96 // Empty
97 }
The interruptCallback() function is a static member function called when the user
clicks the Cancel button on the working dialog. This function simply retrieves the
object pointer
provided as client data and calls interrupt ().
a BE
112 theWorkingDialogManager->unpost () ;
113 cleanup () ;
114
1:17 if ( _callback )
12d. 4
122 theWorkingDialogManager->updateMessage ( msg );
123 }
Figure 10.7 shows a message diagram that illustrates the connections between the
Interrupt-
ibleCmd, PixmapCycler, BusyPixmap, and WorkingDialogManager classes. This diagram
also
shows the connections between the InterruptibleCmd class and a typical derived
class.
interrupt workProc
doit
Derived Class
execute
PixmapCycler
PixmapCycler BusyPixmap
createBusyPixmap
Although very simple, wordCount provides a good test case for the InterruptibleCmd
class and
the techniques described in this chapter. The time required to count the words in
an arbitrary file
may vary, depending on the size of the file, the load on the machine, the speed of
the system, and
perhaps even the nature of the text in the file.
The wordCount program requires four new classes. The program creates a subclass of
MenuWindow, a subclass of InterruptibleCmd, a class that supports the
XmFileSelectionBox
dialog, and a class that represents a single word. The WordCountWindow class is
derived from
MenuWindow and creates the user interface shown in Figure 10.8. The CountWordsCmd
class,
which is derived from InterruptibleCmd, is responsible for collecting and counting
the words in a
text file. The CountWordsCmd class reads a text file and saves each unique word in
an instance of
the Word class. Because the CountWordsCmd class is a subclass of InterruptibleCmd,
the user can
interrupt the operation at any time, and the CountWordsCmd object can report its
progress using the
WorkingDialogManager dialog posted by the InterruptibleCmd class. The following
sections
discuss each new class used in this example.
On nA 0 FWD P
39
IEFATA IAAL ELL ALT PI ELSA ATRAER LA ATA ALA ATLA LALA SAA GA ANDA
// WordCountWindow.h: Count the frequency of each word
PELEA in a text file, as a test of the
iz InterruptibleCmd class
PL IGATLI TASS PETAL AAT ETL TANG ELL LAAT ETA TAT TA AAT A AAT ALA
#ifndef WORDCOUNTWINDOW_H
#define WORDCOUNTWINDOW_H
#include "MenuWindow.h"
class InterruptibleCmd;
class CountWordsCmd;
public: ©
WordCountWindow ( const char * );
protected:
virtual void createMenuPanes () ;
virtual Widget createWorkArea ( Widget );
private:
Widget _list; // List of words found in file
Cursor _busyCursor; // Displayed when application is busy
void setBusyCursor () ;
void setNormalCursor () ;
MF
tendi f
The WordCountWindow constructor is empty and just calls the MenuWindow constructor.
3 ESAIRASA IA AINA BER ETAT IERA AAA EA LAER TTI CL ATA EIT ATA TIE AT
4 #include "Application.h"
5 #include "InfoDialogManager.h"
6 #include "WordCountWindow.h"
7 #include "CountWordsCmd.h"
8 #include "SelectFileCmd.h"
9 #include "QuitCmd.h"
10 #include "MenuBar.h"
11 #include "CmdList.h"
12 #include <Xm/List.h>
13 #include <X11/cursorfont.h>
14 #include <stdio.h>
15
17 MenuWindow ( name )
1B. d
19 // Empty
a0. 3
As in all classes derived from MenuWindow, WordCountWindow creates the widgets that
implement its user interface in the createWorkArea () member function. This
function simply
creates an XmScrolledList widget, which provides a place to display the words found
in the file,
along with their frequency of occurrence. Notice that createWorkArea () returns the
parent of
the list widget. The Motif convenience function XmCreateScrolledList() creates an
XmList widget as the child of an XmScrolledWindow widget and returns the XmList
widget. For
most purposes, the XmScrolledWindow widget can be hidden. However, the MainWindow
class
installs the widget returned by createWorkArea () as the XmNworkArea widget used by
its
XmMainWindow widget. This must be a direct child of the XmMainWindow, not a
grandchild.
Therefore, createWorkArea () must return the XmScrolledWindow widget instead of the
XmList widget.
22 {
25 XtManageChild ( _list );
26
28° 3
The createMenuPanes () member function sets up a single menu pane that contains a
“Quit” command, provided by the QuitCmd class described in Chapter 9. The menu pane
also
includes a “Select File’ command that allows a user to choose the file to be
processed. This
30 1
31 // Create the command objects for this menu
32
34 Cmd *selectFile =
36 &WordCountWindow: : countWordsCallback,
37 (void *) this);
38
42 cmdList->add ( selectFile );
43 cmdList->add ( quit );
44 _menuBar->addCommands ( cmdList );
45 }
The function countWordsCal lback () is called when the user selects a file to be
counted.
This function just calls the WordCountWindow object’s countWords () member
function.
48 {
51 obj->countWords ( filename );
52 }
The countWords () member function initiates the task of counting the words in a
file by
creating a new CountWordsCmd object and calling its execute() member function. This
example creates a new CountWordsCmd object for each command it executes. This is
not strictly
necessary; for many applications it may be more appropriate to create a single
instance of a
command object that can be used more than once. For this simple example, it is
easiest to create a
new object each time one is needed. The CountWordsCmd class is meant to be
instantiated and
destroyed for each individual task.
The countWords () function begins by checking that the specified filename is non-
NULL. If
the filename argument is NULL, there is no point in continuing. Next, this function
instantiates a
new CountWordsCmd object. Before executing the command, countWords () calls the
WordCountWindow class’s setBusyCursor() member function to display a busy cursor.
Finally, countWords () clears the contents of the XmScrolledList widget and
executes the
CountWordsCmd object.
53
54
93
56
97
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
sg
task->execute ( &WordCountWindow::taskFinishedCallback ,
(void. * ) this );
When the CountWordsCmd object has completed its task, it calls taskFinished-
Callback (). The first argument to this function indicates the InterruptibleCmd
object that called
the function. The second argument indicates whether the command completed normally
or whether
the user interrupted the command. The third argument provides any client data
specified when the
task was executed. The countWords () member function provides the this pointer as
client
data when registering the callback on line 71. Notice that the InterruptibleCmd
object must be cast
as a pointer to a CountWordsCmd object for further use by the WordCountWindow
class. Once the
results have been processed, taskFinishedCallback() restores the application's
normal
cursor and deletes the CountWordsCmd object.
74
19
76
VT
78
79
80
81
82
83
84
85
86
87
88
89
Boolean interrupted,
void *clientData)
CountWordsCmd *cwObj = ( CountWordsCmd * ) cmd;
WordCountWindow *obj ( WordCountWindow * ) clientData;
// If the user interrupted the task, just confirm the interrupt
// Otherwise, call taskFinished() to process the results
if ( interrupted )
theInfoDialogManager->post ( "Interrupted!" );
else
obj->taskFinished ( cwObj );
92 obj ->setNormalCursor () ;
93 delete cwO0bj;
94 }
The taskFinished () member function is called when the WordCountCmd finishes its
task
successfully. This function retrieves the words found by the WordCountCmd object
and displays
them in the program’s list widget. The CountWordsCmd class supports several member
functions
that can be used to retrieve the results of the operation. The numWords() member
function
returns the total number of unique words found in the file. The getWord() member
function
returns a string containing a single word, and the getCount () member function
returns the
number of times a word occurs. The taskFinished() member function displays a dialog
informing the user of the number of unique words in the file and then converts the
results to an
array of compound strings to be displayed by the XmList widget.
96 (
97 int 1;
98 char buf[100];
99 XmString *xmstrList;
100
102
105
107
110
ER
117 {
119
LAS
131
132 // The XmList widget makes its own copy of the compound strings
133 // so free all local copies
134
Lod
139 }
The WordCountWindow supports two utility functions that can be used to change the
cursor
displayed when the user moves the mouse into the WordCountWindow. The function
setBusy-
Cursor () calls the Xlib function XCreateFontCursor () to create a cursor shaped
like a
watch. XDefineCursor () installs the busy cursor for the WordCountWindow’s base
widget,
the shell widget. All children inherit this cursor as well, unless they
specifically override it. The
symbol XC_watch is defined in the file cursorfont.h, which is included by
WordCountWindow.C.
(See [Scheifler90] for more information on cursors and related functions.)
JA. 4
143
144 if ( XtIsRealized ( w ) )
145 {
148 if ( !_busyCursor )
156 }
157 }
w ),
Ww Ja
The setNormalCursor () function is similar but creates and installs a left pointer
cursor
instead of the watch shape. Notice that this function assumes that the left pointer
is the normal
cursor, which may not actually be the case.
A Tes
165 if ( !_normalCursor )
171
174
17:5 }
176-3
These last two member functions provide a general-purpose facility that really
belongs at a
higher level than the WordCountWindow class. All applications may need to display
busy cursors
and restore the normal cursor at various times. It is not unusual to discover that
a member function
implemented for a specific use has broader application. In this case, the
setBusyCursor () and
setNormalCursor() member functions, or something similar, should probably be
supported
by the MainWindow class.
Words are represented by a simple Word class, which can be implemented entirely
with inline
functions. The Word class supports public functions for incrementing the count
associated with a
particular word and for comparing the string contained in a Word object to another
character string.
The Word class is defined in the file CountWordsCmd.h, along with the CountWordsCmd
class:
ELIAS RT ELLE LIL AA ETAL LATA IAT ALE LTT AIA ID ALIA TIALS
// CountWordsCmd.h: Count the frequency of words in a file
TELELT CHL ISLA LEIS LAPT TAM ERTL NARA AR IANAA AAA ARANA AAA T AAT AS EAT TY
#ifndef COUNTWORDSCMD_H
#define COUNTWORDSCMD_H
#include <stdio.h>
#include "InterruptibleCmd.h"
#include "SimpleList.h"
wow ON DN MO PF WD BF
10
11
12
13
14
£5
16
17
18
19
20
21
22
23
24
25
26
at
28
29
30
31
32
33
class Word (
public:
Word ( char *str ) { _word = XtNewString ( str ); _count = 1; )
~Word() { XtFree ( _word ); )
The CountWordsCmd class maintains a list of Word objects, along with information
about the
file being read, in the protected portion of the class. CountWordsCmd's public
interface consists of
a constructor and destructor, as well as several member functions that allow the
results of the
command to be retrieved.
35
36 public:
37
44 protected:
45
48 private:
49
54 int _percentDone;
55
57
58.33
59
60 #endif
The CountWordsCmd constructor initializes the class's various data members before
opening
the file specified in its third argument. If the given file cannot be opened for
any reason, the
constructor uses MotifApp’s InfoDialogManager facility to post a warning to the
user before it
returns. Otherwise, the constructor uses stat () to determine the size of the file
in bytes. This
information can be used as the basis of progress reports as the command executes.
If the file is
empty, the constructor posts a warning, closes the file, and returns. Otherwise,
the object is ready to
begin executing the command.
AMAMANTAR ANA LAL ELT LL A IAN IVA TALE I AT AL LAAT ERLE EST]
// CountWordsCmd.C: A simple test of the InterruptibleCmd class
LEVEL PEP MTA ELLA LL ELLUL LECTII A AAT EA ALAA ELS EDA AT ALA TAT LELI
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "CountWordsCmd.h"
#include "InfoDialogManager.h"
Oo ON AHA WM FP WD P
=
o
12 int active,
13 char *filename )
17
19
20 _bytesRead
ral _fileSize
22 _percentDone
a2
27 {
29
31 theInfoDialogManager->post ( buf );
32 }
33 else
34 {
36 // reporting progress
38
40
42 _fileSize = statInfo.st_size;
43
44 if ( _fileSize == 0 )
45 {
47
48 theInfoDialogManager->post ( buf );
49
50 fclose ( _fd );
51 _fd = 0;
52 }
53 }
54 }
ll
i co
55 CountWordsCmd: : -CountWordsCmd ()
56 4
59 an. AE <e
60 fclose ( _fd );
61 }
The doit () member function is the heart of the CountWordsCmd class. This member
function is called repeatedly until the task is completed or interrupted by the
user. The mechanics of
handling the work procedure, checking for interrupts, and so on, are all handled by
the Interrupt-
ibleCmd base class. The doit () function just needs to perform a small subset of
the complete
task and return periodically to allow the InterruptibleCmd to do its job.
The doit() member function reads lines from the file opened in the constructor and
separates the text into individual words. Here, a word is defined as any
alphanumeric sequence of
characters. The function strtok() provides an easy way to separate a string into
individual
tokens. Given a list of separators and a string, strtok() returns the next
available token in the
string each time it is called.
It is important to decide how much work the doit () member function should perform
each
time it is called. If the function does too little, the overhead of calling the
work procedure
repeatedly will be prohibitive. If the function does too much, the application will
not be responsive
to interrupts and exposure events. The animation displayed by the
WorkingDialogManager class
also depends on the application returning frequently to the event queue. In this
example, the
doit () function processes twenty lines of text each time it is called. Each time
through the main
loop, doit() uses fgets () to read a single line from the file. The _bytesRead
member is
updated by counting the number of bytes in the new line to allow the function to
report its progress
to the user. As each word is extracted from the text, the saveWord () member
function is called to
add the word to the list of Word objects. Before returning, doit () computes how
much of the
task has been completed, based on the size of the file and the number of bytes
read. If the resulting
percentage has changed, doit () updates the working dialog.
63 {
67
68 // If the file has not been opened, indicate that the task
69 // is finished
70
71 LE: {- AD
72 {
73 _done = TRUE;
74 return;
75 }
76
79 {
80 char *result;
81 char *word;
82
85
87 {
88 _done = TRUE;
89 return;
90 }
91
95
96 // Extract the first full word and save it in the word list
97
99
101
106 }
107
111
115 {
117
122 }
123 }
The saveWord() member function searches a table of Word objects for an existing
object
that represents a newly found word. If a word is already in the list, the object’s
count is incre-
mented. Otherwise, saveWord () creates a new Word object, expands the list of
Words, and adds
the new Word to the list. For the purposes of this wordCount example, the extreme
inefficiency of
this implementation is unimportant.
L25 {
127
128 if ( !word )
129 return;
130
131 // Search for the word and increment the count if found
T32
134 {
137 _list[i]->incrementCount () ;
138 return;
139 }
140 }
141
142 // If not found, create a new Word object and add it to the
143 if digt
144
146 }
The last class required to complete the wordCount example is a command class that
allows the user
to choose a file to be processed by CountWordsCmd. The SelectFileCmd class uses an
XmFileSe-
lectionBox widget to allow the user to select a file and provides a way to report
the selected file back
to the WordCountWindow class. Like many classes described in this book, the
SelectFileCmd hides
some of the details of its Motif-based implementation. Applications that need to
allow the user to
select a file can simply instantiate a SelectFileCmd object, providing a function
to be called when
the user selects a file. The class handles the details of creating a Motif
XmFileSelectionBox dialog,
setting up callbacks, dealing with compound strings, and so on.
The SelectFileCmd class is derived from NoUndoCmd and is declared as follows:
ELECT IFUL ELA TAAL EL TAT AL LATA GD EEDA IT ALA TIAL LATTA TAAL IAL ITAL
// SelectFileCmd.h: Allow the user to select a file interactively
AMAIA ARAS MAA AAA RARA NAAA NAAA MANARAAAAAA ANAIS AL ASAT AS
tifndef SELECTFILECMD H
#define SELECTFILECMD H
#include "NoUndoCmd.h"
#include <Xm/Xm.h>
JO WM FP WwW bY PA
12 public:
13
16 protected:
1.7
25
26 private:
2d
30 #endif
The SelectFileCmd constructor passes its name and act ive arguments on to its base
class. It
stores the values of the last two arguments in data members for later use.
1 FITATANA AIR RAMAS ALAR ANI AIARA RIA AAN TAL ATA AS ERA EEE
2 // SelectFileCmd.C:
5 #include "Application.h"
6 #include <Xm/FileSB.h>
hi
9 int active,
10 FileCallback callback,
LA void *clientData )
12 NoUndoCmd ( name, active )
13 (
14 _callback = callback;
15 _clientData = clientData;
16 )
E TE
20
21 _fileBrowser =
28 &SelectFileCmd: :fileSelectedCallback,
29 ( XtPointer ) this );
30
i HB
33 XtManageChild ( _fileBrowser );
34: 3
36 XtPointer clientData,
37 XtPointer callData )
38 {
40
41 XmFileSelectionBoxCallbackStruct *cb =
42 ( XmFileSelectionBoxCallbackStruct * ) callData;
43
46 int status = 0;
47
49 {
52
54 &name );
57
58 Lf ( status )
59 obj->fileSelected ( name );
60 }
61
64 }
The fileSelected() member function calls the function specified when this command
is
executed, if the function is non-NULL.
66 {
67 if ( _callback )
Now we can put the classes described in previous sections together to form a
complete program that
demonstrates one way to deal with lengthy operations. As with most MotifApp
programs, the main
program is very simple. The file WordCountApp.C instantiates an Application object
and creates a
WordCountWindow object.
FIRIIIIULA LEFAIN TATA A AAA AMARA AAA AAA AAA AAA TAA AT LETH
// WordCountApp.C: A program that tests the InterruptibleCmd
#include "Application.h"
#include "WordCountWindow.h"
W 0 JO U FWD P
Figure 10.10 shows the inheritance relationships of the classes in the wordCount
program,
including the MotifApp classes used directly or indirectly by this example. Classes
unique to the
wordCount program are shown in bold. All other classes are part of MotifApp. The
SelectFileCmd
is shown as part of MotifApp because it is a general purpose class that can be
added to the library.
Word Application
CmdList Dialog Manager f InfoDialogManager
BasicComponent —— UIComponent WorkingDialogManager
MainWindow
MenuWindow —— WordCountWindow
MenuBar
QuitCmd
PixmapCycler
BusyPixmap
The message diagram shown in Figure 10.11 shows some of the connections between the
major classes used in the wordCount example. This diagram does not show all classes
involved; in
particular, the Application, MainWindow, and MenuWindow classes are missing. Also,
some of the
abstract command classes are not shown, nor is the DialogManager class. The classes
shown boxed
in light gray with bold lettering are the classes that a programmer writing a
program like the
wordCount example must implement. The others, in addition to many classes not shown
here, are
provided by the MotifApp framework.
class is a useful command class independent of this example and should be added to
the MotifApp
library.
To see how the CountWordsCmd class works, run the wordCount program, choose the
“Select
File” entry from the “Application” menu pane, and select a file that contains ASCII
text. A large file
provides the best demonstration. In spite of several inefficiencies in the
implementation of this
example, the program processes most files very quickly. Figure 10.12 shows the
wordCount
program while it is busy counting words.
execute
execute
QuitCmd
execute MenuBar
SelectFileCmd setNormalCursor
SelectFileCmd
setBusyCursor
addCommands
WordCountWindow
CmdList
getCount
getWord
numWords
execute
interrupt
workProc
taskFinishedCallback
doit
saveWord
interruptCallback
post
unpost
updateMessage
word
count
ad
ae
BusyPixmap
WorkingDialogManager
e UN
qa
PixmapCycler | createPixmaps | BusyPixmap
|
|
|
|
|
|
|
It is interesting to notice that with a large file, the dialog eventually reports
that the task is 100
percent completed, and disappears, but the results do not appear immediately
because the XmList
widget takes some time to display a large list. Adding items to the XmList widget
is an example of
a lengthy operation over which the application has very little control. Motif is
busy, and there is
little the application can do until the XmList widget returns. One possible
approach is to add items
to the list one at a time so the program can continue to provide progress reports.
However, adding
individual items is very inefficient and causes the scrollbar to resize as each
item is added, which is
visually distracting. The wordCount program adds the items to the list in the most
efficient way
possible and continues to display the busy cursor until the call to Xt VaSet Values
() returns.
be spent updating the message displayed in the dialog, supporting the animation,
and handling
events.
Summary 357
Eleven seconds out of 38 seems like an acceptable price to pay for the added
benefits of
allowing the user to interrupt the command while in progress, keeping the user
informed of
progress, and so on. However, it is important to find an appropriate balance
between the overhead
of the work procedure mechanism and the amount of work done with each call. For
example,
modifying the doit () function to process only a single line of text with each call
changes the
total time required to process the same 40,000-line file to 5 minutes and 12
seconds. The average
overhead per call remains approximately the same, but this time the program must
make 40,000
calls to doit (). The increased number of calls results in an increase in total
time of over 1,100
percent, which is clearly too great a price to pay for the benefits offered by this
approach.
10.5 Summary
Chapter 11
A Color Chooser
This chapter describes a reusable user interface component that allows users to
select a color. Unlike
other MotifApp classes described earlier, the ColorChooser class has little to do
with the overall
structure of the MotifApp framework. Although most MotifApp classes discussed so
far are
designed to provide architectural support for Motif applications, an application
framework can also
include user interface components. In MotifApp, the basic low-level user interface
components are
supplied by Motif. The ColorChooser class is an example of a high-level, reusable,
user interface
component designed to fit into the MotifApp framework.
Many applications provide multiple graphical views of the same information. For
example, a statis-
tical package may present bar charts, scatter plots, or pie charts of a data set. A
spreadsheet may
simultaneously present many cells whose contents are related by some formula. A
word processor
may present a document as both a formatted page and an outline view. In each of
these cases, it is
usually necessary to ensure that all views remain synchronized at all times. For
example, if a user
creates a new subheading in a word-processor’s outline view, the new entry should
also appear in
the other views of the document. The user should be able to interact with any view
of the document,
and all other views should immediately reflect any changes.
A Model object has no visual characteristics but simply represents raw data. For
example,
consider a program that simulates a bank account. In such a system, a Model object
might maintain
the current balance of the account, a list of transactions, perhaps the current
interest rate for an
interest-bearing account, and so on. The Model might also support various
operations that alter the
state of the modeled data. For example, the Model object might support deposit ()
and
withdraw () methods, as well as methods that retrieve the current balance and
retrieve a list of
recent transactions.
View objects present the current state of a Model to the user. Views must be able
to retrieve the
data in a Model but should not care about the Model object’s internal
implementation. Views
respond to an update () message that causes each View object to display the current
state of a
Model according to its abilities. Each type of View displays some aspect of the
data maintained by
the Model. For example, a banking program might provide one View object that always
displays
the current balance. Another might display a list of deposits, while yet another
could display a list
of recent withdrawals. Some views may be text-based, while others may provide
information
graphically.
The Model object is responsible for notifying all associated View objects when any
aspect of
the Model’s data changes. Views simply format and display the current values
maintained by the
Model. Users sometimes manipulate Views to change the displayed data without
affecting or inter-
acting with the Model. For example, a user might choose to see a list of
transactions sorted by date
or sorted by the size of the transaction. The user might choose to filter the data
to see only transac-
tions between certain dates. The current balance could be displayed in different
currencies,
changing from US dollars to Swiss francs, for example. None of these changes affect
the under-
lying model. They are merely presentation options that determine how each View
displays the data
maintained by the Model.
The third type of object in the MVC approach is the Controller. Controller objects
allow users
to manipulate the data in a Model or alter the way a View displays that data. For
example, the
banking program mentioned earlier could provide a Controller that allows the user
to enter a new
balance, make deposits or withdrawals, change the interest rate, and so on. Views
and Controllers
often work closely. In some cases a single object may function as both a Controller
and a View. For
example, a banking simulation might allow the user to establish a new balance by
typing directly
into the window that displays the current balance.
Figure 11.1 shows the relationships between several objects based on an MVC
architecture.
The Controller manipulates the Model and various Views by sending appropriate
messages, repre-
sented in Figure 11.1 by “change” messages. The Model object responds to the
Controller's
requests and then notifies all dependent View objects that the Model has changed.
Each View object
responds to update messages by retrieving the information it needs from the Model
object and
updating its display.
change
update
P Model
f tDati ‘
an getData View
The MVC approach has many potential uses in interactive systems. Using a basic
architecture
like MVC can help programmers when designing object-oriented systems by providing a
template
that has proved effective in similar situations. Instead of starting from scratch
trying to identify
objects, as in the case study in Chapters 4 and 5, it is often easier to look for
specific objects that
match an architecture like MVC. Not all applications fit the MVC model. However, if
it seems as if
MVC might be the right approach, programmers can start the design process by trying
to identify
which parts of the program belong in the Model and which are Views and Controllers.
There are similarities between the MVC architecture described here and some
techniques
demonstrated earlier in this book. For example, all command objects described in
Chapter 8
support lists of dependent CmdInterface objects. In this sense, a CmdInterface
object can be
thought of as a view of a command object. Applications activate and deactivate
commands by
changing the state of a command object, which broadcasts this change to all
dependent interface
objects.
Another effective way to use command objects draws on the ideas of MVC. Programmers
can
set up command objects to be automatically activated and deactivated when other
commands are
executed. Although this is often useful, it may sometimes be more effective to
think of each
For example, consider a SaveFileCmd object supported by a text editor. This object
should be
active when a file is loaded, when the text buffer has been modified, and when the
corresponding
file on disk is writable. The command should be inactive when no file has been
loaded, the file has
not been modified since the last time the user saved the file, or the file cannot
be written on disk for
any reason. The mechanism described in Chapter 8 cannot handle all these situations
easily.
However, by representing a file as a Model object that has a “savable” attribute as
part of its state,
we could treat a SaveFileCmd object as a view of that model. The command should
receive update
messages whenever the state of the file model changes.
The MVC model can be used as the basis of entire systems and can also be used to
implement
smaller subsystems within an application. The remainder of this chapter presents a
moderately
complex user interface component that borrows its basic architecture from the MVC
approach. The
component is part of the MotifApp framework and allows the user to select the
components of a
color. The component allows the user to manipulate various parameters that affect a
color, while
viewing the color in several different ways. This component could be useful for
drawing editors,
paint programs, and so on and provides another example of how a C++ class can
combine Motif
widgets to form a higher-level, self-contained user interface component.
e 5 5 5 5 5 5 5 EE
The following sections present several classes that collectively form a user
interface component that
allows a user to select a color. The ColorChooser class encapsulates a collection
of objects and
defines the external interface for this component. Each ColorChooser object
supports a dialog that
allows the user to choose a color by moving sliders that control its red, green,
and blue (RGB) color
components. Figure 11.2 shows the ColorChooser dialog as it appears on the screen.
Saturation: [oss |
: a A Value: | 072 a
Figure 11.2 The ColorChooser dialog.
In
z AAA ALTA
NN irik
INS
II A gl eee a
A AA
MA
The dialog provides only one way to manipulate a color but provides several
different views,
which update dynamically as the color is edited. The ColorChooser dialog displays
the RGB
components of a color as numeric values and also shows the color’s hue, saturation,
and value
(HSV) characteristics. The dialog also displays a “swatch” of color that shows how
the chosen
Shade appears on the screen. When the user clicks an OK button, the dialog is
dismissed and the
ColorChooser makes the selected color available to the application through a
callback function.
The ColorModel object functions as the “model” in an MVC system. Each ColorChooser
subsystem includes a single ColorModel object, which maintains the current state of
a color. Each
ColorModel object must allow other objects to retrieve various components of the
color it repre-
sents to allow the color to be displayed. It must also provide an external
interface that allows the
color to be manipulated. Each ColorModel object also maintains a list of ColorView
objects that
depend on this ColorModel and notifies each dependent ColorView object when any
aspect of the
color model changes. Figure 11.3 shows a class card that summarizes the ColorModel
class’s
responsibilities.
ColorModel Concrete
1. Represents a color
2. Changes values of color components
The ColorModel class defines three protected members that represent the red, green,
and blue
components of a color. The public portion of the class supports member functions
for setting and
retrieving RGB components of the color and for associating ColorView objects with
the
ColorModel.
3 PIELII EIEII ELIA ARANA ERA TAMAAARRA RNA AAA AAA IRA AAA A
4 #ifndef COLORMODEL_H
5 #define COLORMODEL_H
6 #include "ColorView.h"
7 #include "SimpleList.h"
9 class ColorModel {
10
11 public:
L2
13 ColorModel () ;
14
16
18
31
32 private:
33
a7 int _blue;
38
40 };
41 +#endif
EELFLAL ERD LLL AAA ET ALATA AA TIA EAA TAT ARANRAAAANA NAAA DANI AT
// ColorModel.C: A class that represents a single color, using
pi an RGB representation
PAEIT EEN ETIANI AAL LITAI ILATE ATEI TIET LELIA TILAT EI ASIS
#include "ColorModel.h"
#include "ColorView.h"
ColorModel: :ColorModel ()
{
_red = Os
_green
_blue
rFPrwOoOPON DU PWD PE
2 S&S
—
NO
to H
oo
H
Ww
}
Each ColorModel object maintains a list of objects that must be updated when any of
the RGB
components of the object’s color changes. In the ColorChooser subsystem, all views
are derived
from the ColorView class. The attachView() member function adds a ColorView object
to the
list of dependent views supported by a ColorModel object.
16 tne 33
LT
18 _views.add ( view );
19
22 view->update ( this );
aa -}
The updat eViews () function sends an update () message to all ColorView objects
regis-
tered with a ColorModel object. Notice that this function provides no information
about the data
maintained by the ColorModel. In general, a Model object cannot know what
information a
particular view may be interested in, All ColorView objects are responsible for
retrieving infor-
mation they need from the ColorModel and updating themselves appropriately.
aa Ñ
26 E Be
as
30 }
A ColorChooser Dialog 365
The setRGB() member function allows the ColorModel’s red, green, and blue
components
to be altered. This function calls updateViews () to notify dependent views that
the model has
changed.
32 {
33 _red = Y;
34 _blue = b;
35 _green = g;
36
37 updateViews () ;
38 )
Notice that the ColorModel object is little more than a data structure with support
for notifying
other objects when the value of its data changes. The ColorModel does not allocate
colors and does
not depend on the X color model in any way. With the approach used here, the
ColorModel class is
independent of any particular type of hardware. The model uses an RGB color model
but could
easily be changed to support any abstract color model. The internal representation
is of no impor-
tance and should not matter to other classes in the ColorChooser system. For
simplicity, the
ColorModel class shown here presents an RGB external interface and also uses an RGB
model
internally. There is no reason the external interface must match the internal
implementation, and the
ColorModel could be changed to use any suitable color model. However, it must
provide an
external RGB interface for the views in the ColorChooser.
All views in the ColorChooser are derived from a common ColorView class. The main
purpose of
this abstract class is to define a protocol for all objects that depend on the
ColorModel object. The
ColorView class is derived from UlComponent, because all “views” are visible user
interface
components. The class defines an inline constructor that calls the UlComponent base
class and
declares a pure virtual member function, update (), which must be implemented by
all derived
classes.
1 TEIPET ALIA EIA ERA RISA AA ESAAAAA SAR ELTA TIAL ATT ALA PATE
2 // ColorView.h: Abstract base class. Defines protocol for
3 Ed all views attached to the ColorModel
4 FEET IL ETT ITALLAL ATT EE RIERA A ELA LATTA ET AA AEREA ATA ED ETL ET ALY
5 #ifndef COLORVIEW_H
6 #define COLORVIEW_H
7 #include "UIComponent.h"
8 class ColorModel ;
11
12 public:
13
17 protected:
18
20 };
21
22 +#endif
The RGBController class is the only “controller” supported by the ColorChooser. The
RGBCon-
troller allows the user to change each red, green, or blue component of the
ColorModel’s color
independently. Although the RGBController class’s main responsibility is to
manipulate a Color-
Model object, this class also provides a view of a ColorModel. Each RGBController
object supports
a slider for each component of a color; the position of each of these sliders
provides a measure of
the relative contribution of each red, green, or blue value. Therefore, the
RGBController class
functions as both a controller and a view. To allow RGBController objects to
receive update ( )
messages from a ColorModel object, the RGBController class is derived from
ColorView.
Figure 11.4 shows the RGBController’s user interface as it appears on the screen.
The public interface to the RGBController class consists of the constructor and an
update ( )
function. The constructor requires a pointer to the ColorModel on which this
controller operates.
Because the RGBController is also a user interface component that creates widgets,
the constructor
also requires a name and a widget to be used as a parent.
1 PPSILELI LIA TAILED AAT EL AAT A AAA THRILL A EET CPPS EPL EL ATA PERERA
3 PERTIL ELT PALL EDT ETAL ELT AT EA ELEITA TAS A TAS TAAL AT EE TATA GL
4 #ifndef RGBCONTROLLER_H
5 #define RGBCONTROLLER_H
6 #include <Xm/Xm.h>
7 #include "ColorView.h"
9 class ColorModel;
10
12
13 public:
14
19 protected:
20
22
368 Chapter 11 A Color Chooser
28
29 private:
30
a5 Widget _blueSlider;
34
36
41 };
42
43 #endif
The RGBController constructor creates an XmRowColumn widget and the three sliders
that
control the three color components. The XmRowColumn widget is configured to stack
the sliders
vertically in a single column. Each slider controls a value between O and 255 and
has a horizontal
orientation. Once all widgets have been created, the RGBController constructor
registers a pair of
callbacks for each slider, to be called when the user moves any slider.
1 ELENI BETALT ADA LS ATLAS AIDA DRA ABRA AA EEI TOI TITLUL IAIL EET AT
2 // RGBController.C: Control the ColorModel
3 LIF PRIFF TAT BRAND RIA RIA AMABA FEET TLL IT LAT TAAL ATA LE TLE
4 #include "RGBController.h"
5 tinclude "ColorModel.h"
6 tinclude <Xm/Xm.h>
7 #include <Xm/RowColumn.h>
8 #include <Xm/Scale.h>
11 Widget parent,
14 Arg args[10];
15 16 f
16
18
21
22
23
24
25
26
2)
28
29
30
31
32
33
34
35
36
37
38
33
40
41
42
43
44
45
46
47
48
49
50
SL
52
53
54
55
56
S4
58
59
60
61
62
63
64
65
66
67
68
69
parent,
XmNnumColumns, 1,
installDestroyHandler () ;
A = Us
XtSetArg ( args[n], XmNminimum, G F ate
XtSetArg ( args[n], XmNmaximum, 255 ); n++;
XtAddCallback ( _redSlider,
XmNvalueChangedCallback,
&RGBController: :redChangedCallback,
( XtPointer ) this );
XtAddCallback ( _redSlider,
XmNdragCallback,
&RGBController: :redChangedCallback,
( XtPointer ) this );
XtAddCallback ( _greenSlider,
XmNvalueChangedCallback,
&RGBController: :greenChangedCallback,
( XtPointer..) this );
XtAddCallback ( _greenSlider,
XmNdragCallback,
&RGBController: :greenChangedCallback,
( XtPointer ) this );
HEM TR
FESR oS
OTRAS LRT E SEIS EIT SRT GUNS ERT E OTST Ie Fok EEE RST ANTI RANE? SIG SESE
St r PGN A A A OSES
PLR al be INEA
SR FUL a FO E Cr o DADA
A me) A DAA AR E AS
raire TA A A Sy SO
70 XtAddCallback ( _blueSlider,
71 XmNvalueChangedCallback,
72 &RGBController: :blueChangedCallback,
73 ( XtPointer ) this );
74
75 XtAddCallback ( _blueSlider,
76 XmNdragCallback,
qa &RGBController: :blueChangedCallback,
78 ( XtPointer ) this );
ib A:
The RGBController class registers one function for each slider’s XmNdragCallback
and
XmNvalueChangedCallback callback lists. These callback functions retrieve the
current
value of the XmScale widget from the callData argument and call the object’s
corresponding
redChanged(), blueChanged(), or greenChanged () member functions.
81 XtPointer clientData,
82 XtPointer callData )
a3). 4
87 obj->redChanged ( cb->value );
88 }
90 XtPointer clientData,
91 XtPointer callData )
92 (
96 obj->greenChanged ( cb->value );
97 }
99 XtPointer clientData,
101 |
106 )
108 {
109 _model->setRed ( value );
110 }
113 4
113 _model->setGreen ( value );
114 )
115 void RGBController::blueChanged ( int value )
116 (
117 _model->setBlue ( value );
158.)
120 {
121 XtVaSetValues ( _redSlider, XmNvalue, model->red(), NULL );
122 XtVaSetValues ( _greenSlider, XmNvalue, model->green(), NULL );
123 XtVaSetValues ( _blueSlider, XmNvalue, model->blue(), NULL );
124 }
widget’s background color. Any widget could be used for this purpose, but the
SwatchView class
creates an XmDrawingArea widget. Besides creating the widget, the SwatchView class
has only a
1 FEEL LELIL ELT EEL ET IELTS EERE LCL TT CL ELI LT AS LIL LEA ET EP TL EA TI
2 // SwatchView.h: Display a color "swatch" corresponding to
3 // the color in the ColorModel
4 PEL ETEL LEAL EA TALE SE LATE LEC LE TTA TAIT LE CEPA ELA TE CELL AAD EL LET SE
5 #ifndef SWATCHVIEW_H
6 #define SWATCHVIEW_H
7 #include "ColorView.h"
9 class ColorModel;
10
13 public:
19 protected:
20
25 #endif
Like all classes derived from UIComponent, the SwatchView constructor begins by
creating
the base widget and installing the destruction callback provided by UlComponent.
Next, the
constructor calls XAllocColorCells() te allocate a single read-write color cell.
(See
[Scheifler90] for detailed information on allocating colors.) If this call is
successful, the pixel value
of the allocated color cell determines the value of the XmDrawingArea widget’s
XmNback-
ground resource. The members of this color cell, which determine the color
displayed by the
XmDrawingArea widget, are set in response toupdate () messages from the ColorModel.
1 PLECIA ELAR EPA PLA EEL LT LE LLL ALS LETT A AAA AAA AA AAA A CAS
2 // SwatchView.C: Display a swatch of color
| FULALELAL TELE LLL AA AAA NAAA AAA TAAL CEL ERLE SAS EPL EF ETL
4 #include <Xm/Xm.h>
5 #include <Xm/DrawingA.h>
6 #include <Xm/Frame.h>
7 tinclude "SwatchView.h"
8 tinclude "ColorModel.h"
13 int status;
14 Pixel pixels[1];
15
18
20 parent, NULL, O );
21
22 installDestroyHandler () ;
23
28 status =
29 XAllocColorCells ( XtDisplay( w ),
30 DefaultColormapOfScreen ( XtScreen ( w ) ),
32
33 if ( status == FALSE )
34 {
35
36 // If the color allocation fails, use the parent’s background
37 // color as the color of the swatch and set the frame’s
40 _enabled = FALSE;
41
44 }
45 else
46 {
49
50 _enabled
51 _index
52 }
53
57 xmDrawingAreaWidgetClass,
58 W,
59 XmNbackground, _index,
60 NULL );
61 }
TRUE;
pixels[0];
The SwatchView class is designed to work with displays that support a PseudoColor
visual, in
which applications reference colors through a pixel index into a color lookup
table. If the entry in
the color lookup table is writable, the program can change the contents of that
entry in the table,
which changes the color displayed by any object drawn using that pixel index. X
supports several
types of visuals, and the SwatchView will not work with all of them. However, the
ColorChooser
separates all dependencies on the X color models from the remaining components, and
the rest of
the ColorChooser classes work with all color models. (See [Scheifler90] for
information on the X
color model and visual types.)
color of the component’s parent. Then, the XmFrame widget’s shadow width is set to
zero to effec-
tively hide the SwatchView component. A more complex implementation of this class
could try to
deal with the failure in a more sophisticated way. For example, the view could
attempt to display
dithered patterns that simulate the intensity of a color on monochrome screens.
The update() member function retrieves the RGB value maintained by the ColorModel.
These values are placed in an XColor structure and passed to XStoreColor (). This
Xlib
function changes the values of the RGB components for the background pixel used by
the
XmDrawingArea widget. Notice that the 0-255 range used by the ColorModel must be
converted to
the 0-65535 range expected by the X server.
63 {
66
68 {
69 XColor color;
70
74
79 color.pixel = _index;
80
84 XStoreColor ( XtDisplay ( _w ),
85 DefaultColormapOfScreen ( XtScreen ( w ) ),
86 &color );
87 }
88 F
The ColorChooser supports two views that consist of text fields: RGBView and HSV
View. Because
these views are very similar in function and appearance, it is convenient to
implement a common
base class that implements those features common to both classes. Text View is an
abstract base class
that creates the widget layout used by both the RGBView and HSVView classes. The
class creates
three labels and three text areas. Derived classes must provide the information
displayed by each of
these widgets.
The file TextView.h contains the TextView class declaration. The class supports six
widgets:
three labels and three output fields. These are identified generically as_label1,
_label2,
_label3 and _fieldl, _field2, _field3, because derived classes determine the exact
function of these members.
4 BESTEL ELIT EEL AAI AT TIT AIA TALIA TILES TTT LAT APTA ELT TE PET ET E
5 #ifndef TEXTVIEW_H
6 #define TEXTVIEW_H
7 tinclude "ColorView.h"
10
11 public:
12
16 protected:
17
19 Widget _field2;
20 Widget _field3;
as
23 Widget _label2;
24 Widget _label3;
25 };
26
27 #endif
The TextView constructor creates two columns of widgets: three labels and three
corre-
sponding output fields. The labels are implemented as XmLabel widgets, and the
output areas are
XmTextField widgets. All six widgets are managed by a single XmRowColumn widget.
PIT ELF ELIT ILIA TAL EAE IE LATA AAAS ET EILER TELE TATE EA ETT I EAD OATES
// TextView.C: Abstract base class for all text (numerical)
E views of a ColorModel
PAESAAARA AAA REA ARA ARANA RARA AAT AE ER IEA AAA EAT AA
tinclude "TextView.h"
tinclude <Xm/TextF.h>
#include <Xm/RowColumn.h>
#include <Xm/Label.h>
wo 0 JA UU fF UNEP
12
Lo
14
13
16
LY
18
19
20
21
22
23
24
25
26
27
28
29
30
3
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
SL
52
53
54
55
56
57
58
5:9
60
int n;
Arg args[10];
parent,
XmNorientation, XmMHORIZONTAL,
XmNpacking, XmPACK_COLUMN,
XmNnumColumns , a;
XmNentryAlignment, XmALIGNMENT_END,
XmNadjustLast, FALSE,
NULL );
installDestroyHandler () ;
n = 0;
XtSetArg ( args[n], XmNcolums, 5 ); n++;
XtSetArg ( args[n], XmNeditable, FALSE );n++;
// label text
xmTextFieldWidgetClass, _w,
args, n);
The XmTextField widgets are used as output areas in this example. To be sure these
widgets
are not mistaken for editable text areas, each widget is configured as read-only by
setting the
XmNeditable resource to FALSE. Setting XmNcursorPositionVisible to FALSE
removes the text insertion cursor from each widget.
Figure 11.6 shows how a component derived from TextView appears on the screen.
The RGBView class is derived from the TextView class. RGBView displays red, green,
and blue
values between 0 and 255, corresponding to the current values of the RGB components
maintained
by the ColorModel. Because the TextView class creates and arranges the widgets used
by this view,
the RGBView class is very simple. The RGBView just updates the information shown in
the
AN HA 0 FWD P
PrPrPrPF Fr wo
Uu ps WNH O
16
17
The File RGB View.h contains the declaration of the RGB View class.
PEELE LEI ELT EAL ILLITE ITI EEE ETT OPTS ETA TELEL EII TACIS
// RGBView.h: Display the contents of a ColorModel as
FEEST ELT ALA ASA IRA ARIAS PRESEA EALT ATTA TT EES AEREA ICAA EA ELA TS
#ifndef RGBVIEW_H
#define RGBVIEW_H
#include "TextView.h"
y;
#endif
The RGB View constructor just calls the TextView constructor to create the widgets
used by
this component.
1 VEELAEIL LE ELL SLIT LA ELL EAI ALTA NAAA A STAT LAA AA PITT ALT ALT
2 // RGBView.C: Display the contents of a ColorModel as
4 FATALE EEA ARANA A AMA LAT EL AFIT TTA AE EES TAS AA LT LETT RIAS
5 #include "RGBView.h"
6 tinclude "ColorModel.h"
7 tinclude <Xm/Xm.h>
8 #include <Xm/TextF.h>
9 #include <stdio.h>
10
14 // Empty
15 3
The update () member function retrieves the red, green, and blue color components
of a
color from the ColorModel object and displays each as a three-digit integer in the
appropriate
output field.
7 {
18 char buf[100];
19
25 |
28 } |
The HSV View class is similar to the RGB View class, except that it displays a
color’s hue, saturation,
and value components. The HSVView class uses the widgets created by the TextView
class and
implements the update () member function and a member function that converts from
the RGB
to the HSV color models. This conversion is necessary because the ColorModel
provides only RGB
values.
TL ELE EL EL ELET CELA AA AAA EEL SS NAAA ATA CLL ATLA ARA AAA AAA ARIAS
// HSVView.h: Display the contents of a ColorModel as
m W N F
14
21
WD 0 JO UU fF WD PF
bhperprRp
Oo pp WN FE OO
#include
"TextView.h"
class ColorModel;
public:
hy
#endif
Like the RGBView constructor, the HSVView constructor calls the TextView
constructor to
build the widgets used by the view.
PUPILS TRI AITAV AA IAS ARA RIA IRRADIA AA EAS ARIANE RI LEAT TEES TE
// HSVView.C: Display the contents of a ColorModel as
//
tinclude
tinclude
tinclude
#include
#include
HSVView: :
"HSVView.h"
"ColorModel .h"
<Xm/ Xm. h>
<Xm/TextF.h>
<stdio.h>
// Empty
The update () member function retrieves the red, green, and blue values from the
ColorModel and
converts them to an HSV color model. The resulting hue, saturation, and value
components are
displayed in the corresponding XmTextField widgets.
16
47
18
19
20
21
{
char
int
buf [100];
hue, value, saturation;
23
26
35 09
RGBTOHSV () is a protected member function that takes the RGB components of a color
and
computes the corresponding hue, saturation, and value components. The following
function is
adapted from an algorithm that can be found in [Foley82]. In this implementation,
the hue is
computed as an angle that lies between O and 360 degrees; the value and saturation
components are
expressed as percentages and lie between O and 100.
Notice that the last three parameters to this function are passed by reference to
allow these
parameters to be used as return values. i
38
40 int green,
41 int blue,
47
53
55
S7
59
61 if (v == 0.0 )
62 s = 0.0;
63 else
64 s = (v- temp ) / V;
65
68 if (s != 0.0)
69 {
74 h = Cb = Ca}
75 else if (g == v)
76 h = 2.0 + Cr - Cb;
77 else if ( b == v )
78 h = 4.0 + Cg - Cr;
79
80 nh = 60.0 * hy
81 El A < O.0 ]
82 h += 360.0;
83 }
84 else
85 h = 0.0%
86
91 hue = ( int ) h;
92 )
The ColorChooser class ties together the ColorModel, the RGBController, and all
views described
in previous sections to form a single logical user interface component. The
ColorChooser class
creates and displays a dialog that allows an application to request a color from
the user. The MVC
objects form a subsystem that allows the user to view and manipulate a color; the
ColorChooser
defines the external programmatic interface for this subsystem.
V ON DU PWD P
PPP RPP PP BE
INU BRB WDNR O
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
a
38
39
40
41
42
43
44
45
46
#define COLORCHOOSER_H
#include "UIComponent.h"
// Function type to be passed to pickColor()
class ColorModel;
class ColorView;
ColorSelectedCallback _clientOkCallback;
CancelCallback _ _clientCancelCallback;
void * clientData;
3 PIS LIFT TAI TAT LTA LA TET EET A FREE CCE COLES EPIL ATAN SE
4 #include "ColorModel .h"
5 #include "HSVView.h"
6 #include "SwatchView.h"
7 #include "RGBController.h"
8 #include "RGBView.h"
9 #include "ColorChooser.h"
10 include <Xm/Xm.h>
11 #include <Xm/Form.h>
12 #include <Xm/BulletinB.h>
14 #include <Xm/PushB.h>
15
16 // Default resources needed by the ColorChooser component
L?
19 "*rgbView.xX: 150%,
20 "*rgbView.y: 20",
21 "*rgbView*labell*labelString: Red:",
22 "*rgbView*label2*labelString: Green:",
23 "*rgbView*label3*labelString: Blue:",
24 "*hsvView.X: 300",
25 "*hsvView.y: 20%,
26 "*hsvView*label1*labelString: Hue:",
27 "*hsvView*label2*labelString: Saturation:",
28 "*hsvView*label3*labelString: Value:",
29 "*rgbController.x: DO
30 "*rgbController.y: 190%;
31 "*rgbController*scaleWidth: 315%,
32 "*rgbController*scaleHeight : aay
33 "*xcolorView.X: 20%,
34 "*colorView.y: 20",
35 "*swatch.width: 100",
36 "*swatch.height: 100",
41 "*cancel.x: 400",
42 "*cancel.y: 240",
43 "*cancel*labelString: Cancel",
44 // Debatable use of color
45 "*rgbController*red*troughColor: red",
46 "*rgbController*green*troughColor: green",
47 "*rgbController*blue*troughColor: blue",
48 NULL,
49- Fi
The resources specified by the ColorChooser include three color specifications for
the sliders
created by the RGBController class. In general, it is best to avoid specifying
colors in application
resource files, and it is almost never appropriate to hard-code colors in a
program. The user should
be able to choose colors according to his or her taste. However, in this case, the
three colors serve
as labels for the controls on the ColorChooser, so the colors have an intrinsic
meaning that the user
should not alter. In addition, the resources that accompany the ColorChooser are
only default
values; the user can override them easily. Specifying colors in an application can
also make the
program nonportable; not all systems support color. However, the ColorChooser
itself depends on a
color system, so specifying these three colors as defaults should not pose a
serious problem in this
example.
Dialog() that creates and configures both widgets. The XmBulletinBoard widget
provides
support for Motif-style dialogs. For example, the constructor declares the
_okButton to be the
XmNdefaultButton for the dialog and the _cancelButton widget to be the dialog’s
XmNcancelButton. Setting these resources informs the XmBulletinBoard widget of the
function these widgets perform. Once these buttons are registered, the
_cancelButton
automatically unmaps the dialog, with no action required by the program. The
_okButton
functions as the default button, which is activated when the user presses the
<RETURN> key. The
ColorChooser constructor registers the okCallback() function with the _okButton
widget,
to be called when the user clicks on the button.
50
S1
52
D3
54
59
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
_clientData = NULL;
// Load ColorChooser component’s resources into the esource database
setDefaultResources ( parent , colorChooserResources );
XtCreateManagedWidget ( "ok",
xmPushButtonWidgetClass,
W, NULL, 0 );
_okButton
y, NULL, 0 );
XtVaSetValues ( _w,
XmNdefaultButton, _okButton,
XmNcancelButton, _cancelButton,
NULL );
XtAddCallback ( _okButton,
XmNactivateCallback,
&ColorChooser: :okCallback,
(XtPointer) this );
XtAddCallback ( _cancelButton,
XmNactivateCallback,
&ColorChooser: :cancelCallback,
(XtPointer) this );
AS
EIA e A
E TR
ee e
a Pe eee
A A AT NE
LEILA RRA
-rmy LI
ES
99 _model->attachView ( _swatch );
100 _model->attachView ( _rgbView );
101 _model->attachView ( _hsvView );
102 _model->attachView ( _rgbSliders );
103
106 _rgbSliders->manage() ;
107 _Swatch->manage () ;
108 _rgbView->manage () ;
109 _hsvView->manage() ;
taps
The ColorChooser destructor deletes the various objects instantiated by the
constructor. The
UlComponent destructor destroys the widgets.
cA So Sara
The pickColor() member function provides the primary external interface to the
Color-
Chooser class. Applications that need to allow the user to choose a color call this
member function,
specifying a callback function to be invoked when the user makes his or her
selection and a second
callback to be called if the user cancels the selection. ThepickColor () function
stores a pointer
to these callbacks for later use and displays the dialog.
126
137 manage () ;
148:
The okCallback() function is called when the user clicks on the ColorChooser’s OK
button. This function just retrieves the ColorChooser object from the clientData
argument and
calls the ok () member function.
iat q
135: }
The ok () member function calls the “ok” callback function passed to pickColor ().
The
arguments to this function provide the current value of each color component, as
well as the client
data passed topickColor ().
137 {
138 if ( _clientOkCallback )
The cancelCallback() function is called when the user clicks on the ColorChooser’s
Cancel button. This function just retrieves the ColorChooser object from the
clientData
argument and calls the cancel () member function.
147 {
The cancel() member function calls the “cancel” callback function passed to
pickColor() to inform the application that the user has canceled the color
selection without
choosing a color. The arguments to this function provide the client data passed to
pickColor ().
152 [
153 if ( _clientCancelCallback )
Figure 11.7 shows the inheritance relationships between the classes used to create
the Color-
Chooser component.
BasicComponent ColorModel
UlComponent
ColorChooser ColorView
Figure 11.8 shows a message diagram of the classes that make up the ColorChooser.
This
diagram is relatively complete but does not include any abstract base classes.
ColorChooser be r-
= setRed
Y m set
=I Eb
Swatch View
setBlue
update
RGBController
RGBToHSV
Ke)
+
3
yo.
e
3
red
gree
update
update
This section describes a small program that tests the ColorChooser component. This
program
displays a single Motif XmPushButton widget that displays the ColorChooser dialog
when the user
clicks on it. The program registers a callback with the ColorChooser that simply
prints the values
selected using the ColorChooser.
The program defines a ColorTestWindow class that creates a button that allows the
user to post
the ColorChooser dialog.
1 IDA AAA AAA AAA AAA AAA AA AAA AAA TAA AAT AAA AAA AAT AAT AAT
4 #include "MainWindow.h"
6 class ColorChooser;
10 public:
11
13
16 protected:
17
19 ColorChooser *_colorChooser;
20
2d private:
22
26
30
The ColorTestWindow class is similar to the test classes used to display dialogs in
Chapter 7.
The function createWorkArea() just creates an XmPushButton widget and registers a
callback to be called when the user clicks on the button.
1 EE,
2 // ColorTestWindow.C: Test the ColorChooser dialog
5 #include "ColorTestWindow.h"
6 #include "ColorChooser.h"
7 #include <Xm/PushB.h>
LO oF
12
14 xmPushButtonWidgetClass,
15 parent,
16 NULL, O );
17
19
ei &ColorTestWindow: :pickColorCallback,
22 ( XtPointer ) this );
23
as
28 }
The pickColorCallback() function retrieves the ColorTestWindow object pointer and
calls the ColorChooser’s pickColor() member function to post the dialog. The
function
colorSelectedCallback() is registered as a callback function to be called when the
user
makes a selection. This function does not require any client data and does not care
if the user
cancels the dialog, so the final arguments to pickColor () are NULL.
30 XtPointer clientData,
31 XtPointer )
saad
36 NULL, NULL );
37 3
Summary 391
39 int green,
40 int blue,
41 void * )
42 {
44 red = td, hn
45 green = %d,\n\
46 blue = %d\n",
47 red, green, blue );
48 )
The file ColorTestApp.C completes the test program. This file creates an
Application object
and a ColorTestWindow object.
tinclude "ColorTestWindow.h"
COND MN FP WD P
Before this program is built, the classes that implement the ColorChooser should be
added to
the MotifApp library. The program can then be built with the command:
11.3 Summary
present information. Each Model object can support many Controllers and many Views.
The Model-View-Controller (MVC) architecture is particularly useful for
applications that
The MVC architecture demonstrated in this chapter varies in several ways from the
original
MVC implementation supported by Smalltalk. The model lends itself to many
variations, and the
key ideas of this architecture can be applied to many situations. In particular, it
is often useful to
separate presentation aspects of a system from the underlying semantic information.
Broadcasting
change notifications to collections of dependent objects has applications in any
situation that
requires synchronization among loosely coupled objects.
Chapter 12
A MotifApp Application
This chapter examines a complete application that exercises more of the MotifApp
framework than
the small test examples shown earlier. The program is named “bounce” because it
allows users to
control a simple animation that consists of a set of bouncing balls. The
application is designed to be
flexible; the appearance and behavior of the animated figures displayed by the
program can be
altered by defining new classes and making some minor modifications.
Section 12.1 provides an overview of the application and shows how it takes
advantage of the
MotifApp framework. Sections 12.2 through 12.7 discuss the application-specific
classes used in
bounce. Section 12.8 shows how to build and run the application.
TF 5 5 5 5 5 == ==
Unlike video players, bounce allows the user to add and remove elements of the
animation
interactively. In this respect, bounce borrows some ideas from a play or movie. The
area in which
the animation occurs is referred to as a “stage,” and graphical items that appear
on the stage are
known as “actors.” The user can add actors to the stage by choosing from the
available types of
actors listed in a pulldown menu. The last actor to be added can be removed by
“undoing” the
previous command.
In this example, the only available actors are different colors of “bouncing
balls,” filled circles
that bounce off the sides of the stage. However, bounce can support arbitrary actor
objects, and
radically different animations could be created by defining new actor classes.
Figure 12.1 shows the layout of the bounce program. The stage occupies most of the
window,
below the menubar. Below the stage is the control area, which provides three
buttons that start,
stop, and step through the animation. A slider in the lower right allows the user
to change the speed
of the animation from one frame per second to thirty frames per second. The menubar
along the top
of the bounce window contains two pulldown menu panes. The first pane provides an
Undo
command and a Quit command. The second pane lists the available actors that can be
added to the
animation.
Bounce
Each main area in the bounce window corresponds to a class derived from
UIComponent. The
stage area in the middle part of the bounce window is implemented as a Stage class.
This class not
only provides a drawing area but provides additional support for the Actor classes
that display
themselves on the Stage. The controls in the window’s bottom left are implemented
as a Control-
Panel class. A general-purpose Clock class provides a control that determines the
speed of the
animation displayed by the Stage.
The main window layout, including the menubar, is handled by the Bounce Window
class, a
subclass of MotifApp's MenuWindow class. All commands in the application, including
the start,
stop, and step commands, are based on classes derived from the Cmd class. This
allows the Undo
menu item to apply to all operations the user can perform within the program,
except changing the
clock speed.
Figure 12.2 shows a class inheritance diagram of the bounce program, including
those
CmdInterface ButtonInterface
ColorModel ColorChooser HSV View
i RGBController
itá RGBView
Application SwatchView
Stage
CmdList ControlPanel
Clock = BounceClock
ControlPanel
AskFirstCmd — WarnNoUndoCmd — QuitCmd
NoUndoCmd —— UndoCmd
RunCmd
AddBallCmd
StepCmd
StopCmd
Cmd
Only those classes shown in bold must be implemented for the bounce program.
MotifApp
provides the remaining classes. Of course, bounce does not use every MotifApp
class, and this
chapter. Although this class is created in response to the bounce program's need
for a clock, the
Clock class is designed to be reusable and can be added to the MotifApp framework.
BounceClock
is a subclass of Clock that is specific to the bounce program.
IR SODAS eee S
e SEL ST AAA
eC
OO OOOO EE Sa SPP ae OO On O
The Stage class provides an area in which individual animated graphical items can
be displayed. The
simplest implementation of the Stage class would just be an XmDrawingArea widget,
into which the
application could draw, as needed. This would place the burden of displaying each
frame, refreshing
the screen when Expose events occur, and so on, on some other part of the program.
For example,
each Actor placed on the Stage could handle refreshing itself as needed and could
also be responsible
for the graphical techniques necessary to produce smooth animation.
However, it requires only a little more effort to create a more powerful class that
provides
better support for the Actor objects displayed in the stage area. The Stage class
described here
makes it easy for Actor objects to produce animations by providing double-
buffering. Double-
buffering is a technique that uses two drawing canvases. At any given time, one of
these canvases is
visible, while the other remains hidden. All objects that will appear in the next
frame of the
animation must be drawn in the hidden canvas. When the entire scene has been
rendered, the entire
canvas is displayed at once. The previously visible canvas is hidden, erased, and
made available for
drawing the next frame. The result is a very smooth animation.
In the Stage class, these drawing canvases are implemented as pixmaps. ! A pixmap
can be
drawn to just as a window can, but it does not appear on the screen. However, the
contents of a
pixmap can be copied to a window. The Stage class provides two pixmaps that switch
between
serving as a back (hidden) buffer and a front (visible) buffer. Actors display
themselves in the back
buffer. The buffers are then switched. The new front buffer is copied to the Stage
window to display
the new frame. Whenever the Stage window needs to be redrawn, the current front
buffer can be
used to restore the contents of the window. Meanwhile, all Actor objects draw
themselves in the
back buffer to render the next frame. Because each pixmap buffer is erased and
completely redrawn
with each frame, Actor objects never need to erase themselves, nor do they need to
rely on XOR
drawing techniques to produce a smooth animation. Each Actor can simply draw itself
at the appro-
priate position for each frame.
2 // Stage.h
S SPATS RELIES LAILAI NAAA REA EOE PL LEE ETE EL EEL ARAN
4 tifndef STAGE_H
5 #define STAGE_H
6 #include "UIComponent.h"
7 #include "SimpleList.h"
9 class Actor;
10
12
13 public:
14
16 ~Stage ();
Le
18 virtual void nextFrame() ; // Move all actors to the next frame
19
22
25 protected:
26
35
36 private:
37
41 #endif
The Stage class’s public protocol consists of a constructor, a destructor, and two
methods for
adding and removing Actor objects from the Stage. The Stage class also supports a
public method
that moves the animation to the next frame. The private and protected protocol
includes a list of
Actor objects and various member functions and data members that support double
buffering.
The Stage constructor initializes all data members and creates an XmDrawingArea
widget that
serves as the visible Stage window. The constructor installs callbacks to handle
widget destruction,
resizing and exposure events, and then creates a graphics context to be used in
manipulating the
pixmaps. Before returning, the constructor calls the resize () member function,
which creates
pixmaps based on the current size of the class's base widget.
ow ON HU FP WD FP
10
LA
12
13
14
ES
16
ay
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
CEIM ERE RIETI AAA A BARRI ATA SN AAN RIERARA DIA AAA SIR AA ARS
tinclude "Application.h"
tinclude "Stage.h"
tinclude "Actor.h"
tinclude <Xm/DrawingA.h>
XGCValues gcv;
// Initialize all data members
NULL;
NULL;
front
_back
The Stage destructor frees the pixmaps and graphics contexts created by this class.
44 Stage::~Stage()
45 {
48 if f -Erone )
51 if ( back )
54 if ( _w && _gc )
56 )
The resizeCallback () function is a static member function that just calls the
virtual
resize() member function when the base widget's size changes.
58 XtPointer clientData,
59 XtPointer )
60 (
62
63 obj->resize();
64 }
The resize() member function creates the pixmaps used to implement double-
buffering.
Whenever the Stage window changes size, the old pixmap buffers must be destroyed
and replaced
by two new pixmaps, whose sizes match the size of the drawing area widget.
Although many Xlib functions work with either windows or pixmaps, pixmaps have no
inherent background color. Therefore, neither XClearWindow() nor XClearArea() can
be
used to erase a pixmap. Pixmaps must be cleared by calling XFillRectangle() to fill
the
pixmap with the desired color. Because the pixmaps used in this example are copied
to the Stage
class’s base widget, the pixmap is filled with the base widget’s background color.
65 void Stage::resize()
66 (
67
73 if ( _width || !_height )
74 return;
75
TA
78 Lf.1{. trent )
80
81 1£ { back )
83
85
86 _back = XCreatePixmap ( XtDisplay ( w ),
87 DefaultRootWindow ( XtDisplay ( w ) ),
88 _width, _height,
89 DefaultDepthOfScreen ( XtScreen ( w ) ) );
90
92 DefaultRootWindow ( XtDisplay ( w ) ),
93 _width, _height,
94 DefaultDepthOfScreen ( XtScreen ( w ) ) );
95
97
100
103. }
The Stage class supports animation by displaying the next frame on demand. The
nextFrame() member function calls swapBuffers() to display the frame currently
drawn
in the _back buffer. Then, it sends a nextFrame () message to each registered Actor
object. The
Actor class's nextFrame () method expects a drawable (a Window or pixmap) and the
current
size of the stage. Notice that the Stage: :nextFrame() member function passes the
_back
pixmap as the drawable. The Actor objects drawn during any given frame will not be
visible until
the next frame.
105 (
106 // For each new frame, simply swap buffers and have each
1907 // Actor object draw its next frame in the back buffer
108
| 109 swapBuffers() ;
| 110
de ge for { ant i = 07 oe ¿cast eivet) + es)
112 _cast[i]->nextFrame ( _back, _width, _height );
LES. Y
The swapBuf fers () member function is very simple. If the Stage object’s base
widget is
not realized, the function does nothing. If the widget has been realized, this
function swaps pointers
to the pixmap buffers and copies the previously hidden pixmap into the drawing
area. The new
back buffer is erased by filling it with the drawing area widget's background
color, thus making it
ready for the next frame.
115 {
117
119 {
121
123
127
133 // Erase the new back buffer to get ready for the next scene
134
138 }
The double-buffering technique used by the Stage class allows bounce to handle
Expose
events easily. When the Stage window needs to be redrawn, the redisplayCallback ()
function is called. This function calls the virtual function redisplay (), which
restores the
window’s contents by recopying the contents of the current _f ront buffer to the
XmDrawingArea
widget's window.
142° 4
144 obj->redisplay();
SACRA Stn IS
A y ——
A pobre et
A Soles sae E AS Se
Dia
me
LS ary A, UI a phe ta S ad
QS eS BAL LA 2
oe
26 EI A
wha fase
SE SE | AA a
ee eS LS ve
E e Se: i
150
154 )
The final two member functions supported by the Stage class allow Actor objects to
be added
and removed from the Stage. The addActor () member function simply adds a new
object to the
_cast list. The remove Actor () function performs the opposite operation, removing
the
specified Actor object from the _cast list.
156 {
157 _cast.add ( newActor );
158 )
The animation displayed by bounce requires the Stage: :nextFrame () member function
to be
called repeatedly, at a fairly rapid rate. Although thirty frames per second is a
common speed that
produces a smooth animation, users may also want to control the speed of the
animation. The bounce
program provides a slider control that allows the user to change the rate of the
animation from about
one frame per second to the full thirty frames per second required for a smooth
animation.
————
uo EEEEEEEEOEOEOEOEeEO
The Clock class is an abstract class that calls a pure virtual member function
implemented by a
derived class at a rate determined by the user. The Clock class is derived from
UlComponent and
provides a slider that allows the user to change the clock rate within a fixed
range. The minimum
and maximum rates supported by a Clock object can be specified as arguments to the
Clock
constructor. In addition to its interactive user interface, the Clock class also
supports a programmatic
interface that allows the clock to be started and stopped.
The clock can also be forced to issue a single pulse programmatically. This is
useful because it
allows applications to control each tick of the clock, if needed. For example, the
bounce program
uses this feature to allow the user to step though an animation frame by frame.
Rather than
bypassing the clock to step the Stage through individual frames, bounce always
allows the Clock to
control the animation. However, the control panel allows the user to control the
Clock manually, if
desired.
Most of the members supported by the Clock class are for internal use only and are
declared to
be private. The public interface consists of the constructor and destructor, plus
methods for starting,
stopping, and single-stepping the Clock. The subclass protocol consists of a single
pure virtual
member function that must be defined by derived classes.
2 ## Clock:h
3 LAREET ALLEL TA LTA TAA TLL AT LEAL APIT EAL LL ASL AAA SAA AMAS LE
4 #ifndef CLOCK_H
5 #define CLOCK_H
6 #include "UIComponent.h"
10 public:
a Fe l
16
22 protected:
23
25
26 private:
27
30
34 // Xt Callbacks
35
39 #endif
The Clock constructor initializes various data members and creates an XmScale
widget that
allows the user to control the clock rate. After checking for a valid lower bound
on the time range,
the constructor calculates an initial speed that lies midway between the given
minimum and
maximum clock rates.
The Clock class uses XtAppAddTimeOut () to generate each clock pulse. This function
requires an argument that specifies the time in milliseconds between clock pulses.
This number is
computed on line 26 and stored in the _delta member, because the timeout function
must be
reinstalled each time it is called.
3 LELLI PILI LALA LAE ILII ARA ARA TI TAL ETAT ATLETI IEEE PAT Aa
4 #include "Clock.h"
5 #include <Xm/Xm.h>
6 #include <Xm/Scale.h>
cd int middle;
12
14
16
19 if ( minFPS < 1 )
18 minFPS = 1;
19
22
25
30 XmNmaximum, maxFPS,
31 XmNvalue, middle,
32 XmNshowValue, TRUE,
33 NULL );
34 installDestroyHandler ();
35
38 XtAddCallback ( _w,
39 XmNvalueChangedCallback,
40 &Clock: :speedChangedCallback,
41 (| XtPointer ) this );
42 }
43 Clock: :~Clock()
44 {
45 17 ELA
46 XtRemoveTimeOut ( _id );
47 }
The start () member function must be called programmatically to start the Clock.
This
function adds the timeoutCallbackFunction() as a timeout callback. Notice that
XtAppAddTimeOut () requires an XtAppContext structure as its first argument. The
Appli-
cation class supports a member function that can be used throughout MotifApp when
an
XtAppContext is needed. However, the Clock class supports a widget from which this
structure
can readily be retrieved, using the Xt function XtWidgetToApplicationContext ().
There
is no reason to create unnecessary dependencies, and using this function allows the
Clock class to
be independent of the rest of the MotifApp framework.
48 void Clock::start()
49 {
51
52 id = XtAppAddTimeOut ( XtWidgetToApplicationContext ( w ),
53 _ delta, &Clock::timeoutCallback,
54 ( XtPointer ) this );
55 )
Xt calls the timeoutCallback() function when _delta milliseconds have expired. Like
most callbacks, this function just retrieves the object pointer and calls a
corresponding member
function, timeout ().
The timeout () member function calls the tick () member function, which must be
imple-
mented by a derived class. Then, tick () reinstalls the tickCallback () function to
be called
again in _delta milliseconds.
61 void Clock: :timeout ()
62 +1
64
67 ( XtPointer ) this );
68 )
The stop () member function removes the timeout, if one is currently active. The
Clock class
uses the _id member to indicate whether the clock is currently running. Therefore,
after removing
the timeout, stop () sets the _id member to NULL.
70 {
71 LE O E YE
72 XtRemoveTimeOut ( _id );
73 id = NUL;
74 }
The pulse () member function calls the tick () member function provided by derived
classes. Calling pulse () does not involve a timeout but merely triggers a “tick”
immediately.
This function can be called whether the Clock is currently running or not. It has
no effect on the
regularly scheduled clock tick.
TE 4
77 tickt);
TB. 4
The speedChangedCallback () function is called whenever the user changes the value
of the Clock’s XmScale widget. This function retrieves the object pointer for the
Clock object and
Driving the Animation 407
calls the object’s speedChanged () member function with the current value of the
XmScale
widget.
80 XtPointer clientData,
81 XtPointer callData )
82 {
85
86 obj->speedChanged ( cb->value );
87 )
The speedChanged () member function computes a new value for _delta, based on the
new clock rate. If the clock is running, as indicated by a non-NULL _id member,
speed-
Changed () removes the current timeout and adds a new timeout callback with the new
value.
89 (
92
93 LE 1, 34 )
94 {
95 // Remove the old timeout and set a new one using the
96 // new interval (Note that there may be a glitch before
97 // the first call, depending on how much of the old
98 // time has already elapsed)
99
101
105 }
106
107 }
Figure 12.3 shows the completed Clock user interface component, as used in bounce.
Rae
do
Sat
sil]
y
|
(788
5 é
Fe
ye
K?
J
$
Now we can look at a class derived from Clock that drives the bounce program’s
Stage object. The
BounceClock class just defines the pure virtual tick() member function and sends a
nextFrame () message to the Stage object with each clock pulse.
The BounceClock class is declared as follows:
1 ESTARIA LECE EGE GEL IPT EEL TEP EET EAL LEALE LELLES ELUI TEAL ETI ELT
2 // BounceClock.h
3 LER EERISA PLETAL LE LE AAA MAR AITANA EAE A ECT RTL III RINA LP CARITAS
4 #ifndef BOUNCECLOCK_H
5 #define BOUNCECLOCK_H
6 #include "Clock.h"
7 class Stage;
10
11 public:
15 protected:
a7
18 private:
21 Htendif
Like all classes derived from UlComponent, the BounceClock constructor requires two
basic
arguments: a widget to be used as the parent of the user interface component and a
name. In
addition, the BounceClock constructor requires a pointer to the Stage object it is
to control. The
constructor passes the parent widget and the name on to the Clock constructor,
along with minimum
and maximum clock rates. The constructor stores a pointer to the Stage object for
later use.
ELTATIAIE SIT I AAA A LETITIA ERA ARA RABIA AA ASA A IS A EA RA AAA AAA TEI TALS
// BounceClock.C: The clock that controls the animation in bounce
ELIIRAEMINATESTAT FAA AAA NANA PTA TRIANA SARNA ANT TARADO AAA AA ASA ILLE
#include "BounceClock.h"
tinclude "Stage.h"
DN 10 ss awn A UY NN >
Pp
o
_stage = stage;
—
—
we
The tick () member function is called by the Clock base class with each pulse of
the clock.
This function just calls the nextFrame () member function for the Stage object
controlled by this
clock.
13 4
14 _stage->nextFrame() ;
ES: 3
Figure 12.4 shows a message diagram that charts the interactions between the Stage,
Clock,
and BounceClock classes.
swapBuffers
redisplay
resize
speedChanged
nextFrame
BounceClock
timeout
The RunCmd class is derived from Cmd and supports both doit () and undoit () member
functions. The RunCmd class maintains a pointer to a Clock object. Executing the
RunCmd calls
this Clock object's start () member function, and undoing the command calls the
Clock’s
stop () member function.
The RunCmd class is declared as follows:
1 PAPI ETE EEL AAA AAA AEREA AAA AAA IAB AI EE TENETE ERRATE E E
2 // RunCmd.h
3 FAITES ELIF ALIIT EIA EELA SSCL STOLE LL TE ER EE OES Lae ea:
4 #ifndef RUNCMD_H
5 #define RUNCMD_H
6 #include "Cmd.h"
8 class Clock;
11
12 public:
L3
15
16 protected:
EJ
22 Fi
23 #endif
The RunCmd constructor requires a pointer to a Clock object, which is stored for
later use. The
doit () member function calls the start () member function for this Clock object,
and the
undoit () member function calls stop () for the same Clock object. Notice that the
RunCmd's
undoit () member function is based on the assumption that the clock is in a stopped
state before
the RunCmd’s execute () function is called. This is a reasonable assumption,
because the Clock
object has only two states: stopped or running. In more complex cases, a Cmd object
like RunCmd
might need to query the object it controls to determine its previous state to
support undo.
a} CREF ELE SS EEA TL AITE CERIS EALLA LILLE IF ETAT TIA PATI ILA I
2 // RunCmd.C
4 #include "RunCmd.h"
5 #include "Clock.h"
e=
—
tw
13 {
14 _clock->start(); // Start the animation
15 }
17 (
The StopCmd class is very similar to the RunCmd class. StopCmd is derived from Cmd
and supports
both doit() and undoit() member functions. The StopCmd class is the opposite of the
RunCmd class. It calls the Clock object's stop () member function when the command
is executed
and calls the Clock’s start () member function when the command is undone.
The StopCmd class is declared as follows:
1 PLEA RIE ELIA ELT AAI SEA A ARA A ANA AAA TEE ELVEIT ARANA
2 // StopCmd.h
3 EEPESAAESAAA A TELE TEE TL AA ALEC ELE CELI TOL EL ETT TIT OLE 2
4 #ifndef STOPCMD_H
5 #define STOPCMD_H
6 #include "Cmd.h"
8 class Clock;
a Ne!
12 public:
13
16 protected:
17
18 Clock *_clock;
19
22 F?
23 #endif
The StopCmd constructor stores a pointer to a Clock object. The doit () member
function
stops the associated Clock object, and undoit () restarts the clock.
1 PRET EELE EA RATA ARA AAA RA AAA AAA AAA ERT IAN ET ET CERA
2 // StopCmd.C
3 DACIAAA AA AAA AAA ASIAN AA EEE ELTA EELS PA AAA AGL EEG FETT AA
412 Chapter 12 A MotifApp Application
4 #include "StopCmd.h"
5 #include "Clock.h"
10 Clock. = clock;
We
de
15...)
e D a |
19 }
The StepCmd class does not support an undoit () member function and is therefore
derived from
the NoUndoCmd class. Neither the Clock class nor the Stage class support moving
backward, so
there is no way to undo a clock pulse. Like the other commands, the StepCmd class
maintains a
pointer to a Clock object. The StepCmd: :doit() member function calls the
associated Clock
object's pulse () member function when the command is executed.
OD Oo JO Una WD P
LEMA FIAR IA AAA AA AAA RARA TEL CHITA AA ERES ERA NAS
// StepCmd.h
LITE LERPELL aE TFI ET ETL IL EELS LAT ITS AAA TATIANA NADA PLETE
#ifndef STEPCMD_H
#define STEPCMD_H
tinclude "NoUndoCmd.h"
class Clock;
class StepCmd : public NoUndoCmd {
public:
protected:
Clock * clock;
virtual void doit();
y;
tendif
Oo O N HD U0 PWD P
h ua p
NRO
13
14
i5
16
ESTAN A ELT FEEL DAA ELPA GET ALL AAA TT TITEL AL TIAL ES ATEN GT
// StepCmd.C
SILEF ELEL FELL EEE AI AAA ES CETL EEL AT ATI RAR FUSS TS AAA IMA AER ES
#include "StepCmd.h"
#include "Clock.h"
int active,
Clock *clock ) : NoUndoCmd ( name, active )
{
_clock = clock;
}
void StepCmd: :doit()
{
_clock->pulse() ; // Cause the clock to issue a single tick
}
The ControlPanel class implements the row of buttons in the lower left of the
bounce window, as
shown in Figure 12.1. The ControlPanel class instantiates each of the command
classes described in
the previous section and also creates a ButtonInterface object for each command
object. The widgets
supported by the ButtonInterface objects are managed by an XmRowColumn widget.
Chapter 9
demonstrated how the Cmd and CmdInterface classes could be used to create menu
items, but these
classes can also be used in many different situations. Here, they simply create a
row of buttons that
The ControlPanel class is derived from UlComponent and supports only a constructor.
The
wo on aun fF WN PF
EELS ELI LA LAL LID ETL TE CES CET AI LEAR PLAT EA ELIT EA TALL EEL OLE I ACE A
// ControlPanel.h:
FLEES SEE EF LIL TEPID TEEPE AAA IATA TIAS IIA EEL ET TS ETE TR EFT
#ifndef CONTROLPANEL_H
#define CONTROLPANEL_H
#include "UIComponent.h"
class Clock;
class ControlPanel : public UIComponent {
pubiic:
ControlPanel ( const char *, Widget, Clock *clock );
virtual const char *const className() { return ( "ControlPanel" ); }
13
#endif
The ControlPanel controls the BounceClock object, which in turn drives the Stage
object. The
ControlPanel constructor requires a pointer to the Clock object to be controlled,
as well as the usual
parent widget and name string. The constructor creates an XmRowColumn widget to
hold a row of
button widgets, and then creates an instance of each command class described in the
previous
section. The Clock object, given as an argument to the ControlPanel constructor, is
passed to each
of these command objects.
The bounce program enables and disables commands in the ControlPanel depending on
the
actions available to the user at any given time. The RunCmd and StepCmd objects are
initially
active, and the StopCmd object is inactive because the Clock is stopped when the
program begins.
The ControlPanel constructor also specifies various dependencies between the
commands to allow
the Cmd architecture to activate and deactivate these commands automatically.
Finally, the ControlPanel constructor creates a ButtonInterface object for each Cmd
object.The
ControlPanel’s base widget, an XmRowColumn widget, arranges these buttons in a
single row.
1 ESCENAAAA ARIES ET EIDE ALAA AL NE ETL ILE AAA ANA NAAA TANIA FL NA IAN
2 // ControlPanel.C
4 tinclude "ControlPanel.h"
5 #include "ButtonInterface.h"
6 #include "Clock.h"
7 #include "RunCmd.h"
8 #include "StopCmd.h"
9 #include "StepCmd.h"
10 #include <Xm/RowColumn.h>
11
12 ControlPanel::ControlPanel ( const char *name,
13 Widget parent,
18
23 XmNnumColumns, 1,
24 XmNorientation, XmHORIZONTAL,
25 NULL );
26 installDestroyHandler () ;
27
35
42
43 runCmd->addToActivationList ( stopCmd );
44 runCmd->addToDeactivationList ( stepCmd );
45 runCmd->addToDeactivationList ( runCmd );
46
47 stopCmd->addToActivationList ( runCmd );
48 stopCmd->addToActivationList ( stepCmd );
49 stopCmd->addToDeactivationList ( stopCmd );
50
51 stepCmd->addToActivationList ( runCmd );
52 stepCmd->addToDeactivationList ( stopCmd );
53
56
60
61 runBtn->manage ();
62 stopBtn->manage () ;
63 stepBtn->manage ();
64
65 3
Figure 12.5 uses a message diagram to show the connections between the objects
contained in
the ControlPanel and the Clock classes.
BounceClock
stop
StopCmd
StepCmd
addToActivationL ist
addToDeactivationL ist
addToActivationList
addToDeactivationList
2
E
a
>
i
=
E
addToActivationList
ControlPanel
While “lines of code” is far from the only consideration, the benefits of a
general-purpose
Panel class do not seem to be overwhelming, at least for the bounce program. Still,
there could be
advantages to implementing assorted command and control panels based around the Cmd
and
CmdInterface architectures. Groups of related applications often have similar needs
that can be
addressed by such a Panel class. For example, related applications may have similar
customizable
parameters. A PreferencePanel class that supports a common interface for setting
user preferences
could be very useful in such situations. A general PreferencePanel would be useful
in this case
because it is possible to support application-specific features that go beyond the
capabilities of a
generic widget set like Motif. A PreferencePanel class could support an
architecture for applica-
tions that need to provide user-settable preferences and could enforce both
behavioral and visual
consistency across the applications that use the component.
Actors 417
12.5 Actors
AAA IE AMM
This section introduces classes that represent graphical items that can be
displayed in the stage area
as elements of an animation. The Actor class is an abstract base class that defines
a basic protocol
for all objects that can be added to a Stage. In addition to defining a protocol
for derived classes, the
Actor class automatically registers and unregisters all instances with a Stage
object, as needed. The
protected portion of the Actor class maintains a pointer to the Stage object in
which an Actor object
is displayed.
The BouncingBall class is derived from Actor. The BouncingBall class draws a filled
circle
that moves across the Stage in a randomly chosen speed and direction. When a
BouncingBall
object encounters a Stage boundary, it bounces off in a new direction.
The Actor class declares a pure virtual member function, nextFrame (), that must be
imple-
mented by all derived classes. The Stage object calls this member function to
request each Actor
object to draw itself in the next frame. The Stage passes a Drawable in which the
Actor should render
itself. Some Actor objects may find it useful to know the size of the Stage, and
therefore
1 CSEPAANSAMIE PREM ELL EET LIP TESI ALIAE AAA FSD LAT EP ITI GAIL I IIS
2 // Actor.h: Abstract base class for all Actor objects
3 EXAMAAAAAAAR SIS IA LATAS IES AAAI ES ETA BO E RENA Tttt PATI EAT
4 #ifndef ACTOR_H
5 #define ACTOR_H
6 #include <Xm/Xm.h>
7 class Stage;
9 class Actor {
10
11 public:
12 Actor ( Stage * );
13 virtual ~Actor();
14
16
19 protected:
22 #endif
The Actor constructor saves a pointer to the given Stage object and registers the
Actor object
with the Stage object. Derived classes may be able to use the _stage member, but it
is meant
primarily for the Actor destructor. The destructor must remove the deleted object
from the list of
Actors maintained by the Stage, to prevent the Stage object from referencing a
pointer to a freed
object.
1 FEIL TA IILI IIIT NAAA ETT RINA IAS ER ANA LAA ETA LEE
2 // Actor.C: Abstract base class for all "Actor objects
3 PIEEREIE II AII TS IA LATA TALES ALIA A ELTAAA RIA LALA AT ETD ALLEL ELT
4 #include "Actor.h"
5 #include "Stage.h"
8 {
9 _stage = stage;
10
H
tO
13 Actor::-Actor()
14 (
15 _stage->removeActor ( this ); // Remove this object from the Stage
16 )
The bounce program supports only one type of Actor, a “bouncing ball.” The
BouncingBall class is
derived from the Actor class and defines the behavior of a graphical object that
can appear in the
bounce stage area. The BouncingBall class displays a small colored circle that
moves across the
stage at a random velocity. When a BouncingBall object encounters the boundary of
the Stage, it
changes direction, simulating a ball that bounces off a wall.
#define BOUNCINGBALL_H
tinclude "Acter.n"
#include "Xm/Xm.h"
wow O JA UU + WN FP
Pp
©
class Stage;
—
H
Actors 419
13
14 public:
ES
19 protected:
20
24
25 // Called from the ColorChooser when the user has picked a color
26
28 int green,
29 int blue,
30 void *clientData );
31
The BouncingBall constructor initializes the size, color, and initial velocity of
each ball. The
size of each object is determined by a hard-coded parameter, defined here as 25
pixels. The initial
velocity is based on the value returned by drand48 ( ). This function returns a
floating-point
number between 0.0 and 1.0. The initial velocity of each BouncingBall is computed
by multiplying
this random number by the size of the object.
The _bounds structure stores the current position of the object between frames.
This infor-
mation is used to detect collisions between an object and the sides of the stage
area and to compute
the object’s position in each frame. Each ball is initially placed in the upper
left corner of the stage.
in Chapter 11 to allow the user to choose a color. Because the ColorChooser uses a
callback model,
the ball must not create a graphics context or render itself until the user has
chosen a color. Setting
the _gc member to NULL provides a way to check whether the object can be drawn
while waiting
for the user to select a color.
2 // BouncingBall.c
3 FLELL IIRL ETT TAL EDEL IIIA TAL TA TPA I AA ARA ALAAAAAR IRA I Ai
4 #include "Application. h"
5 #include "Stage.h"
6 #include "BouncingBall.h"
7 #include "ColorChooser.h"
8 #include "InfoDialogManager.h"
9 #include "libc.h"
10 #define SIZE 25
11
ee |
16
19
23 _bounds.x = _bounds.width + 1;
24 _bounds.y = _bounds.height + 1;
25 _gc = NULL;
26
30 if ( colorName )
31 {
32 XGCValues gcv;
37
41 &color, &ignore ) )
42 gcv.foreground = color.pixel;
43 else
45
49 else
Actors 421
50 {
54 colorChooser->pickColor ( &BouncingBall::colorSelectedCallback,
55 &BouncingBall: :canceledCallback,
56 (vota * ) this ja
57 )
58 )
The nextFrame () member function draws the ball in a new position each time it is
called.
The function begins by checking for a valid _gc member. If _gc is NULL, nextFrame()
returns. Otherwise, nextFrame () updates the current position of the ball and
checks this object's
bounds to determine if the ball has hit the side of the Stage area. If so, the
velocity is recomputed to
make the ball appear to bounce off the side of the Stage. In any case, the ball is
drawn in its new
position. Recall that the drawable passed to all Actor objects is a pixmap, which
is currently the
Stage object's “back” buffer. Therefore, the ball drawn by this method does not
appear until the
next frame.
60 Dimension width,
61 Dimension height )
62 (
65
67
69
T2
74 {
76 _delta.x = -_delta.x;
77 }
80 _bounds.x = 0;
81 delta.x = -_delta.x;
82 }
83
86
87 if ( _bounds.y + _bounds.height >= height )
88 {
90 _delta.y = -_delta.y;
91 }
94 _bounds.y = 0;
95 _delta.y = -_delta.y;
96 }
97
102°. 3
The remaining BouncingBall methods work with the ColorChooser to allow the user to
choose
an object’s color interactively. The ColorChooser calls colorSelectedCallback ()
when the
user has chosen a color. This function passes the red, green, and blue components
of the chosen
color to colorSelected () for further processing.
110) 3
The colorSelected() member function tries to allocate a color based on the red,
green,
and blue color components provided by the ColorChooser. This function fills in the
members of an
XColor structure, scaling the 0-255 range of values provided by the ColorChooser to
the 0-65535
range expected by X. Then, colorSelected() calls XAllocColor() to allocate the
color
and determine a pixel value that can be used to create a graphics context. If the
color allocation
fails, the system’s default black pixel is used instead. Once a valid pixel is
available, the _gc
member can be initialized by calling XtGetGC (). Notice that the base widget of the
Stage object
is used for the call to XtGetGC (). Also, the BouncingBall class takes advantage of
the global
theApplication object to retrieve a display to be passed to the various Xlib
functions. |
Actors 423
121
t34 3}
Figure 12.6 shows the connections between the BouncingBall class and other closely
related
classes.
addActor
Actor Application
baseWidget display
Stage
pickColor
a colorSelectedCallback
BouncingBall ColorChooser
canceledCallback
InfoDialogManager
Figure 12.6 A message diagram of BouncingBall and its related classes.
The bounce program allows the user to add new Actor objects by choosing a command
on a
pulldown menu. This requires one more command class, an AddBallCmd class. The
AddBallCmd
class creates a BouncingBall object when itis executed. The AddBallCmd is derived
from Cmd and
supports an undoit () member function, which just deletes the BouncingBall object
created in the
doit () member function.
The AddBallCmd constructor requires both a pointer to a Stage object and the name
of a color
to create a BouncingBall object.
BEEPETAPEAL IIA A A NINA RRA NA SERIA RITE ALT ELTA TA TAIRA LAS
// AddBallCcmd.c
ARANA TRAS AAA NARA A AAA AL TA RARA NARA PVA AAT AAPA RRA
#include "AddBallCmd.h"
#include "BouncingBall.h"
© DOAN HM BW bw Pp
pt
©
is Y
12 _stage = stage;
13 _color = color;
14 _ball = NULL?
i 4
ss |
18 _ball = new BouncingBall ( _color, _stage );
19 +}
The undoit () member function deletes the ball created by this object and sets the
object
pointer to NULL as insurance against future references. The Actor destructor
removes the object
from the Stage.
24 1
22 delete _ball;
23 _ball = NULL;
24 }
Previous sections in this chapter have discussed all the key elements of the bounce
program. All that
remains is to create the top-level window that contains and connects these
elements. The
Bounce Window class is a subclass of MenuWindow. The Menu Window class
automatically creates
the menubar used by bounce. The BounceWindow class arranges the Stage,
ControlPanel, and
BounceClock components in the window’s work area. The Bounce Window class maintains
pointers
to these three major elements of the application in the private portion of the
class. The protected
portion supports the pure virtual functions, createWorkArea () and createMenuPanes
(),
which must be implemented by classes derived from MenuWindow. The public protocol
consists of
the constructor and destructor.
The Bounce Window class is declared as follows.
wort nw & WD FP
e He
e oO
ELLA REA AENA RTS IAL CA AAA RANA AAA A AA REZA AEVIAA AAA RELE RRA
// BounceWindow.h:
FEESATAEIAA TIA ASARA AA RANA AAA NAL IA DAA TASA AARAA AAA E I EI
#ifndef BOUNCEWINDOW_H
#define BOUNCEWINDOW_H
#include
"MenuWindow.h"
class Clock;
class Stage; ,
class ControlPanel;
class BounceWindow :
public:
public MenuWindow {
protected:
virtual void
private:
Clock
Stage
createMenuPanes ();
*. clock;
* stage;
ControlPanel *_controlPanel;
+7
#endif
The BounceWindow constructor initializes all data members to NULL after calling the
PwowmoN HU &WD P
"Application.h"
"BounceWindow.h"
"Stage.h"
"ControlPanel.h"
"BounceClock.h"
"QuitCmd.h"
"UndoCmd.h"
"CmdList.h"
"AddBallCmd.h"
"MenuBar.h"
<Xm/Form.h>
<Xm/Separator.h>
18 Clock = NULL;
19 _stage = NULL;
20 _controlPanel = NULL;
21. +
22 BounceWindow: : -BounceWindow ()
aS 1
24 delete _clock;
25 delete _stage;
26 delete _controlPanel;
27 )
The createWorkArea () member function handles the creation of the other components
of
the bounce window and sets up the layout shown in Figure 12.1. This function must
return a single
widget that is installed as the work area widget for the XmMainWindow created by
the
MainWindow class. The BounceWindow creates an XmForm widget that serves as the work
area
widget and arranges all other components of the interface as children within that
form.
29 {
35
37
48 XmNrightPosition, 50,
49 XmNrightAttachment, XmATTACH_POSITION,
50 XmNbottomAttachment, XmATTACH_NONE,
51 NULL );
52
54 XMNtopAttachment, XmATTACH_NONE,
55 XmNleftPosition, 50,
56 XmNleftAttachment, XmMATTACH_POSITION,
aT XmNrightAttachment, XmATTACH_FORM,
59 NULL );
60
61 Widget sep =
62 XtVaCreateManagedWidget ( "sep",
63 xmSeparatorWidgetClass,
64 form,
65 XmNleftAttachment, XmATTACH_FORM,
66 XmNrightAttachment, XmATTACH_FORM,
67 XmNtopAttachment, XmATTACH_NONE,
68 XmNbottomWidget , _clock->baseWidget (),
69 XmNbottomAttachment, XmMATTACH_WIDGET,
70 NULL );
V4.
ae XtVaSetValues ( _stage->baseWidget(),
73 AmNtopAttachment, XmATTACH_FORM,
74 XmNleftAttachment, XmATTACH_FORM,
oS XmNrightAttachment, XmATTACH_FORM,
76 XmNbottomWidget, sep,
79
81
82 _controlPanel->manage();
83 _Stage->manage() ;
84 _clock->manage () ;
85
86 Fecturn ( form );
87 3
The MenuWindow class calls createMenuPanes () to add items to the menubar. The
Bounce Window class adds two menu panes to the menubar. The first contains the
object theUn-
doCma, described in Chapter 8, and the “user friendly” QuitCmd described in Chapter
9. This
menu pane is labeled as the “Application” pane. The second pane allows the user to
add new Actors
to the Stage. In this example, createMenuPanes () creates four instances of the
AddBallCmd
class. The first three are preselected to use the colors red, green, and blue, and
the fourth allows
users to choose a color interactively. Each of these commands is added to a CmdList
and placed in
a second menu pane, labeled “Actors.”
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
e E
118
ELA
120
// Create the main application menu with just a quit and undo cmd
CmdList *cmdList
Cmd *quit
= new QuitCmd (
cmdList->add ( theUndocCmd );
cmdList->add ( quit );
_menuBar->addCommands ( cmdList );
"Quit" ) ;
Cmd *addRed =
Cmd *addGreen
Cmd *addBlue =
Cmd *addAny =
actorList->add
actorList->add
actorList->add
actorList->add
(
(
(
(
new AddBal1Cmd
new AddBal1Cma
new AddBal1Cma
new AddBal1Cmd
addRed );
addGreen );
addBlue );
addAny );
( ®*Actoers*
stage,
TRUE,
_stage,
_stage );
_menuBar->addCommands ( actorList );
associated classes.
"Green" );
"Add Blue Ball",
"Blue" Jo
"Add Ball...",
TRUE,
TRUE,
TA
TERA
EA
ORT
ARA
A a lt
DERROTA
VERS
ATINA
POEL STA
STATE
A
Mens nn o
ST AISI BOERS a O
EN
=a DE MainWindow
= E Stage
ai
S| 2
MenuBar E a 3
a E op
5 o =
e)
addCommnds F
CmdList
| p Bounce Window
BounceClock
AddBallCmd ControlPanel
Ad ~
2 5h
pasad "9
O —
D z
Si $
© Oo
[aa]
AddBallCmd
manage
base Widget
ControlPanel
delete
Now we can build and run the bounce program. First, we must create the “main body”
of the
program. Like all MotifApp programs, the body of the program is extremely simple.
The file
BounceApp.C instantiates an Application object followed by a BounceWindow object.
2 // "Main" program
3 IMITA AAA RANA ELA AAT AAS TT AAT AA LAT TADA ESSE
5 #include "BounceWindow.h"
The bounce program can be built by compiling all the classes described in this
chapter and
linking them with the MotifApp library. The Clock class should be added directly to
the MotifApp
library first. The following commands can be used to build bounce.
The program can now be tested by typing “bounce” in a terminal window. To start the
animation, the clock can be started by clicking on the Run button, and “actors” can
be added by
choosing an entry from the Actors menu pane. The Undo command on the Application
pane allows
the user to undo the most recent command, whether the command was to add a new
actor to the
stage area or to stop or start the animation. Changes to the speed of the animation
cannot be
undone.
Another interesting experiment is to modify the file BounceApp.C to create multiple
Bounce Window objects. Just like the “Hello World” example in Chapter 6, this is
very easy to do.
5 #include "BounceWindow.h"
settings. For example, the following application resource file produces the bounce
window shown
Figure 12.1:
3 PRE RVERTT LEERY EET EL LOPE PERE LEP ESEET LEDER ae TERA
5 Bounce*fontList: -*-helvetica-bold-r-normal--14-*-*-*-*-*-1s08859-1
6 Bounce*quit*labelString: Quit
7 Bounce*XmScale*orientation: horizontal
9 Bounce*stage*width: 400
10 Bounce*stage*height: 250
In Figure 12.1, the ControlPanel buttons are shown as icons. This can be done very
easily by
creating a bitmap image, using the bitmap program available on most X systems. Once
these
bitmaps have been saved to a file, they can be displayed by setting resources that
name these files as
the XmNlabel Pixmap resource for the ControlPanel buttons. For example:
11 Bounce*Run*labelType: pixmap
12 Bounce*Run*labelPixmap: run
13 Bounce*Run*labelInsensitivePixmap: runlI
14
15 Bounce*Stop*labelType: pixmap
16 Bounce*Stop*labelPixmap: stop
17 Bounce*Stop*labelInsensitivePixmap: stopI
18
19 Bounce*Step*labelType: pixmap
20 Bounce*Step*labelPixmap: step
21 Bounce*Step*labelInsensitivePixmap: stepI
Figure 12.8 shows the widget tree formed by the bounce program.The gray areas show
some of
the component boundaries. The largest area shows the widgets controlled by the
BounceWindow
class, which includes the menubar and the entries in the menu. The ControlPanel,
Stage, and
BounceClock classes encapsulate subparts of the overall widget tree.
bounce
ApplicationShell
Bounce
ApplicationShell
mainWindow
XmMainWindow
menubar workArea
XmRowColumn
popup_Application Application Actors S control clock ‘sta
p ge
XmMenuShell XmCascadeButton XmSeparator XmRowColumn XmScale XmDrawingArea
XmPushButton XmPushButton
Summary 433
12.9 Summary
For example, imagine a framework that supports programs that display animations,
like
bounce. Such a framework could start with many of the elements present in both
MotifApp and the
bounce program. However, this framework could provide a more powerful way for the
programmer
to control various aspects of the animation. For example, one might start by
replacing the simple
clock-driven approach with a mechanism that allows more flexibility. Perhaps some
type of Score
class that allows applications to describe each step of the animation in arbitrary
units of time could
be designed.
As a framework supports more and more specific features, it becomes less useful for
applica-
tions outside the domain for which it was designed. The animation framework
described above
would probably not provide the right kind of support for a word processor. However,
it would be
possible to design frameworks to support wordprocessing, graphical editors,
spreadsheets, and
many other types of applications.
Using an appropriate application framework can reduce the time spent designing,
developing,
and maintaining most applications. A good framework can also promote consistency
across a set of
applications. A framework can provide structural consistency, which helps the
programmers
working on a system, as well as visual and behavioral consistency, which benefits
the eventual user
of any application.
Bibliography
eee
Asente90 Asente, Paul, and Ralph Swick, The X Window System Toolkit, DEC Press,
1990.
Beck89 Beck, Kent, and Ward Cunningham, “A Laboratory for Teaching Object-Ori-
ented Thinking,” OOPSLA *89 Conference Proceedings, 1989.
Booch89 Booch, Grady, “What Is and Isn't Object-Oriented Design,” American Pro-
grammer, Vol 2 (7,8),, Summer, 1989.
Coad90 Coad, Peter, and Edward Yourdon, Object-Oriented Analysis, Prentice Hall,
1990.
Foley82 Foley, J.D., and A. Van Dam, F undamentals of Computer Graphics, Addi-
son-Wesley, 1982.
George94 George, Alistair, and Mark Riches, Advanced Motif Programming Technqi-
ues, Prentice Hall, 1994
NAS
AO dE
A A
O E NE Re
RA PLUEN
ES aE ie Sate |
A ee eh
e
AS
> ee EN Da
PAN
A a a) A o do
A A A E TAN DAA DE
436
Gorlen90 Gorlen, Keith, Sanford M. Orlow, and Perry S. Plexico, Data Abstraction
and
Object Oriented Programming in C++, John Wiley and Sons, 1990.
Jones89 Jones, Oliver, Introduction to the X Window System, Prentice Hall, 1989.
Linton89 Linton, Mark A., John M. Vlissides, and Paul R. Calder, “Composing User
Interfaces with InterViews,” JEEE Computer, Vol. 22(2), February, 1989.
McMinds95 McMinds, Donald, and Joseph Whitty, Writing Your Own OSF/Motif Wid-
gets, Prentice Hall, 1995
Rosenthal88 Rosenthal, David S., “A Simple X.11 Client Program, or, How Hard Can It
Really Be to Write ‘Hello, World’?,” Proceedings of the Winter, 1988
USENIX Conference, 1988.
Scheifler90 Scheifler, Robert W., and James Gettys, X Window System, 2nd Edition,
DEC
Press, 1990.
Swick91 Swick, Ralph, and Mark Ackerman, “To Subclass or Not to Subclass,” Pro-
ceedings of the Sth Annual X Technical Conference, 1991.
Vlissides89 Vlissides, John, and Mark Linton, “Unidraw: A Framework for Building
Do-
main-specific Graphical Editors,” 4th Annual X Technical Conference: Tech-
nical Papers, 1989.
Wilson90 Wilson, David, Larry Rosenstein, and Dan Shafer, C++ Programming with
MacApp, Addison-Wesley, 1990.
Y oung94 Young, Douglas A., The X Window System: Programming and Applications
with Xt, OSF/Motif Edition, Prentice Hall, 1994.
Y oung95 Young, Douglas A., Motif Debugging and Performance Tuning , Prentice
Hall, 1995.
Le
CE AS ES a a ee
OO
The source code for the examples described in this book are available free of
charge to anyone with
network access. The examples may be downloaded using ftp from either
ftp.prenhall.com or
ftp.x.org. On ftp.x.org, the examples were in the directory contrib/book_examples
at the time of this
publication. On ftp.prenhall.com, they can be found in the directory
pub/software/doug_young. Note
that these machines are public locations whose administrators retain the right to
reorganize, rename,
or even remove files at will. Therefore, these locations are subject to change at
any time. Becasue
anyone is free to distribute these examples, you may also find the sources at other
ftp or UUCP sites.
You should also be able to locate the sources using any of the net-searching
facilities that are
commonly available. Assuming you have access to the net, you can download the
software from
ftp.prenhall.com with the following command sequence. (Give “ftp” or “anonymous” at
the login
% ftp ftp.prenhall.com
> cd pub/software/doug_young
> binary
> quit
Once you have the compressed tar file, you can unpack the files with the commands:
% uncompress young.cpp.tar.Z
% tar xvf young.cpp.tar
PE CGS A A 406-408
TICLES AA 18, 98
o AS ee ae A E 421
o A 83
Arg20
ArgList20
CEI iaa 67
index
B
DIES uc 6
DO rt 65, 102
BasicComponent class .....:....s:cccccccrccsseres 65-69
A aspeieiedacngitenaestsaicee 180
SCL. | 5: ea cae eee 145
A A 144
INDICARON som pornaa 180
bounce, example program.................... 381-422
A A A 421
E A AAA 383
A A 420
e A AMM A 382
o MA 383
DODNCICIOCK CLASS ssscensesdesvesbrssonesvevsien 397-398
Bounce Window class.......................... 415-419
TCCRARS MAA 420
440
a A 398
e A thoes oakvatben cr Gees 397
eLO TOTA AEA A, 203, 253-264
o A ras oes ies 261
o. E S E e O A EA 259
E A O A EE HA 233
A T EE 261
a IMA 275
MAA 276
supporting CmdInterface objects ........ 260
COMETA ES:C1ASS aiaks 265-268
E EA A 265
CAN SS ce 254-257
A A A 254
collaboration graphs .................... 114, 118-120
a AAA AA 114
BEE A A EE T 118-120
SEAI a BNO R TEE PRS 141, 147
EAEE A A 110
Color NOOSE? OLA8S aesir irii 349, 370-377
PPI PAOD a A 376
q ADTA eicere STI
Color Mode onas auiii 350-353
A phan ATRAE FRE 350
EN AT ica ns 354
COOR CON aaae a aT 176
10 DEAR EE OEE EI A EAE PE 138, 141
daa Etita la. A tans 176
command classes
MEROS CURIE ee roer 276
commands
BG MOOS A A A AEA 252-276
o A IA A 263
a A A O AA 264
compiling Motif programMsS.........cocrcccccnononcnnns. 24
a AAA ANI EN BI 65, 102
UTO neos Ll CAE LE Poo bones 90-102
ae oo A A codiysoseuvancenie 22
COON CIN aa e Eaa TA 70-73
CRAG A AM A A EA Piy
441
O oN 284—296
o A A 27, 27-29
Qi MAP 249
a AA A 69-85
VICTOR AAA 133-195
using an XmForm Widget.........occcccccccns. 39
o A A 43—46
SEENA LER OTT IA A 324-345
srm C decla i tarea 14
F
A PEERI A A ET TIT 73-75
A MA A 116
Fe A biciouisinaivoci 339
FileSelectionBox widget ...escssssessesseessesssesssss 24
friends, as callback functions ........................ 61
Ty TI aaa 14, 24
G
A 49
EA A 161
GORE errar 137, 142
Mple Monta ÓN rage 161
getResources, member function .....oocccccoononos. 93
siobal ODOC iria 246
pierde a aan masenani¥oxse 222
H
BPW NOW C1 06 PARO E PEEN 367-370
i
a A e in oO IA MA AO 9,11
fey der AAA 286-287
InfoDialog Manager class..................... 242-244
inheritance hierarchies...............oooomoomoomoomoomoo 122
SR oe E, E NA O
AA AR PAL A where
CA e A ol it Rel
NAO ET a VADO
dad des Gm
O he RA A UN IA A AAA AID e Re
NL. La Pd
NAAA A el tl AI e ib
442
a AN A AN 204
A NE O D A A OS 146
installDestroyHandier,...... oerni 88, 103
InterruptibleCmd class ........................ 317-324
AE ANIMAS T E RA 317
DIOSA CARTAMA 5s iieicelcssictcossdendiccaevss 324
interrupting
With CODES cs debidas caca 301
E A 30, 298-345
E
Labeled TORE OTOA icira aei a 57
M
main
A A O 218, 220
CFG OT BIO O A E 18
MainWindow class...................... 203, 213-218
SER GIN P RONO GATE TA PATA 214
TOSSAMS MM 219
manage, member function .............ommmmmmmmmmo..o. 67
Manage CO CIASS iriennerien 285-286
manager WIN esneari 103
MAnaging WOOS 19, 23
a AA O 14
ao AMA IÓ 6
Menu Bar CIO caco sctisevsenivesevess 203, 279-281
MenuDemoApp Class ................o..o...... 289-292
MenuDemoWindow class ................... 293-295
o E E PEAN PE A 47, 277-296
MenuWindow class ..................... 203, 282-283
A A brine 174
A eulsetnsersbasivccenssao vette 139, 142
SEP TOTS EA 174
message diagrams
po EATI PROA A 219
SIR E
i ee ee A A A
aS ee ee O DS AN A eee
AAA
NA
P
PRESO CU ronca 59
ENSS AA 114,113
COUSDOTALION STAIN sarria ds 114
a A AP AA 310-312
pixmaps
clearing with XFillRectangle .............. 387
used in double-buffering ..................... 384
pushime, example program ecanaonsns cnenenrarensonec 27
Q
QuestionDialogManager class............. 247-248
QuestionTestWindow class .......................... 249
bie MA 284-285
R
SONAS Mars 23
PP EE A T T 8
pi MAA 101
CODO TION carr 50
ESOO TNA ROT i oaccssssoncesearcarotcarcnneass 50, 90
name-space collisions ..................oo..... 100
DICCAUONCOE TIAS naa 101
RESOURCE_MANAGER, property ............ 51
AI rro 50
AAA 100
de I IRE P A E A Si
o MMM 97
a MAA 110
A MPAA, 354-359
E A AA 309
RGBTOHSV conversion function ............... 369
a CLASS nonnen 365-367
e GE rena 399—400
S
SelectEle Crni cs scansa 339-341
443
o IP A 384-391
Static Tmember TUECIOON o ociscannoneiirorccriracers 61
a is RE A A 401-402
A A 400-401
SOPAR CLASS racs aina 19
CSS CA atera 117
customizable VETSION icssessssssvecessessnsnevans 95
UNE default TESOOICES rr 101
stopwatch, example program................... 69-85
e NÓ. A 83
Br AMA 70-73
cl MM AA 73-75
inheritance NICTALCDY seoseis 82,122
MESAS an 121
Stopwatch class ..............:. 79-82, 130-131
TMr CIAGS raras 75-79, 125-130
pc AMA 69
Sopwith Timer saciar 128
2 A RAS 6
O AN A EE TEEN 6
Swath VIOW CLOGS oeisio 360-363
T
Tak Done CANDACE uscar 318, 319
TO YOV CSS ran aa 363-365
the Application, global object............... 208, 219
thelnfoDialog Manager ....,ssisscceresressnesesissosa 242
theQuestionDialogManagert................. 248, 273
A A A 259, 269
XAML Milani 294
theWorkingDialogManaget...................0006 306
this, C++ instance pointer
BUG CALINGCKS star arc 58
BB CUO OE AMM 62
TicTacToe, example program ............. 133-154
collaboration graph...... 140-141, 146-147
ct AA AA 192
TicTacToe, example program, cont.
CEI ri 133-154
DONOTA nnas 155-195
INNSTURANCE SLAP aonana 146
444
HPOOIEIN: Statement... nia eai 134
URGE G ni è AEO A 148-154
TICA CPN OES y ica 135
A A EEE 153
WIG ERB si PEPIN E E 179
TICL O os 156
ae o o A OA 156
A A A 301
TI a a aat 75-79
ADORNAN ir 126-128
CAUPACK VOFSION.. caisievsccasuccecocerstes 128-131
OPN RE II AAEE EINA T TE 116
SEES BFA a a oe
wideetDlestroyedCalIDBCK .........0ccccseesvsszereraes 88
WII a o ai A AAI E Ea O AE 53
windows
AR A 300
a RA AA 9
A OE 332-333
A A RA 316
aes
X
A a eet 12
pO lg DA oa ch DIR a 31
ALTO FOCUS iaa 331
EA PA 299, 331
KENVIRONMEN morron 51
APILESPARCIPA TE oscars 51
XFillRectangle
AmSTRING_DEFAULT_CHARSET .......... a3
BG CA CARIES rotar 93
pi AAA 23
A AX 92
AMD ala 51
is A AA 23
A MA EEEE AE 91
ASESOR rr 91,92
A A ne. ere tei 20, 21
A A 21
o BOGOR PIO AMP 71
AN RADIALES da 21
AtVaCreateManaged Widget socorro 21
A BE TEOLSP ODDS NOU ira 21
pa A A 21
EA AAA 22
KXUSERFILESEARCHPATH occrosscorarocionosnonss 5
X Window System
mt | Object-Oriented —
Programming with C++
Second Edition
Douglas A. Young
Updated to cover Motif Release 2.0 and X/11 R6, this revised edition shows
programmers how to design reusable user interface components and how Motif and
C++ can be used together effectively. It goes beyond how to display and manipulate
widgets and shows how the object-oriented approach affects an application’s design
and implementation.
Part I introduces the basic mechanics of using C++ and Motif to create interactive
applications and covers C++ classes, widgets; and designing with objects. Part II -
deals with application frameworks, dialogs, and command classes among other
topics. Throughout the book, the focus is on practical solutions to the complex
problems surrounding the integration of X and Motif with the object-oriented
programming model.
Key Features:
e Revised and updated to cover Motif 2.0 and X/11 R6
e Shows how object-oriented programming affects the structure of real applications
PRENTICE HALL |
Upper Saddle River, NJ 07458
ISBN O-13-209255-7