Chapter 12

Persistence and File I/O


CONTENTS

One of the most important things a program must do is save a user's data after that data has been changed in some way. Without the capability to save edited data, the work the user performs with an application exists only as long as the application is running, vanishing the instant the user exits the application. Not a good way to get work done! In many cases, especially when using AppWizard to create an application, Visual C++ provides much of the code you need to save and load data. However, in some cases-most notably when you create your own object types-you have to do a little extra work to keep your user's files up-to-date.

Objects and Persistence

When you're writing an application, you deal with a lot of different types of objects. Some of your data objects might be simple types like integers and characters. Other objects might be instances of classes, like strings from the CString class or even objects created from your own custom classes. When using objects in applications that must create, save, and load documents, you need a way to save and load the state of those objects, so that you can re-create them exactly as the user left them at the end of the last session.

An object's capability to save and load its state is called persistence. Almost all of the MFC classes are persistent because they are derived either directly or indirectly from MFC's CObject class, which provides the basic functionality for saving and loading an object's state. You've already had some experience with this feature of Visual C++'s MFC. In the following section, though, you'll get a review of how MFC makes a document object persistent.

The File Demo Application

When you create a program using Visual C++'s AppWizard, you get an application that uses document and view classes to organize, edit, and display its data. As you know, the document object, which is derived from the CDocument class, is responsible for holding the application's data during a session and for saving and loading the data so that the document persists from one session to another.

In the CHAP12\FILE folder of this book's CD-ROM, you'll find the File Demo application, which demonstrates the basic techniques behind saving and loading data of an object derived from CDocument. When you run the application, you see the window shown in Figure 12.1. This window displays the contents of the current document. In this case, a document is a single string containing a short message.

Figure 12.1 : The File Demo application demonstrates basic document persistence.

When the program first begins, the message is automatically set to the string "Default Message." However, you can change this message to anything you like. To do this, select the Edit, Change Message command. You then see the dialog box shown in Figure 12.2. Type a new message in the edit box and click the OK button. The new message appears in the window.

Figure 12.2 : You can use the Change Message dialog box to edit the application's message string.

If you choose to exit the program, the document's current state is lost. The next time you run the program, you again have to change the message string. To avoid this complication, you can save the document before exiting the program. Choose the File, Save command to do this (see fig. 12.3). After saving the document, you can reload it at any time by selecting File, Open.

Figure 12.3 : Use the File menu to save and load documents.

A Review of Document Classes

What you've just experienced is object persistence from the user's point of view. The programmer, of course, needs to know much more about how this persistence stuff works. Although you had some experience with document classes in Chapter 6 "The Document/View Paradigm," you'll now review the basic concepts with an eye towards extending those concepts to your own custom classes.

When working with an application created by AppWizard, there are several steps you must complete to enable your document to save and load its state. Those steps, as they apply to an SDI (Single Document Interface) application, are as follows:

  1. Define the data members that will hold the document's data.
  2. Initialize the data members in the document class's OnNewDocument() member function.
  3. Display the current document in the view class's OnDraw() member function.
  4. Provide member functions in the view class that enable the user to edit the document.
  5. Add, to the document class's Serialize() member function, the code needed to save and load the data that comprises the document.

A Quick Look at File Demo's Source Code

In the File Demo application, the document class declares its document storage in its header file (FILEDOC.H), like this:


// Attributes

public:

    CString m_message;

In this case, the document's storage is nothing more than a single string object. Usually, your document's storage needs are much more complex. This single string, however, is enough to demonstrate the basics of a persistent document.

The document class must also initialize the document's data, which it does in the OnNewDocument() member function, as shown in Listing 12.1.


Listing 12.1  LST12_1.TXT-Initializing the Document's Data

BOOL CFileDoc::OnNewDocument()

{

    if (!CDocument::OnNewDocument())

        return FALSE;

    // TODO: add reinitialization code here

    // (SDI documents will reuse this document)

    m_message = _Default Message_;

    return TRUE;

}


With the document class's m_message data member initialized, the application can display the data in the View window, which it does in the view class's OnDraw() function, as shown in Listing 12.2.


Listing 12.2  LST12_2.TXT-Displaying the Document's Data

void CFileView::OnDraw(CDC* pDC)

{

    CFileDoc* pDoc = GetDocument();

    ASSERT_VALID(pDoc);



    // TODO: add draw code for native data here

    pDC->TextOut(20, 20, pDoc->m_message);

}


As long as the user is happy with the contents of the document, the program doesn't need to do anything else. But, of course, an application that doesn't enable the user to edit the application's documents is mostly useless. The File Demo application displays a dialog box the user can use to edit the contents of the document, as shown in Listing 12.3.


Listing 12.3  LST12_3.TXT-Changing the Document's Data

void CFileView::OnEditChangemessage()

{

    // TODO: Add your command handler code here

    CChngDlg dialog(this);

    CFileDoc* pDoc = GetDocument();

    dialog.m_message = pDoc->m_message;



    int result = dialog.DoModal();

    if (result == IDOK)

    {

        pDoc->m_message = dialog.m_message;

        pDoc->SetModifiedFlag();

        Invalidate();

    }

}


This function, which responds to the application's Edit, Change Message command, displays the dialog box and, if the user exits the dialog box by clicking the OK button, transfers the string from the dialog box to the document's data member. The call to the document class's SetModifiedFlag() function notifies the class that its contents have been changed.

Once the user has changed the document's contents, the data must be saved before exiting the application (unless, that is, the user doesn't want to save the changes he made). The document class's Serialize() function, shown in Listing 12.4, handles the saving and loading of the document's data.


Listing 12.4  LST12_4.TXT-The Document Class's Serialize() Function

void CFileDoc::Serialize(CArchive& ar)

{

    if (ar.IsStoring())

    {

        // TODO: add storing code here

        ar << m_message;

    }

    else

    {

        // TODO: add loading code here

        ar >> m_message;

        UpdateAllViews(NULL);

    }

}


Because the CString class (of which m_message is an object) defines the >> and << operators for transferring strings to and from an archive, it's a simple task to save and load the document class's data. If the document's data contained simple data types like integers or characters, it would also be easy to save and load the data. However, what if you've created your own custom class for holding the elements of a document? How can you make an object of this class persistent? You'll find the answers to these questions in the following section.

Creating a Persistent Class

Suppose that you now want to enhance the File Demo application so that it contains its data in a custom class called CMessages. This class holds three CString objects, each of which must be saved and loaded if the application is going to work correctly. You have a couple of options. The first option is to save and load each individual string, as shown in Listing 12.5.


Listing 12.5  LST12_5.TXT-One Way to Save the New Class's Strings

void CFileDoc::Serialize(CArchive& ar)

{

    if (ar.IsStoring())

    {

        // TODO: add storing code here

        ar << m_messages.m_message1;

        ar << m_messages.m_message2;

        ar << m_messages.m_message3;

    }

    else

    {

        // TODO: add loading code here

        ar >> m_messages.m_message1;

        ar >> m_messages.m_message2;

        ar >> m_messages.m_message3;

        UpdateAllViews(NULL);

    }

}


In the preceding example, m_messages is an object of the CMessages class. The CMessages class has three data members that make up the document's data. These data members, which are objects of the CString class, are called m_message1, m_message2, and m_message3.

Although the solution shown in Listing 12.5 is workable, it's not particularly elegant. It would be better to make the CMessages class capable of creating persistent objects. This involves completing the following steps:

  1. Derive the new class from CObject.
  2. Place the DECLARE_SERIAL() macro in the class's declaration.
  3. Place the IMPLEMENT_SERIAL() macro in the class's implementation.
  4. Override the Serialize() function in the class.
  5. Provide an empty, default constructor for the class.

In the following section, you explore an application that creates persistent objects exactly as described in the preceding steps.

The File Demo 2 Application

The next sample application, File Demo 2, demonstrates the steps you take to create a class from which you can create persistent objects. You'll find this application in the CHAP12\FILE2 folder of this book's CD-ROM. When you run the application, you see the window shown in Figure 12.4. The program's window displays the three strings that make up the document's data. These three strings are contained in a custom class.

Figure 12.4 : The three strings displayed in the window are data members of a custom class.

You can edit any of the three strings, by selecting the Edit, Change Messages command. When you do, the dialog box shown in Figure 12.5 appears. Type the new string or strings that you want to display in the window, and then click the OK button. The program displays the edited strings, as well as stores the new string values in the data object.

Figure 12.5 : Use the Change Messages dialog box to edit the application's data.

The application's File menu contains the commands you need to save or load the contents of a document. If you save the changes you make before exiting the application, you can reload the document when you restart the application. In this case, unlike the first version of the program, the document class is using a persistent object'an object that knows how to save and load its own state'as the document's data.

Looking at the CMessages Class

Before you can understand how the document class manages to save and load its contents successfully, you have to understand how the CMessages class, of which the document class's m_messages data member is an object, works. As you examine this class, you'll see how the aforementioned five steps for creating a persistent class have been implemented. Listing 12.6 shows the class's header file.


Listing 12.6  MESSAGES.H-The CMessages Class's Header File

// messages.h

class CMessages : public CObject

{

    DECLARE_SERIAL(CMessages)

    CMessages(){};



protected:

    CString m_message1;

    CString m_message2;

    CString m_message3;



public:

    void SetMessage(UINT msgNum, CString msg);

    CString GetMessage(UINT msgNum);

    void Serialize(CArchive& ar);

};


First, notice that the CMessages class is derived from MFC's CObject class. Also, notice the DECLARE_SERIAL() macro near the top of the class's declaration. This macro's single argument is the name of the class you're declaring. MFC uses this macro to create the additional function declarations needed to implement object persistence. Next, the class declares a default constructor that requires no arguments. This constructor is necessary because MFC needs to be able to create objects of the class when loading data from disk.

After the default constructor comes the class's data members, which are three objects of the CString class. The public member functions are next. SetMessage(), whose arguments are the number of the string to set and the string's new value, enables a program to change a data member. GetMessage(), on the other hand, is the complementary function, enabling a program to retrieve the current value of any of the strings. Its single argument is the number of the string to retrieve.

Finally, the class overrides the Serialize() function, where all the data saving and loading takes place. The Serialize() function is the heart of a persistent object, with each persistent class implementing it in a different way. Listing 12.7 is the class's implementation file, which defines the various member functions.


Listing 12.7  MESSAGES.CPP-The CMessages Class's Implementation File

// messages.cpp

#include "stdafx.h"

#include "messages.h"



IMPLEMENT_SERIAL(CMessages, CObject, 0)

void CMessages::SetMessage(UINT msgNum, CString msg)

{

    if (msgNum == 1)

        m_message1 = msg;

    else if (msgNum == 2)

        m_message2 = msg;

    else if (msgNum == 3)

        m_message3 = msg;

}



CString CMessages::GetMessage(UINT msgNum)

{

    if (msgNum == 1)

        return m_message1;

    else if (msgNum == 2)

        return m_message2;

    else if (msgNum == 3)

        return m_message3;

    else

        return "";

}



void CMessages::Serialize(CArchive& ar)

{

    CObject::Serialize(ar);

    if (ar.IsStoring())

    {

        ar << m_message1 << m_message2 << m_message3;

    }

    else

    {

        ar >> m_message1 >> m_message2 >> m_message3;

    }

}


The IMPLEMENT_SERIAL() macro is the counterpart to the DECLARE_SERIAL() macro. The IMPLEMENT_SERIAL() macro instructs MFC in how to define the functions that give the class its persistent capabilities. The macro's three arguments are the name of the class, the name of the immediate base class, and a schema number, which is like a version number. In most cases, you'll use 1 for the schema number.

There's nothing tricky about the SetMessage() and GetMessage() functions, which perform their assigned tasks straightforwardly. The Serialize() function, however, may inspire a couple of questions. First, note that the first line of the body of the function calls the base class's Serialize() function. This is a standard practice for many functions that override functions of a base class. In this case, the call to CObject::Serialize() doesn't do much, since the CObject class's Serialize() function is empty. Still, calling the base class's Serialize() function is a good habit to get into, because you may not always be working with classes derived directly from CObject.

After calling the base class's version of the function, Serialize() saves and loads its data in much the same way a document object does. Because the data members that must be serialized are CString objects, the program can use the >> and << operators to write the strings to the disk.

Using the CMessages Class in the Program

Now that you know how the CMessages class works, you can examine how it's used in the File Demo 2 application's document class. As you look over the document class, you see that the class uses the same steps to handle its data as the original File Demo application. The main difference is that it's now dealing with a custom class, rather than simple data types or classes defined by MFC. First, the object is declared in the document class's declaration, like this:


// Attributes

public:

    CMessages m_messages;

Next, the program initializes the data object in the document class's OnNewDocument() class, as seen in Listing 12.8.


Listing 12.8  LST12_8.TXT-Initializing the Data Object

BOOL CFile2Doc::OnNewDocument()

{

    if (!CDocument::OnNewDocument())

        return FALSE;

    // TODO: add reinitialization code here

    // (SDI documents will reuse this document)

    m_messages.SetMessage(1, "Default Message 1");

    m_messages.SetMessage(2, "Default Message 2");

    m_messages.SetMessage(3, "Default Message 3");

    return TRUE;

}


Because the document class cannot directly access the data object's data members, it must initialize each string by calling the CMessages class's SetMessage() member function. The view class must edit the data the same way, by calling the CMessages object's member functions, as shown in Listing 12.9. The view class's OnDraw() function also calls the GetMessage() member function in order to access the CMessages class's strings.


Listing 12.9  LST12_9.TXT-Editing the Data Strings

void CFile2View::OnEditChangemessages()

{

    // TODO: Add your command handler code here

    CFile2Doc* pDoc = GetDocument();

    CChngDlg dialog(this);

    dialog.m_message1 = pDoc->m_messages.GetMessage(1);

    dialog.m_message2 = pDoc->m_messages.GetMessage(2);

    dialog.m_message3 = pDoc->m_messages.GetMessage(3);

    int result = dialog.DoModal();

    if (result == IDOK)

    {

        pDoc->m_messages.SetMessage(1, dialog.m_message1);

        pDoc->m_messages.SetMessage(2, dialog.m_message2);

        pDoc->m_messages.SetMessage(3, dialog.m_message3);

        pDoc->SetModifiedFlag();

        Invalidate();

    }

}


The real action, however, happens in the document class's Serialize() function, where the m_messages data object is serialized out to disk. This is accomplished by calling the data object's own Serialize() function inside the document's Serialize(), as shown in Listing 12.10.


Listing 12.10  LST12_10.LST-Serializing the Data Object

void CFile2Doc::Serialize(CArchive& ar)

{

    m_messages.Serialize(ar);

    if (ar.IsStoring())

    {

        // TODO: add storing code here

    }

    else

    {

        // TODO: add loading code here

        UpdateAllViews(NULL);

    }

}


As you can see, after serializing the m_messages data object, there's not much left to do in the document class's Serialize() function, except call UpdateAllViews() if data is being loaded rather than saved. Notice that the call to m_messages.Serialize() passes the archive object as its single parameter.

Reading and Writing Files Directly

Although using MFC's built-in serialization capabilities is a handy way to save and load data, sometimes you need more control over the file-handling process. For example, you might need to deal with your files non-sequentially, something the Serialize() function and its associated CArchive object can't handle. In this case, you can handle files almost exactly as you did in your DOS programs, by creating, reading, and writing files directly. Even when you need to dig down to this level of file handling, though, MFC offers help. Specifically, you can use the CFile class to handle files directly.

The File Demo 3 Application

This book's CD-ROM contains an example program that shows how the CFile class works. You'll find this program in the CHAP12\FILE3 folder. When you run the program, you see the window shown in Figure 12.6. By selecting the Edit, Change Message command, you can edit the string that's displayed in the window (see fig. 12.7). Finally, you can save and load the displayed text string by selecting the File, Save and File, Open commands, respectively (see fig. 12.8).

Figure 12.6 : The File Demo 3 application uses the CFile class for direct file handling.

Figure 12.7 : Use the Change Message dialog box to edit the application's display string.

Figure 12.8 : The File menu enables you to save and load the application's display string.

The CFile Class

MFC's CFile class encapsulates all the functions you need to handle any type of file. Whether you want to perform common sequential data saving and loading or want to construct a random-access file, the CFile class gets you there. Using the CFile class is a lot like handling files the old-fashioned C-style way, except the class hides some of the busy-work details from you, so that you can get the job done quickly and easily. For example, you can create a file for reading with only a single line of code. Table 12.1 shows the CFile class's member functions and their descriptions.

Table 12.1  Member Functions of the CFile Class

FunctionDescription
Abort()Immediately closes the file with no regard for errors
Close()Closes the file
Duplicate()Creates a duplicate file object
Flush()Flushes data from the stream
GetFileName()Gets the file's filename
GetFilePath()Gets the file's full path
GetFileTitle()Gets the file's title (the filename without the extension)
GetLength()Gets the file's length
GetPosition()Gets the current position within the file
GetStatus()Gets the file's status
LockRange()Locks a portion of the file
Open()Opens the file
Read()Reads data from the file
Remove()Deletes a file
Rename()Renames the file
Seek()Sets the position within the file
SeekToBegin()Sets the position to the beginning of the file
SeekToEnd()Sets the position to the end of the file
SetFilePath()Sets the file's path
SetLength()Sets the file's length
SetStatus()Sets the file's status
UnlockRange()Unlocks a portion of the file
Write()Writes data to the file

As you can see from the table, the CFile class offers up plenty of file-handling power. The File Demo 3 application demonstrates how to call a few of the CFile class's member functions. However, most of the other functions are just as easy to use.

Exploring the File Demo 3 Application

When the File Demo 3 application starts up, the program sets its display string to "Default Message," sets the file path to "None," and sets the file's length to 0. This is all accomplished in the view class's constructor. (For the sake of simplicity, all of the file handling is done in the view class.) When the user selects the Edit, Change Message command, the program displays the Change Message dialog box, which happens in the view class's OnEditChangemessage() member function, shown in Listing 12.11.


Listing 12.11  LST12_11.TXT-Changing the Display String

void CFile3View::OnEditChangemessage()

{

    // TODO: Add your command handler code here

    CChngDlg dialog(this);

    dialog.m_message = m_message;



    int result = dialog.DoModal();

    if (result == IDOK)

    {

        m_message = dialog.m_message;

        Invalidate();

    }

}


In this function, the program displays the dialog box, and, if the user exits the dialog box by clicking the OK button, sets the view class's m_message data member to the string entered into the dialog box. A call to Invalidate() ensures that the new string is displayed in the window. The process of displaying dialog boxes and extracting data from them should be very familiar to you by now.

When the user selects the File, Save command, MFC calls the view class's OnFileSave() member function, which is shown in Listing 12.12.


Listing 12.12  LST12_12.TXT-The Application's OnFileSave() Function

void CFile3View::OnFileSave()

{

    // TODO: Add your command handler code here

    // Create the file.

    CFile file("TESTFILE.TXT",

        CFile::modeCreate | CFile::modeWrite);

    // Write data to the file.

    int length = m_message.GetLength();

    file.Write((LPCTSTR)m_message, length);

    // Obtain information about the file.

    m_filePath = file.GetFilePath();

    m_fileLength = file.GetLength();



    // Close the file and repaint the window.

    file.Close();

    Invalidate();

}


In OnFileSave(), the program first creates the file, as well as sets the file's access mode, by calling the CFile class_s constructor. The constructor takes as arguments the name of the file to create and the file access mode flags. You can use several flags at a time simply by ORing their values together, as you can see in the previous listing. These flags, which describe how to open the file and which specify the types of valid operations, are defined as part of the CFile class and are listed in Table 12.2 along with their descriptions. For more details, please refer to your Visual C++ online documentation.

Table 12.2   The File Mode Flags

FlagDescription
CFile::modeCreateCreate a new file or truncate an existing file to length 0
CFile::modeNoInheritDisallow inheritance by a child process
CFile::modeNoTruncateWhen creating the file, do not truncate the file if it already exists
CFile::modeReadAllow read operations only
CFile::modeReadWriteAllow both read and write operations
CFile::modeWriteAllow write operations only
CFile::shareCompatAllow other processes to open the file
CFile::shareDenyNoneAllow other processes read or write operations on the file
CFile::shareDenyReadDisallow read operations by other processes
CFile::shareDenyWriteDisallow write operations by other processes
CFile::shareExclusiveDeny all access to other processes
CFile::typeBinarySet binary mode for the file
CFile::typeTextSet text mode for the file

After creating the file, OnFileSave() gets the length of the current message and writes it out to the file by calling the CFile object's Write() member function. This function requires, as arguments, a pointer to the buffer containing the data to write and the number of bytes to write. Notice the LPCTSTR casting operator in the call to Write(). This operator is defined by the CString class and extracts the string from the class.

Finally, the program calls the CFile object's GetFilePath() and GetLength() member functions to get the file's complete path and length, after which a call to Close() closes the file and a call to the view class's Invalidate() function causes the window to display the new information.

Reading from a file is not much different from writing to one, as you can see in Listing 12.13, which shows the view class's OnFileOpen() member function.


Listing 12.13  LST12_13.TXT-Reading from the File

void CFile3View::OnFileOpen()

{

    // TODO: Add your command handler code here

    // Open the file.

    CFile file("TESTFILE.TXT", CFile::modeRead);

    // Read data from the file.

    char s[81];

    int bytesRead = file.Read(s, 80);

    s[bytesRead] = 0;

    m_message = s;



    // Get information about the file.

    m_filePath = file.GetFilePath();

    m_fileLength = file.GetLength();



    // Close the file and repaint the window.

    file.Close();

    Invalidate();

}


This time the file is opened using the CFile::modeRead flag, which opens the file for read operations only, after which the program creates a character buffer and calls the file object's read() member function to read data into the buffer. The read() function's two arguments are the address of the buffer and the number of bytes to read. The function returns the number of bytes actually read, which, in this case, is almost always less than the 80 requested. Using the number of bytes read, the program can add a 0 to the end of the character data, thus creating a standard C-style string that can be used to set the m_message data member. As you can see, the OnFileOpen() function calls the file object's GetFilePath(), GetLength(), and Close() member functions exactly as OnFileSave() did.

Creating Your Own CArchive Objects

Although you can handle files using CFile objects, you can go a step further and create your own CArchive object that you can use exactly as you use the CArchive object in the Serialize() function. To do this, you create a CFile object and pass that object to the CArchive constructor, like this:


CFile file("FILENAME.EXT", CFile::modeRead);

CArchive ar(&file, CArchive::store);

After creating the archive object, you can use it just like the archive objects that are created for you by MFC. When you're through with the archive object, you must close both the archive and the file, like this:


ar.Close();

file.Close();

From Here...

When it comes to file handling, Visual C++ and MFC give you a number of options. The easiest way to save and load (serialize) data is to take advantage of the CArchive object created for you by MFC and passed to the document class's Serialize() function. Sometimes, however, you need to create your own persistent objects, by deriving the object's class from MFC's CObject class, and then adding a default constructor, as well as the DECLARE_SERIAL() and IMPLEMENT_SERIAL() macros. You can then override the Serialize() function in your new class. If necessary, you can control file handling more directly by creating a CFile object and using that object's member functions to save and load data. For more information on related topics, please refer to the following chapters: