7. Reflection and Persistence
This chapter begins with a description of the CObject class at the root
of the MFC class hierarchy. The CObject class is associated with the CRuntimeClass,
which supports reflection in MFC: runtime type identification, dynamic
instantiation, and serialization. Several non-trivial examples of serialization
are presented.
The control loop of an MFC application was already described in Chapters
1 and 5, but the details of how an MFC application is initialized have
only been mentioned. We describe the details of this process in this chapter.
This not only presents another example of reflection, but it also introduces
the CDocTemplate class, which binds together instances of the CRuntimeClass
representing the application's view and document classes. Modifying this
structure is the key to creating applications with multiple view and document
types. As an example of such an application, we present Brick CAD, a CAD/CAM
system for designing bricks! Brick CAD supports four view types: top view,
front view, side view, and control panel view.
The chapter ends with a detailed description of a simple Windows application
developed in the traditional way, without using MFC. Without message maps
and the Class Wizard, users must implement window procedures by hand. This
serves as a good introduction to MFC message maps.
The CObject Base Class
As we saw in Chapter 1, most, but not all, of the 200-plus MFC classes
appear in the MFC inheritance hierarchy:

Some notable examples of classes not included in this inheritance hierarchy
are locks (CSingleLock), archives (CArchive), points (CPoint), rectangles
(CRect), strings (CString), and runtime classes (CRuntimeClass).
The root of the inheritance hierarchy is the CObject class, which bequeaths
a pointer to an instance of CRuntimeClass to its heirs:
class CObject
{
public:
virtual CRuntimeClass*
GetRuntimeClass() const;
BOOL IsKindOf(const
CRuntimeClass* pClass) const;
virtual void Serialize(CArchive&
ar);
// etc.
};
Reflection
After correctness, efficiency has traditionally been regarded as the
next most important design goal, and after modularity, abstraction has
traditionally been regarded as the next most important design principle.
But the complexity and economics of modern commercial applications has
challenged tradition to some degree. Now flexibility-the ability to easily
adapt to new requirements-- is as important as efficiency. In some cases
the adaptation must happen dynamically, while the program is running and
runtime state information is available.
Dynamically adapting to a new requirement means some of the problems
programmers solve when writing a program-which algorithm or data structure
should be used-- must now be solved by the running program. But the abstraction
principle asserts that how a class or function achieves its purpose should
be hidden from its clients. Clearly, abstraction must be relaxed to some
degree if a client must alter the behavior of a component. In addition
to basic services, a reflective component allows clients to inspect
and possibly change its implementation. (Components that hide their implementation
from clients are called black-box components.)
The CRuntimeClass Meta-class
The purpose of the CObject base class is to provide some degree of reflection
to the other classes in the MFC hierarchy. The simplest form of reflection
is runtime type identification-the ability to query an object about the
classes it instantiates. This presents a bit of a paradox for C++ programmers:
C++ objects only exist at runtime, while C++ classes only exist at compile
time. What sort of answer do we expect an object to give when asked what
class it instantiates? The object-oriented view is that the answer should
take the form of an object, an object representing a class.
At first, the notion of an object representing a class seems strange,
but C++ objects represent application domain objects such as submarines,
egg beaters, and employees, why not include C++ objects that represent
solution domain objects such as classes, functions, and variables?
MFC programs represent classes as objects that instantiate the CRuntimeClass
class:
struct
{
LPCSTR m_lpszClassName;
int m_nObjectSize;
CRuntimeClass*
m_pBaseClass; // for statically linked
CRuntimeClass*
m_pGetBaseClass(); // for dynamically linked
CObject* CreateObject();
BOOL IsDerivedFrom(const
CRuntimeClass* pBaseClass) const;
// etc.
} CRuntimeClass;
MFC uses the CRuntimeClass to support three levels of reflection: runtime
type identification, dynamic instantiation, and serialization.
Runtime Type Identification
We will demonstrate the basics of reflection using a console application
consisting of an Airplane and a Blimp class, both derived from an abstract
Aircraft class, which in turn is derived from MFC's CObject class:

We begin by creating a project that uses MFC.
1. Create an empty Win32 Console Application called Flight. Use the
list control in the [Project]/[Settings]/[General] dialog to select "Use
MFC in a shared DLL" or "Use MFC in a Static Library".
Curiously, the [Insert]/[New Class ...] dialog doesn't allow programmers
to create an MFC class derived from CObject. Instead, programmers must
trick Visual C++ by using this dialog to create a generic class derived
from CObject.
2. From the [Insert]/[New Class ...] menu create a generic class called
Aircraft derived from CObject. A dialog box will warn that no header file
for the CObject base class can be found. Dismiss this dialog by clicking
its [OK] button.
CObject, along with all of the other MFC classes, is defined in the <afx.h>
header file. Our console application will also require the standard I/O
machinery from the <iostream> header file.
3. Add the following include directives at the top of Airplane.h
#include <afx.h>
#include <iostream>
using namespace std;
The Aircraft class is similar to the Aircraft interface discussed in Appendix
1. It requires all derived classes to provide implementations of Takeoff(),
Fly(), and Land() member functions. In turn, these classes will inherit
protected speed and altitude member variables.
4. Add public, pure virtual Takeoff(), Fly(), and Land() functions
to the Aircraft class. Add protected member variables representing the
aircraft's speed and altitude. The constructor should initialize these
variables to zero and a virtual Display() function should display these
variables. Also, add the DECLARE_DYNAMIC macro to the class declaration.
Unfortunately, the GetRuntimeClass() function inherited from the CObject
base class doesn't work. It must be redefined in the Aircraft class. This
would seem to defeat the purpose of deriving Aircraft from CObject, but
fortunately, MFC provides two macros that expand into the necessary declarations.
The DECLARE_DYNAMIC macro:
DECLARE_DYNAMIC(Aircraft)
is placed inside the Aircraft class declaration in the Aircraft.h file.
Copy Mechanisms
Recall that C++ automatically provides each class with a copy constructor
that initializes a new class instance by making a member-wise copy of an
existing class instance. Member-wise copy is also used when a variable
containing an object is assigned to another. Unfortunately, CObject makes
its copy constructor and assignment operator private. This can cause errors
when we attempt to copy instances of classes derived from CObject. Therefore,
it is common practice to add a public copy constructor and assignment operator
to CObject-derived classes.
The Aircraft class can provide a public copy constructor and assignment
operator for all of its derived classes. Both functions call a virtual
helper function that makes a make member-wise copy of its parameter. Of
course classes derived from Aircraft will need to extend this helper function.
5 Add a public copy constructor and assignment operator to the Aircraft
class. Both call a protected, virtual helper function named Copy(), which
performs a member-wise copy of its parameter.
Here is a complete listing of the declaration:
class Aircraft : public CObject
{
public:
DECLARE_DYNAMIC(Aircraft)
Aircraft() { altitude
= speed = 0; }
virtual ~Aircraft()
{}
Aircraft(const
Aircraft& a) { Copy(a); }
Aircraft& operator=(const
Aircraft& a);
virtual void Takeoff()
= 0;
virtual void Fly()
= 0;
virtual void Land()
= 0;
float GetSpeed()
{ return speed; }
float GetAltitude()
{ return altitude; }
virtual void Display();
protected:
float altitude,
speed;
virtual void Copy(const
Aircraft& a)
{ // make a member-wise
copy:
altitude = a.altitude;
speed = a.speed;
}
};
Recall that if x and y are objects, then an assignment of the form x =
y translates into
x.operator=(y)
If z is also an object, then a compound assignment of the form x = y =
z translates into:
x.operator=(y.operator=(z))
In other words, the output produced by operator=() might also be its input.
Therefore, the output and input types of the overloaded assignment operator
are the same. Here is our implementation of the Aircraft assignment operator:
Aircraft& Aircraft::operator=(Aircraft&
a)
{
if (&a != this)
Copy(a); // ignore x = x
return a;
}
The DECLARE_DYNAMIC macro only expands into the necessary declarations
needed to support reflection. The IMPLEMENT_DYNAMIC macro is placed in
the Aircraft.cpp file and expands into the corresponding implementations.
6. Add the IMPLEMENT_DYNAMIC macro to the Aircraft.cpp file.
This macro requires programmers to specify the Aircraft base class:
IMPLEMENT_DYNAMIC(Aircraft,
CObject)
Airplanes
Of course the Aircraft class is abstract and can't be instantiated.
Before we can demonstrate runtime type information, we must create a concrete
Airplane subclass. This class must implement all of the pure virtual member
functions inherited from the Aircraft base class. Of course Airplane can
implement additional member functions and provide additional member variables.
7. Repeat step 2 to add an Airplane class to the Flight project that's
derived from the Aircraft class. Add yaw, pitch, and roll member variables
that describe the plane's orientation. The constructor should initialize
these variables. Implement the inherited virtual functions. Add a function
called Bank() that rolls the plane. Don't forget to include Aircraft.h
at the top of Airplane.h
Here is a complete listing of the Airplane class. Note that the DECLARE_DYNAMIC
macro must appear in the Airplane class declaration:
class Airplane : public Aircraft
{
public:
DECLARE_DYNAMIC(Airplane)
Airplane() { yaw
= pitch = roll = 0; }
virtual ~Airplane()
{}
void Takeoff();
void Fly();
void Bank(float
degrees);
void Land();
void Display();
private:
float yaw, pitch,
roll;
void Copy(const
Airplane& a)
{
Aircraft::Copy(a);
yaw = a.yaw;
pitch = a.pitch;
roll = a.roll;
}
};
Pending the advise of a pilot, our implementations of the Airplane member
functions simply print diagnostic messages and adjust a few member variables.
8. Provide simple implementations for the Airplane member functions.
Here is an example from Airplane.cpp:
void Airplane::Bank(float degrees)
{
cout << "An
airplane is banking\n";
roll = degrees;
}
9. Insert the IMPLEMENT_DYNAMIC macro in the Airplane.cpp file.
IMPLEMENT_DYNAMIC(Airplane,
Aircraft)
To build the project, we'll need a main() function that serves as a test
driver.
10. Add an implementation file to the Flight project called test.cpp.
Implement a main() function in test.cpp that calls a function that displays
aircraft meta information. This function should also be defined in test.cpp.
Dynamic Downcasting in MFC
Our main() begins by pointing an Aircraft pointer at a newly created
Airplane object. The pointer is passed to a global DisplayMetaInfo() function,
which will be explained below. Next, our aircraft takes off and flies.
However, before the aircraft banks, it must be downcast to an Airplane
pointer. We could perform a static downcast, but recall from Appendix 1,
that this will result in a runtime error if the pointer happens to point
to a blimp or some other type of aircraft. To prevent this, we perform
an MFC-style dynamic cast. This involves using MFC's RUNTIME_CLASS macro
to create a pointer to an instance of CRuntimeClass that represents the
Airplane class. This pointer is passed to the IsKindOf() member function.
If the result is true, then we safely perform a static downcast and call
the Bank() function:
int main()
{
Aircraft* craft
= new Airplane();
DisplayMetaInfo(craft);
craft->Takeoff();
craft->Fly();
// MFC style dynamic
cast:
if (craft->IsKindOf(RUNTIME_CLASS(Airplane)))
((Airplane*)craft)->Bank(30);
else
cout << "aircraft is not an airplane\n";
craft->Land();
return 0;
}
At this point one might wonder why MFC doesn't simply use RTTI, the Runtime
Type Information feature that's part of the standard C++ library. This
feature supports the standard dynamic_cast<> operator. The answer is
simple, MFC pre-dates the standard C++ library. But also, runtime type
identification in MFC goes far beyond RTTI. We can already see this in
the DisplayMetaInfo() function, which extracts a pointer to a runtime class
from an Aircraft pointer using the GetRuntimeClass() member function. Using
this object, we can get the name of the class, as well as the names of
all base classes:
void DisplayMetaInfo(Aircraft*
c)
{
cout << "aircraft's
meta information:\n";
CRuntimeClass*
rtc = c->GetRuntimeClass();
CRuntimeClass*
base = rtc->m_pfnGetBaseClass();
CRuntimeClass*
base2 = base->m_pfnGetBaseClass();
cout << "
class = " << rtc->m_lpszClassName << endl;
cout << "
base class = " << base->m_lpszClassName << endl;
cout << "
base of base = " << base2->m_lpszClassName << endl;
}
11. Build and test the application.
Here's the output produced by main():
aircraft's meta information:
class = Airplane
base class = Aircraft
base of base = CObject
An airplane is taking off
An airplane is flying
An airplane is banking
An airplane is landing
Dynamic Instantiation
Sometimes programmers can't anticipate the types of objects they must
create. This is especially true for framework developers who must instantiate
classes that will be defined much later in customizations of the framework.
In these situations programmers turn to the Factory Method design pattern:
Factory Method [Go4]
Other Names
Virtual constructor.
Problem
A "factory" class can't anticipate the class of "product" objects
it must create.
Solution
Provide the factory class with an ordinary member function that creates
product objects. This is called a factory method. The factory method can
be a virtual function implemented in a derived class, a template function
parameterized by a product constructor, or a "smart" function that constructs
products from their types.
In this context we can think of CRuntimeClass as a factory class and CObject
as the base class for all product classes. If Blimp is a class derived
from CObject, and if type points to an instance of CRuntimeClass
that represents Blimp, RUNTIME_CLASS(Blimp), then calling the factory method,
type->CreateObject(), creates a new instance of Blimp.
We can demonstrate dynamic instantiation by creating a Blimp class derived
from Aircraft that supports this feature.
12. Repeat steps 6, 7, and 8 to create an Aircraft-derived Blimp class.
Here is a complete listing of the Blimp class. The main difference is that
the DECLARE_DYNAMIC macro is replaced by the DECLARE_DYNCREATE macro:
class Blimp : public Aircraft
{
public:
DECLARE_DYNCREATE(Blimp)
Blimp() { inflated
= TRUE; }
virtual ~Blimp()
{}
void Takeoff();
void Fly();
void Deflate(float
degrees);
void Inflate(float
degrees);
void Land();
void Display();
private:
BOOL inflated;
void Copy(const
Blimp& a)
{
Aircraft::Copy(a);
inflated = a.inflated;
}
};
Similarly, the IMPLEMENT_DYNCREATE macro is placed in Blimp.cpp instead
of the IMPLEMENT_DYNAMIC macro:
IMPLEMENT_DYNCREATE(Airplane,
Aircraft)
The Blimp member functions can have any implementation, as long as they
are different from the corresponding Airplane implementations:
void Blimp::Takeoff()
{
if (inflated)
{
cout << "A blimp is taking off\n";
speed = 50;
altitude = 500;
}
else
cout << "Blimp is deflated!\n";
}
The global ReflectionDemo() function is modified so that it creates a Blimp
using the CreateObject() factory method.
13. Include "Blimp.h" at the top of test.cpp. Alter the ReflectionDemo()
function so that it uses dynamic instantiation to create a blimp.
Here is a complete listing of the function:
void ReflectionDemo()
{
CRuntimeClass*
rtc = RUNTIME_CLASS(Blimp);
Aircraft* craft
= (Aircraft*)rtc->CreateObject();
DisplayMetaInfo(craft);
craft->Takeoff();
craft->Fly();
// MFC style dynamic
cast:
if (craft->IsKindOf(RUNTIME_CLASS(Airplane)))
((Airplane*)craft)->Bank(30);
else
cout << "aircraft is not an airplane\n";
craft->Land();
}
14. Build and test the application.
Here is the output that is now produced:
aircraft's meta information:
class = Blimp
base class = Aircraft
base of base = CObject
A blimp is taking off
A blimp is flying
aircraft is not an airplane
A blimp is landing
Persistence
A persistent object is an object that can be saved to and restored from
secondary memory-a file or database. Not all objects need to be persistent.
Obviously objects that represent application data need to be persistent,
for example we have seen that the document class of an application is often
persistent, because it holds all of the application data.. On the other
hand, views and other user interface components do not need to be persistent,
because they can be easily recreated when the application starts. Objects
that are not persistent are called transient objects.
Saving an object to a file is called serialization, because the
file representation of the object is a series of bytes. Restoring an object
from a file is called de-serialization.
Serializing the Transitive Closure of an Object
It may seem as though serializing an object is simply a matter of writing
its member variables to a file. But some of the members might be embedded
objects with member variables that are inaccessible to the outer object,
while still other members might be pointers to objects.
The network of all objects that can be reached by following pointers
originating in an object a is called the transitive closure of
a:

Of course the transitive closure of an object isn't necessarily a tree;
it can be an arbitrary network of objects:

Saving and restoring object a to and from secondary memory actually
means saving and restoring the transitive closure of a. But saving
and restoring a network of objects to secondary memory poses three problems:
1. A pointer restored from
secondary memory may no longer be valid.
2. When saving a complex network
of objects to secondary memory, how can we avoid saving the same object
multiple times?
3. When restoring an object from
secondary memory, how will memory for the objects in the transitive closure
be allocated? In the example above, the memory for a might be pre-allocated,
but how would anyone know to pre-allocate memory for objects b through
g?
MFC provides several persistence mechanisms that solve these problems.
We are primarily interested in archives. An archive is an MFC object that
represents an IO stream associated with a binary file.
Saving an object to an archive automatically saves the object's transitive
closure. A pointer to an object of type T is translated into a pair of
the form (T, OID), where OID is a unique integer called an object identifier.
When (T, OID) is extracted from an archive, dynamic instantiation is used
to create a new object from T. The object identifier is then translated
into a pointer to this new object. Archives keep track of these translations
to avoid duplicating objects. (This technique is sometimes referred to
as "pointer swizzling".)
Serialization in MFC
As a simple demonstration of serialization, let's make the Blimp class
persistent. Although Blimp inherits a Serialize() function from CObject,
it will be our job to redefine this function.
15. Replace the DECLARE_DYNCREATE macro in the declaration of Blimp
with the DECLARE_SERIAL macro. Add a Serialize member function.
class Blimp : public Aircraft
{
public:
DECLARE_SERIAL(Blimp)
void Serialize(CArchive&
ar);
// etc.
};
Like the Serialize() functions we have seen before, the parameter to the
Blimp's Serialize() function is a CArchive reference. Internally, the archive's
IsStoring() function is called. If this function returns true, then the
blimp is being serialized into the archive, otherwise the blimp is being
de-serialized from the archive. In the first case, the blimp's member variables
are inserted into the archive using the insertion operator. In the de-serialization
case the blimp's member variables are extracted from the archive using
the extraction operator:
void Blimp::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
ar << altitude;
ar << speed;
ar << inflated;
}
else
{
ar >> altitude;
ar >> speed;
ar >> inflated;
}
}
16. Change main() in test.cpp so that it serializes, then de-serializes
several blimps.
Here is the beginning of the new main():
int main()
{
Blimp b1, b2;
b1.Takeoff();
b1.Display();
b2.Takeoff();
b2.Display();
Normally, the MFC framework automatically creates an archive for us when
the user selects the [File]/[Save], [File]/[Save As ...] or [File]/[Open]
menu items. In our example we must create our own archive. This is a two
step process. First, we must create an object representing the binary file
where our blimps will be serialized. In MFC binary files are represented
by instances of the CFile class. The constructor we use requires the name
of the file and a sequence of flags describing how the file will be opened:
UINT outFlags
= CFile::modeCreate | CFile::modeWrite |
CFile:: shareDenyNone;
cout << "Serializing
2 blimps ... \n";
CFile outFile("blimps",
outFlags);
Next, an archive associated with the address of the CFile object is created
and passed to the Serialize() functions of the two blimps:
CArchive archive1(&outFile,
CArchive::store);
b1.Serialize(archive1);
b2.Serialize(archive1);
archive1.Close();
Although de-serialization could be done by a different program, we will
simply pretend that the second half of main() is a different program that
creates two Blimp objects, a Cfile, and an archive:
cout <<
"Deserializing 2 blimps ... \n";
Blimp b3, b4;
UINT inFlags =
CFile::modeCreate | CFile::modeNoTruncate |
CFile::modeRead | CFile:: shareDenyNone;
CFile inFile("blimps",
inFlags);
CArchive archive2(&inFile,
CArchive::load);
Finally, the blimps in the archive are de-serialized into the new blimp
variables, which are then displayed:
b3.Serialize(archive2);
b4.Serialize(archive2);
b3.Display();
b4.Display();
archive2.Close();
return 0;
}
17. Build and test the application.
Here is the output produced:
A blimp is taking off
altitude = 500
speed = 50
inflated = 1
A blimp is taking off
altitude = 500
speed = 50
inflated = 1
Serializing 2 blimps ...
Deserializing 2 blimps ...
altitude = 500
speed = 50
inflated = 1
altitude = 500
speed = 50
inflated = 1
Notice that although the default Blimp constructor creates blimps b3 and
b4 with initial altitude and speed set to zero, these blimps have the non-zero
speeds and altitudes of blimps b1 and b2 after de-serialization.
Database Browsers
A database browser is a program that allows users to create, modify,
browse, search, and delete persistent collections of data records. For
example, an address book browser allows users to create, modify, browse,
and delete address books. Each book is a persistent collection of person
records. A person record contains the name, address, and phone number of
a person.
A view of an address book is a form with a list box showing the names
of all people in the associated book. The highlighted name in the list
box is the selected entry. The name, address, and phone number of the selected
entry are displayed in labeled text boxes:

Users can change the selected entry by double-clicking on any name in
the list box. The associated address book can be searched by using [Book]
menu commands to change the selected entry to the next, previous, first,
or last names in the list box. The selected entry can be removed from the
book by selecting [Rem] from the [Book] menu. In this case
the previous name becomes the selected entry. Users can type new information
into the text boxes, then select either the [Add] or [Update]
entries from the [Book] menu. Selecting [Add] uses the information
stored in the text boxes to create a new Person record, which becomes both
the selected and last entry in the book. Selecting [Update] uses
the information stored in the text boxes to modify the corresponding fields
of the selected entry. Of course there are toolbar buttons and hot keys
that duplicate the menu commands, and of course different views of the
same address book may have different selected entries.
Finally, users can use the [Save] and [Save As...] items on the [File]
menu to save address books to files with a .abk extension, and the [Open]
item to load address books from .abk files.
Design of the Browser (copy semantics version)
Our design instantiates MFC's Document-View architecture. An address
book is a document that "owns" a sequence of Person objects. Each Person
object "owns" an Address object and a Phone object. Persons, addresses,
phone numbers, and documents must be persistent and therefore must be derived
from MFC's CObject class.
One interesting design choice is how the phrase "a owns b" should be
interpreted. There are two possibilities: "a contains b" or "a contains
a pointer to b". The first interpretation is easier to program, but produces
systems where multiple C++ objects represent a single real world object.
For example, if an address book contains two people who happen to be roommates,
then the corresponding Person objects will contain their own Address and
Phone objects, even though these objects represent the same address and
phone in the real world. This can lead to synchronization problems. For
example, if the roommates change their phone number, then the user must
remember to modify both Phone objects.
UML uses the term "composite" to refer to an object that contains its
components as embedded objects, and the term "aggregate" to refer to an
object that contains pointers or references to its components. Recall from
Appendix1 that an association between a composite and its component is
an association arrow with a solid diamond on the composite end. An association
between an aggregate and its component is similar, except a hollow diamond
appears on the aggregate end.
Although potentially inefficient and confusing, interpreting associations
as compositions leads to an easier implementation, so we begin with it.
Here is our design in the form of a class diagram:

Implementation of the Browser
The browser has much in common with the stack calculator developed in
Chapter 3 (as well as many of the programs developed in the problem section
of Chapter 3). In both cases the document manages a collection of objects:
a stack of rational numbers or an array of persons. In both cases views
of the document allow use a list control to show all members of the collection,
and a text box to show the "selected" member of the collection. In the
case of the stack the selected member is always the top member.
The MFC App Wizard allows us to set the extension of all data files
created by the application. (We use the [Advanced Options] dialog to do
this.) This creates an entry in the Window's registry, so each time the
user double clicks on a file with a .adb extension, the address book browser
will automatically be start and load the file.
1. Use the MFC App Wizard to create a multi-document project called
"Book". On the [Step 4 of 6] dialog click the [Advanced ...] button. Type
"abk" in the [File extension] edit box of the [Advanced Options] dialog
box, then click the [Close] button. On the [Step 6 of 6] dialog select
CFormView as the base class of CBookView.
Addresses, Phones, and Persons
The Address, Person, and Phone classes must be persistent. We will follow
the same procedure we used earlier to make the Blimp class persistent.
2. Use the [Insert]/[New Class] dialog to insert a generic class called
Address into the Book project. Specify [CObject] as the base class. Include
<afx.h> at the top of Address.h. Add fields to the class to hold the
building number, street, city, state, and zip code of an address. Following
the Blimp example, make Address serializable. Follow the same steps to
create a Phone and Person classes. Be sure to include Address.h and Phone.h
at the top of Person.h
Recall the seven steps required to make a class persistent:
1. Derive the class from CObject.
2. Include <afx.h> in the
header file.
3. Place the DECLARE_SERIAL
macro in the class declaration
4. Place the IMPLEMENT_SERIAL
macro in the source file.
5. Provide default and copy
constructors.
6. Provide an assignment operator.
7. Provide a Serialize() function.
Here is a listing of the Person class. Completion of the Address and Book
classes is left as an exercise, as are the implementations of most of the
routine Address book member functions.
class Person : public CObject
{
public:
DECLARE_SERIAL(Person)
Person(Address
a, CString f, CString l, Phone p)
:first(f), last(l),
address(a), phone(p) {}
Person();
Person(const Person&
p) { Copy(p); }
Person& operator=(Person&
x);
virtual ~Person()
{}
void Serialize(CArchive&
ar);
// getters:
Address GetAddress()
const { return address; } // etc.
// setters:
void SetAddress(
int bld, CString st, CString c, CString ste, int z); // etc.
private:
CString first,
last; // name
Phone phone;
Address address;
void Copy(const
Person& p);
};
Notice that every Person object will have embedded Phone and Address objects
rather than pointers to Phone and Address objects.
The Person class provides a copy constructor and an assignment operator.
Recall that these functions overwrite the corresponding functions made
private in the CObject base class. As such, it will be sufficient for both
functions to make a member-wise copy of their argument. This is done by
the private Copy() function.
The Serialize function needs to serialize and de-serialize the name,
address, and phone. Of course the Phone and Address member variables are
private, hence inaccessible to Person. Therefore, the Person object must
ask its embedded Phone and Address objects to serialize and de-serialize
themselves. This is done by simply passing the archive parameter to the
phone and address Serialize() functions.
void Person::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
ar << first;
ar << last;
}
else
{
ar >> first;
ar >> last;
}
phone.Serialize(ar);
address.Serialize(ar);
}
The Document
CBookDoc, the browser's document class, manages a dynamic array of Person
objects. It's worth noting that the array literally contains Person objects
rather than pointers to Person objects.
5. Include <afxtempl.h> and "Person.h" at the top of BookDoc.h.
Add a CArray of Person objects to the CBookDoc class. Add functions for
adding, removing, getting, and updating Person objects in the CArray. Set
the size of the CArray to 0 in the constructor.
Here is a partial listing of the CBookDoc class:
class CBookDoc : public CDocument
{
CArray<Person,
Person> book;
public:
void Add(Person
p);
void Rem(int pos);
Person GetPerson
(int pos) const { return book.GetAt(pos); }
void Set(int pos,
CString fn, CString ln, Address a, Phone ph);
int GetSize() const
{ return book.GetSize(); }
// etc.
};
The CArray template arguments are both Person. In other words, the CArray
holds Person objects, and the CArray member functions pass Person arguments
by value. This simply reflects our decision to avoid pointers.
The Rem(), Add(), and Set() functions modify the CArray, and so must
set the inherited modified flag to true, and must notify all registered
views of the change.
void CBookDoc::Set(
int pos, CString
fn, CString ln, Address a, Phone ph)
{
if (0 <= pos
&& pos < book.GetSize())
{
book[pos].SetFirst(fn);
book[pos].SetLast(ln);
book[pos].SetAddress(a);
book[pos].SetPhone(ph);
SetModifiedFlag(TRUE);
UpdateAllViews(NULL);
}
}
In theory, a CArray of CObjects should know how to serialize itself, and
so the document's Serialize() function simply passes its archive argument
to the CArray's Serialize() function:
void CBookDoc::Serialize(CArchive&
ar)
{
book.Serialize(ar);
}
6. Specialize the global SerializeElements() template function.
The CArray Serialize() function calls the global SerializeElements() function.
Given a pointer, p, to the first element in a CArray, this function traverses
the CArray, calling the Serialize() function of each element. Of course
this function is a template function parameterized by the type of object
p points to. Unfortunately, this function is too general. We need to specialize
the function. In other words, we must define a version of the function
in which the template parameter is set to the Person class. C++ has a special
syntax for doing this:
template <>
void AFXAPI SerializeElements
<Person> (
CArchive& ar, Person* p, int size)
{
for (int i = 0; i < size;
i++, p++ )
p->Serialize( ar ); // serialize each element
}
The View
7. Use the dialog editor to create the book view form. (Use the snapshot
given earlier as a model.) Use the [View]/[Class Wizard]/[Member Variables]
dialog to create CBookView member variables associated with the text boxes.
Use the [Class Wizard]/[Message Maps] to add an OnUpdate() function to
the CBookView class. Add an integer member variable called position to
the CBookView class. This variable should be initialized to 0 by the constructor
and represents the position of the selected entry in the associated address
book.
When the user adds, removes, or changes a person from the document, the
UpdateAllViews() functions is called after the document has been modified.
Recall that this function calls the OnUpdate() function of each associated
view. OnUpdate() converts the identifier of the list box control into a
temporary CListBox wrapper, uses this wrapper to place the names of each
person in the list box, then calls a private helper function named SetBoxes():
void CBookView::OnUpdate(
CView* pSender,
LPARAM lHint, CObject* pHint)
{
CListBox* pLB =
(CListBox*) GetDlgItem(IDC_BOOK);
pLB->ResetContent();
CBookDoc* pDoc
= GetDocument();
int size = pDoc->GetSize();
for(int i = 0;
i < size; i++)
{
pLB->InsertString(-1, pDoc->GetPerson(i));
}
SetBoxes();
}
SetBoxes() places the name, address, and phone of the selected person in
the text boxes, then highlights the name of the selected person. Using
the Class Wizard, we created view member variables corresponding to the
view's text boxes: m_first, m_last, m_city, etc. The name, phone, and address
of the selected person are copied into these variables, then UpdateData(FALSE)
is called. Recall that this transfers the data stored in these variables
into the corresponding text boxes:
void CBookView::SetBoxes()
{
CBookDoc* pDoc
= GetDocument();
if (0 < pDoc->GetSize())
{
Person p = pDoc->GetPerson(position);
m_last = p.GetLast();
m_first = p.GetFirst();
Address a = p.GetAddress();
Phone ph = p.GetPhone();
m_bldg = a.GetBuilding();
m_street = a.GetStreet();
m_state = a.GetState();
m_city = a.GetCity();
m_phone = ph.GetNumber();
m_areaCode = ph.GetAreaCode();
CListBox* pLB = (CListBox*) GetDlgItem(IDC_BOOK);
pLB->SetCurSel(position);
UpdateData(FALSE);
}
}
User input comes through a special [Book] menu, with corresponding toolbar
buttons and hot keys.
8. Use the menu editor to create a [Book] menu containing entries
labeled Add, Rem, Update, Next, Prev,
First and Last. Use the [Class Wizard] to add handlers and
disablers for the items to the CBookView class.
When the user selects [Next] from the [Book] menu, the view's OnBookNext()
function is called. This function simply increments the view's position
variable by one modulo the number of entries in the book.
void CBookView::OnBookNext()
{
CBookDoc* pDoc
= GetDocument();
int size = pDoc->GetSize();
if (0 < size)
{
position = (position + 1) % size;
SetBoxes();
}
}
Recall that before a menu is displayed, an UPDATE_COMMAND_UI message is
sent to each menu item. The handler for this message can modify the appearance
of the item. In our case, the user shouldn't be able to do anything to
an empty address book except add a new entry:
void CBookView::OnUpdateBookNext(CCmdUI*
pCmdUI)
{
CBookDoc* pDoc
= GetDocument();
if (!pDoc->GetSize())
pCmdUI->Enable(FALSE);
}
When the user selects [Add] from the [Book] menu, the handler uses UpdateData(TRUE)
to transfer the data from the text boxes into the corresponding view member
variables. This data is used to construct a Person object. The object is
added to the document (which will update all of the views), the position
is updated, and SetBoxes() is called:
void CBookView::OnBookAdd()
{
UpdateData(TRUE);
Address a(m_bldg,
m_street, m_city, m_state);
Phone ph(m_areaCode,
m_phone);
Person p(a, m_first,
m_last, ph);
CBookDoc* pDoc
= GetDocument();
pDoc->Add(p);
position = pDoc->GetSize()
- 1;
SetBoxes();
}
The remaining handler functions are similar and are left as an exercise
for the reader. To finish, we only need to handle double clicking in the
list box.
9. Use the Class Wizard to add to the view class a handler for the
LBN_DBL_CLK message that is sent to the list box when the user double clicks
on an entry. (Select the list box's identification number in the [Object
IDs:] list box to see this message.) Implement this handler.
void CBookView::OnDblclkBook()
{
CListBox* pLB =
(CListBox*) GetDlgItem(IDC_BOOK);
position = pLB->GetCurSel();
SetBoxes();
}
11. Build and test the application.
Version 2: Using Reference Semantics
Version 2 of the Address Book Browser uses pointers to rather than copies
of objects. The overall design is the same, except that aggregation associations
replace the composition associations.
We begin by modifying the Person class so that it encapsulates pointers
to the associated Phone and Address objects:
class Person : public CObject
{
public:
DECLARE_SERIAL(Person)
Person(Address*
a, CString f, CString l, Phone* p)
:first(f), last(l),
address(a), phone(p) {}
Person();
Person(const Person&
p) { Copy(p); }
Person& operator=(Person&
x);
virtual ~Person()
{ Free(); }
void Serialize(CArchive&
ar)
// getters:
CString GetName()
const { return last + ", " + first; } // etc.
// setters:
void SetAddress(Address*
a) { address = a; } // etc.
private:
CString first,
last;
Phone* phone;
Address* address;
void Copy(const
Person& p);
void Free();
};
The Serialize function uses the extraction and insertion operators to read
and write its Address and Phone pointers. This is where the archive will
use the pointer swizzling trick to read and write pointers. Memory for
the Phone and Address objects will automatically be allocated by the extraction
operator. (The IMPLEMENT_SERIAL macro provides the extraction and insertion
operator for pointers.)
void Person::Serialize(CArchive&
ar)
{
CObject::Serialize(ar);
if (ar.IsStoring())
{
ar << first;
ar << last;
ar << phone;
ar << address;
}
else
{
ar >> first;
ar >> last;
ar >> phone;
ar >> address;
}
}
Orthodox Canonical Form
The private Free() function called by the destructor deletes the associated
phone and address objects:
void Person::Free()
{
if (address) delete
address;
if (phone) delete
phone;
}
We need to be careful here. The original motivation for using reference
semantics was to allow roommates to share the same Address and Phone objects.
If one of the roommates is deleted from the address book, then his destructor
will automatically delete the phone and address of the remaining roommate.
However, in our example we won't actually be sharing Address and Phone
objects. Like version one, each person must be responsible for the creation
and destruction of his associated Phone and address objects.
To avoid aliasing problems, the Copy() function creates new Address
and Phone objects on the heap that are clones of its parameter's Address
and Phone objects:
void Person::Copy(const Person&
p)
{
first = p.first;
last = p.last;
phone = new Phone(*p.phone);
address = new Address(*p.address);
}
The assignment operator also requires a slight modification. Instead of
simply copying its parameter, it must first delete its former Address and
Phone objects by calling the Free() function:
Person& operator=(Person&
x)
{
if (&x != this)
{
Free();
Copy(x);
}
return x;
}
When a class contains pointers to private objects, then it must redefine
its destructor, copy constructor, and assignment operator in exactly the
way we have done. Of course the implementations of the Copy() and Free()
helper functions will vary from case to case. This is an example of the
Orthodox Canonical Form design pattern:
Orthodox Canonical Form [COP], [HORST], [PEA]
Problem
When using the Wrapper-Body design pattern in C++, copying the wrapper
fails to copy the associated body, and deleting the wrapper fails to delete
the associated body. This can lead to memory leaks and aliasing bugs.
Solution
Provide the wrapper with a destructor that deletes the body. Redefine
the wrapper's copy constructor so that it clones the body of its argument.
Redefine the wrapper's assignment operator so that it first deletes its
old body, then clones its argument's body.
Type Safe Pointer Containers
Unfortunately, MFC's CArray , CList, and CMap templates don't work well
with pointers. MFC does provide an archaic CObArray class that manages
a dynamic array of CObject pointers, but of course CObArray doesn't guarantee
that all of its members are pointers to the same subclass of CObArray.
For example, it's possible to create a CObArray that holds a pointer to
an Address, a pointer to a Phone, and a third pointer to a Person. This
might be what the programmer intended, but in other situations it can lead
to runtime errors.
MFC now has a CTypedPtrArray template that provides a type safe wrapper
for CObArray and makes it behave more like CArray:
CTypedPtrArray<Body, Storable>
The first parameter is the type of the body: CObArray or CPtrArray. The
second argument is the type of elements that will be stored in the array.
MFC also provides CTypedPtrList and CTypePtrMap templates for lists
anbd maps that store pointers.
Changes to Document
We modify the declaration of the private book member variable so that
it now becomes a CTypedPtrArray of Book pointers. Of course we need to
change the parameters and return values of a few member functions so that
they deal with Person pointers instead of Person objects:
class CBookDoc : public CDocument
{
CTypedPtrArray<CObArray,
Person*> book;
public:
void Add(Person*
p);
void Rem(int pos);
Person* GetPerson
(int pos) const { return book.GetAt(pos); }
void Set(int pos,
Address* a, CString fn, CString ln, Phone* ph);
int GetSize() const
{ return book.GetSize(); }
// etc.
};
Changes to the View
We also need to change view references to Address, Phone, and Person
objects to references to Address, Phone, and Person pointers. as an example,
the [Book]/[Add] menu handler now must create Address, Phone, and Person
objects on the heap. The Person pointer is passed to the document's Add()
function:
void CBookView::OnBookAdd()
{
UpdateData(TRUE);
Address* a = new
Address(m_bldg, m_street, m_city, m_state);
Phone* ph = new
Phone(m_areaCode, m_phone);
Person* p = new
Person(a, m_first, m_last, ph);
CBookDoc* pDoc
= GetDocument();
pDoc->Add(p);
position = pDoc->GetSize()
- 1;
SetBoxes();
}
Initializing an MFC Application
In Chapter 5 we introduced CWinApp, the base class of the master user
interface thread for a typical MFC application. Recall that when we use
the MFC App Wizard to create an application named Test, a subclass of CWinApp
is automatically generated:
class CTestApp : public CWinApp
{
public:
CTestApp();
virtual BOOL InitInstance();
// make frame, view, & doc
afx_msg void OnAppAbout();
// handler for Help/About menu item
// etc.
};
In addition, an instance of this class is declared:
CTestApp theApp;
The application begins by calling AfxWinMain():
int AfxWinMain(...)
{
CWinApp* pApp =
AfxGetApp(); // = &theApp
if (!hPrevInstance)
// if first run
pApp->InitApplication(); // register window
pApp->InitInstance();
// create frame, document, & view
return pApp->Run();
// message loop
}
The call to InitApplication() creates an initial entry for the application
in the system registry. The call to InitInstance() creates the application's
document and view. The call to Run() starts the message pump.
The AppWizard generates an override of the InitInstance() function.
This function uses a document template to group the application's document,
view, and main frame classes. The template is then added to a list of document
templates managed by theApp:
BOOL CTestApp::InitInstance()
{
// etc.
CSingleDocTemplate*
pDocTemplate;
pDocTemplate =
new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CTestDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CTestView));
AddDocTemplate(pDocTemplate);
// Parse command
line for standard shell commands, DDE, file open
CCommandLineInfo
cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands
specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// window has been
initialized, so show and update it.
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}
CSingleDocTemplate and CMultiDocTemplate are derived from the CDocTemplate
class, which encapsulates three protected CRuntimeClass pointers:
class CDocTemplate : public
CCmdTarget
{
public:
virtual CDocument*
CreateNewDocument();
virtual CFrameWnd*
CreateNewFrame(CDocument* pDoc,
CFrameWnd* pOther);
virtual CDocument*
OpenDocumentFile(...) = 0;
// etc.
protected:
CRuntimeClass*
m_pDocClass;
CRuntimeClass*
m_pFrameClass;
CRuntimeClass*
m_pViewClass;
};
InitInstance() creates a document template holding pointers to objects
representing CTestDoc, CMainFrame, and CTestView, then adds the document
template to a list of templates managed by theApp.
The following class diagram shows the relationship between these classes:

Although an application could have many document templates, it is more
common to have one.
InitInstance() indirectly calls the OpenDocumentFile() member function
of the first document template on its list:
pTemplate->OpenDocumentFile(NULL);
This function (eventually) creates the application's main frame, initial
document, and initial view using the dynamic instantiation feature discussed
earlier:
CDocument* pDocument = (CDocument*)m_pDocClass->CreateObject();
CFrameWnd* pFrame = (CFrameWnd*)m_pFrameClass->CreateObject();
CWnd* pView = (CWnd*) m_pNewViewClass->CreateObject();
Dynamic instantiation is used because the framework can't anticipate the
types of document and views we will create until we run the App Wizard.
Applications with Multiple Document and View Types
While a multi-document application allows users to open multiple views
of multiple documents, the documents are generally all instances of the
same CDocument-derived class, and the views are generally all instances
of the same CView-derived class. But what if an application requires multiple
types of documents and views? For example, workspaces, projects, and C++
files can be viewed as three distinct types of documents users may edit
in Visual C++. While Microsoft Word only allows users to edit a single
type of document, users may view their documents using three different
types of views: normal, outline, and page layout.
It's not too difficult to create MFC applications with multiple view
and document types. The trick is to edit InitInstance() by adding the creation
and installation of additional document templates.
Brick CAD
CAD/CAM stands for "Computer Aided Design/Computer Aided Manufacturing".
CAD/CAM systems are used by engineers to design everything from spark plugs
to skyscrapers. In this context the object being designed is the document.
Engineers can create and modify different types of views of the document,
such as two dimensional cross section views, three dimensional wire frame
views, three dimensional solid surface views, statistical views, schematics,
blueprints, even views of individual document components.
Brick CAD is a CAD/CAM system for designing bricks. Of course no one
would really need a CAD/CAM system for designing bricks, because bricks
are pretty simple. They have height, width, length, area, volume, and that's
about it. We could have developed a CAD/CAM system for designing nuclear
submarines, but then we would need to actually know something about submarines.
A Brick CAD document represents a single brick with height, width, length,
area, and volume attributes. Users can open four types of views of a brick:
side view, top view, front view, and a control panel view that displays
the attributes of the brick in edit boxes:

A Brick CAD user can use the panel view to change the height, width,
or length of the associated brick. When the [Change] button is clicked,
all open views that are affected by the change automatically redraw themselves.
(Some views do not need to redraw themselves. For example, a top view doesn't
need to redraw itself if the user changes the height of the brick.) Naturally,
bricks can be saved to and restored from files using MFC's serialization
mechanism.
Users can open new views by selecting either [Side View], [Front View],
[Top View], or [Panel View] items from the [View] menu.
Design of Brick CAD
Brick CAD is a multi-document application. The document class encapsulates
the attributes of a single brick. These attributes can be changed using
the SetProps() function, which notifies all registered views by calling
UpdateAllViews(). Of course there are four types of CView-derived classes:

Version 1.0: Dedicated Windows
Version 1.0 of Brick CAD has already been described. Users are allowed
to open new types of views by selecting items from the [View] menu.
Implementation
Begin by creating a workspace and a project.
1. Use the [File]/[New]/[Workspace] dialog to create a new workspace
named BC (Brick CAD).
2. Use the MFC App Wizard to add a new, multi-document project to the
BC workspace called BC1. Click the [Advanced] button in the [Step 4 of
6] dialog to open the [Advanced Options] dialog box. Type bc1 into
the [File extension] edit box. Type Brick CAD in the [Main frame
caption:] edit box. Type Brick in the [Doc type name:] edit box, then click
the [Close] button. In the [Step 6 of 6] dialog type make sure CBC1View
is the selected class, then type CFrontView in the [Class name]
edit box.
Implementing the Brick CAD document
The Brick CAD document encapsulates the height, width, length, volume,
and area of a single brick.
3. Add five, private, integer member variables to the CBC1Doc class
named height, width, length, area, and volume. Provide public "getter"
functions for each variable. Provide a private function named CalcProps()
that computes the area and volume from height, width, and length. The default
constructor should initialize width, height, and volume to 100, 150, and
200, respectively. The constructor should initialize area and volume by
calling CalcProps(). Use #define to create a color reference named BRICK_COLOR.
The color of a brick is defined for all views at the top of the CBC1Doc.h
file:
#define BRICK_COLOR RGB(255,
80, 50)
Users don't directly set the volume and area of a brick. Instead, these
are automatically computed from height, width, and length by a private
member function:
void CBC1Doc::CalcProps()
{
volume = length
* height * width;
area = 2 * (length
* (width + height) + width * height);
}
The Serialize() function only needs to save the height, width, and length
of a brick. When these values are de-serialized, the CalcProps() function
is called to compute the remaining attributes. Although it doesn't save
us much disk space for Brick CAD documents, it does demonstrate a general
principle: don't save data that can be re-computed.
4. Implement Serialize() so that it inserts height, width, and length
into the archive during a store, and extracts them during a load. After
the extraction, call CalcProps() to restore the values of area and volume.
Here is a listing of the Serialize() function:
void CBC1Doc::Serialize(CArchive&
ar)
{
if (ar.IsStoring())
{
ar << height;
ar << width;
ar << length;
}
else
{
ar >> height;
ar >> width;
ar >> length;
CalcProps();
}
}
Changing properties is done by calling SetProps(), which calculates area
and volume, sets the modified flag, then updates all of the views.
5. Add a public member function to BC1Doc called SetProps(). SetProps()
takes a new height, width, and length as parameters, and assigns them to
the corresponding member variables. CalcProps() is called to set area and
volume, set the modified flag to true, and update all of the views.
Here is a listing of the SetProps() function:
void CBC1Doc::SetProps(int
len, int wth, int ht)
{
if (len <= 0
|| wth <= 0 || ht <= 0)
throw CString("non-positive dimension detected");
length = len;
width = wth;
height = ht;
CalcProps();
SetModifiedFlag(TRUE);
UpdateAllViews(NULL);
}
Implementing the Front View
We changed the name of the view class to CFrontView in the App Wizard.
We can use its implementation as a template for the side and front views.
6. Implement CFrontView's OnDraw() function so that it draws a BRICK_COLOR
rectangle in the center of the client rectangle. The dimensions of the
rectangle should be the height and length of the associated document. Build
and test the application. Also, use the Class Wizard to add an OnUpdate()
member function to this class. This function simply calls Invalidate().
The OnDraw() function creates a new brush using the BRICK_COLOR color,
then draws a rectangle using the length and height from the associated
document:
void CFrontView::OnDraw(CDC*
pDC)
{
CBC1Doc* pDoc =
GetDocument();
ASSERT_VALID(pDoc);
CBrush *oldBrush,
*newBrush;
newBrush = new
CBrush(BRICK_COLOR);
oldBrush = pDC->SelectObject(newBrush);
pDC->TextOut(0,
0, "Front View");
CRect cliRect;
GetClientRect(&cliRect);
int length = pDoc->GetLength();
int height = pDoc->GetHeight();
int ulx = cliRect.CenterPoint().x
- length/2;
int uly = cliRect.CenterPoint().y
- height/2;
pDC->Rectangle(ulx,
uly, ulx + length, uly + height);
pDC->SelectObject(oldBrush);
delete newBrush;
}
Adding Additional CView-derived Views
We follow the same pattern for the top and side view classes.
7. Use the [Insert]/[New Class ...] dialog to add new MFC classes
called CSideView and CTopView to the BC1 project, both of these classes
should be derived from CView. Implement the CSideView OnDraw() function
to draw a rectangle using the width and height of the associated document.
Implement the CTopView OnDraw() function to draw a rectangle using the
length and width of the associated document. Use the Class Wizard to add
an OnUpdate() function that calls Invalidate() to each class. (Be sure
to include "CBC1Doc.h" at the top of SideView.h and Topview.h files.)
The top view's OnDraw() function uses the length and width of the associated
document"
void CTopView::OnDraw(CDC*
pDC)
{
CBC1Doc* pDoc =
(CBC1Doc*)GetDocument();
ASSERT_VALID(pDoc);
CBrush *oldBrush,
*newBrush;
newBrush = new
CBrush(BRICK_COLOR);
oldBrush = pDC->SelectObject(newBrush);
pDC->TextOut(0,
0, "Top View");
CRect cliRect;
GetClientRect(&cliRect);
int length = pDoc->GetLength();
int width = pDoc->GetWidth();
int ulx = cliRect.CenterPoint().x
- length/2;
int uly = cliRect.CenterPoint().y
- width/2;
pDC->Rectangle(ulx,
uly, ulx + length, uly + width);
pDC->SelectObject(oldBrush);
delete newBrush;
}
Creating and Installing Additional Templates
8. Include SideView.h and TopView.h at the top of the BC1.h file.
Add CMultiDoc pointer member variables to the CBC1App class named frontTemp,
topTemp, and sideTemp. Initialize these variables in CBC1App's InitInstance()
function, and pass them to the AddDocTemplate() function.
The InitInstance() function is implemented in BC1.cpp. Replace the creation
and installation of the local variable, pDocTemplate, by the following
lines of code:
frontTemp = new CMultiDocTemplate(
IDR_BRICKTYPE,
RUNTIME_CLASS(CBC1Doc),
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CFrontView));
AddDocTemplate(frontTemp); //
add template to template manager
Initialize and install the topTemp and sideTemp member variables
in the same way.
Dynamically Creating New Views
The [View] menu handlers are view-independent, hence are implemented
in the CMainFrame class.
9. Use the Menu Editor to add three new items under the [View] menu
of the IDR_BRICKTYPE menu bar: [Front View], [Top View], and [Side View].
Use the Class Wizard to add handlers for these items to the CMainFrame
class. Each handler should use the corresponding template pointer member
variable of the CBC1App class to create and update a new frame associated
with the document associated with the currently active view. (Remember,
there may be several brick documents that are currently open for editing.)
Each menu handler creates a new view using the appropriate template from
the CBC1App class. The view is associated with the document associated
with the active view. For example, here is the implementation of the [View]/[Front]
handler.
void CMainFrame::OnViewFrontview()
{
CMDIChildWnd* pActiveChild
= MDIGetActive();
CDocument* pDocument
= pActiveChild->GetActiveDocument();
CDocTemplate* pTemplate
=
((CBC1App*) AfxGetApp())->frontTemp;
CFrameWnd* pFrame
=
pTemplate->CreateNewFrame(pDocument, pActiveChild);
pTemplate->InitialUpdateFrame(pFrame,
pDocument);
}
Creating A CFormView-derived View
After we layout the panel view in the Dialog Editor, the Class Wizard
automatically creates the corresponding wrapper class.
10. (a) Use the [Insert]/[Resource ...] menu to display the [Insert
Resource] dialog. Use this dialog to insert a new IDD_FORMVIEW dialog.
Use the Dialog Editor to layout the new dialog using the screen snapshot
shown earlier as a model.
(b) Invoke the Class Wizard from the shortcut menu of the dialog box.
You will be warned that no wrapper class exists that corresponds to this
dialog. Click the [New Class] radio button. In the following dialog box
specify CPanelView as the name of the class, and specify CFormView
as the base class.
(c) Following the earlier examples, add a new CMultiDocTemplate pointer
named panelTemp to the CBC1App class. Initialize and install this pointer
in InitInstance(). Use the Menu Editor to add [Panel View] to the [View]
menu. Use the Class Wizard to add a handler for this item to the CMainFrame
class, and implement it following the earlier example. Build and test.
While the Class Wizard is visible, we add member variables and a button
handler.
11. Use the [Class Wizard]/[Member Variables] dialog to add integer
member variables to the CPanelView class that correspond to the control
panel's edit boxes. Use the [Class Wizard]/[Message Maps] dialog to add
a BN_CLICKED handler for the [Change] button and an OnUpdate() member function.
Implement these functions (see below). Build and test.
Assume the button handler is called OnChange and the edit box member variables
are called m_height, m_width, m_length, m_area, and m_volume.
Here's the implementation:
void CPanelView::OnChange()
{
UpdateData(TRUE);
CBC1Doc* pDoc =
(CBC1Doc*)GetDocument();
pDoc->SetProps(m_length,
m_width, m_height);
}
The OnUpdate() function copies the document attributes to the panel view
member variables, then uses the dynamic dialog exchange mechanism discussed
in Chapter 2 to display the values.
void CPanelView::OnUpdate(
CView* pSender,
LPARAM lHint, CObject* pHint)
{
CBC1Doc* pDoc =
(CBC1Doc*)GetDocument();
m_area = pDoc->GetArea();
m_volume = pDoc->GetVolume();
m_height = pDoc->GetHeight();
m_length = pDoc->GetLength();
m_width = pDoc->GetWidth();
UpdateData(FALSE);
}
Version 1.1: Brick CAD with Changeable Views
Version 1.1 of Brick CAD allows users to change the view displayed by
an existing view window by selecting the view type from the [Window] menu.

Implementation
Changing the view displayed by an existing view window must be done
by the child frame that surrounds the active view.
12. Add [Top View], [Side View], [Front View], and [Panel view] items
to the [Window] menu of the IDR_BRICKTYPE menu bar. Use the Class Wizard
to create handlers for the COMMAND and UPDATE_COMMAND_UI messages sent
by these items. These handlers should be added to the CChildFrame
class. Implement these handlers. (See below.)
Each menu handler simply calls the private SwitchViews() member function.
void CChildFrame::OnWindowFrontview()
{
SwitchViews(RUNTIME_CLASS(CFrontView));
}
We use runtime type identification to identify the type of view surrounded
by a child frame. This information is used to disable the corresponding
menu item.
void CChildFrame::OnUpdateWindowFrontview(CCmdUI*
pCmdUI)
{
CView* pView =
GetActiveView();
if (pView->IsKindOf(RUNTIME_CLASS(CFrontView)))
pCmdUI->Enable(FALSE);
}
13. Add a private member function called SwitchViews() to the CChildFrame
class. Implement this function. (See below.) Build and test.
If a document template holds the document and view classes, then a document-view
context (CCreateContext) holds a view object and its associated document.
The SwitchView() function creates a document-view context, then sets the
view as the active view. The full details are a bit obscure.
void CChildFrame::SwitchViews(CRuntimeClass
*rtc)
{
static UINT id = 1; // an ID
for the next new view
CView* pOldView = GetActiveView();
CView* pNewView = (CView*) rtc->CreateObject();
CCreateContext context;
context.m_pCurrentDoc = pOldView->GetDocument();
pNewView->Create(NULL, NULL,
NULL,
CFrameWnd::rectDefault, this,
id++, &context);
pNewView->OnInitialUpdate();
SetActiveView(pNewView);
pNewView->ShowWindow(SW_SHOW);
pOldView->ShowWindow(SW_HIDE);
pNewView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
RecalcLayout();
}
Version 1.2 Brick CAD with Static Splitter Views
The final version of Brick CAD is a step backwards in usability from
the previous version, but demonstrates how to create splitter windows.
A splitter window allows us to display several views within the same child
frame.

Implementation
If we were creating version 1.2 of Brick CAD from scratch, we could
use the [Window Styles] tab of the [Advanced Options] dialog (which is
accessed by clicking the [Advanced] button on the [Step 4 of 6] App Wizard
dialog) to select "use split window". Instead, we simply add the necessary
machinery to the child frame class.
14. Add a protected CSplitterView member variable to the CChildFrame
class. Use the Class Wizard to add an OnCreateClient() function to the
CChildFrame class. Implement the function so that it initializes the splitter
view member. (See below.)
Assume we call our splitter window member variable m_wndSplitter:
CSplitterWnd m_wndSplitter;
OnCreateClient() calls the splitter window's CreateStatic() member function
to set the number of rows and columns to two. Next, the splitter window's
CreateView() function is called to specify the view in each quadrant of
the frame:
BOOL CChildFrame::OnCreateClient(
LPCREATESTRUCT
lpcs, CCreateContext* pContext)
{
if (SPLIT)
{
m_wndSplitter.CreateStatic(this, 2, 2);
m_wndSplitter.CreateView(
0, 0, RUNTIME_CLASS(CFrontView), CSize(100, 100), pContext);
m_wndSplitter.CreateView(
1, 0, RUNTIME_CLASS(CSideView), CSize(100, 100), pContext);
m_wndSplitter.CreateView(
0, 1, RUNTIME_CLASS(CTopView), CSize(100, 100), pContext);
m_wndSplitter.CreateView(
1, 1, RUNTIME_CLASS(CPanelView), CSize(100, 100), pContext);
return TRUE;
}
else
return CMDIChildWnd::OnCreateClient(lpcs, pContext);
}
Dynamic splitters are possible for applications in which all views are
of the same type.
Message Passing
Windows Applications without MFC
MFC Message Maps
Customized Message Passing
Problems
Problem 6.1
Finish and test version 1 of the Address Book Browser.
Problem 6.2
Finish and test version 2 of the Address Book Browser.
Problem 6.3
An inventory browser is a database browser that allows users to browse
inventories. An inventory is a sequence of line items. A line item is a
product record together with a quantity. For example: 2 egg beaters. A
product record encapsulates the name, product id number, price, and description
of a product. Using the address book browser as an example, build and test
an inventory browser. Uses should be able to change line item quantities
and add new line items.
Problem 6.4
Finish the Brick CAD application (version 1.1). Add a fifth type of
view called an orthogonal view, which shows the height, width, and length
of a brick:
