Chapter 8

Persistence and File I/O


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 recreate 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. In the following section you 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 discussed in Chapter 5, "Documents and Views," 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 CHAP9\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 8.1. This window displays the contents of the current document. In this case, a document is a single string containing a short message.

Fig. 8.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 8.2. Type a new message in the edit box and click the OK button. The new message appears in the window.

Fig. 8.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 Figure 8.3). After saving the document, you can reload it at any time by choosing File, Open.

Fig. 8.3 Use the File menu to save and load documents.

A Review of Document Classes

What you've just experienced, saving and opening files, is object persistence from the user's point of view. The programmer, of course, needs to know much more about how persistence works. Although you had some experience with document classes in Chapter 5, "Documents and Views," 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, will be discussed in this section. The steps are as follows:

  1. Define the member variables that will hold the document's data.
  2. Initialize the member variables 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.

You will see these new member variables, and the way that OnDraw() and Serialize use them, in the samples in this chapter.

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 also must initialize the document's data, which it does in the OnNewDocument() member function, as shown in Listing 8.1.

Listing 8.1ñFILEDOC.CPPñ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 8.2.

Listing 8.2ñFILEVIEW.CPPñ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 allow 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 8.3.

Listing 8.3ñFILEVIEW.CPPñChanging the Document's Data

void 
CFileView::OnEditChangemessage()
{
    // TODO: Add your command handler code here
   
    
CChangeDlg 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. After the user has changed the document's contents, they must save the data before exiting the application (unless, that is, the user doesn't want to save the changes). The document class's Serialize() function, handles the saving and loading of the document's data. Listing 8.4 shows the empty shell of Serialize() generated by AppWizard.

Listing 8.4ñFILEVIEW.CPPñThe Document Class's Serialize() Function

void CFileDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        
// TODO: add storing code here
}
    else
    {
        // TODO: add loading code 
here
}
}

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. Simply add this line where the comment reminds you to add storing code:

     ar << m_message;

Add this similar line where the loading code belongs:

     ar >> m_message;

The << operator sends the CString m_message to the archive; the >> operator fills m_message from the archive. As long as all the document's member variables are simple data types like integers or characters, or MFC classes like CString with these operators already defined, it is easy to save and load the data. The operators are defined for these simple data types:

Creating a Persistent Class

But 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 find the answers to these questions in this section.

Suppose that you now want to enhance the File Demo application so that it contains its data in a custom class called CMessages. The member variable is now called m_messages and is an instance of CMessages. This class holds three CString objects, each of which must be saved and loaded if the application is going to work correctly. One way to arrange this is to save and load each individual string, as shown in Listing 8.5.

Listing 8.5ñOne Possible Way to Save the New Class's Strings

void CFileDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        ar << m_messages.m_message1;
        ar << 
m_messages.m_message2;
        ar << m_messages.m_message3;
    }
    else
    
{
        ar >> m_messages.m_message1;
        ar >> m_messages.m_message2;
        ar >> 
m_messages.m_message3;
}
}

You can write the code in Listing 8.5 only if the three member variables of the CMessages class are public and if you know the implementation of the class itself. Later, if the class is changed in any way, this code also has to be changed. It's more object-oriented to delegate the work of storing and loading to the CMessages class itself. This requires some preparation. The basic steps to create a class that can serialize its member variables are the following:

  1. Derive the 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. When you run the application, you see the window shown in Figure 8.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.

Fig. 8.4 The three strings displayed in the window are data members of a custom class.

You can edit any of the three strings by choosing the Edit, Change Messages command. When you do, the dialog box shown in Figure 8.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, and stores the new string values in the document object.

Fig. 8.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 see how the aforementioned five steps for creating a persistent class have been implemented. Listing 8.6 shows the class's header file.

Listing 8.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 provide the additional function and member variable 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. If your class had no default constructor, MFC cannot create a blank object to fill from an archive.

After the default constructor comes the class's data members, which are three objects of the CString class. Notice that they are protected member variables now. The public member functions are next. SetMessage(), whose arguments are the index of the string to set and the string's new value, enables a program to change a data member. GetMessage() 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 8.7 is the class's implementation file, which defines the various member functions.

Listing 8.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)
{
     switch (msgNum)
     {
     case 1:
          m_message1 = 
msg;
          break;
     case 2:
        m_message2 = msg;
          break; 
     case 3:
        m_message3 = msg;
          break;
     }
}
CString CMessages::GetMessage(UINT msgNum)
{
   switch (msgNum)
   
{
      case 1:
         return m_message1;
      case 2:
         return 
m_message2;
      case 3:
         return m_message3;
      default
         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 partner to the DECLARE_SERIAL() macro, providing implementation for 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 use 0 or 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 8.8.

Listing 8.8ñFILE2DOC.CPPñ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 8.9. The view class's OnDraw() function also calls the GetMessage() member function in order to access the CMessages class's strings.

Listing 8.9ñFILE2VIEW.CPPñEditing the Data Strings

void CFile2View::OnEditChangemessages()
{
    // TODO: Add your command handler code here
    CFile2Doc* pDoc = 
GetDocument();
   
    CChangeDialog 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 8.10.

Listing 8.10ñFILE2DOC.CPPñ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
    }
}

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. 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 because they do stream I/O. In this case, you can handle files almost exactly as they are handled by non-Windows programmers: 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 and its derived classes to handle files directly.

The File Demo 3 Application: working with CFile

This book's CD-ROM contains an example program that shows how the CFile class works. You can find this program in the CHAP9\FILE3 folder. When you run the program, you see the window shown in Figure 8.6. By choosing Edit, Change Message, you can edit the string that's displayed in the window (see Figure 8.7). Finally, you can save and load the displayed text string by choosing the File, Save, and File, Open commands, respectively (see Figure 8.8). (File Demo 3 uses one hardcoded file name, as you'll see shortly.)

Fig. 8.6 The File Demo 3 application uses the CFile class for direct file handling.

Fig. 8.7 Use the Change Message dialog box to edit the application's display string.

Fig. 8.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 8.1 shows the CFile class's member functions and their descriptions.

Table 8.1ñMember Functions of the CFile Class

Function Description
Constructor
Creates the CFile object. If passed a file name, it opens the file.
Destructor
Cleans up a CFile object that is going out of scope. If the file is open, it closes that file.
Abort()
Immediately closes thefile 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. Whenever the user saves a document to a file, or opens a file, the view is updated with new file information. (For the sake of simplicity, all of the file handling is done in the view class.) When the user chooses 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 8.11.

Listing 8.11ñFILE3VIEW.CPPñChanging the Display String

void CFile3View::OnEditChangemessage()
{
    // 
TODO: Add your command handler code here
   
    CChangeDlg 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, the program sets the view class's m_message data member to the string entered in the dialog box. A call to Invalidate() ensures that the new string displays in the window. The process of displaying dialog boxes and extracting data from them was first covered in Chapter 2, "Dialog Boxes and Controls."

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

Listing 8.12ñFILE3VIEW.CPPñ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();
    // Repaint the 
window.
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. Notice that you do not have to explicitly open the file when you pass a file name to the constructor. Its arguments are the name of the file 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. They are listed in Table 8.2 along with their descriptions.

Table 8.2ñThe File Mode Flags

Flag Description
CFile::modeCreate
Creates a new file or truncates an existing file to length 0
CFile::modeNoInherit
Disallows inheritance by a child process
CFile::modeNoTruncate
When creating the file, does not truncate the file if it already exists
CFile::modeRead
Allows read operations only
CFile::modeReadWrite
Allows both read and write operations
CFile::modeWrite
Allows write operations only
CFile::shareCompat
Allows other processes to open the file
CFile::shareDenyNone
Allows other processes read or write operations on the file
CFile::shareDenyRead
Disallows read operations by other processes
CFile::shareDenyWrite
Disallows write operations by other processes
CFile::shareExclusive
Denies all access to other processes
CFile::typeBinary
Sets binary mode for the file
CFile::typeText
Sets text mode for the file

If you continue to examine the code in Listing 8.12, you will see that 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 so that they can be displayed by OnDraw(), not shown here. Finally a call to the view class's Invalidate() function causes the window to display the new information. Notice that there is no call to Close()ñthe CFile destructor will close the file automatically.

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

Listing 8.13ñFILE3VIEW.CPPñ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();
    // Repaint the window.
Invalidate();
}

This time the file is opened by 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. This lets you take advantage of Serialize functions already written for other objects, passing them a reference to your own archive object.

To create an archive, you create a CFile object and pass that object to the CArchive constructor. For example, if you plan to write out objects to a file through an archive, create the archive like this:

CFile file("FILENAME.EXT", CFile::modeWrite);
CArchive ar(&file, CArchive::store);

After creating the archive object, you can use it just like the archive objects that MFC creates for you. Since you've created it with the CArchive::store flag, any calls to IsStoring() return TRUE, and the code that dumps objects to the archive executes. When you're through with the archive object, you can close both the archive and the file like this:

ar.Close();
file.Close();

If the objects go out of scope soon after you're finished with them, you can safely omit the calls to Close() since both CArchive and CFile have Close() calls in the destructor.

The Registry

In the early days of Windows programming, applications saved settings and options in initialization files, typically with the INI extension. The days of huge WIN.INI files or myriad private INI files are now goneñwhen an application wants to store information about itself, it does so using a centralized system registry. And, although the Registry makes sharing information between processes easier, it can make things more confusing for the programmer. In this section, you uncover some of the mysteries of the Registry and learn how to manage it in your applications.

How the Registry Is Set Up

Unlike INI files, which are plain text files that can be edited with any text editor, the Registry contains binary and ASCII information that can be edited only using the Registry Editor or special API function calls specially created for managing the Registry. If you've ever used the Registry Editor to browse your system's Registry, you know that it contains a huge amount of information that's organized into a tree structure. Figure 8.9 shows how the Registry appears when you first run the Registry Editor. (You can find the Registry Editor, called REGEDIT.EXE, in your main Windows folder, or you can run it with the Start menu's Run command by choosing Run, typing regedit, and then clicking OK.)

Fig. 8.9 The Registry Editor displays the Registry.

The left-hand window lists the Registry's predefined keys. The plus marks next to the keys in the tree indicate that you can "open" the keys and view more detailed information associated with them. Keys can have subkeys, and subkeys can themselves have subkeys. Any key or subkey may or may not have a value associated with it. If you explore deep enough in the hierarchy, you see a list of values in the right-hand window. In Figure 8.10, you can see the values associated with the current user's screen appearance. To see these values yourself, browse from HKEY_CURRENT_USER to Control Panel to Appearance to Schemes, and you will see the desktop schemes that are installed on your system.

Fig. 8.10 The Registry is structured as a tree containing a huge amount of information.

The Predefined Keys

In order to know where things are stored in the Registry, you need to know about the predefined keys and what they mean. From Figure 8.9, you can see that the six predefined keys are:

The HKEY_CLASSES_ROOT key holds document types and properties, as well as class information about the various applications installed on the machine. For example, if you explored this key on your system, you'd probably find an entry for the DOC file extension, under which you'd find entries for the applications that can handle this type of document (see Figure 8.11).

Fig. 8.11 The HKEY_CLASSES_ROOT key holds document information.

The HKEY_CURRENT_USER key contains all the system settings the current user has established, including color schemes, printers, and program groups. The HKEY_LOCAL_MACHINE key, on the other hand, contains status information about the computer, and the HKEY_USERS key organizes information about each user of the system, as well as the default configuration. Finally, the HKEY_CURRENT_CONFIG key holds information about the hardware configuration, and the HKEY_DYN_DATA key contains information about dynamic registry data, which is data that changes frequently.

Using the Registry in an MFC Application

Now that you know a little about the Registry, let me say that it would take an entire book to explain how to fully access and use the Registry. As you may imagine, the Win32 API features many functions for manipulating the Registry. And, if you're going to use those functions, you sure better know what you're doing! However, you can easily use the Registry with your MFC applications to store information that the application needs from one session to another. To make this task as easy as possible, MFC provides the CWinApp class with the SetRegistryKey() member function, which creates (or opens) a key entry in the Registry for your application. All you have to do is supply a key name (usually a company name) for the function to use, like this:

SetRegistryKey("MyCoolCompany");

You should call SetRegistryKey() in the application class's InitInstance() member function, which is called once at program startup.

After you've called SetRegistryKey(), your application can create the subkeys and values it needs by calling one of two functions. The WriteProfileString() function adds string values to the Registry, and the WriteProfileInt() function adds integer values to the Registry. To get values from the Registry, you can use the GetProfileString() and GetProfileInt() functions. (You also can use RegSetValueEx() and RegQueryValueEx() to set and retrieve Registry values.)

When they were first written, the WriteProfileString(), WriteProfileInt(), GetProfileString(), and GetProfileInt() functions transferred information to and from an INI file. Used alone, they still do. But when you call SetRegistryKey() first, MFC reroutes these profile functions to the Registry, making using the Registry an almost painless process.

The File Demo 2 Application Revisited

In this chapter, you have already built an application that used the Registry. Listing 8.14 is an excerpt from CFile2App::InitInstance()ñthis code was generated by AppWizard.

Listing 8.14ñFILE3VIEW.CPPñReading from the File

     // Change 
the registry key under which our settings are stored.
     // You should modify this string to be something appropriate
     // such as the name of your company or 
organization.
     SetRegistryKey(_T("Local AppWizard-Generated Applications"));
     LoadStdProfileSettings();  // Load standard INI file options (including 
MRU)

MRU stands for Most Recently Used and refers to the list of files that appears on the File menu after you have opened files with an application. Figure 8.12 shows the Registry Editor displaying the key that stores this information, HKEY_CURRENT_USER\Software\Local AppWizard-Generated Applications\File2\Recent File List. In the foreground, File2's File menu shows the single entry in the MRU list.

Fig. 8.12 The most recently used files list is stored in the Registry automatically.

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.

Although not a visible component of an application, the Registry is the place where you should store data that your application needs from session to session. Using the Registry can be tricky when relying solely on the Win32 Registry functions. However, MFC makes dealing with the Registry as easy as writing values to the old-fashioned INI files.

For more information on related topics, please refer to the following chapters:


© 1997, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.