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: