
Beginning Visual C++ 2005 (2006) [eng]-1
.pdf
Storing and Printing Documents
There are three things here that relate to serializing a document object:
1.
2.
3.
The DECLARE_DYNCREATE() macro.
The Serialize() member function.
The default class constructor.
DECLARE_DYNCREATE() is a macro that enables objects of the CSketcherDoc class to be created dynamically by the application framework during the serialization input process. It’s matched by a complementary macro, IMPLEMENT_DYNCREATE(), in the class implementation. These macros apply only to classes derived from CObject, but as you will see shortly, they aren’t the only pair of macros that can be used in this context. For any class that you want to serialize, CObject must be a direct or indirect base because it adds the functionality that enables serialization to work. This is why the CElement class was derived from CObject. Almost all MFC classes are derived from CObject and, as such, are serializable.
The Hierarchy Chart in the Microsoft Foundation Class Reference for Visual C++ 2005 shows those classes which aren’t derived from CObject. Note that CArchive is in this list.
The class definition also includes a declaration for a virtual function Serialize(). Every class that’s serializable must include this function. It’s called to perform both input and output serialization operations on the data members of the class. The object of type CArchive that’s passed as an argument to this function determines whether the operation that is to occur is input or output. You’ll explore this in more detail when considering the implementation of serialization for the document class.
Note that the class explicitly defines a default constructor. This is also essential for serialization to work because the default constructor is used by the framework to synthesize an object when reading from a file, and the synthesized object is then filled out with the data from the file to set the values of the data members of the object.
Serialization in the Document Class Implementation
There are two bits of the file containing the implementation of CSketcherDoc that relate to serialization. The first is the macro IMPLEMENT_DYNCREATE() that complements the DECLARE_DYNCREATE() macro:
// SketcherDoc.cpp : implementation of the CSketcherDoc class
//
#include “stdafx.h” #include “Sketcher.h” #include “PenDialog.h”
#include “SketcherDoc.h”
#ifdef _DEBUG #define new DEBUG_NEW #endif
// CSketcherDoc
IMPLEMENT_DYNCREATE(CSketcherDoc, CDocument)
// Message maps and the rest of the file...
869

Chapter 17
All this macro does is define the base class for CSketcherDoc as CDocument. This is required for the proper dynamic creation of a CSketcherDoc object, including members inherited from the base class.
The Serialize() Function
The class implementation also includes the definition of the Serialize() function:
void CSketcherDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
This function serializes the data members of the class. The argument passed to the function is a reference to an object of the CArchive class, ar. The IsStoring() member of this class object returns TRUE if the operation is to store data members in a file and FALSE if the operation is to read back data members from a previously stored document.
Because the Application wizard has no knowledge of what data your document contains, the process of writing and reading this information is up to you, as indicated by the comments. To understand how this is done, look a little more closely at the CArchive class.
The CArchive Class
The CArchive class is the engine that drives the serialization mechanism. It provides an MFC-based equivalent of the stream operations in C++ that you used for reading from the keyboard and writing to the screen in the console program examples. An object of the MFC class CArchive provides a mechanism for streaming your objects out to a file, or recovering them again as an input stream, automatically reconstituting the objects of your class in the process.
A CArchive object has a CFile object associated with it which provides disk input/output capability for binary files, and provides the actual connection to the physical file. Within the serialization process, the CFile object takes care of all the specifics of the file input and output operations, and the CArchive object deals with the logic of structuring the object data to be written or reconstructing the objects from the information read. You need to worry about the details of the associated CFile object only if you are constructing your own CArchive object. With the document in Sketcher, the framework has already taken care of it and passes the CArchive object ar, that it constructs, to the Serialize() function in CSketcherDoc. You’ll be able to use the same object in each of the Serialize() functions you add to the shape classes when you implement serialization for them.
The CArchive class overloads the extraction and insertion operators (>> and <<) for input and output operations respectively on objects of classes derived from CObject, plus a range of basic data types. These overloaded operators work with the following object types and primitive types:
870

|
|
Storing and Printing Documents |
|
|
|
|
Type |
Definition |
|
|
|
|
bool |
Boolean value, true or false |
|
float |
Standard single precision floating point |
|
double |
Standard double precision floating point |
|
BYTE |
8-bit unsigned integer |
|
char |
8-bit character |
|
wchar_t |
16-bit character |
|
int and short |
16-bit signed integer |
|
LONG and long |
32-bit signed integer |
|
LONGLONG |
64-bit signed integer |
|
ULONGLONG |
64-bit unsigned integer |
|
WORD and unsigned int |
16-bit unsigned integer |
|
DWORD and unsigned int |
32-bit unsigned integer |
|
CObject* |
Pointer to CObject |
|
CString |
A CString object defining a string |
|
SIZE and CSize |
An object defining a size as a cx, cy pair |
|
POINT and CPoint |
An object defining a point as an x, y pair |
|
RECT and CRect |
An object defining a rectangle by its upper-left and lower-right |
|
|
corners |
|
CObject* |
Pointer to CObject |
|
|
|
For basic data types in your objects, you use the insertion and extraction operators to serialize the data. To read or write an object of a serializable class which you have derived from CObject, you can either call the Serialize() function for the object, or use the extraction or insertion operator. Whichever way you choose must be used consistently for both input and output, so you should not output an object using the insertion operator and then read it back using the Serialize() function, or vice versa.
Where you don’t know the type of an object when you read it, as in the case of the pointers in the list of shapes in our document, for example, you must only use the Serialize() function. This brings the virtual function mechanism into play, so the appropriate Serialize() function for the type of object pointed to is determined at run time.
A CArchive object is constructed either for storing objects or for retrieving objects. The CArchive function IsStoring()returns TRUE if the object is for output, and FALSE if the object is for input. You saw this used in the if statement in the Serialize() member of the CSketcherDoc class.
There are many other member functions of the CArchive class which are concerned with the detailed mechanics of the serialization process, but you don’t usually need to know about them to use serialization in your programs.
871

Chapter 17
Functionality of CObject-Based Classes
There are three levels of functionality available in your classes when they’re derived from the MFC class CObject. The level you get in your class is determined by which of three different macros you use in the definition of your class:
Macro |
Functionality |
|
|
DECLARE_DYNAMIC() |
Support for run-time class information |
DECLARE_DYNCREATE() |
Support for run-time class information and dynamic object creation |
DECLARE_SERIAL() |
Support for run-time class information, dynamic object creation and |
|
serialization of objects |
|
|
Each of these macros requires a complementary macro, named with the prefix IMPLEMENT_ instead of DECLARE_, be placed in the file containing the class implementation. As the table indicates, the macros provide progressively more functionality, so I’ll concentrate on the third macro, DECLARE_SERIAL(), because it provides everything that the preceding macros do and more. This is the macro you should use to enable serialization in your own classes. It requires the macro IMPLEMENT_SERIAL()be added to the file containing the class implementation.
You may be wondering why the document class uses DECLARE_DYNCREATE() and not DECLARE_SERIAL(). The DECLARE_DYNCREATE() macro provides the capability for dynamic creation of the objects of the class in which it appears. The DECLARE_SERIAL() macro provides the capability for serialization of the class, plus the dynamic creation of objects of the class, so it incorporates the effects of DECLARE_DYNCREATE(). Your document class doesn’t need serialization because the framework only has to synthesize the document object and then restore the values of its data members; however, the data members of a document do need to be serializable because this is the process used to store and retrieve them.
The Macros Adding Serialization to a Class
With the DECLARE_SERIAL() macro in the definition of your CObject-based class, you get access to the serialization support provided by CObject. This includes special new and delete operators that incorporate memory leak detection in debug mode. You don’t need to do anything to use this because it works automatically.
The macro requires the class name to be specified as an argument, so for serialization of the CElement class, you would add the following line to the class definition:
DECLARE_SERIAL(CElement)
There’s no semicolon required here because this is a macro, not a C++ statement.
It doesn’t matter where you put the macro within the class definition, but if you always put it as the first line, you’ll always be able to verify that it’s there, even when the class definition involves a lot of lines of code.
The IMPLEMENT_SERIAL() macro, which you place in the implementation file for the class, requires three arguments to be specified. The first argument is the name of the class, the second is the name of the
872

Storing and Printing Documents
direct base class, and the third argument is an unsigned 32-bit integer identifying a schema number, or version number, for your program. This schema number allows the serialization process to guard against problems that can arise if you write objects with one version of a program and read them with another, in which the classes may be different.
For example, you could add the following line to the implementation of the CElement class:
IMPLEMENT_SERIAL(CElement, CObject, 1)
If you subsequently modified the class definition, you would change the schema number to something different, such as 2. If the program attempts to read data that was written with a different schema number from that in the currently active program, an exception is thrown. The best place for this macro is as the first line following the #include directives and any initial comments in the .cpp file.
Where CObject is an indirect base of a class, as in the case of the CLine class, for example, each class in the hierarchy must have the serialization macros added for serialization to work in the top level class. For serialization in CLine to work, the macros must also be added to CElement.
How Serialization Works
The overall process of serializing a document is illustrated in a simplified form in Figure 17-1.
Document Output Using Serialization |
|
||
Serialize() |
Document |
|
|
Object |
|
|
|
|
|
|
|
Serialize() |
Serialize() |
|
|
Internal |
|
|
Internal |
Object |
|
|
Object |
Serialize() |
Serialize() |
Serialize() |
Serialize() |
Basic Data |
Basic Data |
|
|
Type |
Type |
|
|
<< |
<< |
|
|
|
FILE |
|
|
Figure 17-1 |
|
|
|
873

Chapter 17
The Serialize() function in the document object calls the Serialize() function (or uses an overloaded insertion operator) for each of its data members. Where a member is a class object, the Serialize() function for that object serializes each of its data members in turn until ultimately basic data types are written to the file. Because most classes in MFC ultimately derive from CObject, they contain serialization support, so you can almost always serialize objects of MFC classes.
The data that you’ll deal with in the Serialize() member functions of your classes and the application document object are, in each case, just the data members. The structure of the classes that are involved and any other data necessary to reconstitute your original objects is automatically taken care of by the
CArchive object.
Where you derive multiple levels of classes from CObject, the Serialize() function in a class must call the Serialize() member of its direct base class to ensure that the direct base class data members are serialized. Note that serialization doesn’t support multiple inheritance, so there can only be one base class for each class defined in a hierarchy.
How to Implement Serialization for a Class
From the previous discussion, I can summarize the steps that you need to take to add serialization to a class:
1.Make sure that the class is derived directly or indirectly from CObject.
2.Add the DECLARE_SERIAL() macro to the class definition (and to the direct base class if the direct base is not CObject).
3.Declare the Serialize() function as a member function of your class.
4.Add the IMPLEMENT_SERIAL() macro to the file containing the class implementation.
5.Implement the Serialize() function for your class.
Now take a look at how you can implement serialization for documents in the Sketcher program.
Applying Serialization
To implement serialization in the Sketcher application, you must implement the Serialize() function in CSketcherDoc so that it deals with all of the data members of that class. You need to add serialization to each of the classes which specify objects that may be included in a document. Before you start adding serialization to your application classes, you should make some small changes to the program to record when a user changes a sketch document. This isn’t absolutely necessary, but it is highly desirable because it enables the program to guard against the document being closed without saving changes.
Recording Document Changes
There’s already a mechanism for noting when a document changes; it uses an inherited member of CSketcherDoc, SetModifiedFlag(). By calling this function consistently whenever the document changes, you record the fact that the document has been altered in a data member of the document class object. This causes an automatically displayed when you try to exit the application without saving the
874

Storing and Printing Documents
modified document. The argument to the SetModifiedFlag() function is a value of type BOOL, and the default value is TRUE. If you have occasion to specify that the document was unchanged, you can call this function with the argument FALSE, although circumstances where this is necessary are rare.
There are only three occasions when you alter a document object:
When you call the AddElement() member of CSketcherDoc to add a new element.
When you call the DeleteElement() member of CSketcherDoc to delete an element.
When you move an element.
You can handle these three situations easily. All you need to do is add a call to SetModifiedFlag() to each of the functions involved in these operations. The definition of AddElement() appears in the CSketcherDoc class definition. You can extend this to:
void AddElement(CElement* pElement) |
// Add an element to the list |
{ |
|
m_ElementList.AddTail(pElement); |
|
SetModifiedFlag(); |
// Set the modified flag |
} |
|
You can get to the definition of DeleteElement() in CSketcherDoc by clicking the function name in the Class View pane. You should add one line to it, as follows:
void CSketcherDoc::DeleteElement(CElement* pElement)
{
if(pElement)
{
//If the element pointer is valid,
//find the pointer in the list and delete it
SetModifiedFlag(); |
// Set the modified flag |
POSITION aPosition = m_ElementList.Find(pElement); |
|
m_ElementList.RemoveAt(aPosition); |
|
delete pElement; |
// Delete the element from the heap |
}
}
Note that you must only set the flag if pElement is not null, so you can’t just stick the function call anywhere.
In a view object, moving an element occurs in the MoveElement() member called by the handler for the WM_MOUSEMOVE message, but you only change the document when the left mouse button is pressed. If there’s a right-button click, the element is put back to its original position, so you only need to add the call to the SetModifiedFlag() function for the document to the OnLButtonDown() function, as follows:
void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
{
CClientDC aDC(this); |
// Create a device context |
OnPrepareDC(&aDC); |
// Get origin adjusted |
aDC.DPtoLP(&point); |
// convert point to Logical |
if(m_MoveMode) |
|
875

Chapter 17
{
// In moving mode, so drop the element
m_MoveMode = FALSE; |
// Kill move mode |
m_pSelected = 0; |
// De-select element |
GetDocument()->UpdateAllViews(0); |
// Redraw all the views |
GetDocument()->SetModifiedFlag(); |
// Set the modified flag |
} |
|
// Rest of the function as before... |
|
}
You call the inherited GetDocument() member of the view class to get access to a pointer to the document object and then use this pointer to call the SetModifiedFlag() function. You now have all the places where you change the document covered.
If you build and run Sketcher, and modify a document or add elements to it, you’ll now get a prompt to save the document when you exit the program. Of course, the File > Save menu option doesn’t do anything yet except clear the modified flag and save an empty file to disk. You must implement serialization to get the document written away to disk properly, and that’s the next step.
Serializing the Document
The first step is the implementation of the Serialize() function for the CSketcherDoc class. Within this function, you must add code to serialize the data members of CSketcherDoc. The data members that you have declared in the class are as follows:
class CSketcherDoc : public CDocument
{
protected: // create from serialization only CSketcherDoc(); DECLARE_DYNCREATE(CSketcherDoc)
// Attributes public:
protected:
COLORREF m_Color; |
// |
Current drawing color |
unsigned int m_Element; |
// |
Current element type |
CTypedPtrList<CObList, CElement*> m_ElementList; // |
Element list |
|
int m_PenWidth; |
// |
Current pen width |
CSize m_DocSize; |
// |
Document size |
|
|
|
// Rest of the class...
};
Note that you don’t need to add any of the preceding code at this point as it’s there already. All that’s necessary is to insert the statements to store and retrieve these five data members in the Serialize() member of the class. You can do this with the following code:
876

|
|
Storing and Printing Documents |
|
void CSketcherDoc::Serialize(CArchive& ar) |
|
|
{ |
|
|
m_ElementList.Serialize(ar); |
// Serialize the element list |
|
if (ar.IsStoring()) |
|
|
{ |
|
|
ar << m_Color |
// Store the current color |
|
<< m_Element |
// the current element type, |
|
<< m_PenWidth |
// and the current pen width |
|
<< m_DocSize; |
// and the current document size |
|
} |
|
|
else |
|
|
{ |
|
|
ar >> m_Color |
// Retrieve the current color |
|
>> m_Element |
// the current element type, |
|
>> m_PenWidth |
// and the current pen width |
|
>> m_DocSize; |
// and the current document size |
|
} |
|
|
} |
|
For four of the data members, you just use the extraction and insertion operators that are overloaded in the CArchive class. This works for the data member m_Color, even though its type is COLORREF,
because type COLORREF is the same as type long. You can’t use the extraction and insertion operators for m_ElementList, because its type isn’t supported by the operators, but as long as the CTypedPtrList class is defined from the collection class template using CObList, as you have done in the declaration of m_ElementList, the class automatically supports serialization. You can, therefore, just call the Serialize() function for the object.
You don’t need to place calls to the Serialize() member of the object m_ElementList in the if-else statement because the kind of operation performed is determined automatically by the CArchive argument, ar. The single statement calling the Serialize() member of m_ElementList takes care of both input and output.
That’s all you need for serializing the document class data members, but serializing the element list, m_ElementList, causes the Serialize() functions for the element classes to be called to store and retrieve the elements themselves, so you also need to implement serialization for those classes.
Serializing the Element Classes
All the shape classes are serializable because you derived them from their base class CElement, which in turn is derived from CObject. The reason that you specified CObject as the base for CElement was solely to get support for serialization. You can now add support for serialization to each of the shape classes by adding the appropriate macros to the class definitions and implementations, and adding the code to the Serialize() function member of each class to serialize its data members. You can start with the base class, CElement, where you need to modify the class definition as follows:
class CElement: public CObject
{
DECLARE_SERIAL(CElement)
protected:
877

Chapter 17
COLORREF m_Color; |
// Color of an element |
CRect m_EnclosingRect; |
// Rectangle enclosing an element |
int m_Pen; |
// Pen width |
public: |
|
virtual ~CElement(){} |
// Virtual destructor |
// Virtual draw operation
virtual void Draw(CDC* pDC, CElement* pElement=0){}
virtual void Move(CSize& aSize){} |
// |
Move an element |
CRect GetBoundRect(); |
// |
Get the bounding rectangle for an element |
|
||
virtual void Serialize(CArchive& ar);// Serialize function for the class |
||
protected: |
|
|
CElement(void); |
// Here to prevent it being called |
|
}; |
|
|
You add the DECLARE_SERIAL() macro and a declaration for the virtual function Serialize().
You already have the default constructor that was created by the Application wizard. You changed it to protected in the class, although it doesn’t matter what its access specification is as long as it appears explicitly in the class definition. It can be public, protected, or private, and serialization still works. If you forget to include a default constructor in a class though, you’ll get an error message when the
IMPLEMENT_SERIAL() macro is compiled.
You should add the DECLARE_SERIAL() macro to each of the derived classes CLine, CRectangle, CCircle, CCurve and CText, with the relevant class name as the argument. You should also add a declaration for the Serialize() function as a public member of each class.
In the file Elements.cpp, you must add the following macro at the beginning:
IMPLEMENT_SERIAL(CElement, CObject, VERSION_NUMBER)
You can define the constant VERSION_NUMBER in the OurConstants.h file by adding the lines:
// Program version number for use in serialization
const UINT VERSION_NUMBER = 1;
You can then use the same constant when you add the macro for each of the other shape classes. For instance, for the CLine class you should add the line:
IMPLEMENT_SERIAL(CLine, CElement, VERSION_NUMBER)
and similarly for the other shape classes. When you modify any of the classes relating to the document, all you need to do is change the definition of VERSION_NUMBER in the OurConstants.h file, and the new version number applies in all your Serialize() functions. You can put all the IMPLEMENT_SERIAL() statements at the beginning of the file if you like. The complete set is:
IMPLEMENT_SERIAL(CElement, CObject, VERSION_NUMBER)
IMPLEMENT_SERIAL(CLine, CElement, VERSION_NUMBER)
878