In Chapter 2, "Using AppWizard to Create an MFC Program," you learned the basics of generating an application with AppWizard. The example application featured 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 didn'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'll learn the basics of how MFC's document/view architecture works.
In Chapter 2, you created a basic AppWizard application. When you looked over the various files generated by AppWizard, you discovered 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. Because you didn't modify the AppWizard-generated files, the CApp1Doc class really holds no 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 3.1, which shows the header file, AppWizard, created for the CApp1Doc class.
Listing 3.1 APP1DOC.HThe Header File for the CApp1Doc Class
#if !defined(APP1_H__99206D24_7535_11D0_847F_444553540000__INCLUDED_) #define APP1_H__99206D24_7535_11D0_847F_444553540000__INCLUDED_ // app1.h : main header file for the APP1 application // #ifndef __AFXWIN_H__ #error include 'stdafx.h' before including this file for PCH #endif #include "resource.h" // main symbols ///////////////////////////////////////////////////////////////////////////// // CApp1App: // See app1.cpp for the implementation of this class // class CApp1App : public CWinApp { public: CApp1App(); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CApp1App) public: virtual BOOL InitInstance(); //}}AFX_VIRTUAL // Implementation //{{AFX_MSG(CApp1App) 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() }; ///////////////////////////////////////////////////////////////////////////// //{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations immediately before the previous line. #endif // !defined(APP1_H__99206D24_7535_11D0_847F_444553540000__INCLUDED)
Near the top of Listing 3.1, 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 you'll create a little later in this chapter, in the section titled "Creating the Rectangles Application," 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 chooses 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. When you build this chapter's sample program, you won't believe how easy it is to load and save 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. (Yes, I realize that this sort of interactivity between classes breaks all the object-oriented programming (OOP) rules of data encapsulation, but sometimes you have to give up some things in order to gain others.) If you look at Listing 3.2, you can see how the CApp1View class, which you created in Chapter 2, obtains pointers to the document object.
See Creating Your First MFC Program, (ch. 2)
Listing 3.2 APP1VIEW.HThe Header File for the CApp1View Class
#if !defined(APP1VIEW_H__99206D2E_7535_11D0_847F_444553540000__INCLUDED_) #define APP1VIEW_H__99206D2E_7535_11D0_847F_444553540000__INCLUDED_ // 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 ///////////////////////////////////////////////////////////////////////////// //{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations immediately before the previous line. #endif // !defined(APP1VIEW_H__99206D2E_7535_11D0_847F_444553540000__INCLUDED)
Near the top of Listing 3.2, 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 the preceding 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 when the less clear GetDocument()->variable version makes code difficult 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().
When you first start using an application framework, such as 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 it is also an entity unto itself. The window element becomes associated with the window class when MFC calls the OnCreate() member function.
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 following steps, you'll build the Rectangles application, which, not only demonstrates the manipulation of documents and views, but also gives you a chance to edit an application's resources as well as use ClassWizard to add message response functions to an application.
Follow the first steps to create the basic Rectangles application and modify its resources:
The complete source code and executable file for the Rectangles application can be found in the CHAP03\Recs directory of this book's CD-ROM.
***BEGIN NUMBERED LIST
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 |
Step 5 of 6 | Leave set to defaults. |
Step 6 of 6 | Leave set to defaults. |
The first segment of the IDR_MAINFRAME string appears in your main window's title bar.
Now that you have the application's resources the way you want them, it's time to add code to the document and to the view classes in order to create an application that actually does something. Perform the following steps to add the code that modifies the document class to handle the application's data (which is an array of CPoint objects that determines where rectangles should be drawn in the view window):
CPoint m_points[100]; UINT m_pointIndex;
m_pointIndex = 0;
Listing 3.3 LST03_03.CPPCode 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 3.4 LST03_04.CPPCode 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 3.5 LST03_05.CPPCode for Displaying the Application's Data (Rectangles)
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 code in Listing 3.5, which iterates through the document object's m_points[] array and displays rectangles at the coordinates it finds in the array, is executed whenever the application's window needs repainting.
pInfo->SetMaxPage(1);
Listing 3.6 LST03_06.CPPCode 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();
The code in Listing 3.6 adds a point to the document's point array each time the user clicks the left mouse button over the view window. The call to Invalidate() causes MFC to call the OnDraw() function, where the window's display is redrawn with the new data.
You've now finished the complete application. Click the Build button on the toolbar or choose the Build, Build command from the menu bar to
compile and link the application.
Once you have the Rectangles application compiled and linked, run it by choosing Build, Execute. When you do, you'll 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 Figure 3.16).
The Rectangles application's document consists of rectangles that the user places on the screen.
To save your work (this is work?), choose the File, Save. You can view your document in print preview by choosing File, Print Prev
iew, or just go ahead and print by choosing File, Print. Of course, you can create a new document by choosing File, New, or
load a document you previously saved by choosing File, Open. Finally, if you choose Help, About Rectangles, you'll see the
application's About dialog box (see Figure 3.17).
The About dialog box provides information about the application.
If this is your first 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'll examine the key parts of the Rectangles application. Keep in mind that what you learn here is only the first step toward understanding MFC. Only the primary issues of using AppWizard and MFC are covered in the following section. Subsequent chapters in this book solve many of the remaining MFC mysteries.
As you've read again and again since the beginning of this book, 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 3.7 shows the relevant code.
Listing 3.7 LST03_07.CPPDeclaring the Document Data of the Rectangles Application
// Attributes public: CPoint m_points[100]; UINT m_pointIndex;
In Listing 3.7, 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 classvariables 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.
Once 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, the 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 3.8.
Listing 3.8 LST03_08.CPPThe Rectangles Application's OnNewDocument() Function
BOOL CRecsDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // TODO: add reinitialization code here // (SDI documents will reuse this document) /////////////////////////////////////// /////////////////////////////////////// // START CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// m_pointIndex = 0; /////////////////////////////////////// /////////////////////////////////////// // END CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// return TRUE; }
In many of the listings in this chapter, code that you added to a function is located between START CUSTOM CODE and END CUSTOM CODE comment blocks. That is, all of the code in Listing 3.8, except the line m_pointIndex = 0; was created by AppWizard.
MFC calls the OnNewDocument() function whenever the user starts a new document, usually by choosing 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, you can perform general document initialization in the class's constructor in an MDI application.
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 book 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. 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 3.9 shows the Rectangles application's Serialize() function.
Listing 3.9 LST03_09.CPPThe Rectangles Application's Serialize() Function
void CRecsDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: add storing code here /////////////////////////////////////// /////////////////////////////////////// // START CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// ar << m_pointIndex; for (UINT i=0; i<m_pointIndex; ++ i) { ar << m_points[i].x; ar << m_points[i].y; } /////////////////////////////////////// /////////////////////////////////////// // END CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// } else { // TODO: add loading code here /////////////////////////////////////// /////////////////////////////////////// // START CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// ar >> m_pointIndex; for (UINT i=0; i<m_pointIndex; ++ i) { ar >> m_points[i].x; ar >> m_points[i].y; } UpdateAllViews(NULL); /////////////////////////////////////// /////////////////////////////////////// // END CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// } }
As you can see in Listing 3.9, 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++ input/output objects. If you look at the Serialize() function, you'll 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 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.
Now that you've got your document class 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 OnDraw() member function of the view class that is responsible for displaying data, either on the screen or the printer. Listing 3.10 shows the Rectangles application's version of OnDraw().
Listing 3.10 LST03_10.CPPThe Rectangles Application's OnDraw() Function
void CRecsView::OnDraw(CDC* pDC) { CRecsDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here /////////////////////////////////////// /////////////////////////////////////// // START CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// 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); } /////////////////////////////////////// /////////////////////////////////////// // END CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// }
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.
See Exploring the Paint1 Application, (ch. 5)
Also notice thatbecause an application that uses the document/view model stores its data in the document classAppWizard 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 3.11 shows the OnLButtonDown() function where this data update occurs.
Listing 3.11 LST03_11.CPPThe Rectangles Application's OnLButtonDown() Function
void CRecsView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default /////////////////////////////////////// /////////////////////////////////////// // START CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// CRecsDoc *pDoc = GetDocument(); if (pDoc->m_pointIndex == 100) return; pDoc->m_points[pDoc->m_pointIndex] = point; ++pDoc->m_pointIndex; pDoc->SetModifiedFlag(); Invalidate(); /////////////////////////////////////// /////////////////////////////////////// // END CUSTOM CODE /////////////////////////////////////// /////////////////////////////////////// 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.
In this chapter, you got a quick look at 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. Because AppWizard can disguise much of what is going on in an MFC application, the rest of this book concentrates on writing MFC applications without AppWizard's help. However, most of what you'll learn in the upcoming chapters can be applied to AppWizard-generated applications, as well. Remember that although AppWizard is a useful tool, there's really nothing magical about it: It creates the same type of MFC code that you can create on your own.