The main topic for this section is Document/View, the architecture used by programs written using AppWizard and the MFC class library. In this section, you will learn
The Document/View architecture.
Although the name "Document/View" might seem to limit you to only word-processing applications, the architecture can be used in a wide variety of program types. There is no limitation as to the data managed by CDocument; it can be a word processing file, a spreadsheet, or a server at the other end of a network connection providing information to your program. Likewise, there are many types of views. A view can be a simple window, as used in the simple SDI applications presented so far, or it can be derived from CFormView, with all the capabilities of a dialog box. You will learn about form views in Section 23, "Advanced Views."
Several different types of documents can be used in an MDI program, with each document having one or more views. Several documents can be open at a time, and the open document often uses a customized toolbar and menus that fit the needs of that particular document.
The Document/View architecture defines several main categories for classes used in a Windows program. Document/View provides a flexible framework that you can use to create almost any type of Windows program. One of the big advantages of the Document/View architecture is that it divides the work in a Windows program into well-defined categories. Most classes fall into one of the four main class categories:
The basic difference between an SDI application and an MDI application is that an MDI application must manage multiple documents and, usually, multiple views. The SDI application uses only a single document, and normally only a single view.
Both SDI and MDI applications use an object called a document template to create a relationship between a view, a document, and a frame class, as well as an identifier used for the program's menu, icon, and other resources. You use the CSingleDocTemplate class for SDI applications and the CMultiDocTemplate class for MDI applications. These two classes share a common base class, CDocTemplate. Listing 9.1 is an example of a document template used for an SDI program.
CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CTestDoc), RUNTIME_CLASS(CMainFrame), RUNTIME_CLASS(CTestView)); AddDocTemplate(pDocTemplate);Two types of frame windows exist in an MDI program: the main frame, which encompasses the entire client area, and the child frame, which contains each MDI child window. The different windows used in an MDI program are shown in Figure 9.2.
Figure 9.2
The windows used in a typical MDI program.
The C++ source code generated by Developer Studio for an MDI program is slightly different than the code it generates for an SDI program. Examine this code, shown in Listing 9.2, to see some of the differences between MDI objects and SDI objects.
CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_TESTTYPE, RUNTIME_CLASS(CTestDoc), RUNTIME_CLASS(CChildFrame), RUNTIME_CLASS(CTestView)); AddDocTemplate(pDocTemplate);CChildFrame is a class included in every MDI project created by AppWizard, and is derived from CMDIChildFrame. This class is provided to make customizing the frame to suit your needs easy. Every MDI child window has a frame that owns the minimize, maximize, and close buttons and the frame around the view. Any customization you want to do to the frame is done in the CChildFrame class.
You will learn about the interfaces used by the document and view classes in the next section. However, you add almost all these interfaces using ClassWizard. Let's look at one of these interfaces, GetFirstViewPosition. A document can obtain a pointer to the first view associated with the document using this function. Normally, the framework will maintain a list of the views associated with a document, but you can keep this list yourself by overriding this function. Because the GetFirstViewPosition function is virtual, your implementation of it is always called if available.
To add an implementation for one of the Document/View interface functions, follow these steps, which are similar to the steps used to add message-handling functions:
2. Select the name of the class that supplies the interface to be added; in this case, a class derived from CDocument.
3. Select the Message Maps tab.
4. Select the CDocument-derived class as the object ID.
5. Select the interface function to be added from the list box.
6. Click the Add Function button.
7. Close ClassWizard.
For the remaining examples in this section, you will create an MDI project named DVTest. To create the DVTest example, use AppWizard to create a default MDI program. Name the program DVTest. Feel free to accept or change any of the default parameters offered by AppWizard because they have no impact on these examples. When finished, DVTest displays a collection of names stored by the document class.
New Term: A pointer is simply a numeric variable. This numeric variable is an address, or location in memory where the actual data resides. Pointers must also follow the same rules that are applied to other variables. They must have unique names, and they must be declared before they can be used.
Every object or variable that is used in an application takes up a location or multiple locations in memory. This memory location is accessed via an address (see Figure 9.3).
Figure 9.3
The text Hello stored beginning at address 1000.
In this figure, the text Hello is stored in memory beginning at address 1000. Each character takes up a unique address space in memory. Pointers provide a method for holding and getting to these addresses in memory. Pointers make manipulating the data easier because they hold the address of another variable or data location.
Just a Minute: Pointers give flexibility to C++ programs and enable the programs to grow dynamically. By using a pointer to a block of memory that is allocated at runtime, a program can be much more flexible than one that allocates all its memory at once.
A pointer is also easier to store than a large structure or class object. Because a pointer just stores an address, it can easily be passed to a function. However, if an object is passed to a function, the object must be constructed, copied, and destroyed, which can be costly for large objects.
void DisplayBtnSize()
{
int nSize = sizeof(CButton);
CString strMsg;
strMsg.Format("The size of CButton is %d bytes", nSize);
AfxMessageBox(strMsg);
}
The address-of operator, &, returns the address of a variable or object. This operator is associated with the object to its right, like this:
&myAge;This line returns the address of the myAge variable.
The indirection operator, *, works like the address-of operator in reverse. It also is associated with the object to its right, and it takes an address and returns the object contained at that address. For example, the following line determines the address of the myAge variable; then it uses the indirection operator to access the variable and give it a value of 42:
*(&myAge) = 42;
#include <iostream>
using namespace std;
int main()
{
int nVar;
int* pVar;
// Store a value in nVar, and display it. Also
// display nVar's address.
nVar = 5;
cout << "nVar's value is " << nVar << "." << endl;
cout << "nVar's address is " << &nVar << "." << endl;
// Store the address of nVar in pointer pVar. Display
// information about pVar and the address it points to.
pVar = &nVar;
cout << "pVar's value is " << pVar << "." << endl;
cout << "*pVar's value is " << *pVar << "." << endl;
// Change the value of the variable pointed to by pVar.
*pVar = 7;
cout << "nVar's value is " << nVar << "." << endl;
cout << "pVar's value is " << pVar << "." << endl;
cout << "*pVar's value is " << *pVar << "." << endl;
return 0;
}
It's important to remember that the pointer does not contain a variable's
value, only its address. The indirection operator enables you to refer
to the value stored at the address instead of to the address itself.
As shown in Listing 9.4, a pointer variable is declared using the indirection operator, like this:
int* pVar; // declare a pointer to int
CAUTION: If you are in the habit of declaring several variables on one line, look out for pointer declarations. The indirection operator applies only to the object to its immediate right, not to the whole line. The declaration
int* pFoo, pBar;
declares and defines two variables: a pointer to an int named pFoo, and an int named pBar. The pBar variable is not a pointer. If you insist on declaring more than one pointer per line, use this style:
int *pFoo, *pBar;
Pointers are useful when you must change a parameter inside a function. Because parameters are always passed by value, the only way to change the value of a parameter inside a function is to send the address of the variable to the function, as Listing 9.5 does.
#include <iostream>
using namespace std;
void IncrementVar( int* pVar );
int main()
{
int nVar = 0;
cout << "The value of nVar is now " << nVar << "." << endl;
IncrementVar( &nVar );
cout << "The value of nVar is now " << nVar << "." << endl;
return 0;
}
void IncrementVar( int* nVar )
{
*nVar += 1;
}
Figure 9.4 shows how the address is used to change the value of a variable
outside the function.
Changing a variable's address outside a function.
Another use for pointers is to keep a reference to memory that has been requested at runtime from the operating system. You will use pointers like this later, in the section called "Using new and delete to Create Dynamic Objects."
int myAge; int& myRef = myAge;This code defines a reference variable named myRef, which is a reference, or alias, for the myAge variable. The advantage of using a reference instead of a pointer variable is that no indirection operator is required. However, after it is defined, the reference variable cannot be bound to another variable. For example, code such as that in Listing 9.6 often is misunderstood.
void refFunc()
{
int nFoo = 5;
int nBar = 10;
// Define a reference to int that is an alias for nFoo.
int& nRef = nFoo;
// Change the value of nFoo.
nRef = nBar;
CString strMsg;
strMsg.Format("nFoo = %d, nBar = %d", nFoo, nBar);
AfxMessageBox(strMsg);
}
If you use the refFunc function in a Windows program, you will
see that the line
nRef = nFoo;does not change the binding of the nRef variable; instead, it assigns the value of nBar to nFoo, with nFoo being the variable to which nRef is a reference.
References are most commonly used when passing parameters to functions. Passing a class object as a function parameter often is quite expensive in terms of computing resources. Using a pointer to pass a parameter is subject to errors and affects the function's readability. However, if you use references as function parameters, you eliminate unnecessary copies, and you can use the parameter as if a copy were passed. To prevent the called function from changing the value of a reference variable, you can declare the parameter as const, like this:
void Print( const int& nFoo )
{
nFoo = 12; // error - not allowed to change const
cout << "The value is " << nFoo << endl;
}
]Time Saver: References to const objects are often used when large objects are passed to a function because it can be expensive, in terms of computing resources, to generate a copy of a large object that is used only during a function call.
In a C++ program, you can use the new and delete operators to allocate and destroy variables dynamically, as shown in Listing 9.7.
void ptrFunc()
{
int *pFoo = new int;
*pFoo = 42;
CString strMsg;
strMsg.Format("Foo = %d", *pFoo);
AfxMessageBox(strMsg);
delete pFoo;
}
void ptrArrayFunc()
{
// Create array
const int nMaxFoo = 5;
int *arFoo = new int[nMaxFoo];
// Fill array
for(int n = 0; n < nMaxFoo; n++)
{
arFoo[n] = 42 + n;
}
// Read array
for(n = 0; n < nMaxFoo; n++ )
{
CString strMsg;
strMsg.Format("Index %d = %d", n, arFoo[n]);
AfxMessageBox(strMsg);
}
// Free array
delete[] arFoo;
}
Note that in Listing 9.8, it's possible to use a variable to specify the
size of the array.
CRect* pRect = new CRect;This example allocates space for a CRect object and calls the CRect constructor to perform any needed initializations. Of course, after the CRect object is no longer needed, you should make sure that the program calls delete to free the allocated memory and cause the class's destructor to be called.
delete pRect;When using a pointer to a class or structure, you use the member selection operator, or ->, to access member data and functions:
pRect->left = 0; int nHeight = pRect->Height();
CWnd* pWnd = new CDialog(/*Initialization info deleted*/; pWnd->EnableWindow();You might be wondering how this code works--after all, how does the compiler know to call the CWnd version of EnableWindow or the CDialog version of EnableWindow? In order to solve this problem, you must declare functions used through base-class pointers as virtual functions. When a function is declared with the virtual keyword, the compiler generates code that determines the actual type of the object at runtime and calls the correct function.
When a virtual function is used, the compiler constructs a special table,
called a virtual function table. This table is used to keep track
of the correct functions to be called for every object of that class. When
a virtual function is called, the virtual function table is used to access
the correct function indirectly, as shown in Figure 9.5.
The virtual function table, used to determine the correct virtual function.
The added overhead of using the virtual function table is fairly small, but it could be significant if you have thousands of small objects or if execution speed is critical. For that reason, a function must be specified as virtual; it doesn't happen by default. Listing 9.9 is an example of a class declaration that uses a virtual function.
class CUser
{
public:
CUser();
virtual void ClearInfo();
protected:
// ...
};
Time Saver: The virtual keyword is used only in the class declaration, not in the function definition.
New Term: When a base class declares a virtual function, it sometimes makes no sense for the base class to provide any implementation for the function. A base class that includes functions that are declared, but are implemented in derived classes, is an abstract class.
To force all subclasses of a class to implement a virtual function, you can declare that function as a "pure" virtual function by adding = 0; to its declaration in the abstract class, as shown in Listing 9.10.
class CShape
{
public:
virtual void Draw() = 0;
};
The interfaces defined by the Document/View architecture represent guarantees about how each of the MFC classes that make up an application behave with regard to each other. For example, the MFC framework always calls the CDocument::OnNewDocument function when a new document is created. The MFC framework, and other classes that might be part of an MFC-based program, expect the new document to be initialized after this function has been called.
Using well-defined interfaces like CDocument::OnNewDocument to perform specific tasks enables you to modify only the functions where you must take special action; you can let the MFC framework handle most functions and interfaces if you want the default behavior.
The Document/View architecture also makes it easy to separate work. For example, data belongs only to the document; a view calls the GetDocument function to collect a document pointer and then uses member functions to collect or update data.
Return to the DVTest example and add a CArray template object to the document class as a private data member. Add the source code from Listing 9.11 to the CDVTestDoc class header, found in the DVTestDoc.h file. Add the source code to the attributes section of the class declaration, which begins with the // Attributes comment generated by AppWizard.
// Attributes public: CString GetName( int nIndex ) const; int AddName( const CString& szName ); int GetCount() const; private: CArray<CString, CString> m_arNames;Because the CDVTestDoc class contains a CArray member variable, the template collection declarations must be included in the project. Add an #include statement at the bottom of the StdAfx.h file.
#include "afxtempl.h"The next step is to implement the functions described in the CDVTestDoc class interface. These functions provide access to the data stored in the document. Add the source code in Listing 9.12 to the DVTestDoc.cpp file.
CString CDVTestDoc::GetName( int nIndex ) const
{
ASSERT( nIndex < m_arNames.GetSize() );
return m_arNames[nIndex];
}
int CDVTestDoc::AddName( const CString& szName )
{
return m_arNames.Add( szName );
}
int CDVTestDoc::GetCount() const
{
return m_arNames.GetSize();
}
Every document class must specify some access functions to add and retrieve
data. The three functions in Listing 9.12 are typical access functions
in that they do not just expose the CArray template. The data
could also be stored in another type of collection. Storing the data in
a CArray object is an implementation detail that should not be
of interest to users of the CDVTestDoc class. This enables the
internal implementation of CDVTestDoc to be changed in the future,
if necessary.
BOOL CDVTestDoc::OnNewDocument()
{
TRACE( "CDVTest::OnNewDocument" );
if (!CDocument::OnNewDocument())
return FALSE;
m_arNames.RemoveAll();
m_arNames.Add( "Curly" );
m_arNames.Add( "Moe" );
m_arNames.Add( "Shemp" );
return TRUE;
}
Listing 9.13 clears the contents of the m_arNames collection and
adds three new names.
Time Saver: The TRACE macro sends an output message to the compiler's debug window, which displays useful information as the program executes.
In Listing 9.13, the TRACE macro will display a line of text when a new document is created. It's a good idea to have your program provide tracing information whenever an interesting event occurs.
void CDVTestView::OnDraw(CDC* pDC)
{
CDVTestDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// Calculate the space required for a single
// line of text, including the inter-line area.
TEXTMETRIC tm;
pDC->GetTextMetrics( &tm );
int nLineHeight = tm.tmHeight + tm.tmExternalLeading;
CPoint ptText( 0, 0 );
for( int nIndex = 0; nIndex < pDoc->GetCount(); nIndex++ )
{
CString szName = pDoc->GetName( nIndex );
pDC->TextOut( ptText.x, ptText.y, szName );
ptText.y += nLineHeight;
}
}
There are three main parts to Listing 9.14:
Figure 9.6
DVTest displays three names in its view window.
In Section 22, "Serialization," you'll learn how to save the document to a file. In Section 23, "Advanced Views," you'll extend DVTest to include multiple views; one view will enable you to add names to the document.
int *pBadInt; *pBadInt = 42; // Error here
int *pInt = NULL; int *pInt = new int;
int n; int *pInt; pInt = &n; *pInt = 42; // Okay pInt = new int; *pInt = 42; // Okay delete pInt;
A It might seem easier at first. However, the MFC framework will provide a great deal of help for free if you follow the Document/View rules. For example, if you try to store your data in your view class, it will be very difficult to provide multiple views for the same document. As you will see in Section 23, it's fairly straightforward if you follow the Document/View model. Also, as you will see in Section 22, MFC gives you a great deal of support for loading and storing data stored in your document classes.
2. What are some of the differences between pointers and references?
3. What is more efficient to pass as a parameter--a pointer or an object? Why?
4. What keyword is used to dynamically allocate memory?
5. What keyword is used to release dynamically allocated memory?
6. In the Document/View architecture, which classes are responsible for maintaining the user interface?
7. What are the four main categories of classes in the Document/View architecture?
8. What part of the Document/View architecture is responsible for the application's data?
9. What CView member function is used to retrieve a pointer to the document associated with the view?
10. What CDocument member function is used to notify a document's views that their user interface might need to be updated?
2. Modify the DVTest project so that a line number is displayed for each item in the view.
Download Sample Programs - DVTest.zip
|
|
|
|