If there is one thing that sets Windows programming apart from other kinds of programming, it is messages. Most DOS programs, for example, relied on watching (sometimes called polling) possible sources of input like the keyboard or the mouse to await input from them. A program that wasn't polling the mouse would not react to mouse input. In contrast, everything that happens in a Windows program is mediated by messages. A message is a way for the operating system to tell an application that something has happenedófor example, the user has typed, clicked, or moved the mouse, or the printer has become available. A window (and every screen element is a window) can also send a message to another window, and typically most windows react to messages by passing a slightly different message along to another window. MFC has made it much easier to deal with messages, but you must understand what is going on beneath the surface.
Windows messages direct your program to perform all the things that it does.
In Windows C programming, developers write loops to deal with a steady stream of messages.
MFC lightens your conceptual load by letting you catch messages without writing a message loop.
Message map entries are easier to add with ClassWizard.
There are nearly 900 Windows messages.
A command can be routed to parts of your function that can't receive messages.
You can gray some menu items to reflect the current state of your application.
Understanding command updates is tough, but arranging for them is simple with ClassWizard.
Messages are all referred to by their names, though the operating system uses integers to refer to them. An enormous list of #define statements connects names to numbers and lets Windows programmers talk about WM_PAINT or WM_SIZE or whatever message they need to talk about. (The WM stands for Window Message.) As well as a name, a message knows what window it is for, and can have up to two parameters. (Often several different values are packed into these parameters, but thatís another story.)
Different messages are handled by different parts of the operating system or your application. For example, when the user moves the mouse over a window, the window gets a WM_MOUSEMOVE message, which it almost certainly passes to the operating system to deal with. The operating system redraws the mouse cursor at the new location. When the left button is clicked over a button, the button (which is a window) gets a WM_LBUTTONDOWN message, and handles it, often generating another message to the window that contains the button, saying, in effect, "I was clicked."
MFC has allowed many programmers to completely ignore low-level messages like WM_MOUSEMOVE and WM_LBUTTONDOWN. Instead, programmers deal only with higher-level messages that mean things like "The third item in this list box has been selected" or "The Submit button has been clicked." All these kinds of messages move around in your code and the operating system code in the same way as the lower-level messages. The only difference is what piece of code "chooses" to handle them. MFC makes it much simpler to announce, at the individual classes level, which messages each class can handle. The old C way, which you will see in the next section, made those announcements at a higher level and interfered with the object-oriented approach to Windows programming, which involves hiding implementation details as much as possible inside objects.
The heart of any Windows program is the message loop, typically contained in a WinMain() routine. The WinMain() routine is, like the main() in DOS or UNIX, the function called by the operating system when you run the program. You won't write any WinMain() routines because it is now hidden away in the code that AppWizard generates for you. Still, there is a WinMain(), just as there is in Windows C programs. Listing 4.1 shows a typical WinMain().
Listing 4.1óTypical WinMain() Routine
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
if (! InitApplication (hInstance))
return (FALSE);
if (! InitInstance (hInstance, nCmdShow))
return (FALSE);
while (GetMessage (&msg, NULL, 0, 0)){
TranslateMessage (&msg);
DispatchMessage (&msg);
}
return (msg.wParam);
}
In a Windows C program like this, InitApplication() typically calls RegisterWindow(), and InitInstance() typically calls CreateWindow(). (More details on this are in Appendix A, "Windows Programming Review and a Look Inside Cwnd.") Then comes the message loop, the while loop that calls GetMessage(). The API function GetMessage() fills msg with a message destined for this application and almost always returns True, so this loop runs over and over until the program is finished. The only thing that makes GetMessage() return False is if the message it gets is WM_QUIT.
TranslateMessage() is an API function that streamlines dealing with keyboard messages. Most of the time, you don't need to know "the A key just went down" or"the A key just went up," and so on. It's enough to know "the user pressed A." TranslateMessage() deals with that. It catches the WM_KEYDOWN and WM_KEYUP messages, and usually sends a WM_CHAR message in their place. Of course, with MFC, most of the time you don't care that the user pressed A. The user types into an edit box or similar control, and you can get the entire string out of it later, when the user has clicked OK. So don't worry too much about TranslateMessage().
The API function DispatchMessage() calls the WndProc for the window that the message is headed for. The WndProc for a Windows C function is a huge switch statement with one case for each message the programmer planned to catch, like the one in Listing 4.2.
Listing 4.2óTypical WndProc() Routine
LONG APIENTRY MainWndProc (HWND hWnd, // window handle
UINT message, // type of message
UINT wParam, // additional information
LONG lParam) // additional information
{
switch (message) {
case WM_MOUSEMOVE:
//handle mouse movement
break;
case WM_LBUTTONDOWN:
//handle left click
break;
case WM_RBUTTONDOWN:
//handle right click
break;
case WM_PAINT:
//repaint the window
break;
case WM_DESTROY: // message: window being destroyed
PostQuitMessage (0);
break;
default:
return (DefWindowProc (hWnd, message, wParam, lParam));
}
return (0);
}
As you can imagine, these WndProcs get very long in a hurry. Program maintenance can be a nightmare. MFC solves this problem by keeping information about message processing close to the functions that handle the messages, freeing you from maintaining a giant switch statement that is all in one place. Read on to see how it's done.
Message maps are part of the MFC approach to Windows programming. Instead of writing a WinMain() function that sends messages to your WindProc and then writing a WindProc that checks which kind of message this is and then calls another of your functions, you just write the function that will handle the message, and add a message map to your class that says, in effect, ìI will handle this sort of message.î The framework handles whatever routing is required to get that message to you.
If you've worked in Microsoft Visual Basic, you should be familiar with event procedures, which handle specific events like a mouse click. The message-handling functions you will write in C++ are equivalent to event procedures. The message map is the way that events are connected to their handlers.
Message maps come in two parts: one in the .h file for a class and one in the corresponding .cpp. Typically, they are generated by wizards, although in some circumstances you will add entries yourself. Listing 4.3 shows the message map from the header file of one of the classes in a simple application called ShowString, presented in Chapter 9, "Building a Complete Application: ShowString."
Listing 4.3óMessage Map from showstring.h
//{{AFX_MSG(CShowStringApp)
afx_msg void OnAppAbout();
// NOTE - the ClassWizard will add and remove member functions here.
// DO NOT EDIT what you see in these blocks of generated code !
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
This declares a function called OnAppAbout(). The specially formatted comments around the declarations help ClassWizard keep track of which messages are caught by each class. DECLARE_MESSAGE_MAP() is a macro, expanded by the C++ compilerís preprocessor, that declares some variables and functions to set up some of this magic message catching.
The message map in the source file, as shown in Listing 4.4, is quite similar.
Listing 4.4óMessage Map from Chapter 10ís showstring.cpp
BEGIN_MESSAGE_MAP(CShowStringApp, CWinApp)
//{{AFX_MSG_MAP(CShowStringApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
// NOTE - the ClassWizard will add and remove mapping macros here.
// DO NOT EDIT what you see in these blocks of generated code!
//}}AFX_MSG_MAP
// Standard file based document commands
ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
// Standard print setup command
ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP()
Message Map Macros
BEGIN_MESSAGE_MAP and END_MESSAGE_MAP are macros that, like DECLARE_MESSAGE_MAP in the include file, declare some member variables and functions that the framework can use to navigate the maps of all the objects in the system. There are a number of macros used in message maps, including these:
In addition to these, there are about 100 macros, one for each of the more common messages, that direct a single specific message to a member function. For example, ON_CREATE delegates the WM_CREATE message to a function called OnCreate(). You cannot change the function names in these macros. Typically, these macros are added to your message map by ClassWizard, as demonstrated in Chapter 9, "Building a Complete Application: ShowString."
How Message Maps Work
The message maps presented in Listings 4.3 and 4.4 are for the CShowStringApp class of the ShowString application. This class handles application-level tasks like opening a new file or displaying the About box. The entry added to the header file's message map can be read as "there is a function called OnAppAbout() that takes no parameters.î The entry in the source file's map means "when an ID_APP_ABOUT command message arrives, call OnAppAbout()." It shouldn't be a big surprise that the OnAppAbout() member function displays the About box for the application.
But how do message maps really work? Every application has an object that inherits from CWinApp, and has a member function called Run(). That function calls CWinThread::Run(), which is far longer than the simple WinMain() presented earlier, but has the same message loop at its heart: call GetMessage(), call TranslateMessage(), call DispatchMessage(). Almost every window object uses the same old-style windows class, and the same WindProc, called AfxWndProc(). The WindProc, as you've already seen, knows the handle, hWnd, of the window the message is for. MFC keeps something called a handle map, a table of window handles and pointers to objects, and the framework uses this to get a pointer to the C++ object, a CWnd*. Next, it calls WindowProc(), a virtual function of that object. Buttons or views might have different WindowProc() implementations, but through the magic of polymorphism, the right function gets called.
Polymorphism
Virtual functions and polymorphism are important C++ concepts for anyone working with MFC. They only arise when you are using pointers to objects, and when the class of objects to which the pointers are pointing is derived from another class. Consider as an example a class called CDerived that is derived from a base class called CBase, with a member function called Function() that is declared in the base class and overridden in the derived class. There are now two functions: one has the full name CBase::Function() and the other is CDerived::Function().
If your code has a pointer to a base object, and sets that pointer equal to the address of the derived object, it can then call the function, like this:
CDerived derivedobject;CBase* basepointer;basepointer = &derivedobject;basepointer->Function();In this case, CBase::Function() will be called. But there are times when that is not what you want, when you have to use a CBase pointer but you really want CDerived::Function() to be called. To indicate this, in CBase, Function() is declared to be virtual. Think of it as an instruction to the compiler to override this function if there is any way to do it.
Once Function() is declared to be virtual in the base class, CBase, the code fragment above would actually call CDerived::Function() as desired. That's polymorphism, and that shows up again and again when using MFC classes. You use a pointer to a window, a CWnd*, that really points to a CButton or a CView or some other class derived from CWnd, and when a function like WindowProc() is called, it will be the derived function, CButton::WindowProc() for example, that is called.
WindowProc() calls OnWndMsg(), the C++ function that really handles messages. First, it checks to see if this is a message, a command, or a notification. Assuming itís a message, it looks in the message map for the class, using the member variables and functions that were set up by DECLARE_MESSAGE_MAP, BEGIN_MESSAGE_MAP, and END_MESSAGE_MAP. Part of what those macros arrange is to allow access to the message map entries of the base class by the functions that search the message map of the derived class. That means if a class inherits from CView, and doesn't catch a message normally caught by CView, then that message will still be caught by the same CView function as inherited by the derived class. This message map inheritance parallels the C++ inheritance but is independent of it, and saves a lot of trouble carrying virtual functions around.
The bottom line: You add a message map entry and when a message arrives, the functions called by the hidden message loop look in these tables to decide which of your objects, and which member function of the object, should handle the message. That's what's really going on behind the scenes.
Messages Caught by MFC Code
The other great advantage of MFC is that the classes already catch most of the common messages and do the right thing, without any coding on your part at all. For example, you don't need to catch the message that tells you that the user has chosen File, Save AsóMFC classes catch it, put up the dialog box to get the new file name, handle all the behind-the-scenes work, and finally call one of your functions, which must be named Serialize(), to actually write out the document. (App Wizard typically makes an empty Serialize() function for you to fill in). You only need to add message map entries for behavior that is not common to all applications.
Message maps may not be simple to read, but they are simple to create if you use ClassWizard. There are two ways to add an entry to a message map in Visual C++ 5.0: with the main ClassWizard dialog box, or with one of the new dialog boxes that add message handlers or virtual functions.
The ClassWizard Tabbed Dialog Box
The main ClassWizard dialog box is displayed by choosing View, ClassWizard or by pressing Ctrl+W. ClassWizard is a tabbed dialog box, and Figure 4.1 shows the Message Map tab. At the top of the dialog box are two drop-down list boxes, one that reminds you which project you are working on (ShowString in this case) and the other that reminds you which class owns the message map you are editing. In this case, it is the CShowStringApp class, whose message map you have already seen.
Fig. 4.1 ClassWizard makes catching messages simple.
Below those single line boxes are a pair of multi-line boxes. The one on the left lists the class itself and all the commands that the user interface can generate. Commands are discussed in the "Commands" section later in this chapter. With the class name highlighted, the box on the right lists all the Windows messages this class might catch. It also lists a number of virtual functions that catch common messages.
To the right of those boxes are buttons where you can add a new class to the project, add a function to the class to catch the highlighted message, remove a function that was catching a message, or open the source code for the function that catches the highlighted message. Typically, you select a class, select a message, and click Add Function to catch the message. Here's what the Add Function button sets in motion:
After you add a function, clicking Edit Code makes it simple to start filling in the behavior of that function. If you prefer, double-click the function name in the Member Functions list box.
Below the Object IDs and Messages boxes is a list of the member functions of this class that are related to messages. This class has two such functions:
The InitInstance function is called whenever an application first starts. You do not need to understand this function to see that ClassWizard reminds you the function has been overridden.
Finally, under the Member Functions box is a reminder of the meaning of the highlighted message. Called to implement wait cursors is a description of the DoWaitCursor virtual function.
The Add Windows Message Handler Dialog Box
In release 5.0 of Visual C++, a new way of catching messages was added. Rather than bringing up ClassWizard and then remembering to set the right class name in a drop-down list box, you right-click on the class name in ClassView and then choose Add Windows Message Handler from the shortcut menu that appears. Figure 4.2 shows the dialog box that comes up when you make this choice.
Fig. 4.2 The New Windows Message and Event Handlers dialog box is another way to catch messages.
This dialog box does not show any of the virtual functions that were listed in the main ClassView dialog box. It is easy to see that this class catches the command ID_APP_ABOUT but does not catch the command update. (Commands and command updating are discussed in more detail later in this chapter.) To add a new virtual function, you right-click on the class in ClassView and choose Add New Virtual Function from the shortcut menu. Figure 4.3 shows this dialog box.
Fig. 4.3 The New Virtual Override dialog box simplifies implementing virtual functions.
You can see in Figure 4.3 that CShowStringApp already overrides the InitInstance() virtual function, and you can see what other functions are available to be overridden. As in the tabbed dialog box, a message area at the bottom of the dialog box reminds you of the purpose of each function: in fact the textócalled to implement wait cursorsóis identical to that in Figure 4.1.
Which Class should catch the message?
The only tricky part of message maps and message handling is deciding which class should catch the message. That's a decision you can't make until you understand all the different message and command targets that make up a typical application. The choice is usually among the following:
Views, documents, and frames are discussed in the next chapter, "Documents and Views."
There are almost 900 Windows messages, so you won't find a list of them all in this chapter. Usually, you arrange to catch messages with ClassWizard, and are presented with a much shorter list that is appropriate for the class you are catching messages with. Not every kind of window can receive every kind of message. For example, only classes that inherit from CListBox receive list box messages like LB_SETSEL, which directs the list box to move the highlight to a specific list item. The first component of a message name indicates the kind of window this message is destined for, or coming from. These window types are listed in Table 4.1.
Table 4.1óWindows Message Prefixes and Window Types
Prefix | Window Type |
ABM, ABN |
Appbar |
ACM, ACN |
Animation control |
BM, BN |
Button |
CB, CBN |
Combo box |
CDM, CDN |
Common dialog box |
CPL |
Control Panel application |
DBT |
Any application (device change message) |
DL |
Drag list box |
DM |
Dialog box |
EM, EN |
Edit box |
FM, FMEVENT |
File Manager |
HDM, HDN |
Header control |
HKM |
HotKey control |
IMC, IMN |
IME window |
LB, LBN |
List box |
LVM, LVN |
List view |
NM |
Any parent window (notification message) |
PBM |
Progress bar |
PBT |
Any application (battery power broadcast) |
PSM, PSN |
Property sheet |
SB |
Status bar |
SBM |
Scroll bar |
STM, STN |
Static control |
TB, TBN |
Tool bar |
TBM |
Track bar |
TCM, TCN |
Tab control |
TTM, TTN |
ToolTip |
TVM, TVN |
Tree view |
UDM |
Up Down control |
WM |
Generic window |
Whatís the difference between, say, a BM message and a BN message? A BM message is a message to a button, such as "act as though you were just clicked." A BN message is a notification from a button to the window that owns it, such as "I was clicked." The same pattern holds for all the prefixes that end with M or N in the preceding table.
Sometimes the "message" prefix does not end with M; for example CB is the prefix for a message to a combo box while CBN is the prefix for a notification from a combo box to the window that owns it. For example, CB_SETCURSEL is a message to a combo box directing it to select one of its strings, while CBN_SELCHANGE is a message sent from a combo box notifying its parent that the user has changed which string is selected.
What is a command? It is a special type of message. Windows generates a command whenever a user chooses a menu item, clicks a button, or otherwise tells the system to do something. In older versions of Windows, both menu choices and button clicks generated a WM_COMMAND message; these days you get a WM_COMMAND for a menu choice and a WM_NOTIFY for a control notification like button clicking or list box selecting. Commands and notifications get passed around by the operating system just like any other message, until they get into the top of OnWndMsg(). At that point, Windows message passing stops and MFC command routing starts.
Command messages all have, as their first parameter, the resource ID of the menu item that was chosen or the button that was clicked. These resource IDs are assigned according to a standard patternófor example, the menu item File, Save has the resource ID ID_FILE_SAVE.
Command routing is the mechanism OnWndMsg() uses to send the command (or notification) to objects that can't receive messages. Only objects that inherit from CWnd can receive messages, but all objects that inherit from CCmdTarget, including CWnd and CDocument, can receive commands and notifications. That means a class that inherits from CDocument can have a message map. There wonít be any entries in it for messages, only for commands and notifications, but itís still called a message map.
How do the commands and notifications get to the class, though? By command routing. (This gets messy, so if you don't want the inner details, skip this paragraph and the next.) OnWndMsg() calls CWnd::OnCommand() or CWnd::OnNotify(). OnCommand() checks all sorts of petty stuff (like whether this menu item was grayed after the user selected it but before this piece of code started to execute) and then calls OnCmdMsg(). OnNotify() checks different conditions and then it, too, calls OnCmdMsg(). OnCmdMsg() is virtual, which means that different command targets have different implementations. The implementation for a frame window sends the command to the views and documents it contains.
This is how something that started out as a message can end up being handled by a member function of an object that is not a window, and therefore can't really catch messages.
Should you care about this? Even if you don't care how it all happens, you should care that you can arrange for the right class to handle whatever happens within your application. If the user resizes the window, a WM_SIZE message is sent, and you may have to rescale an image or do some other work inside your view. If the user chooses a menu item, a command is generated, and that means your document can handle it if thatís more appropriate. You see examples of these decisions at work in the next chapter, "Documents and Views."
This under-the-hood tour of just how MFC connects user actions like window resizing or menu choices to your code is almost complete. All that's left is to handle the graying of menus and buttons, a process called command updating.
Imagine you are designing an operating system, and you know it's a good idea to have some menu items grayed to show they can't be used right now. There are two ways you can go about implementing this.
One is to have a huge table with one entry for every menu item, and a flag to indicate whether it's available or not. Whenever you have to display the menu, you can quickly check the table. Whenever the program does anything that makes the item available or unavailable, it updates the table. This is called the continuous-update approach.
The other way is not to have a table, but to check all the conditions just before your program displays the menu. This is called the update-on-demand approach and is the approach taken in Windows. In the old C way of doing thingsóto check whether each menu option should be grayed or notóthe system sent a WM_INITMENUPOPUP message, which means "I'm about to display a menu." The giant switch in the WindProc caught that message and quickly enabled or disabled each menu item. This wasn't very object-oriented though. In an object-oriented program, different pieces of information are stored in different objects and are not generally made available to the entire program.
When it comes to updating menus, different objects "know" whether or not each item should be grayed. For example, the document knows whether it has been modified since it was last saved, so it can decide whether File, Save should be grayed or not; but, only the view knows whether or not some text is currently highlighted; therefore, it can decide if Edit, Cut and Edit, Copy should be grayed. This means that the job of updating these menus should be parcelled out to various objects within the application rather than handled within the WindProc.
The MFC approach is to use a little object called a CCmdUI, a command user interface, and give this object to whomever catches a CN_UPDATE_COMMAND_UI message. You catch those messages by adding (or getting ClassWizard to add) an ON_UPDATE_COMMAND_UI macro in your message map. If you want to know what's going on behind the scenes, it's this: The operating system still sends WM_INITMENUPOPUP, then the MFC base classes like CFrameWnd take over. They make a CCmdUI, set its member variables to correspond to the first menu item, and call one of that object's own member functions, DoUpdate(). Then, DoUpdate() sends out the CN_COMMAND_UPDATE_UI message with a pointer to itself as the CCmdUI object the handlers use. Then the same CCmdUI object is reset to correspond to the second menu item, and so on until the entire menu is ready to be displayed. The CCmdUI object is also used to gray and ungray buttons and other controls in a slightly different context.
CCmdUI has the following member functions:
It's usually pretty straightforward to determine which member function you want to use. Here is a shortened version of the message map from an object called CWhoisView, a class derived from CFormView that is showing information to a user. This form view contains several edit boxes and the user may wish to paste text into one of them. The message map contains an entry to catch the update for the ID_EDIT_PASTE command, like this:
BEGIN_MESSAGE_MAP(CWhoisView, CFormView)
...
ON_UPDATE_COMMAND_UI(ID_EDIT_PASTE, OnUpdateEditPaste)
...
END_MESSAGE_MAP()
The function that catches the update, OnUpdateEditPaste(), looks like this:
void CWhoisView::OnUpdateEditPaste(CCmdUI* pCmdUI)
{
pCmdUI->Enable(::IsClipboardFormatAvailable(CF_TEXT));
}
This calls the API function ::IsClipboardFormatAvailable() to see if there is text in the Clipboard. Other applications may be able to paste in images or other non-text Clipboard contents, but this application cannot, and grays the menu item if there is no text available to paste. Most command update functions look just like this: They call Enable() with a parameter that is a call to a function that returns True or False, or perhaps a simple logical expression. Command update handlers must be fast, because five to ten of them must run from the moment the user clicks to display the menu to before the menu is actually displayed.
The ClassWizard dialog box shown in Figure 4.1 has the class name highlighted in the box labeled Object IDs. Below that are resource IDs of every resource (menu, toolbar, dialog box controls and so on) that can generate a command or message when this object (view, dialog, and so on) is on the screen. If you highlight one of those, the list of messages associated with it is much smaller, as you see in Figure 4.4.
Fig. 4.4 ClassWizard allows you to catch or update commands.
There are only two messages associated with each resource ID: COMMAND and UPDATE_COMMAND_UI. The first allows you to add a function to handle the user selecting the menu option or clicking the buttonóthat is, to catch the command. The second enables you to add a function to set the state of the menu item, button, or other control just as the operating system is about to display itóthat is, to update the command. (The COMMAND choice is boldface in Figure 4.4 because this class already catches that command.)
Clicking Add Function to add a function that catches or updates a command involves an extra step. ClassWizard gives you a chance to change the default function name, as shown in Figure 4.5. This is almost never appropriate. There is a regular pattern to the suggested names, and experienced MFC programmers come to count on function names that follow that pattern. Command handler functions, like message handlers, have names that start with On. Typically, the remainder of the function name is formed by removing the ID and the underscores from the resource ID, and capitalizing each word. Command update handlers have names that start with OnUpdate and use the same conventions for the remainder of the function name. For example, the function that catches ID_APP_EXIT should be called OnAppExit(), and the function that updates ID_APP_EXIT should be called OnUpdateAppExit().
Fig. 4.5 It is possible, but not wise, to change the name for your command handler or command update handler from the name suggested by ClassWizard.
Not every command needs an update handler. The framework does some very nice work graying and ungraying for you automatically. Say you have a menu item, Network, Send, whose command is caught by the document. When there is no open document, this menu item is grayed by the framework, without any coding on your part. For many commands, it's enough that an object that can handle them exists, and no special updating is necessary. For others, you may want to check that something is selected or highlighted, or that no errors are present before making certain commands available. That's when you use command updating. If you'd like to see an example of command updating at work, there's one in Chapter 9, "Building a Complete Application: ShowString," in the "Command Updating" section.
This chapter has provided the theory of message handling and command routing, and a behind-the-scenes look at the ways MFC implements these for you. To see this theory in action, check out the following chapters:
© 1997, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.