Teach Yourself Visual C++® 5 in 24 Hours

Previous chapterNext chapterContents


- Hour 9 -
The Document/View Architecture

The main topic for this hour is Document/View, the architecture used by programs written using AppWizard and the MFC class library. In this hour, you will learn

Also in this hour you will build DVTest, a sample program that will help illustrate how documents and views interact with each other in an MFC program.

Visual C++ Support for Document/View

MFC and AppWizard use the Document/View architecture to organize programs written for Windows. Document/View separates the program into four main classes:

Each of these classes has a specific role to play in an MFC Document/View application. The document class is responsible for the program's data. The view class handles interaction between the document and the user. The frame class contains the view and other user interface elements, such as the menu and toolbars. The application class is responsible for actually starting the program and handling some general-purpose interaction with Windows. Figure 9.1 shows the four main parts of a Document/View program.

Figure 9.1.
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 Hour 23, "Advanced Views."

SDI and MDI Applications

There are two basic types of Document/View programs:

An SDI program supports a single type of document and almost always supports only a single view. Only one document can be open at a time. An SDI application focuses on a particular task and usually is fairly straightforward.

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.

Why Use Document/View?

The first reason to use Document/View is that it provides a large amount of application code for free. You should always try to write as little new source code as possible, and that means using MFC classes and letting AppWizard and ClassWizard do a lot of the work for you. A large amount of the code that is written for you in the form of MFC classes and AppWizard code uses the Document/View architecture.

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:

Dividing work done by your program helps you manage the design of your program more effectively. Extending programs that use the Document/View architecture is fairly simple because the four main Document/View classes communicate with each other through well-defined interfaces. For example, to change an SDI program to an MDI program, you must write little new code. Changing the user interface for a Document/View program impacts only the view class or classes; no changes are needed for the document, frame, or application classes.

Using AppWizard

Use AppWizard to create SDI and MDI applications. In earlier chapters, you use AppWizard to create the SDI programs used as examples. Although doing so is more complicated, you can use AppWizard to create an MDI application almost as easily as an SDI.

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.

TYPE: Listing 9.1. How AppWizard uses a document template in an SDI application.

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.

TYPE: Listing 9.2. AppWizard code that uses a document template in an MDI application.

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.

Using ClassWizard

You have used ClassWizard in previous hours to add member variables to dialog box classes, add new classes to a project, and handle messages sent to view windows and dialog boxes. You also use ClassWizard to add interfaces defined as part of the Document/View architecture. In most cases, default behavior provided by the MFC framework is enough for simple programs.

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:

1. Open ClassWizard.

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.

You can use ClassWizard to override all the interfaces defined for programs using the Document/View architecture. Interfaces such as GetFirstViewPosition are rarely overridden, except when debugging. If you provide a new version of GetFirstViewPosition, you probably should override the related function GetNextView as well.

For the remaining examples in this hour, 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.

Pointers and References

Pointers are important topics in C++ programming. A good understanding of the ways in which pointers are used will help you write programs that are more flexible and reliable. C++, and MFC in particular, relies very heavily on proper understanding and use of pointers.

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.

How Are Pointers Used?

When using pointers and memory addresses, it is often useful to know the amount of memory required for each object pointed to. If you must know the amount of storage required for a particular object or variable, you can use the sizeof operator. You can also use sizeof to determine the amount of storage required for your own classes and structures, as shown in Listing 9.3.

TYPE: Listing 9.3. Using sizeof with class types.

void DisplayBtnSize()

{

    int nSize = sizeof(CButton);



    CString strMsg;

    strMsg.Format("The size of CButton is %d bytes", nSize);



    AfxMessageBox(strMsg);

}

The Indirection and Address Operators

Two operators are used when working with addresses in a C++ program: the address-of operator (&) and the indirection operator (*). These operators are different from operators seen previously because they are unary, meaning that they work with only one operand.

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;

Using the Indirection Operator

You can use a pointer with the indirection operator to change the value of the other variable, as shown in the console-mode program in Listing 9.4.

TYPE: Listing 9.4. Using a pointer variable with the indirection operator.

#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.

TYPE: Listing 9.5. Using a pointer and a function to change a variable's value.

#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.

Figure 9.4.
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."

Using References

In addition to using pointers to refer to other variables, the C++ language also has a derived type known as a reference. A reference is declared using the reference operator &, which bears an uncanny resemblance to the address-of operator. Both operators use the same symbol; however, you use them in different contexts. The only time & is used for a reference is in a declaration, like this:

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.

TYPE: Listing 9.6. Using references to change the value of a variable.

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.

Using new and delete to Create Dynamic Objects

So far, you've learned about variables allocated as local objects that are created when a function or block is entered and destroyed when the function or block is exited. Most programs that work in the real world use variables and objects that have a dynamic lifetime, meaning that they are explicitly created and explicitly destroyed.

In a C++ program, you can use the new and delete operators to allocate and destroy variables dynamically, as shown in Listing 9.7.

TYPE: Listing 9.7. Using new and delete for fundamental types.

void ptrFunc()

{

    int *pFoo = new int;

    *pFoo = 42;



    CString strMsg;

    strMsg.Format("Foo = %d", *pFoo);

    AfxMessageBox(strMsg);



    delete pFoo;

}

Using new[] and delete[] to Create Arrays

You also can create arrays dynamically using new[], with the size of the array specified inside the square brackets. When you create an array using new[], you must use delete[] to release the memory allocated for the array. The size of the array is not specified when delete[] is used. Using delete[] is the only clue to the compiler indicating that the pointer is the beginning of an array of objects. Listing 9.8 is an example of a function showing how to allocate and free a dynamic array.

TYPE: Listing 9.8. Using new[] to create a dynamic array.

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.

Using Pointers with Derived Classes

An instance of a class can be allocated and used dynamically, just as if it were one of the fundamental types, like this:

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();

Using a Pointer to a Base Class

Because a class derived from a base class actually contains the base, it's possible to use a pointer to a base class when working with a derived object. For example, in the MFC library, you can use a pointer to a CWnd object in place of a CDialog object. This makes a design much easier to implement because all the functions that work with any type of window can just use pointers to CWnd instead of trying to determine the type of each object. In other words, because CDialog is a CWnd, you should be able to do this:

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.

Using Virtual Functions

New Term: A virtual function is a function that is resolved at runtime.

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.

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.

TYPE: Listing 9.9. An example of declaring 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.

TYPE: Listing 9.10. An example of declaring a pure virtual function.

class CShape

{

public:

    virtual void Draw() = 0;

};

Exploring Document/View Interfaces

The most commonly used interfaces in a Document/View program handle communication between the document and view objects, and between Windows and the document and view objects. Each of these interfaces has a particular purpose. Some are always overridden in the classes you include in your project; many are overridden only when needed. These are three of the major interfaces used in a Document/View program:

Remember, this list is just an overview of the major interfaces. The list does not cover all the interfaces required in an SDI or MDI program. Your mileage may vary; after using the Document/View architecture for a while, you might have another set of favorite interfaces.

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.

Creating a Data Model

Each of the interfaces discussed earlier has a specific role. For the remaining examples in this chapter, you will use the DVTest example created earlier in this hour.

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.

TYPE: Listing 9.11. Changes to the CDVTestDoc class declaration.

// 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.

TYPE: Listing 9.12. New functions added to the CDVTestDoc class.

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.

Initializing a Document's Contents

You create and initialize document objects in two different ways, depending on the type of application using the document:

In most cases, the best place to perform any initialization is in the CDocument::OnNewDocument member function. This function is provided with some default code inserted by AppWizard. Edit the OnNewDocument function so it looks like the code provided in Listing 9.13.

TYPE: Listing 9.13. Changes to the CDVTestDoc::OnNewDocument member function.

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.

Getting the Document Pointer

Every view is associated with only one document. When a view must communicate with its associated document, the GetDocument function is used. If the view is created by AppWizard, as CDVTestView is, the GetDocument function returns a pointer to the proper document type. Listing 9.14 is a version of OnDraw that uses GetDocument to retrieve a pointer to the CDVTestDoc class; then it uses the pointer to collect the names contained in the document.

TYPE: Listing 9.14. Using GetDocument to fetch a document pointer.

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:

Compile and run the DVTest project. DVTest displays the names stored in the document class, as shown in Figure 9.6.

Figure 9.6.
DVTest displays three names in its view window.

In Hour 22, "Serialization," you'll learn how to save the document to a file. In Hour 23, "Advanced Views," you'll extend DVTest to include multiple views; one view will enable you to add names to the document.

Summary

In this hour you've learned about pointers and references, as well as the basic Document/View architecture used in most MFC programs. You learned how to use AppWizard and ClassWizard in SDI and MDI applications, and created a sample program demonstrating the use of Document/View.

Q&A

Q When I use pointers, sometimes I get an Unhandled Exception error from Windows and my program crashes. The code that causes the problem looks something like this:
int *pBadInt;

*pBadInt = 42;   // Error here
A There are two problems. First, the pointer isn't initialized. You should always initialize a pointer to either NULL or to an area of memory that is dynamically allocated, like this:
int *pInt = NULL;

int *pInt = new int;
A pointer doesn't automatically set aside any storage area--in the preceding code, pBadInt is uninitialized and is pointing to a random area of memory. You must assign the pointer either an address of an existing variable or the address of a block of dynamically allocated memory:
int n;

int *pInt;

pInt = &n;

*pInt = 42;   // Okay

pInt = new int;

*pInt = 42;   // Okay

delete pInt;
Q I don't get all this Document/View stuff. Wouldn't it be easier to store all the data in the view class?

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 Hour 23, it's fairly straightforward if you follow the Document/View model. Also, as you will see in Hour 22, MFC gives you a great deal of support for loading and storing data stored in your document classes.

Workshop

The Workshop is designed to help you anticipate possible questions, review what you've learned, and begin thinking ahead to putting your knowledge into practice. The answers to the quiz are in Appendix B, "Quiz Answers."

Quiz

1. What is the sizeof operator used for?

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?

Exercises

1. Use the TRACE macro to see when the view requests information from the document. Add a TRACE macro to the document's GetName function, and experiment with resizing and moving the view to see when the view requests the data.

2. Modify the DVTest project so that a line number is displayed for each item in the view.


Previous chapterNext chapterContents


Macmillan Computer Publishing USA

© Copyright, Macmillan Computer Publishing. All rights reserved.