One of the most important things a program must do is save users' data after that data is changed in some way. Without the capability to save edited data, the work a 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 necessary 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 users' files up to date.
When you're writing an application, you deal with a lot of different object types. Some data objects might be simple types, such as integers and characters. Other objects might be instances of classes, such as 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 users left them at the end of the last session.
An object's capability to save and load its state is called persistence. Almost all MFC classes are persistent because they're derived directly or indirectly from MFC's CObject class, which provides the basic functionality for saving and loading an object's state. The following section reviews how MFC makes a document object persistent.
When you use Visual C++'s AppWizard to create a program, you get an application that uses document and view classes to organize, edit, and display its data. As discussed in Chapter 4, "Documents and Views," the document object, 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 this chapter, you'll build the File Demo application, which demonstrates the basic techniques behind saving and loading data of an object derived from CDocument. File Demo's document is a single string containing a short message, which the view displays.
Three menu items are relevant in the File Demo application. When the program first begins, the message is automatically set to the string Default Message. Users will change this message by choosing Edit, Change Message. The File, Save menu option saves the document, as you'd expect, and File, Open reloads it from disk.
Anyone who's written a program has experienced saving and opening files--object persistence from the user's point of view. In this chapter you'll learn how persistence works. Although you had some experience with document classes in Chapter 4, you'll now review the basic concepts with an eye toward extending those concepts to your own custom classes.
When working with an application created by AppWizard, you must complete several steps to enable your document to save and load its state. Those steps are discussed in this section. The steps are as follows:
When your application can handle multiple documents, you need to do a little extra work to be sure that you use, change, or save the correct document. Luckily, most of that work is taken care of by MFC and AppWizard.
To build the File Demo application, start by using AppWizard to create an SDI application. All the other AppWizard choices should be left at their default values, so you can speed things up by clicking Finish on Step 1 after selecting SDI and making sure that Document/View support is selected.
Double-click CfileDemoDoc in ClassView to edit the header file for the document class. In the Attributes section add a CString member variable called m_message, so that the Attributes section looks 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. It's very common for MFC programmers to use public variables in their documents, rather than a private variable with public access functions. It makes it a little simpler to write the code in the view class that will access the document variables. It will, however, make future enhancements a little more work. These tradeoffs are discussed in more detail in Appendix A, "C++ Review and Object-Oriented Concepts."
This string, like all the document's data, must be initialized. The OnNewDocument() member function is the place to do it. Expand CFileDemoDoc in ClassView and double-click OnNewDocument() to edit it. Add a line of code to initialize the string so that the function looks like Listing 7.1. You should remove the TODO comments because you've done what they were reminding you to do.
BOOL CFileDemoDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; 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. You just need to edit the view class's OnDraw() function (see Listing 7.2). Expand CFileDemoView in ClassView and double-click OnDraw() to edit it. Again, you're just adding one line of code and removing the TODO comment.
void CFileDemoView::OnDraw(CDC* pDC) { CFileDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); pDC->TextOut(20, 20, pDoc->m_message);
}
Getting information onscreen, using device contexts, and the TextOut() function are all discussed in Chapter 5, "Drawing on the Screen."
Build File Demo now, to make sure there are no typos, and run it. You should see Default Message appear onscreen.
Now, you need to allow users to edit the application's document by changing the string. In theory, the application should display a dialog box to let the user enter any desired string at all. For our purposes, you're just going to have the Edit, Change Message menu option assign the string a different, hard-coded value. ShowString, the subject of Chapter 8, "Building a Complete Application: ShowString," shows how to create a dialog box such as the one File Demo might use.
Click the Resource tab to switch to ResourceView, expand the resources, expand Menus, and double-click IDR_MAINFRAME to edit it. Click once on the Edit item in the menu you are editing to drop it down. Click the blank item at the end of the list and type Change &Message. This will add another item to the menu.
Choose View, ClassWizard to make the connection between this menu item and your code. You should see ID_EDIT_CHANGEMESSAGE highlighted already; if not, click it in the box on the left to highlight it. Choose CFileDemoView from the drop-down box on the upper right. Click COMMAND in the lower-right box and then click the Add Function button. Accept the suggested name, OnEditChangemessage(), by clicking OK on the dialog that appears. Click Edit Code to open the new function in the editor and edit it to match Listing 7.3.
void CFileDemoView::OnEditChangemessage() { CTime now = CTime::GetCurrentTime(); CString changetime = now.Format("Changed at %B %d %H:%M:%S"); GetDocument()->m_message = changetime; GetDocument()->SetModifiedFlag(); Invalidate();
}
This function, which responds to the application's Edit, Change Message command, builds a string from the current date and time and transfers it to the document's data member. (The CTime class and its Format() function are discussed in Appendix F, "Useful Classes.") The call to the document class's SetModifiedFlag() function notifies the object that its contents have been changed. The application will warn about exiting with unsaved changes as long as you remember to call SetModifiedFlag() everywhere there might be a change to the data. Finally, this code forces a redraw of the screen by calling Invalidate(), as discussed in Chapter 4.
TIP: If m_message was a private member variable of the document class, you could have a public SetMessage() function that called SetModifiedFlag() and be guaranteed no programmer would ever forget to call it. That's one of the advantages of writing truly object-oriented programs.
The document class's Serialize() function handles the saving and loading of the document's data. Listing 7.4 shows the empty shell of Serialize() generated by AppWizard.
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 such as integers or characters, or MFC classes such as CString with these operators already defined, it's easy to save and load the data. The operators are defined for these simple data types:
Build File Demo and run it. Choose Edit, Change Message, and you should see the new string onscreen, as shown in Figure 7.1. Choose File, Save and enter a filename you can remember. Now change the message again. Choose File, New and you'll be warned about saving your current changes first, as in Figure 7.2. Choose File, Open and browse to your file, or just find your filename towards the bottom of the File menu to re-open it, and you'll see that File Demo can indeed save and reload a string.
FIG. 7.1 File Demo changes the string on command.
FIG. 7.2 Your users will never lose unsaved data again.
NOTE: If you change the file, save it, change it again, and re-open it, File Demo will not ask Revert to saved document? as some applications do. Instead, it will bail out of the File Open process partway through and leave you with your most recent changes. This behavior is built in to MFC. If the name of the file you are opening matches the name of the file that is already open, you will not revert to the saved document.
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 for the application to work correctly. One way to arrange this is to save and load each individual string, as shown in Listing 7.5.
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 7.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 following basic steps create a class that can serialize its member variables:
In the following section, you build an application that creates persistent objects in just this way.
The next sample application, File Demo 2, demonstrates the steps you take to create a class from which you can create persistent objects. It will have an Edit, Change Messages command that changes all three strings. Like File Demo, it will save and reload the document when the user chooses File, Save or File, Open.
Build an SDI application called MultiString just as you built File Demo. Add a member variable to the document, as before, so that the Attributes section of MultiStringDoc.h reads
// Attributes public: CMessages m_messages;
The next step is to write 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 work with this class, you will see how to implement the preceding five steps for creating a persistent class.
To create the CMessages class, first choose Insert, New Class. Change the class type to generic class and name it CMessages. In the area at the bottom of the screen, enter CObject as the base class name and leave the As column set to public, as shown in Figure 7.3.
FIG. 7.3 Create a new class to hold the messages.
This will create two files: messages.h for the header and messages.cpp for the code. It also adds some very simple code to these files for you. (You may get a warning about not being able to find the header file for CObject: just click OK and ignore it because CObject, like all MFC files, is available to you without including extra headers.)
Switch back to Multistringdoc.h and add this line before the class definition:
#include "Messages.h"
This will ensure the compiler knows about the CMessages class when it compiles the document class. You can build the project now if you want to be sure you haven't forgotten anything. Now switch back to Messages.h and add these lines:
DECLARE_SERIAL(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);
The DECLARE_SERIAL() macro provides the additional function and member variable declarations needed to implement object persistence.
Next come the class's data members, which are three objects of the CString class. Notice that they are protected member variables. The public member functions are next. SetMessage(), whose arguments are the index of the string to set and the string's new value, changes 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 7.6 shows the code for each of these new member functions. Add it to messages.cpp.
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; } SetModifiedFlag(); } 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; }
}
There's nothing tricky about the SetMessage() and GetMessage() functions, which perform their assigned tasks precisely. 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 because 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.
Towards the top of messages.cpp, after the include statements, add this line:
IMPLEMENT_SERIAL(CMessages, CObject, 0)
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.
Now that CMessages is defined and implemented, member functions of the MultiString document and view classes can work with it. First, expand CMultiStringDoc and double-click OnNewDocument() to edit it. Add these lines in place of the TODO comments.
m_messages.SetMessage(1, "Default Message 1"); m_messages.SetMessage(2, "Default Message 2"); m_messages.SetMessage(3, "Default Message 3");
Because the document class can't directly access the data object's protected data members, it initializes each string by calling the CMessages class's SetMessage() member function.
Expand CMultiStringView and double-click OnDraw() to edit it. Here's how it should look when you're finished:
void CMultiStringView::OnDraw(CDC* pDC) { CMultiStringDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); pDC->TextOut(20, 20, pDoc->m_messages.GetMessage(1)); pDC->TextOut(20, 40, pDoc->m_messages.GetMessage(2)); pDC->TextOut(20, 60, pDoc->m_messages.GetMessage(3)); }
As you did for File Demo, add a "Change Messages" item to the Edit menu. Connect it to a view function called OnEditChangemessages. This function will change the data by calling the CMessages object's member functions, as in Listing 7.7. The view class's OnDraw() function also calls the GetMessage() member function to access the CMessages class's strings.
void CMultiStringView::OnEditChangemessages() { CMultiStringDoc* pDoc = GetDocument(); CTime now = CTime::GetCurrentTime(); CString changetime = now.Format("Changed at %B %d %H:%M:%S"); pDoc->m_messages.SetMessage(1, CString("String 1 ") + changetime); pDoc->m_messages.SetMessage(2, CString("String 2 ") + changetime); pDoc->m_messages.SetMessage(3, CString("String 3 ") + changetime); pDoc->SetModifiedFlag(); Invalidate();
}
All that remains is to write the document class's Serialize() function, where the m_messages data object is serialized out to disk. You just delegate the work to the data object's own Serialize() function, as in Listing 7.8.
void CMultiStringDoc::Serialize(CArchive& ar) { m_messages.Serialize(ar); if (ar.IsStoring()) { } else { }
}
As you can see, after serializing the m_messages data object, not much is 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. Build MultiString now and test it as you tested File Demo. It should do everything you expect.
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 nonsequentially, 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're handled by non-Windows programmers: creating, reading, and writing files directly. Even when you need to dig down to this level of file handling, MFC offers help. Specifically, you can use the CFile class and its derived classes to handle files directly.
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 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 that 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 7.1 shows the CFile class's member functions and their descriptions.
Function | Description |
CFile | Creates the CFile object. If passed a filename, it opens the file. |
Destructor | Cleans up a CFile object that's going out of scope. If the file is open, it closes that file. |
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 Table 7.1, the CFile class offers plenty of file-handling power. This section 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.
Here's a sample snippet of code that creates and opens a file, writes a string to it, and then gathers some information about the file:
// Create the file. CFile file("TESTFILE.TXT", CFile::modeCreate | CFile::modeWrite); // Write data to the file. CString message("Hello file!"); int length = message.GetLength(); file.Write((LPCTSTR)message, length); // Obtain information about the file. CString filePath = file.GetFilePath(); Int fileLength = file.GetLength();
Notice that you don't have to explicitly open the file when you pass a filename to the constructor, whose 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 in the little snippet above. 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 described in Table 7.2.
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, doesn't 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 |
CFile::Write() takes 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.
One other thing about the code snippet: There is no call to Close()--the CFile destructor closes the file automatically when file goes out of scope.
Reading from a file isn't much different from writing to one:
// 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; CString message = s;
This time the file is opened by the CFile::modeRead flag, which opens the file for read operations only, after which the code 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 buffer's address 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. By 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 a CString variable.
The code snippets you've just seen use a hard-coded filename. To get filenames from your user with little effort, be sure to look up the MFC class CFileDialog in the online help. It's simple to use and adds a very nice touch to your programs.
Although you can use CFile objects to read from and write to files, you can also go a step farther and create your own CArchive object and use it 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, create a CFile object and pass it 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, for example, calling Serialize() yourself and passing the archive to it. Because you created the archive 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 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() because both CArchive and CFile have Close() calls in the destructor.
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 by using a centralized system Registry. Although the Registry makes sharing information between processes easier, it can make things more confusing for programmers. In this section, you uncover some of the mysteries of the Registry and learn how to manage it in your applications.
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 by using the Registry Editor or special API function calls created specifically 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 7.4 shows how the Registry appears when you first run the Registry Editor. (On Windows 95, you can find the Registry Editor, REGEDIT.EXE, in your main Windows folder, or you can run it from the Start menu by choosing Run, typing regedit, and then clicking OK. Under Windows NT, it's REGEDT32.EXE.)
The far left 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 themselves can 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 far right window. In Figure 7.5, 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'll see the desktop schemes installed on your system.
FIG. 7.4 The Registry Editor displays the Registry.
FIG. 7.5 The Registry is structured as a tree containing a huge amount of information.
To know where things are stored in the Registry, you need to know about the predefined keys and what they mean. From Figure 7.4, 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 7.6).
FIG. 7.6 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. (You may not always see this key on your system.)
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 it. As you may imagine, the Win32 API features many functions for manipulating the Registry. If you're going to use those functions, you had better know what you're doing! Invalid Registry settings can crash your machine, make it unbootable, and perhaps force you to reinstall Windows to recover.
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 call 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.)
NOTE: 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.
In this chapter, you've already built applications that used the Registry. Here's an excerpt from CMultiStringApp::InitInstance()--this code was generated by AppWizard and is also in CFileDemoApp::InitInstance().
// 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 open files with an application. Figure 7.7 shows the Registry Editor displaying the key that stores this information, HKEY_CURRENT_USER\Software\Local AppWizard-Generated Applications\MultiString\Recent File List. In the foreground, MultiString's File menu shows the single entry in the MRU list.
FIG. 7.7 The most recently used files list is stored in the Registry automatically.
© Copyright, Macmillan Computer Publishing. All rights reserved.