February 1996
Programming Windows 95 with MFC, Part VII: The Document/View Architecture
Jeff Prosise
Jeff Prosise writes extensively about programming in Windows and is a contributing editor of several computer magazines. He is currently working on a book, Programming Windows 95 with MFC, to be published this winter by Microsoft Press.
Click to open or copy the LIFE project files.
In the early days of MFC, applications were built very much like the sample programs in Parts I through VI of this series. An application had two principal components: an application object representing the application itself, and a window object representing the application's window. The application object's primary duty was to create the window object, and the window object, in turn, processed messages. Other than the provision of general-purpose classes such as CString and CTime to represent non-Windows objects, MFC was little more than a thin wrapper around the Windows API. It grafted an object-oriented interface onto windows, dialog boxes, device contexts, and other objects already present in Windows® in one form or another.
MFC 2.0 changed the way that Windows-based applications are written by introducing the document/view architecture. This architecture is carried through to MFC 4.0. In a document/view application, an application's data is represented by a document object and views of that data are represented by one or more view objects. The document and view objects work together to process the user's input and draw textual and graphical representations of the resulting data. MFC's CDocument class serves as the base class for all document objects, while the CView class and its derivatives serve as base classes for view objects. The top-level window, which is derived from either CFrameWnd or CMDIFrameWnd, is no longer the focal point for message processing, but serves primarily as a container for views, toolbars, status bars, and other objects.
The document/view architecture simplifies the development process. Code to perform routine chores such as prompting the user to save unsaved data before a document is closed is provided for you by the framework. So is code to transform your application's documents into OLE containers, simplify printing, use splitter windows to divide a window into two or more panes, and more.
MFC supports two types of document/view applications. The first is single-document interface (SDI) applications, which support just one open document at a time. The second is multiple-document interface (MDI) applications, which permit the user to have two or more documents open concurrently. MDI apps support multiple views of each document, but SDI apps are generally limited to one view per document. Thanks to the support lent by the framework, writing an MDI application with MFC is only a little more work than writing an SDI application. But because the Windows Interface Guidelines for Software Design (Microsoft Press, 1995) and the online documentation that comes with Visual C++™ suggest you use SDI instead of MDI, this article, the final installment in a series that began last June, discusses documents, views, and other aspects of the document/view architecture with an emphasis on the single-document interface. It concludes with an SDI adaptation of the computer game Life to demonstrate the writing of a document/view application.
Let's begin our exploration of the document/view architecture with a look at the various objects involved and their relationships. Figure 1 shows a schematic representation of an SDI document/view application. The frame window is the application's top-level window. It is normally a WS_OVERLAPPEDWINDOW style window with a resizing border, caption bar, system menu, and minimize, maximize, and close boxes. The view is a child window sized to fit the frame window so that it becomes, for all practical purposes, its parent's client area. When the frame window is resized, the view window is automatically resized also. As mentioned earlier, the application's data is stored in the document object, a visible representation of which appears in the view window. For an SDI application, the frame window is created from a class derived from CFrameWnd, the document from a class derived from CDocument, and the view from a class derived from CView or a related class such as CScrollView.
Figure 1 SDI Document/View
This architecture has very real implications for the design and operation of an application program. In MFC 1.0 apps, a program's data was often stored in member variables assigned to the frame window class. "Views" of that data are drawn by accessing the member variables directly and using GDI functions encapsulated in the CDC class to draw in the frame window's client area. The MFC 2.x and MFC 4.0 document/view architecture enforces a more modular program design by encapsulating the data in a standalone document object and providing a view object for the program's screen output. A document/view application never grabs a device context for its frame window to draw into that window's client area; instead, it uses a device context object for the view window to draw into the view. It looks like the drawing is being done in the frame window, but in reality all output goes to the view window overlaying the frame window's client area. You can draw into the frame window if you wish, but you won't see the output because the client area of an SDI frame window is completely obscured by the view.
One of the most interesting aspects of an SDI document/view application is how the frame window, document, and view objects are created. If you look inside the InitInstance function of an application class generated by the Visual C++ 4.0 AppWizard, you'll see something like this:
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate (
IDR_MAINFRAME,
RUNTIME_CLASS (CMyDoc),
RUNTIME_CLASS (CMyFrameWnd),
RUNTIME_CLASS (CMyView));
AddDocTemplate (pDocTemplate);
CCommandLineInfo cmdInfo;
ParseCommandLine (cmdInfo);
if (!ProcessShellCommand (cmdInfo))
return FALSE;
return TRUE;
There's nothing even remotely resembling the InitInstance code used in my sample programs to date:
m_pMainWnd = new CMainWindow;
m_pMainWnd->ShowWindow (m_nCmdShow);
m_pMainWnd->UpdateWindow ();
return TRUE;
Let's look more closely at the AppWizard-generated code to understand exactly what it's doing.
The first two statements create an SDI document template object from MFC's CSingleDocTemplate class. The document template identifies the frame window, document, and view objects used by the application. MFC's RUNTIME_CLASS macro returns a pointer to a CRuntimeClass structure for the specified class, which enables the framework to create objects of that class at run time. The document template also identifies the resource ID (IDR_MAINFRAME is the ID AppWizard assigns, but you can use any nonzero integer resource ID you like) of several resources that are closely related to the frame window. I'll have more to say about that resource ID in a few moments.
Next, the statement
AddDocTemplate (pDocTemplate);
adds the document template to the list of document templates maintained by the application object. Each document template registered in this manner defines one document type supported by the application. SDI applications register just one document type, but MDI applications can register as many as they like.
The statements
CCommandLineInfo cmdInfo;
ParseCommandLine (cmdInfo);
use MFC 4.0's new CWinApp::ParseCommandLine function to initialize a CCommandLineInfo object with values reflecting the parameters entered on the command line, including a document file name. The statements
if (!ProcessShellCommand (cmdInfo))
return FALSE;
then execute the command-line parameters. Among other things, ProcessShellCommand calls CWinApp::OnFileNew to start the application with an empty document if no file name was entered on the command line, or CWinApp::OpenDocumentFile to start the application and load a document if a document name was specified. It is during this phase of the program's execution that the framework creates the frame window, document, and view objects using run-time class information contained in the document template. ProcessShellCommand returns TRUE if the initialization succeeded and FALSE if it did not. A FALSE return causes InitInstance to return FALSE also, which shuts down the application.
After the application is started and the frame window, document, and view objects are created, the message loop kicks in and the application begins to retrieve and process messages. Unlike MFC 1.0-type applications, which typically map all messages to member functions of the frame window object, document/view applications divide the job of processing messages between the application, frame window, document, and view objects. The framework does a lot of work in the background to make this possible. In Windows, only windows can receive messages, so the framework implements a sophisticated command-routing mechanism that routes command messages-MFC's term for WM_COMMAND messages generated by menus and toolbar buttons-from one object to another in a predefined order until one of the objects processes the message or the message is passed to ::DefWindowProc for default processing. (For a detailed look at writing command messages, see "Meandering Through the Maze of MFC Message and Command Routing," MSJ July 1995.) It may not be obvious right now, but soon it will become apparent to you that command routing is a powerful feature of the framework. Its absence would severely inhibit the usefulness of the document/view architecture, as you'll soon see in the section on command routing.
In a document/view application, data is stored in a document object of a class derived from CDocument. The term "document" is somewhat misleading because it automatically stirs up visions of word processors and spreadsheet programs and other types of applications that deal with what you traditionally think of as documents. In reality, the document/view architecture is much more generic than that. A document can be almost anything, from a deck of cards in a poker simulation to an online connection with a remote data source. The "document" in "document/view" refers to an abstract representation of a program's data that draws a clear boundary between how the data is stored and how it is presented to the user. Typically, the document object provides public member functions that other objects (such as views connected to the document) can use to query, edit, and store document data. All handling of the data is performed by the document object itself.
A document's data is usually stored in member variables belonging to the derived document class. The Scribble tutorial supplied with Visual C++ exposes its data directly to other objects by declaring it public, but stricter encapsulation is achieved by making document data private and providing public member functions for accessing it. For example, the document object in a text editing program might store characters in a CByteArray object and provide AddChar and RemoveChar functions so that other objects (such as views) can add and remove characters. Other functions, such as AddLine and DeleteLine, could further enrich the interface between the document object and the objects that interact with it.
Figure 2 lists some of the important member functions that document objects inherit from CDocument. SetModifiedFlag should be called whenever the document's data is modified. This sets a flag inside the document object that tells the framework that the document contains data that hasn't been saved to disk. You can determine yourself whether a document contains unsaved data by calling the document's IsModified function. GetTitle and GetPathName retrieve the document's file name and the full path to the file. Both functions return a null CString object if the document has no title-that is, if the document hasn't been saved to disk and therefore isn't assigned a file name. The new OnFileSendMail function implements the Send Mail command in the application's File menu by serializing the document to a temporary file and sending the file as a mail message. (A document is titled when it's given a file name.)
Figure 2 Key CDocument Operations
Function | Description |
GetFirstViewPosition | Returns a POSITION value that can be passed to GetNextView to enumerate the document's views. |
GetNextView | Returns a CView pointer to the next view in the list of views attached to this document. |
GetPathName | Retrieves the document's file name and path-for example, "C:\Documents\Personal\MyFile.Lif." Returns a null string if the document has not been named. |
GetTitle | Retrieves the document's title-for example, "MyFile." Returns a null string if the document has not been named. |
IsModified | Returns a nonzero value if the document contains unsaved data, and 0 if it does not. |
SetModifiedFlag | Sets or clears the document's modified flag. |
UpdateAllViews | Updates all views associated with the document by calling each view's OnUpdate function. |
OnFileSendMail | Implements the Send Mail command in the File menu. |
Three of the CDocument functions in Figure 2 are provided so that the document object can interact with its view (or views). UpdateAllViews notifies all views associated with the document to update themselves by invoking the views' virtual OnUpdate functions (I'll talk more about this soon). In an SDI application with just one view, UpdateAllViews frequently isn't used if the view updates itself in response to user input either before or after updating the document's data. But in a multiple-view application, UpdateAllViews is called whenever the document is modified to keep all the views in sync. If desired, a document object can enumerate its views and communicate with each view individually by using GetFirstViewPosition and GetNextView to walk the list of views. The following code fragment mimics the action of UpdateAllViews by calling each view object's OnUpdate function individually:
POSITION pos = GetFirstViewPosition ();
while (pos != NULL)
GetNextView (pos)->OnUpdate (NULL, 0, NULL);
Of course, it's easier just to call UpdateAllViews unless you wish to vary the OnUpdate parameters passed to different views or skip certain views altogether.
CDocument also provides an assortment of virtual functions that can be overridden to customize a document's behavior. Some are almost always overridden in derived document classes, and others are only occasionally overridden. The four most commonly used overridables are listed in Figure 3. OnNewDocument is used to initialize each new document that is created, while OnOpenDocument initializes the unserialized data members of the document object when a new document is loaded from disk. In an SDI application, the document object is constructed just once and reused each time a document is created or opened. Since the object's constructor is only executed one time no matter how many documents are opened and closed, an SDI application should perform one-time initializations in the document's constructor and place initialization code required for every new document in OnNewDocument and OnOpenDocument.
Figure 3 Key CDocument Overridables
Function | Description |
OnNewDocument | Called by the framework when a new document is created. Override to reinitialize the document object when a new document is created. |
OnOpenDocument | Called by the framework when a document is loaded from disk. Override to reinitialize the unserialized data members of the document object before a new document is loaded. |
DeleteContents | Called by the framework to delete the document's contents. Override to free memory and other resources allocated to a document before it is closed. |
Serialize | Called by the framework to serialize a document to or from disk. Override to provide document-specific serialization code so your documents can be loaded and saved. |
Before a new document is created or opened, the framework calls the document's virtual DeleteContents function to delete the document's existing data. An SDI application should override CDocument::DeleteContents and take the opportunity to free any resources allocated to the document and perform other routine cleanup chores in preparation for the document object to be reused. MDI applications generally follow this model also, although MDI document objects, unlike SDI document objects, are individually created and destroyed as documents are created, opened, and closed by the user.
When you override OnNewDocument and OnOpenDocument, it's important to call their base class versions. Otherwise, DeleteContents will not be called when a new document is created or loaded and other important initialization tasks carried out by the framework will not be executed. The CDocument version of OnOpenDocument, for example, not only calls DeleteContents, it also displays an Open dialog box to get a file name from the user and calls the document object's Serialize function to serialize the document's data from disk. The overridden function should call the base class implementation first and refrain from doing anything else if the base class returns FALSE, and it should return TRUE to indicate successful completion, as shown here:
BOOL CMyDoc::OnNewDocument ()
{
if (!CDocument::OnNewDocument ())
return FALSE;
// Initializethedocumentobjectasifitwerenew...
return TRUE;
}
BOOL CMyDoc::OnOpenDocument (LPCTSTR lpszPathName)
{
if (!CDocument::OnOpenDocument (lpszPathName))
return FALSE;
// Initialize members of the document object that
// aren't initialized when the document is
// serialized from disk...
return TRUE;
}
When a document is opened or saved, the framework calls the document object's Serialize function to read or write the document's data. Serialization is the process by which an object writes a record of itself to a persistent storage medium such as a disk file, or reloads itself by reading the record back. You write the Serialize function to stream the document data in and out; the framework does everything else, including opening the file for reading or writing and providing a CArchive object to insulate you from the physical disk I/O. Often, an entire document can be serialized with just a few lines of code using CArchive's insertion and extraction operators, calls to embedded objects' Serialize functions, or both. If necessary, a document object can perform raw reads and writes through the CFile object associated with the archive (a pointer to which may be obtained with CArchive::GetFile) or using the Read and Write functions of the CArchive object. If you make your documents serializable, you can rely on the framework to do all the dirty work involved in loading and saving documents-prompting the user for file names, opening and creating files, performing the disk I/O, and so on. You'll learn more about the serialization process and see the code for a typical Serialize function later on.
Other CDocument overridables that aren't used as often as those listed in Figure 3 but that can be useful under certain circumstances include OnCloseDocument, called when a document is closed; OnSaveDocument, called when a document is saved; SaveModified, which is called before a document containing unsaved data is closed to ask whether changes should be saved; and ReportSaveLoadException, called when an error occurs during serialization. There are others, but for the most part they constitute advanced overridables that you'll rarely find occasion to use.
While the sole purpose of a document object is to store an application's data, view objects exist for two purposes: to render visual representations of a document's data on the screen, and to translate the user's input-particularly mouse and keyboard messages, which are not routed to document objects as command messages are-into commands that operate on the document. Thus, documents and views are tightly related and the information exchanged between them flows both ways.
A document object can have any number of views associated with it, but a view always corresponds to just one document object. The framework stores a pointer to the corresponding object in a view's m_pDocument data member and makes that pointer accessible through the view's GetDocument member function. Just as a document object can identify its views using CDocument::GetFirstViewPosition and CDocument::GetNextView, a view object can find its document by calling GetDocument. When AppWizard generates the source code for a view object, it overrides the base class's GetDocument function with one that casts m_pDocument to the appropriate document type and returns the result. This allows the document object to be accessed in a type-safe manner without requiring an explicit cast each time it is referenced.
MFC's CView class defines the basic properties of a view, and derived view classes impart additional functionality. MFC 4.0 adds several new view classes to the list of views supported in earlier versions, including CListView, CRichEditView, CTreeView, and CDaoRecordView. Like the CDocument class, CView and its derivatives include several virtual member functions that you can override to customize a view's operation. The most important of these is OnDraw, which is called whenever the view receives a WM_PAINT message. In non-document/view applications, WM_PAINT messages are processed by an OnPaint handler that uses a CPaintDC object to do the drawing. In a document/view application, the framework receives the WM_PAINT message and calls the view class's OnDraw function, passing it a CDC pointer for drawing. No message mapping is necessary because OnDraw is virtual. An OnDraw function that displays "Hello, MFC" in the center of the view window looks like this:
void CMyView::OnDraw (CDC* pDC)
{
CRect rect;
GetClientRect (&rect);
pDC->DrawText ("Hello, MFC", -1, rect,
DT_SINGLELINE |
DT_CENTER | DT_VCENTER);
}
The fact that the view doesn't have to construct its own device context object is a minor convenience. The real reason that the framework uses OnDraw is that the same code can be used both for output to a window and for printing. When a WM_PAINT message arrives, the framework passes the view object a pointer to a paint DC so output will go to the window. When printing is in progress, however, OnDraw is passed a pointer to a printer DC to direct output to the printer.
Two other CView overridables that you'll find useful when deriving view classes of your own are OnInitialUpdate and OnUpdate. OnInitialUpdate is called to initialize a view right after it is created and, in an SDI application, whenever a document is closed and a new one is opened or created. The default implementation calls OnUpdate, and the default implementation of OnUpdate, in turn, invalidates the view's client area to force a repaint. Use OnInitialUpdate to initialize data members of the view class and perform other view-related initializations on a per-document basis. In a CScrollView-derived class, for example, it's common for OnInitialUpdate to call CScrollView::SetScrollSizes to initialize the view's line size, page size, and other scrolling parameters.
In a multiple-view application, a view can determine when it is activated and deactivated by overriding CView's OnActivateView function. The first parameter passed to OnActivateView is a BOOL value that is TRUE if the view is being activated and FALSE if it is not. The second and third parameters are CView pointers identifying the views that are being activated and deactivated, respectively. If the pointers are equal, the application's frame window was activated without causing a change in the active view. View objects sometimes use this special feature of the OnActivateView function to realize a logical palette or perform other duties that should only be carried out when the user switches back to the application after momentarily activating another.
In a document/view application, the frame window defines the application's physical workspace on the screen and frames the views of its data. SDI frame windows come from the class CFrameWnd. Top-level MDI frame windows come from CMDIFrameWnd. The related CMDIChildWnd class defines the behavior of the child windows that frame MDI views and float within the top-level MDI frame.
Frame windows are an integral part of the document/viewarchitecture.The CFrameWnd class, for example, builds in OnClose and OnQueryEndSession handlers that give the user a chance to save changes to a document if the application's window is closed or the Windows session is terminated. In a multiple-view application, the frame window keeps track of the active view and takes care of calling the views' OnActivateView functions when the active view changes. A frame window also handles the all-important task of resizing a view window when the frame window is resized or a movable toolbar is docked or undocked, and it includes member functions for hiding and displaying toolbars and status bars.
In order for the framework to create document, view, and frame window objects during the course of a program's execution, the classes from which those objects are constructed must support a feature known as dynamic creation. What MFC's dynamic object creation mechanism amounts to is a way for applications to register classes in such a way that the framework can create objects of those classes. MFC makes it easy to write dynamically creatable classes with its DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros. DECLARE_DYNCREATE is called from the class declaration with the class name as its only parameter, and IMPLEMENT_DYNCREATE is called in the class's implementation file with two parameters: the class name followed by the name of the base class. An object of a class that uses these macros can be created at runtime with a statement like this one:
RUNTIME_CLASS (CMyClass)->CreateObject ();
This is basically no different than using the new operator to create a CMyClass object, but it circumvents a shortcoming of the C++ language that prevents statements like these from working:
CString strClassName = "CMyClass";
CMyClass* ptr = new strClassName;
The compiler, of course, will try to construct an object from a class named "strClassName" because it doesn't realize that strClassName is a variable name and not a literal class name.
What happens when you write a class that is dynamically creatable? It's pretty simple, really. The DECLARE_DYNCREATE macro adds three members to the class declaration: a static data member whose type is CRuntimeClass, a virtual function named GetRuntimeClass, and a static function named CreateObject. When you write
DECLARE_DYNAMIC (CMyClass)
the compiler spits out this:
public:
static AFX_DATA CRuntimeClass classCMyClass;
virtual CRuntimeClass* GetRuntimeClass() const;
static CObject* PASCAL CreateObject();
The IMPLEMENT_DYNCREATE macro initializes the CRuntimeClass structure with information such as the class name, the size of objects created from the class, and the base class's address. It also provides inline code for the GetRuntimeClass and CreateObject functions. If MFC 4.0's IMPLEMENT_DYNCREATE macro is called like this:
IMPLEMENT_DYNCREATE (CMyClass, CBaseClass)
CreateObject is implemented like this:
CObject* PASCAL class_name::CreateObject()
{ return new CMyClass; }
Previous versions of MFC used a different implementation of CreateObject that manually allocated memory using the size information stored in the class's CRuntimeClass structure and then initialized an object in that memory space. MFC 4.0's implementation is truer to the language because if a dynamically creatable class overloads the new operator, CreateObject will now use the overloaded version.
Serialization is the process by which an object writes a record of itself to a persistent storage medium such as a disk file or reloads itself by reading the record back. Serialization is a rather broad topic that could easily stand to have an entire article devoted to it, but the bottom line is this: by overriding CDocument::Serialize in your document class and using the provided CArchive object to serialize the document's data members, you provide all the support the framework needs to implement the Open, Save, and Save As commands in the File menu. Loading and saving documents has never been so easy.
Suppose the data in your document consists of two int data members named m_nWidth and m_nHeight. The document's Serialize function would look like this:
void CMyDoc::Serialize (CArchive& archive)
{
if (archive.IsStoring ())
archive << m_nWidth << m_nHeight;
else
archive >> m_nWidth >> m_nHeight;
}
archive is a CArchive object that serves as an intermediary between the document and a physical disk file represented by a CFile object. CArchive::IsStoring returns TRUE if a document is being saved and FALSE if it's being loaded. The CArchive class overloads the << and >> operators so that primitive data types such as BYTEs, WORDs, DWORDs, LONGs, ints, floats, and doubles can be streamed in and out easily. MFC data types such as CStrings and CRects can be written and read the same way. MFC 4.0 is the first version to support the serialization of the int data type directly; in the past, ints had to be cast to WORDs, DWORDs, or other types whose width was not platform-dependent.
Entire classes can be made serializable just as primitive data types are serializable: by deriving a class from CObject, throwing in a few macros, and adding a Serialize function to serialize the class's data members. MFC builds serialization support into many of its classes, including the collection classes designed to hold data. If the characters and formatting codes comprising a word processing document were stored in a CByteArray object named m_byData, for example, the entire document-whether it's 1 byte or 1 megabyte in length-could be serialized with a one-line Serialize function:
void CMyDoc::Serialize (CArchive& archive)
{
m_byData.Serialize (archive);
}
Earlier you saw an example of an SDI document template object created from the CSingleDocTemplate class. The object's constructor was passed four parameters: an integer value equal to IDR_MAINFRAME and three RUNTIME_CLASS pointers. The purpose of the RUNTIME_CLASS macros should be clear by now, so let's look more closely at the integer passed in the first parameter, which is actually a multipurpose resource ID that identifies the following resources:
In an SDI document/view application, the framework creates an application's frame window by calling CFrameWnd::LoadFrame, which accepts a resource ID identifying the four resources listed above. As you might suspect, the document template passes the resource ID you supply to it to LoadFrame when it creates the frame window. LoadFrame creates a frame window and loads the associated menu, accelerators, icon, and document string all in one step, but to make it work you must assign all these resources the same ID. That's why the RC file generated for an AppWizard application uses the same ID (by default, IDR_MAINFRAME) for a variety of different resources.
The document string is a string resource formed from a combination of up to seven substrings separated by \n characters, each of which describes one characteristic of the frame window or document associated with the document template. In left-to-right order, the substrings specify:
You can omit substrings when you create a document string resource; individual substrings may be omitted by following the previous \n character with another \n, and trailing NULL substrings may be omitted altogether. If you build an application with AppWizard, of course, the document string is created for you. The resource statements for a typical SDI document string look like this:
STRINGTABLE
BEGIN
IDR_MAINFRAME "Draw\n\n\nDraw files(*.drw)\n.drw\n
Draw.Document\nDraw Document"
END
When this application is started with an empty document, its frame window will have the title "Untitled - Draw." The default file name extension for documents saved by this application is DRW, and DRW will be one of the file name extensions listed in the Open and Save As dialog boxes.
After a document template is created, substrings belonging to the document string can be retrieved with MFC's CDocTemplate::GetDocString function. For example, the statements
CString strDefExt;
pDocTemplate->GetDocString (strDefExt,
CDocTemplate::filterExt);
copy the document's default file name extension to the CString named strDefExt.
Aside from creating documents, views, and frame windows that enclose the views, one of a document template's most important functions-one for which it shares responsibility with the application object-is to register the application's document type (or types) with the operating system shell. In Windows 95, double-clicking a document icon or right-clicking a document icon and selecting Open from the context menu opens the document and the application that created it. For this to work, the document type must be registered with the shell, which means writing a series of entries to the registry identifying the document's file name extension and the command used to open documents of that type.
In a conventional Windows-based application, registration is accomplished by supplying a REG file that the user can merge into the registry or by programmatically writing the necessary entries into the registry using Win32® API functions such as ::RegCreateKey and ::RegSetValue. In an MFC application, the process is much simpler. Calling CWinApp::RegisterShellFileTypes after the final call to AddDocTemplate in InitInstance modifies the registry to forge the necessary links between the application, the documents that it creates, and the Windows 95 shell.
A related CWinApp function named EnableShellOpen, which is normally called in conjunction with RegisterShellFileTypes, creates a DDE link between the application and the shell that adds a nifty feature to MDI applications. If an MDI application is running and it registered its document type(s) with EnableShellOpen, and if the user opens one of the application's document icons through the shell, the shell doesn't start a second instance of the application; instead, it sends a DDE message to the existing instance instructing it to open the document itself. Thus, the document appears in a new frame window inside the top-level MDI frame, just as if it had been opened with the application's File Open command. Anyone who has written DDE code before will confirm that implementing this feature on your own, without the framework's help, is anything but trivial.
You can add simple drag-and-drop support to a document/view application by calling the frame window's DragAcceptFiles function. This registers the window to receive WM_DROPFILES messages when it is the target for drops involving files dragged from shell folders or other containersthatarepartoftheshell'snamespace.TheOnDropFiles handler in CFrameWnd responds to a drop notification by calling the application object's OnOpenDocument function with the name of the file that was dropped. DragAcceptFiles is normally called from InitInstance using the m_pMainWnd pointer stored in the application object. In an SDI application, DragAcceptFiles should be called after ProcessShellCommand because the frame window doesn't exist before ProcessShellCommand is called.
One of the most remarkable features of MFC and the document/view architecture is that an application can handle command messages resulting from UI events such as menu selections almost anywhere. The frame window is the recipient of most command messages, but you can handle command messages in the view object, the document object, or even the application object simply by including in the class definition message map entries for the messages you want to handle. This has the very practical effect of freeing you to handle command messages in whatever class makes the most sense, as opposed to handling them all in the frame window class.
During the routine sequence, command messages sent to an SDI frame window follow the path in Figure 4. The active view gets first crack at the message, followed by the document object associated with that view, the document template, the frame window itself, and finally the application object. The routing stops if any object along the way processes the message, but it continues all the way to ::DefWindowProc if none of the objects' message maps contains an entry for the message. Routing is similar for command messages sent to MDI frame windows, with the framework making sure that all the relevant objects, including the child window frame that surrounds the active MDI view, get the opportunity to weigh in.
Figure 4 Path of Command Message to SDI Frame Windows
The value of command routing becomes apparent when you look at how a typical document/view application handles menu commands. By convention, the File New, File Open, and File Exit commands are mapped to the application object where CWinApp provides convenient OnFileNew, OnFileOpen, and OnAppExit member functions for handling them. File Save and File Save As, by contrast, are normally mapped to the document object, which provides default implementations for both commands in the form of CDocument::OnFileSave and CDocument::OnFileSaveAs. Commands to show and hide toolbars and status bars are handled by the frame window using CFrameWnd member functions, and most other commands are handled in the view class.
An important point to keep in mind when considering where to put your message handlers is that only command messages are subject to routing. Standard Windows messages such as WM_CHAR,WM_LBUTTONDOWN, WM_CREATE,andWM_SIZE must be handled by the object whose window received the message. Mouse and keyboard messages generally go to the view, while most other messages go to the frame window. Document objects and application objects never receive noncommand messages because neither is a CWnd object.
When you write a document/view application, you typically don't have to write all the handlers for the menu commands yourself. CWinApp, CDocument, CFrameWnd, and other MFC classes provide default implementations for common menu commands such as File Open and File Save. In addition, the framework provides a range of standard menu-item command IDs such as ID_FILE_OPEN and ID_FILE_SAVE, many of which are prewired into the message maps of the classes that provide default command implementations.
Figure 5 lists the core group of predefined command IDs. (See MFC's AFXRES.H header file for a complete list.) For each ID, the table also lists the corresponding menu item, the MFC function (if any) that provides a default implementation, and, if a default implementation is provided, whether the handler is called automatically (Prewired=Yes) or the command ID must be connected to a handler through a message map (Prewired=No). For example, default implementations for the File New and File Open commands are provided by CWinApp's OnFileNew and OnFileOpen functions, but neither is connected to the application unless you provide ON_COMMAND message map entries for them. (If you allow AppWizard to generate the skeleton of an SDI or MDI application, it will write ON_COMMAND entries for OnFileNew, OnFileOpen, and other default command handlers for you.) CWinApp::OnAppExit, on the other hand, works all by itself and requires no message map entry. All you have to do is assign the File Exit menu item the command ID ID_APP_EXIT, and File Exit will automatically call CWinApp::OnAppExit to close the application.
Figure 5 Key Predefined Command IDs and Command Handlers
Command ID | Menu Item Name | Default Implementation | Prewired? |
File menu | |||
ID_FILE_NEW | New | CWinApp::OnFileNew | No |
ID_FILE_OPEN | Open | CWinApp::OnFileOpen | No |
ID_FILE_CLOSE | Close | CDocument::OnFileClose | Yes |
ID_FILE_SAVE | Save | CDocument::OnFileSave | Yes |
ID_FILE_SAVE_AS | Save As | CDocument::OnFileSaveAs | Yes |
ID_FILE_PAGE_SETUP | Page Setup | None | N/A |
ID_FILE_PRINT_SETUP | Print Setup | CWinApp::OnFilePrintSetup | No |
ID_FILE_PRINT | CView::OnFilePrint | No | |
ID_FILE_PRINT_PREVIEW | Print Preview | CView::OnFilePrintPreview | No |
ID_FILE_SEND_MAIL | Send Mail | CDocument::OnFileSendMail | No |
ID_FILE_MRU_FILE1-16 | N/A | CWinApp::OnOpenRecentFile | Yes |
ID_APP_EXIT | Exit | CWinApp::OnAppExit | Yes |
Edit menu | |||
ID_EDIT_CLEAR | Clear | None* | N/A |
ID_EDIT_CLEAR_ALL | Clear All | None | N/A |
ID_EDIT_COPY | Copy | None* | N/A |
ID_EDIT_CUT | Cut | None* | N/A |
ID_EDIT_PASTE | Paste | None* | N/A |
ID_EDIT_PASTE_LINK | Paste Link | None | N/A |
ID_EDIT_PASTE_SPECIAL | Paste Special | None | N/A |
ID_EDIT_FIND | Find | None* | N/A |
ID_EDIT_REPLACE | Replace | None* | N/A |
ID_EDIT_REPEAT | Repeat | None* | N/A |
ID_EDIT_SELECT_ALL | Select All | None* | N/A |
ID_EDIT_UNDO | Undo | None* | N/A |
ID_EDIT_REDO | Redo | None | N/A |
View menu | |||
ID_VIEW_TOOLBAR | Toolbar | CFrameWnd::OnBarCheck | Yes |
ID_VIEW_STATUS_BAR | Status Bar | CFrameWnd::OnBarCheck | Yes |
Window menu | |||
ID_WINDOW_NEW | New Window | CMDIFrameWnd::OnWindowNew | Yes |
ID_WINDOW_ARRANGE | Arrange Icons | CMDIFrameWnd::OnMDIWindowCmd | Yes |
ID_WINDOW_CASCADE | Cascade | CMDIFrameWnd::OnMDIWindowCmd | Yes |
ID_WINDOW_TILE_HORZ | Tile Horizontal | CMDIFrameWnd::OnMDIWindowCmd | Yes |
ID_WINDOW_TILE_VERT | Tile Vertical | CMDIFrameWnd::OnMDIWindowCmd | Yes |
ID_WINDOW_SPLIT | Split | CView::OnSplitCmd | Yes |
Help menu | |||
ID_APP_ABOUT | About AppName | None | N/A |
* CEditView and CRichEditView provide default implementations and message-map entries for these commands |
The framework also provides ON_UPDATE_COMMAND_UI update handlers for some of the commands in Figure 5. Once again, AppWizard lends a hand here by writing the necessary message map entries for you when applicable.
You don't have to use the command IDs defined in AFXRES.H or the default command handlers provided by the framework. You can always strike out on your own and define your own command IDs, supply message map entries to correlate them to default command handlers, or even replace the default command handlers with handlers of your own. If you do write your own command IDs and want context-sensitive menu help displayed in a status bar, you'll also have to add a string table to your application's RC file with help strings for the new command IDs. The default string table added to your application when you include AFXRES.H contains help strings for the predefined command IDs. In short, you can use as much or as little of the framework's support as you want to. But the more you lean on the framework, the less code you'll have to write on your own.
Figure 6 contains the source code for an SDI document/view application named Life that's a little different from the programs used to introduce doc/view in most texts. Life is a computer adaptation of the game of Life that was introduced to the world in the October 1970 issue of Scientific American. The game simulates the birth, life, death, and regeneration of cells in a universe consisting of a two-dimensional array of squares on the computer screen (see Figure 7). You draw cell patterns in the grid and then evolve those patterns by repeatedly pressing the F10 key or selecting Step from the Options menu. Each "step" evolves the grid one generation using a simple set of rules:
A neighbor is any cell that touches a given cell horizontally, vertically, or diagonally. How neighbors are computed at the edges of the grid depends on what's selected in the Options menu. Open Boundary enables a wraparound effect that gives cells an infinite space to grow in. In an open grid, cells in the leftmost column of the grid are neighbors of cells in the rightmost column, and cells on the top row are neighbors of cells on the bottom row. Closed Boundary closes the grid so that a cell positioned along the edge has a maximum of five neighbors and a cell in a corner has a maximum of three.
Figure 7 Life
The majority of Life's functionality comes from four classes:
In developing Life, I tried to include as many features as possible without compromising its value as a learning tool. It doesn't print, but it does just about everything else, including loading and saving game grids. I originally intended to include an Options Start command to run an automated simulation, but halfway through adding that feature I realized that doing it properly was going to add 50 percent or more to the code length. That's not my purpose here. Life is designed to show you firsthand what a real, honest-to-goodness SDI document/view application looks like. I'll leave it to you add the bells and whistles.
Here are a few points of interest to note as you browse the source code. First, the document class CLifeDoc provides a thorough encapsulation of the application's data by storing bits representing the on/off states of individual cells in a CByteArray object named m_byGrid. CByteArray is a private data member, so it cannot be manipulated outside of its own class. The state of a cell at a given row and column address can be retrieved with the public member function CLifeDoc::GetCell, and a cell can be toggled off and on with CLifeDoc::ToggleCell. CLifeDoc::Evolve evolves the grid one generation. Clicking a cell in the grid activates the view object's OnLButtonDown handler, which in turn translates the cursor position into a cell address and toggles the cell:
CLifeDoc* pDoc = GetDocument ();
BOOL bState = pDoc->ToggleCell (nRow, nCol);
Selecting Step from the Options menu activates CLifeDoc's OnOptionsStep handler, which calls Evolve to evolve the grid. The command is handled by the document class, not the frame window, because Step performs an action on the document. Without command routing, of course, the command handler would have to be placed elsewhere.
Something that's not obvious from the source code is that the document class handles the Save and Save As commands in the File menu also. ID_FILE_SAVE and ID_FILE_SAVE_AS are prewired command IDs that activate the CDocument functions OnFileSave and OnFileSaveAs. You can look at the CDocument message map in the MFC source code file DOCCORE.CPP to see how the mapping is performed.
Take a look at CLifeDoc::OnNewDocument and you'll see how the document object reinitializes itself when a new document is created. (Remember, in an SDI app, the same document object is reused over and over, so OnNewDocument is where you place the code to initialize a fresh document.) If this is the first time a document has been created, it defaults to a grid size of 24 by 24. Otherwise, the user is prompted for the grid dimensions. Once the grid's horizontal and vertical dimensions are set, the m_byGrid and m_byBuffer arrays are initialized with SetSize statements that allocate memory for the arrays. (m_byBuffer is a temporary buffer used to store a copy of the grid before evolving it.) CLifeDoc::DeleteContents does essentially the opposite by calling CObArray::RemoveAll to empty the m_byGrid and m_byBuffer arrays and deallocate the memory allocated to them. CLifeDoc doesn't override CDocument::OnOpenDocument because loading a document from disk initializes all the necessary data members.
Life's view class, CLifeView, is derived from CScrollView so the view can be scrolled if the grid is too large to be seen in its entirety. The view's constructor, which is only called once, loads the two bitmaps used to draw cells. OnInitialUpdate, which is called whenever a document is created or loaded, sets the scrolling parameters based on the grid dimensions and the view dimensions. The scrolling parameters are calculated anew by CLifeView::OnSize if the view size changes.
CLifeView's implementation of OnUpdate is a little unusual. It contains a single statement:
Invalidate (lHint ? FALSE : TRUE);
lHint is an application-defined parameter that an object that calls UpdateAllViews can use to pass information to a view. Life's view-update requirements are rather unique. If this is the view's initial update, the entire view should be redrawn-background included-with an
Invalidate (TRUE);
statement. Otherwise, if the grid size changes, the grid may not be repainted properly. If this isn't the initial update-that is, if OnUpdate was called because CLifeDoc::Evolve called UpdateAllViews-then repainting should be done with an
Invalidate (FALSE);
statement to prevent the flashing that would occur if the grid were erased and then repainted.
To that end, CLifeDoc::Evolve calls UpdateAllViews like this:
UpdateAllViews (NULL, 1);
Calling UpdateAllViews causes the framework to call OnUpdate for each view connected to a CDocument object. Since there's only one view in this case, calling UpdateAllViews calls CLifeView::OnUpdate. The 1 passed to UpdateAllViews initializes the lHint parameter to OnUpdate and results in Invalidate being called with a FALSE parameter. The only other time OnUpdate is called is when CLifeView::OnInitialUpdate calls the base class implementation of OnInitialUpdate:
CScrollView::OnInitialUpdate ();
When this happens, the framework calls OnUpdate with lHint equal to 0 and CLifeView::OnUpdate calls Invalidate with a TRUE parameter to redraw everything.
When the user selects Open, Save, or Save As from the application's File menu, the framework serializes the current document to or from disk by calling CLifeDoc::Serialize. Serialization requires just a few lines of code:
void CLifeDoc::Serialize (CArchive& archive)
{
if (archive.IsStoring ())
archive << (WORD) m_cx << (WORD) m_cy;
else {
WORD cx, cy;
archive >> cx >> cy;
m_cx = (int) cx;
m_cy = (int) cy;
}
m_byGrid.Serialize (archive);
m_byBuffer.SetSize (m_byGrid.GetSize ());
}
Life serializes three elements: the grid's width and height and the m_byGrid array that holds the grid itself. The if-else block serializes the m_cx and m_cy data members holding the grid's dimensions. The m_byGrid.Serialize statement serializes the grid with one easy function call; it works because CByteArrays are smart enough to serialize themselves. The final statement sets m_byBuffer to the same size as m_byGrid. m_byGrid's size is set automatically because it is serialized. m_byBuffer is not, so its size must be set explicitly, else there will no memory allocated for it and attempts to access it will fail.
Understand the source code for Life and you'll understand the mechanics of the document/view architecture, too. Using the source code in Figure 6 as a starting point, it would be relatively easy to add a splitter window based on MFC's CSplitterWnd class, convert Life into an MDI app, add printing support, and more. These are exercises that you might want to try on your own to familiarize yourself with some of the finer points of documents and views.
The document/view architecture really pays off when you write applications that act as OLE containers or servers. MFC builds in more than 20,000 lines of OLE code that are pretested by Microsoft. Few of us have either the knowledge or the desire to write a full-blown implementation of an OLE container or server in C, and writing to the document/view architecture prevents us from having to do so. With Visual C++ to lend a hand, writing an OLE container or server amounts to little more than letting AppWizard generate the outline for you and filling in the blanks with OnNewDocument, OnUpdate, and other virtual function overrides-basically the same process that you would follow to build a non-OLE-enabled version of Life from AppWizard-generated code.
In short, programming in Windows isn't getting easier. But MFC makes it easier by hiding thousands of grungy little details in the framework. MFC is more than a class library; it's a different way of writing applications that frees you to focus on the code that makes your application unique. Why take 12 months to develop what you could do in six? Take it from one who's been there: if you're still doing your Windows-based programming in C, you owe it to yourself to give C++ and MFC a good, hard look. The time is now.
From the February 1996 issue of Microsoft Systems Journal.