Program examples compiled using Visual C++ 6.0 (MFC 6.0) compiler on Windows XP Pro machine with Service Pack 2. Topics and sub topics for this Tutorial are listed below. You can compare the standard C file I/O, standard C++ file I/O and Win32 directory, file and access controls with the MFC serialization. So many things lor! Similar but not same. Those links also given at the end of this tutorial.
|
Reading and Writing Documents: SDI Applications
As you've probably noticed, every AppWizard-generated program has a File menu that contains the familiar New, Open, Save, and Save As commands. In this module, you'll learn how to make your application respond to read and write documents. Here we'll stick with the Single Document Interface (SDI) application because it's familiar territory. Module 12 introduces the Multiple Document Interface (MDI) application, which is more flexible in its handling of documents and files. In both modules, you'll get a heavy but necessary dose of application-framework theory; you'll learn a lot about the various helper classes that have been concealed up to this point. The going will be rough, but believe me, you must know the details to get the most out of the application framework. This module's example, MYMFC17, is an SDI application based on the MYMFC16 example from the previous module. It uses the student list document with a CFormView-derived view class. Now the student list can be written to and read from disk through a process called serialization. Module 12 shows you how to use the same view and document classes to make an MDI application.
Serialization: What Is It?
The term "serialization" might be new to you, but it's already seen some use in the world of object-oriented programming. The idea is that objects can be persistent, which means they can be saved on disk when a program exits and then can be restored when the program is restarted. This process of saving and restoring objects is called serialization. In the MFC library, designated classes have a member function named Serialize(). When the application framework calls Serialize() for a particular object, for example, an object of class CStudent, the data for the student is either saved on disk or read from disk. In the MFC library, serialization is not a substitute for a database management system. All the objects associated with a document are sequentially read from or written to a single disk file. It's not possible to access individual objects at random disk file addresses. If you need database capability in your application, consider using the Microsoft Open Database Connectivity (ODBC) software or Data Access Objects (DAO). The MFC framework already uses structured storage (for database) for container programs that support embedded objects. Disk Files and Archives
How do you know whether Serialize() should read or write data? How is Serialize() connected to a disk file? With the MFC library, objects of class CFile represent disk files. A CFile object encapsulates the binary file handle that you get through the Win32 function CreateFile(). This is not the buffered FILE pointer that you'd get with a call to the C runtime fopen() function; rather, it's a handle to a binary file. The application framework uses this file handle for Win32 ReadFile(), WriteFile(), and SetFilePointer() calls. If your application does no direct disk I/O but instead relies on the serialization process, you can avoid direct use of CFile objects. Between the Serialize() function and the CFile object is an archive object of class CArchive, as shown in Figure 1. The CArchive object buffers data for the CFile object, and it maintains an internal flag that indicates whether the archive is storing (writing to disk) or loading (reading from disk). Only one active archive is associated with a file at any one time. The application framework takes care of constructing the CFile and CArchive objects, opening the disk file for the CFile object and associating the archive object with the file.
|
All you have to do (in your Serialize() function) is load data from or store data in the archive object. The application framework calls the document's Serialize() function during the File Open and File Save processes.
Figure 1: The serialization process.
Making a Class Serializable
A serializable class must be derived directly or indirectly from CObject. In addition (with some exceptions), the class declaration must contain the DECLARE_SERIAL macro call, and the class implementation file must contain the IMPLEMENT_SERIAL macro call. See the Microsoft Foundation Class Reference for a description of these macros. This module's CStudent class example is modified from the class in Module 10 to include these macros.
Writing a Serialize Function
In Module 10, you saw a CStudent class, derived from CObject, with these data members:
public:
CString m_strName;
int m_nGrade;
Now, your job is to write a Serialize() member function for CStudent. Because Serialize() is a virtual member function of class CObject, you must be sure that the return value and parameter types match the CObject declaration. The Serialize() function for the CStudent class is below.
void CStudent::Serialize(CArchive& ar)
{
TRACE("Entering CStudent::Serialize\n");
if (ar.IsStoring())
{
ar << m_strName << m_nGrade;
}
else
{
ar >> m_strName >> m_nGrade;
}
}
Most serialization functions call the Serialize() functions of their base classes. If CStudent were derived from CPerson, for example, the first line of the Serialize() function would be:
CPerson::Serialize(ar);
The Serialize() function for CObject (and for CDocument, which doesn't override it) doesn't do anything useful, so there's no need to call it. Notice that ar is a CArchive reference parameter that identifies the application's archive object. The CArchive::IsStoring member function tells us whether the archive is currently being used for storing or loading. The CArchive class has overloaded insertion operators (<<) and extraction operators (>>) for many of the C++ built-in types, as shown in the following table.
Type |
Description |
BYTE |
8 bits, unsigned |
WORD |
16 bits, unsigned |
LONG |
32 bits, signed |
DWORD |
32 bits, unsigned |
float |
32 bits |
double |
64 bits, IEEE standard |
int |
32 bits, signed |
short |
16 bits, signed |
char |
8 bits, unsigned |
unsigned |
32 bits, unsigned |
Table 1 |
The insertion operators are overloaded for values; the extraction operators are overloaded for references. Sometimes you must use a cast to satisfy the compiler. Suppose you have a data member m_nType that is an enumerated type. Here's the code you would use:
ar << (int) m_nType;
ar >> (int&) m_nType;
MFC classes that are not derived from CObject, such as CString and CRect, have their own overloaded insertion and extraction operators for CArchive.
Loading from an Archive: Embedded Objects vs. Pointers
Now suppose your CStudent object has other objects embedded in it, and these objects are not instances of standard classes such as CString, CSize, and CRect. Let's add a new data member to the CStudent class:
public:
CTranscript m_transcript;
Assume that CTranscript is a custom class, derived from CObject, with its own Serialize() member function. There's no overloaded << or >> operator for CObject, so the CStudent::Serialize function now becomes:
void CStudent::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << m_strName << m_nGrade;
}
else
{
ar >> m_strName >> m_nGrade;
}
m_transcript.Serialize(ar);
}
Before the CStudent::Serialize function can be called to load a student record from the archive, a CStudent object must exist somewhere. The embedded CTranscript object m_transcript is constructed along with the CStudent object before the call to the CTranscript::Serialize function. When the virtual CTranscript::Serialize function does get called, it can load the archived transcript data into the embedded m_transcript object. If you're looking for a rule, here it is: always make a direct call to Serialize() for embedded objects of classes derived from CObject. Suppose that, instead of an embedded object, your CStudent object contained a CTranscript pointer data member such as this:
public:
CTranscript* m_pTranscript;
You could use the Serialize() function, as shown below, but as you can see, you must construct a new CTranscript object yourself.
void CStudent::Serialize(CArchive& ar)
{
if (ar.IsStoring())
ar << m_strName << m_nGrade;
else
{
m_pTranscript = new CTranscript;
ar >> m_strName >> m_nGrade;
}
m_pTranscript->Serialize(ar);
}
Because the CArchive insertion and extraction operators are indeed overloaded for CObject pointers, you could write Serialize() this way instead:
void CStudent::Serialize(CArchive& ar)
{
if (ar.IsStoring())
ar << m_strName << m_nGrade << m_pTranscript;
else
ar >> m_strName >> m_nGrade >> m_pTranscript;
}
But how is the CTranscript object constructed when the data is loaded from the archive? That's where the DECLARE_SERIAL and IMPLEMENT_SERIAL macros in the CTranscript class come in. When the CTranscript object is written to the archive, the macros ensure that the class name is written along with the data. When the archive is read, the class name is read in and an object of the correct class is dynamically constructed, under the control of code generated by the macros. Once the CTranscript object has been constructed, the overridden Serialize() function for CTranscript can be called to do the work of reading the student data from the disk file. Finally the CTranscript pointer is stored in the m_pTranscript data member. To avoid a memory leak, you must be sure that m_pTranscript does not already contain a pointer to a CTranscript object. If the CStudent object was just constructed and thus was not previously loaded from the archive, the transcript pointer will be null. The insertion and extraction operators do not work with embedded objects of classes derived from CObject, as shown here:
ar >> m_strName >> m_nGrade >> &m_transcript; // Don't try this
Serializing Collections
Because all collection classes are derived from the CObject class and the collection class declarations contain the DECLARE_SERIAL macro call, you can conveniently serialize collections with a call to the collection class's Serialize() member function. If you call Serialize() for a CObList collection of CStudent objects, for example, the Serialize() function for each CStudent object will be called in turn. You should, however, remember the following specifics about loading collections from an archive:
If a collection contains pointers to objects of mixed classes (all derived from CObject), the individual class names are stored in the archive so that the objects can be properly constructed with the appropriate class constructor.
If a container object, such as a document, contains an embedded collection, loaded data is appended to the existing collection. You might need to empty the collection before loading from the archive. This is usually done in the document's virtual DeleteContents() function, which is called by the application framework.
When a collection of CObject pointers is loaded from an archive, the following processing steps take place for each object in the collection:
The object's class is identified.
Heap storage is allocated for the object.
The object's data is loaded into the newly allocated storage.
A pointer to the new object is stored in the collection.
The MYMFC17 example shows serialization of an embedded collection of CStudent records.
The Serialize() Function and the Application Framework
OK, so you know how to write Serialize() functions, and you know that these function calls can be nested. But do you know when the first Serialize() function gets called to start the serialization process? With the application framework, everything is keyed to the document (the object of a class derived from CDocument). When you choose Save or Open from the File menu, the application framework creates a CArchive object (and an underlying CFile object) and then calls your document class's Serialize() function, passing a reference to the CArchive object. Your derived document class Serialize() function then serializes each of its non-temporary data members. If you take a close look at any AppWizard-generated document class, you'll notice that the class includes the DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros rather than the DECLARE_SERIAL and IMPLEMENT_SERIAL macros. The SERIAL macros are unneeded because document objects are never used in conjunction with the CArchive extraction operator or included in collections; the application framework calls the document's Serialize() member function directly. You should include the DECLARE_SERIAL and IMPLEMENT_SERIAL macros in all other serializable classes.
The SDI Application
You've seen many SDI applications that have one document class and one view class. We'll stick to a single view class in this module, but we'll explore the interrelationships among the application object, the main frame window, the document, the view, the document template object, and the associated string and menu resources.
The Windows Application Object
For each of your applications, AppWizard has been quietly generating a class derived from CWinApp. It has also been generating a statement such as this:
CMyApp theApp;
Listing 1.
What you're seeing here is the mechanism that starts an MFC application. The class CMyApp is derived from the class CWinApp, and theApp is a globally declared instance of the class. This global object is called the Windows application object. Here's a summary of the startup steps in a Microsoft Windows MFC library application:
Windows loads your program into memory.
The global object theApp is constructed. All globally declared objects are constructed immediately when the program is loaded.
Windows calls the global function WinMain(), which is part of the MFC library. WinMain() is equivalent to the non-Windows main function, each is a main program entry point.
WinMain() searches for the one and only instance of a class derived from CWinApp.
WinMain() calls the InitInstance() member function for theApp, which is overridden in your derived application class.
Your overridden InitInstance() function starts the process of loading a document and displaying the main frame and view windows.
WinMain() calls the Run() member function for theApp, which starts the processes of dispatching window messages and command messages.
You can override another important CWinApp member function. The ExitInstance() function is called when the application terminates, after all its windows are closed. Windows allows multiple instances of programs to run. The InitInstance() function is called each time a program instance starts up. In Win32, each instance runs as an independent process. It's only incidental that the same code is mapped to the virtual memory address space of each process. If you want to locate other running instances of your program, you must either call the Win32 FindWindow() function or set up a shared data section or memory-mapped file for communication.
The Document Template Class
If you look at the InitInstance() function that AppWizard generates for your derived application class, you'll see that the following statements are featured:
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CStudentDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CStudentView));
AddDocTemplate(pDocTemplate);
Listing 1.
Unless you start doing fancy things with splitter windows and multiple views, this is the only time you'll actually see a document template object. In this case, it's an object of class CSingleDocTemplate, which is derived from CDocTemplate. The CSingleDocTemplate class applies only to SDI applications because SDI applications are limited to one document object. AddDocTemplate() is a member function of class CWinApp. The AddDocTemplate() call, together with the document template constructor call, establishes the relationships among classes, the application class, the document class, the view window class, and the main frame window class. The application object exists, of course, before template construction, but the document, view, and frame objects are not constructed at this time. The application framework later dynamically constructs these objects when they are needed.
This dynamic construction is a sophisticated use of the C++ language. The DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros in a class declaration and implementation enable the MFC library to construct objects of the specified class dynamically. If this dynamic construction capability weren't present, more relationships among your application's classes would have to be hard-coded. Your derived application class, for example, would need code for constructing document, view, and frame objects of your specific derived classes. This would compromise the object-oriented nature of your program. With the template system, all that's required in your application class is use of the RUNTIME_CLASS macro. Notice that the target class's declaration must be included for this macro to work. Figure 2 illustrates the relationships among the various classes, and Figure 3 illustrates the object relationships. An SDI application can have only one template (and associated class groups), and when the SDI program is running, there can be only one document object and only one main frame window object.
Figure 2: Class relationships.
|
Figure 3: Object relationships.
The MFC library dynamic construction capability was designed before the runtime type identification (RTTI) feature was added to the C++ language. The original MFC implementation goes beyond RTTI, and the MFC library continues to use it for dynamic object construction.
The Document Template Resource
The first AddDocTemplate() parameter is IDR_MAINFRAME, the identifier for a string table resource. Here is the corresponding string that AppWizard generates for MYMFC17 in the application's RC file:
IDR_MAINFRAME
"mymfc17\n" // application window caption
"\n" // root for default document name
// ("Untitled" used if none provided)
"Mymfc1\n" // document type name
"Mymfc1 Files (*.myext)\n" // document type description and filter
".myext\n" // extension for documents of this type
"Mymfc17.Document\n" // Registry file type ID
"Mymfc1 Document" // Registry file type description
You can see this by double clicking the String Table in ResourceView and IDR_MAINFRAME as shown below.
Figure 4: String table in ResourceView.
Figure 5: String for IDR_MAINFRAME.
Figure 6: String properties for IDR_MAINFRAME (double-clicking the previous figure).
The resource compiler won't accept the string concatenations as shown above. If you examine the mymfc17.rc file, you'll see the substrings combined in one long string. IDR_MAINFRAME specifies one string that is separated into substrings by newline characters (\n). The substrings show up in various places when the application executes. The string myext is the default document file extension specified to AppWizard.
The IDR_MAINFRAME ID, in addition to specifying the application's strings, identifies the application's icon, toolbar resources, and menu. AppWizard generates these resources, and you can maintain them with the resource editors. So now you've seen how the AddDocTemplate() call ties all the application elements together. Be aware, though, that no windows have been created yet and therefore nothing appears on the screen.
Multiple Views of an SDI Document
Providing multiple views of an SDI document is a little more complicated. You could provide a menu item that allows the user to choose a view, or you could allow multiple views in a splitter window. Module 14 shows you how to implement both techniques.
Creating an Empty Document: The CWinApp::OnFileNew Function
After your application class's InitInstance() function calls the AddDocTemplate() member function, it calls OnFileNew() (indirectly through CWinApp::ProcessShellCommand), another important CWinApp member function. OnFileNew() sorts through the web of interconnected class names and does the following:
Constructs the document object but does not attempt to read data from disk.
Constructs the main frame object (of class CMainFrame); also creates the main frame window but does not show it. The main frame window includes the IDR_MAINFRAME menu, the toolbar, and the status bar.
Constructs the view object; also creates the view window but doesn't show it.
Establishes connections among the document, main frame, and view objects. Do not confuse these object connections with the class connections established by the call to AddDocTemplate().
Calls the virtual CDocument::OnNewDocument member function for the document object, which calls the virtual DeleteContents() function.
Calls the virtual CView::OnInitialUpdate member function for the view object.
Calls the virtual CFrameWnd::ActivateFrame for the frame object to show the main frame window together with the menus, view window, and control bars.
Some of the functions listed above are not called directly by OnFileNew() but are called indirectly through the application framework.
In an SDI application, the document, main frame, and view objects are created only once, and they last for the life of the program. The CWinApp::OnFileNew function is called by InitInstance. It's also called in response to the user choosing the File New menu item. In this case, OnFileNew() must behave a little differently. It can't construct the document, frame, and view objects because they're already constructed. Instead, it reuses the existing document object and performs steps 5, 6, and 7 above. Notice that OnFileNew() always calls DeleteContents() (indirectly) to empty the document.
The Document Class's OnNewDocument() Function
You've seen the view class OnInitialUpdate() member function and the document class OnNewDocument() member function in Module 10. If an SDI application didn't reuse the same document object, you wouldn't need OnNewDocument() because you could perform all document initialization in your document class constructor. Now you must override OnNewDocument() to initialize your document object each time the user chooses File New or File Open. AppWizard helps you by providing a skeleton function in the derived document class it generates.
It's a good idea to minimize the work you do in constructor functions. The fewer things you do, the less chance there is for the constructor to fail, and constructor failures are messy. Functions such as CDocument::OnNewDocument and CView::OnInitialUpdate are excellent places to do initial housekeeping. If anything fails at creation time, you can pop up a message box and in the case of OnNewDocument(), you can return FALSE. Be advised that both functions can be called more than once for the same object. If you need certain instructions executed only once, declare a "first time" flag data member and then test/set it appropriately.
Connecting File Open to Your Serialization Code: The OnFileOpen() Function
When AppWizard generates an application, it maps the File Open menu item to the CWinApp::OnFileOpen member function. When called, this function invokes a sequence of functions to accomplish these steps:
Prompts the user to select a file.
Calls the virtual function CDocument::OnOpenDocument for the already existing document object. This function opens the file, calls CDocument::DeleteContents, and constructs a CArchive object set for loading. It then calls the document's Serialize() function, which loads data from the archive.
Calls the view's OnInitialUpdate() function.
The Most Recently Used (MRU) file list is a handy alternative to the File Open menu item. The application framework tracks the four (default) most recently used files and display their names on the File menu. These filenames are stored in the Windows Registry between program executions. You can change the number of recent files tracked by supplying a parameter to the LoadStdProfileSetting() function in the application class InitInstance() function.
The Document Class's DeleteContents() Function
When you load an existing SDI document object from a disk file, you must somehow erase the existing contents of the document object. The best way to do this is to override the CDocument::DeleteContents virtual function in your derived document class. The overridden function, as you've seen in Module 10, does whatever is necessary to clean up your document class's data members. In response to both the File New and File Open menu items, the CDocument functions OnNewDocument() and OnOpenDocument() both call the DeleteContents() function, which means DeleteContents() is called immediately after the document object is first constructed. It's called again when you close a document. If you want your document classes to work in SDI applications, plan on emptying the document's contents in the DeleteContents() member function rather than in the destructor. Use the destructor only to clean up items that last for the life of the object.
Connecting File Save and File Save As to Your Serialization Code
When AppWizard generates an application, it maps the File Save menu item to the OnFileSave() member function of the CDocument class. OnFileSave() calls the CDocument function OnSaveDocument(), which in turn calls your document's Serialize() function with an archive object set for storing. The File Save As menu item is handled in a similar manner: it is mapped to the CDocument function OnFileSaveAs(), which calls OnSaveDocument(). Here the application framework does all the file management necessary to save a document on disk. Yes, it is true that the File New and File Open menu options are mapped to application class member functions, but File Save and File Save As are mapped to document class member functions. File New is mapped to OnFileNew(). The SDI version of InitInstance() also calls OnFileNew() (indirectly). No document object exists when the application framework calls InitInstance(), so OnFileNew() can't possibly be a member function of CDocument. When a document is saved, however, a document object certainly exists.
The Document's "Dirty" Flag
Many document-oriented applications for Windows track the user's modifications of a document. If the user tries to close a document or exit the program, a message box asks whether the user wants to save the document. The MFC application framework directly supports this behavior with the CDocument data member m_bModified. This Boolean variable is TRUE if the document has been modified (has become "dirty"); otherwise, it is FALSE. The protected m_bModified flag is accessed through the CDocument member functions SetModifiedFlag() and IsModified(). The framework sets the document object's flag to FALSE when the document is created or read from disk and when it is saved on disk. You, the programmer, must use the SetModifiedFlag() function to set the flag to TRUE when the document data changes. The virtual function CDocument::SaveModified, which the framework calls when the user closes the document, displays a message box if the m_bModified flag is set to TRUE. You can override this function if you need to do something else. In the MYMFC17 example, you'll see how a one-line update command UI function can use IsModified() to control the state of the disk button and the corresponding menu item. When the user modifies the file, the disk button is enabled; when the user saves the file, the button changes to gray. In one respect, MFC SDI applications behave a little differently from other Windows SDI applications such as Notepad. Here's a typical sequence of events:
The user creates a document and saves it on disk under the name (for example) test.dat.
The user modifies the document.
The user chooses File Open and then specifies test.dat.
When the user chooses File Open, Notepad asks whether the user wants to save the changes made to the document (in Step 2 above). If the user answers no, the program rereads the document from disk. An MFC application, on the other hand, assumes that the changes are permanent and does not reread the file.
Continue on next module...part 2.
Further reading and digging:
MSDN MFC 9.0 class library online documentation - latest version.
DCOM at MSDN.
COM+ at MSDN.
COM at MSDN.
Unicode and Multi-byte character set: Story and program examples.