When you generate your source code with AppWizard, you get an application featuring all the bells and whistles of a commercial Windows 95 application, including a toolbar, a status bar, tool tips, menus, and even an About dialog box. However, in spite of all those features, the application really doesn't do anything useful. In order to create an application that does more than look pretty on your desktop, you've got to modify the code that AppWizard generates. This task can be easy or complex, depending upon how you want your application to look and act.
Before you can perform any modifications, however, you have to know about MFC's document/view architecture, which is a way to separate an application's data from the way the user actually views and manipulates that data. Simply, the document object is responsible for storing, loading, and saving the data, whereas the view object (which is just another type of window) enables the user to see the data on the screen and to edit that data as is appropriate to the application. In the sections that follow, you learn the basics of how MFC's document/view architecture works.
Suppose you create a basic AppWizard application named App1, after which you examine the various files generated by AppWizard. You find a class called CApp1Doc, which was derived from MFC's CDocument class. In the App1 application, CApp1Doc is the class from which the application instantiates its document object, which is responsible for holding the application's document data. It's up to you to add storage for the document by adding data members to the CApp1Doc class.
To see how this works, look at Listing 6.1, which shows the header file AppWizard creates for the CApp1Doc class.
Listing 6.1 APP1DOC.H-The Header File for the CApp1Doc Class
// app1Doc.h : interface of the CApp1Doc class // /////////////////////////////////////////////////////////////////////// class CApp1Doc : public CDocument { protected: // create from serialization only CApp1Doc(); DECLARE_DYNCREATE(CApp1Doc) // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CApp1Doc) public: virtual BOOL OnNewDocument(); virtual void Serialize(CArchive& ar); //}}AFX_VIRTUAL // Implementation public: virtual ~CApp1Doc(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: //{{AFX_MSG(CApp1Doc) // 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() }; /////////////////////////////////////////////////////////////////////////////
Near the top of the listing, you can see the class declaration's Attributes section, which is followed by the public keyword. This is where you declare the data members that will hold your application's data. In the program that you create a little later in this chapter, the application must store an array of CPoint objects as the application's data. That array is declared as a member of the document class like this:
// Attributes public: CPoint points[100];
Notice also in the class's header file that the CApp1Doc class includes two virtual member functions called OnNewDocument() and Serialize(). MFC calls the OnNewDocument() function whenever the user selects the File, New command (or its toolbar equivalent, if a New button exists). You can use this function to perform whatever initialization must be performed on your document's data. The Serialize() member function is where the document class loads and saves its data.
As I mentioned previously, the view class is responsible for displaying and enabling the user to modify the data stored in the document object. To do this, the view object must be able to obtain a pointer to the document object. After obtaining this pointer, the view object can access the document's data members in order to display or modify them. If you look at Listing 6.2, you can see how the view class obtains a pointer to the document object.
Listing 6.2 APP1VIEW.H-The Header File for the CApp1View Class
// app1View.h : interface of the CApp1View class // ///////////////////////////////////////////////////////////////////////////// class CApp1View : public CView { protected: // create from serialization only CApp1View(); DECLARE_DYNCREATE(CApp1View) // Attributes public: CApp1Doc* GetDocument(); // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CApp1View) public: virtual void OnDraw(CDC* pDC); // overridden to draw this view virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: virtual BOOL OnPreparePrinting(CPrintInfo* pInfo); virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo); virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo); //}}AFX_VIRTUAL // Implementation public: virtual ~CApp1View(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: //{{AFX_MSG(CApp1View) // 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() }; #ifndef _DEBUG // debug version in app1View.cpp inline CApp1Doc* CApp1View::GetDocument() { return (CApp1Doc*)m_pDocument; } #endif /////////////////////////////////////////////////////////////////////////////
Near the top of the listing, you can see the class's public attributes, where it declares the GetDocument() function as returning a pointer to a CApp1Doc object. Anywhere in the view class that you need to access the document's data, you can call GetDocument() to obtain a pointer to the document. For example, to add a CPoint object to the aforementioned array of CPoint objects stored as the document's data, you might use the following line:
GetDocument()->m_points[x] = point;
You could, of course, do this a little differently by storing the pointer returned by GetDocument() in a local pointer variable, and then using that pointer variable to access the document's data, like this:
pDoc = GetDocument(); pDoc->m_points[x] = point;
The second version is more convenient when you need to use the document pointer in several places in the function, or if using the less clear GetDocument()->variable version makes the code hard to understand.
Notice that the view class, like the document class, also overrides a number of virtual functions from its base class. As you'll soon see, the OnDraw() function, which is the most important of these virtual functions, is where you paint your window's display. As for the other functions, MFC calls PreCreateWindow() before the window element (that is, the actual Windows window) is created and attached to the MFC window class, giving you a chance to modify the window's attributes (such as size and position). Finally, the OnPreparePrinting() function enables you to modify the Print dialog box before it's displayed to the user; the OnBeginPrinting() function gives you a chance to create GDI objects like pens and brushes that you need to handle the print job; and OnEndPrinting() is where you can destroy any objects you may have created in OnBeginPrinting().
NOTE |
When you first start using an application framework like MFC, it's easy to get confused about the difference between an object instantiated from an MFC class and the Windows element it represents. For example, when you create an MFC frame-window object, you're actually creating two things: The MFC object that contains functions and data, and a Windows window that you can manipulate using the functions of the MFC object. The window element is associated with the MFC class, but is also an entity unto itself. |
Now that you've had an introduction to documents and views, a little hands-on experience should help you better understand how these classes work. In the steps that follow, you build the Rectangles application, which demonstrates the manipulation of documents and views. Follow the first steps to create the basic Rectangles application and modify its resources:
NOTE |
The complete source code and executable file for the Rectangles application can be found in the CHAP06\RECS directory of this book's CD-ROM. |
Dialog Box Name | Options to Select |
New Project | Name the project recs, and set the project path to the directory into which you want to store the project's files. Leave the other options set to their defaults. |
Step 1 | Select Single Document. |
Step 2 of 6 | Leave set to defaults. |
Step 3 of 6 | Leave set to defaults. |
Step 4 of 6 | Turn off all application features except Printing and Print Preview. |
Step 5 of 6 | Leave set to defaults. |
Step 6 of 6 | Leave set to defaults. |
Now that you have the application's resources the way you want them, it's time to add code to the document and view classes in order to create an application that actually does something. Follow these steps to add the code that modifies the document class to handle the application's data, which is an array of CPoint objects that determine where rectangles should be drawn in the view window:
Listing 6.3 LST6_3.TXT-Code for Saving the Document's Data
ar << m_pointIndex; for (UINT i=0; i<m_pointIndex; ++ i) { ar << m_points[i].x; ar << m_points[i].y; }
Listing 6.4 LST6_4.TXT-Code for Loading the Document's Data
ar >> m_pointIndex; for (UINT i=0; i<m_pointIndex; ++ i) { ar >> m_points[i].x; ar >> m_points[i].y; } UpdateAllViews(NULL);
This finishes the modifications you must make to the document class. In the following steps, you make the appropriate changes to the view class, enabling the class to display, modify, and print the data stored in the document class:
Listing 6.5 LST6_5.TXT-Code for Displaying the Application's Data
UINT pointIndex = pDoc->m_pointIndex; for (UINT i=0; i<pointIndex; ++i) { UINT x = pDoc->m_points[i].x; UINT y = pDoc->m_points[i].y; pDC->Rectangle(x, y, x+20, y+20); }
Listing 6.6 LST6_6.TXT-Code to Handle Left-Button Clicks
CRecsDoc *pDoc = GetDocument(); if (pDoc->m_pointIndex == 100) return; pDoc->m_points[pDoc->m_pointIndex] = point; ++pDoc->m_pointIndex; pDoc->SetModifiedFlag(); Invalidate(); CRecsDoc *pDoc = GetDocument(); if (pDoc->m_pointIndex == 100) // if the index is greater than the 100 rect ;maximum leave the function return; pDoc->m_points[pDoc->m_pointIndex] = point; // get the present point ++pDoc->m_pointIndex; //increment the pointIndex pDoc->SetModifiedFlag(); //"dirty" the file, see reference below for more ;infor about dirty files Invalidate(); //redraw the displayed window
You've now finished the complete application. Click the toolbar's Build button or select the Build, Build command from the menu bar to compile and link the application.
After you have the Rectangles application compiled and linked,
run it by selecting Build, Execute from the menu bar. When you
do, you see the application's main window. Place your mouse pointer
over the window's client area and left click. A rectangle appears.
Go ahead and keep clicking. You can place up to 100 rectangles
in the window (see fig. 6.6).
Figure 6.6 : You can place up to 100 rectangles in the application's window.
To save your work (this is work?), select the File, Save command. You can view your document in print preview by selecting the File, Print Preview command, or just go ahead and print by selecting the File, Print command. Of course, you can create a new document by selecting File, New, or load a document you previously saved by selecting File, Open. Finally, if you click Help, About Rectangles, you see the application's About dialog box.
If you don't have much experience with AppWizard and MFC, you're probably amazed at how much you can do with a few mouse clicks and a couple of dozen lines of code. You're also probably still a little fuzzy on how the program actually works, so in the following sections, you examine the key parts of the Rectangles application.
As you've heard already in this chapter, it is the document object in an AppWizard-generated MFC program that is responsible for maintaining the data that makes up the application's document. For a word processor, this data would be strings of text, whereas for a paint program, this data might be a bitmap. For the Rectangles application, the document's data is the coordinates of rectangles displayed in the view window.
The first step in customizing the document class, then, is to provide the storage you need for your application's data. How you do this, of course, depends on the type of data you must use. But, in every case, the variables that will hold that data should be declared as data members of the document class, as is done in the Rectangles application. Listing 6.7 shows the relevant code.
Listing 6.7 LST6_7.TXT-Declaring the Rectangles Application's Document Data
// Attributes public: CPoint m_points[100]; UINT m_pointIndex;
In the preceding listing, the document-data variables m_points[] and m_pointIndex are declared as public members of the document class. (The m prefix indicates that the variables are members of the class, rather than global or local variables. This is a tradition that Microsoft started. You can choose to follow it or not.) The m_points[] array holds the coordinates of the rectangles displayed in the view window, and the m_pointIndex variable holds the number of the next empty element in the array. You can also think of m_pointIndex as the current rectangle count. These variables are declared as public so that the view class can access them. If you were to declare the data variables as protected or private, your compiler would whine loudly when you tried to access the variables from your view class's member functions.
The data storage for the Rectangles application is pretty trivial in nature. For a commercial- grade application, you'd almost certainly need to keep track of much more complex types of data. But the method of declaring storage for the document would be the same. You may, of course, also declare data members that you use only internally in the document class, variables that have little or nothing to do with the application's actual document data. However, you should declare such data members as protected or private.
After you have your document's data declared, you usually need to initialize it in some way each time a new document is created. For example, in the Rectangles application, the m_pointIndex variable must be initialized to zero when a new document is started. Otherwise, m_pointIndex may contain an old value from a previous document, which could make correctly accessing the m_points[] array as tough as getting free cash from an ATM. In the Rectangles application, m_pointIndex gets initialized in the OnNewDocument() member function, as shown in Listing 6.8.
Listing 6.8 LST6_8.TXT-The Rectangles Application's OnNewDocument() Function
BOOL CRecsDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // TODO: add reinitialization code here // (SDI documents will reuse this document) m_pointIndex = 0; return TRUE; }
MFC calls the OnNewDocument() function whenever the user starts a new document, usually by selecting File, New. As you can see, OnNewDocument() first calls the base class's OnNewDocument(), which calls DeleteContents() and then marks the new document as clean (meaning it doesn't yet need to be saved due to changes).
What's DeleteContents()? It's another virtual member function of the CDocument class. If you want to be able to delete the contents of a document without actually destroying the document object, you can override DeleteContents() to handle this task.
Keep in mind that how you use OnNewDocument() and DeleteContents() depends on whether you're writing an SDI or MDI application. In an SDI application, the OnNewDocument() function indirectly destroys the current document by reinitializing it in preparation for new data. An SDI application, after all, can contain only a single document at a time. In an MDI application, OnNewDocument() simply creates a brand new document object, leaving the old one alone. For this reason, in an MDI application, you can perform general document initialization in the class's constructor.
If your application is going to be useful, it must be able to do more than display data; it must also be able to load and save data sets created by the user. Writing this chapter would have been a nightmare if my text disappeared every time I shut down my word processor! The act of loading and saving document data with MFC is called serialization. And, in spite of the complications you may have experienced with files in the past, loading and saving data with MFC is a snap, thanks to the CArchive class, an object of which is passed to the document class's Serialize() member function. Listing 6.9 shows the Rectangles application's Serialize() function.
Listing 6.9 LST6_9.TXT-The Rectangles Application's Serialize() Function
void CRecsDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: add storing code here ar << m_pointIndex; for (UINT i=0; i<m_pointIndex; ++ i) { ar << m_points[i].x; ar << m_points[i].y; } } else { // TODO: add loading code here ar >> m_pointIndex; for (UINT i=0; i<m_pointIndex; ++ i) { ar >> m_points[i].x; ar >> m_points[i].y; } UpdateAllViews(NULL); } }
As you can see in the listing, the Serialize() function receives a reference to a CArchive object as its single parameter. At this point, MFC has done all the file-opening work for you. All you have to do is use the CArchive object to load or save your data. How do you know which to do? MFC has already created the lines that call the CArchive object's IsStoring() member function, which returns TRUE if you need to save data and FALSE if you need to load data.
Thanks to the overloaded << and >> operators in the CArchive class, you can save and load data exactly as you're used to doing using C++ I/O objects. If you look at the Serialize() function, you may notice that about the only difference between the saving and loading of data is the operator that's used. One other difference is the call to UpdateAllViews() after loading data. UpdateAllViews() is the member function that notifies all views attached to this document that they need to redraw their data displays. When calling UpdateAllViews(), you'll almost always use NULL as the single parameter. If you should ever call UpdateAllViews() from your view class, you should send a pointer to the view as the parameter. You would only do this sort of thing, though, when using multiple views with a single document.
Now that you've got your document class all ready to store, save, and load its data, you need to customize the view class so that it can display the document data, as well as enable the user to modify the data. In an MFC application using the document/view model, it's the view class's OnDraw() member function that is responsible for displaying data, either on the screen or the printer. Listing 6.10 shows the Rectangles application's version of OnDraw().
Listing 6.10 LST6_10.TXT-The Rectangles Application's OnDraw() Function
void CRecsView::OnDraw(CDC* pDC) { CRecsDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here UINT pointIndex = pDoc->m_pointIndex; for (UINT i=0; i<pointIndex; ++i) { UINT x = pDoc->m_points[i].x; UINT y = pDoc->m_points[i].y; pDC->Rectangle(x, y, x+20, y+20); } }
The first thing you should notice about OnDraw() is that its single parameter is a pointer to a CDC object. A CDC object encapsulates a Windows' device context, automatically initializing the DC and providing many member functions with which you can draw your application's display. Because the OnDraw() function is responsible for updating the window's display, it's a nice convenience to have a CDC object all ready to go. (For more information on device contexts, see Chapter 11, "Drawing on the Screen.")
Also notice that, because an application that uses the document/view model stores its data in the document class, AppWizard has generously supplied the code needed to obtain a pointer to that class. In the custom code in OnDraw(), the function uses this document pointer to retrieve the value of the document's index variable (the number of rectangles currently displayed), and then uses it as a loop-control variable. The loop simply iterates through the document's m_points[] array, drawing rectangles at the coordinates contained in the CPoint objects stored in the array.
The view object is not only responsible for displaying the application's document data; it must also (if appropriate) enable the user to edit that data. Exactly how you enable the user to edit an application's data depends a great deal upon the type of application you're building. The possibilities are endless. In the simple Rectangles application, the user can edit a document only by clicking in the view window, which adds another rectangle to the document. This happens in response to the WM_LBUTTONDOWN message, which Windows sends the application every time the user clicks the left mouse button when the mouse pointer is over the view window.
If you recall, you used ClassWizard to add the OnLButtonDown() function to the program. This is the function that MFC calls whenever the window receives a WM_LBUTTONDOWN message. It is in OnLButtonDown(), then, that the Rectangles application must modify its list of rectangles, adding the new rectangle at the window position the user clicked. Listing 6.11 shows the OnLButtonDown() function, where this data update occurs.
Listing 6.11 LST6_11.TXT-The Rectangles Application's OnLButtonDown() Function
void CRecsView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default CRecsDoc *pDoc = GetDocument(); if (pDoc->m_pointIndex == 100) return; pDoc->m_points[pDoc->m_pointIndex] = point; ++pDoc->m_pointIndex; pDoc->SetModifiedFlag(); Invalidate(); CView::OnLButtonDown(nFlags, point); }
Of the two parameters received by OnLButtonDown(), it is point that is most useful to the Rectangles application, because this CPoint object contains the coordinates at which the user just clicked. In the custom code you added to OnLButtonDown(), the function first obtains a pointer to the document object. Then, if the document object's m_pointIndex data member is equal to 100, there is no room for another rectangle. In this case, the function immediately returns, effectively ignoring the user's request to modify the document. Otherwise, the function adds the new point to the m_points[] array and increments the m_pointIndex variable.
Now that the document's data has been updated as per the user's modification, the document must be marked as "dirty" (needing saving) and the view must display the new data. A call to the document object's SetModifiedFlag() function takes care of the first task. If the user now tries to exit the program without saving the data, or tries to start a new document, MFC displays a dialog box warning the user of possible data loss. When the user saves the document's data, the document is set back to "clean." The call to Invalidate() notifies the view window that it needs repainting, which results in MFC calling the view object's OnDraw() function.
Throughout this chapter, you've been using MFC's CView class for your view window. The truth is, however, that MFC offers several different view classes that are derived from CView. These additional classes provide your view window with special abilities such as scrolling and text editing. Table 6.1 lists the various view classes along with their descriptions.
Class | Description |
CView | The base view class from which the specialized view classes are derived. |
CScrollView | A view class that provides scrolling abilities. |
CCtrlView | A base class from which view classes that implement new Windows 95 common controls (such as the ListView, TreeView, and RichEdit controls) are derived. |
CEditView | A view class that provides basic text-editing features. |
CRichEditView | A view class that provides more sophisticated text-editing abilities using the Windows 95 RichEdit control. |
CListView | A view class that displays a Windows 95 ListView control in its window. |
CTreeView | A view class that displays a Windows 95 TreeView control in its window. |
CFormView | A view class that implements a form-like window using a dialog-box resource. |
CRecordView | A view class that can display database records along with controls for navigating the database. |
CDaoRecordView | Same as CrecordView, except used with the new DAO database classes. |
To use one of these classes, you just substitute the desired class for the CView class in the application's project. When using AppWizard to generate your project, you can specify the view class you want in the wizard's Step 6 of 6 dialog box, as shown in Figure 6.7. Once you have the desired class installed as the project's view class, you can use the specific class's member functions to control the view window.
Figure 6.7 : You can use AppWizard to select your application's base view class.
For example, when using the CScrollView class, you can call the SetScrollSizes() member function to set the view's dimensions, mapping mode, and the vertical and horizontal scroll amounts. In addition, you can retrieve the scroll position or the size of the view by calling the GetScrollPosition() or GetTotalSize() member functions, respectively. Other member functions provide additional abilities.
A CEditView object, on the other hand, gives you all the features of a Windows edit control in your view window. Using this class, you can handle various editing and printing tasks, including find-and-replace. You can retrieve or set the current printer font by calling the GetPrinterFont() or SetPrinterFont() member function or get the currently selected text by calling GetSelectedText(). Moreover, the FindText() member function locates a given text string and OnReplaceAll() replaces all occurrences of a given text string with another string.
The CRichEditView class adds many features to an edit view, including paragraph formatting (such as centered, right-aligned, and bulleted text), character attributes (including underlined, bold, and italic), and the ability to set margins, fonts, and paper size. As you may have guessed, the CRichEditView class features a rich set of methods you can use to control your appli-cation's view object.
Figure 6.8 shows how the view classes fit into MFC's class hierarchy. Describing these various view classes fully is beyond the scope of this chapter. However, you can find plenty of information about them in your Visual C++ online documentation.
Figure 6.8 : The view classes all trace their ancestry back to CView.
Because you've been working with AppWizard-generated applications in this chapter, you've taken for granted a lot of what goes on in the background of an MFC document/view program. That is, much of the code that enables the frame window (your application's main window), the document, and the view window to work together is automatically generated by AppWizard and manipulated by MFC.
For example, if you look at the InitInstance() method of the Rectangles application's CRecsApp class, you'll see (among other stuff) the lines shown in Listing 6.12:
Listing 6.12 LST6_12.TXT-Initializing an Application's Document
CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CRecsDoc), RUNTIME_CLASS(CMainFrame), RUNTIME_CLASS(CRecsView)); AddDocTemplate(pDocTemplate);
In Listing 6.12, you discover one of the secrets that makes the document/view system work. In that code, the program creates a document-template object. (In this case, the template is of the CSingleDocTemplate class.) It is the document template that links together the application's resources, document class (in this case, CRecsDoc), frame window (always CMainFrame in an AppWizard program), and the view class (in this case, CRecsView).
What's all that RUNTIME_CLASS stuff? The RUNTIME_CLASS macro enables the framework to create instances of a class at runtime, which the application object must be able to do in a program that uses the document/view architecture. In order for this macro to work, the classes that will be created dynamically must be declared and implemented as such. To do this, the class must have the DECLARE_DYNCREATE macro in its declaration (in the header file) and the IMPLEMENT_DYNCREATE macro in its implementation.
If you look at the header file for the Rectangles application's CMainFrame class, for example, you see the following line near the top of the class's declaration:
DECLARE_DYNCREATE(CMainFrame)
As you can see, the DECLARE_DYNCREATE macro requires the class's name as its single argument.
Now, if you look near the top of CMainFrame's implementation file (MAINFRM.CPP), you see this line:
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
The IMPLEMENT_DYNCREATE macro requires as arguments the name of the class and the name of the base class.
If you explore the application's source code further, you'll find that the document and view classes also contain the DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros.
After creating the document-template object, the program calls AddDocTemplate() in order to pass the object on to the application. The application then adds the document template to its list of documents. Finally, the application object uses the document template to create the document object, as well as the frame and view windows.
MFC programs support two types of document templates, one that represents SDI (single-document interface) applications, which can hold only one document at a time, and one that represents MDI (multiple-document interface) applications, which can handle multiple documents concurrently. MFC supplies two classes for these document types: CSingleDocTemplate and CMultiDocTemplate. Bet you can figure out which class goes with which document-template type!
In this chapter, you examined how an AppWizard-generated application uses MFC to coordinate an application's document and view objects. There is, of course, a great deal more to learn about MFC before you can create your own sophisticated Windows 95 applications. If you'd like to learn more about some topics presented in this chapter, refer to the following: