November 1997

Implementing a recent-files list

by Kent Reisdorph

Most programs that use document files maintain a list of most-recently-used files. This list, often called an MRU list, is dynamically created by the application as the user works with document files.

VCL doesn’t provide built-in support for an MRU list. So, in order to implement an MRU list in your C++Builder applications, you’ll have to do it yourself. In this article, we’ll show you how to create and maintain such a list. You’ll use the Registry to store the list of files (see "Putting the Registry to Work," on page 8, for details on using the Registry). Along the way, we’ll introduce some of the features of the TMenu class.

A typical MRU list

While no standards committee governs the use of MRU lists, you’ll see certain common features in most MRU lists. Let’s take a quick look at the feature set for a typical list.

First, the list shouldn’t be visible when you run the application for the first time. In other words, if there are no recently used files, then the list should be hidden. As files are opened, they’ll appear in the list. Typically, an MRU list will have a maximum number of entries--generally five to ten. The list should be maintained on a first-in, first-out (FIFO) basis so the last file used is always at the top of the list. Once the list grows to the maximum size, the oldest file is removed to make room for the newest file.

Typically, the MRU list shows the complete path and filename of the document file. The list usually associates a hotkey with each menu item. For example, the hotkey for the first item is usually 1, the hotkey for the second item is 2, and so on. The MRU list should appear on the File menu, just above the Exit menu item. 

Not every application uses this exact approach, but such a layout seems to be the most widely used. (Note that C++Builder uses a different scheme; it implements the MRU list on a pop-up menu called Reopen. I don’t particularly like this non-standard approach, and I wouldn’t write my own applications using this method.) Some applications place the MRU list at the very end of the File menu, which is a slight variation. In any case, the decision of where to put the list is up to you. Now that you know how the MRU list should look and act, you can get to work on the design stage.

Designing the MRU list

There are many ways to approach the creation of an MRU list. First, let’s decide what ingredients we need. At a minimum, you’ll require the following:
bulletA way to show the MRU list when it’s needed and hide it when it isn’t
bulletA way to detect when an MRU menu item is clicked
bulletAn event handler that performs some action when an MRU item is clicked
bulletA routine to manage the list (adding new items and deleting old items)
bulletA place to store the filename strings while the program is running
bulletA more persistent place to store the filename strings between application instances
VCL easily handles the first three items via the TMenu class and the use of events. For string maintenance, you’ll use a TStringList to store the list of filenames at runtime. Finally, you’ll use the Registry for persistent storage of your MRU list. Let’s break this process down into three pieces so it makes more sense.

Managing the menu

You can insert the items into the File menu in one of several ways. You could use TMenuItem’s Insert method to insert the menu items into the File menu as needed. You could also use the API menu functions. There’s an easier way, however--you’ll create a menu separator and as many blank menu items as needed for your MRU list. Then you’ll just hide the new menu items until you’re ready to display them. You can set the Visible property of the menu items and separator to False at design time, then set it to True at runtime when you need to display them.

Begin by dropping a MainMenu component on a blank form. Then, double-click the MainMenu icon to start the C++Builder Menu Designer. To quickly insert a pre-built File menu into the main menu, use the Insert From Template option. Now, create a separator just above the Exit item separator (type a dash in the menu item’s Caption property and press [Enter]) and change its Name property to MRUSeparator. Create five menu items under the separator and name them MRU1 through MRU5. You can leave the Caption property blank, since the menu item text will be supplied at runtime. Select all five blank menu items and set the OnClick event handler to MRUClick--doing so will allow all the menu items to use the same OnClick event handler. Set the Visible property for the separator and the blank menu items to False.

Now, close the Menu Designer and test the menu. It should look like a regular File menu, since the MRU list is initially hidden. But at what point do you set the Visible property to True? Once again, it’s VCL to the rescue. You can create an OnClick handler for the top-level File menu. This OnClick event will give you a chance to modify the menu before it’s displayed--a perfect time to show the necessary MRU menu items. You can’t set up the event handler for the MRU click events at this point, because you haven’t yet determined how you’re going to store the filenames. Let’s take a look at that issue now, then return later to the OnClick event handler.

Storing the list of files

You could use the Tag property of each menu item to store the filenames--an idea that has some merit, but also some drawbacks. Instead, you’ll store the filenames in a separate TStringList object, which will be a member of your main form’s class.

As an added benefit, the TStringList gives you a convenient way to implement the FIFO aspect of the MRU list. When the user opens a new file, you’ll add the filename to the top of the list and delete the last item if necessary. For example,

const int maxItems = 5;
...
if (OpenDialog1->Execute()) {
// Open a file, etc.
// Add the filename to the MRU list. 
MruList->Insert(0, OpenDialog1->FileName);
// Remove the last item if the list is full.
if (MruList->Count == maxItems + 1)
MruList->Delete(maxItems);
}
is all the code required to maintain a FIFO list of filenames--slick and easy. Naturally, you’ll create your string list in the form’s OnCreate event handler and delete it in the OnDestroy event handler.

The OnClick event handler

Now that you know how to store the file list, let’s back up and address the issue of the OnClick event handler for the MRU menu items. You need to translate the menu item clicked to an item in the string list. Since you know that the first MRU item is named MRU1, you can get the menu index of that menu item and figure out from there which MRU item was clicked, as follows:
void __fastcall 
TForm1::MRUClick(TObject *Sender)
{
// cast Sender to a TMenuItem*
TMenuItem* itemClicked = 
dynamic_cast(Sender);
// cCalculate 0-based index from there
int index = 
itemClicked->MenuIndex - MRU1->MenuIndex;
// MruList[index] is the string we’re after
String FileName = MruList[index];
// do something with FileName
}
You can use similar logic to determine which of the MRU menu items to make visible. See Listing A for details.

Storing the strings in the Registry

You’ll load the string list from the Registry when the form is created and write the string list to the Registry again when the form is destroyed. See Listing B on the following page for the code to save and restore the MRU list. Using the Registry to store the strings provides a near-foolproof method of keeping the MRU list around after the application has closed.

An example

Listings A and B contain a program that implements an MRU list. (You can download our sample files from www.cobb.com/cpb; click the Source Code hyperlink.) The program contains a MainMenu component, a Memo component, an OpenDialog component, and a CheckBox component (the check box deletes the Registry key created by the program). The first time you run the program, the MRU list will be empty and hidden. You can use the File | Open... menu item to open any text file, which will be displayed in the Memo component.

As you open each file, its filename will be added to the MRU list. Close and restart the program, and you’ll see that the MRU list is persistent between application instances. When you click on one of the MRU items, the file corresponding to that menu item will then be loaded in the Memo component.

Listing A: MRUUNIT.H

//----------------------------------------------------
#ifndef MruUnitH
#define MruUnitH
//----------------------------------------------------
#include 
#include 
#include 
#include 
#include 
#include 
#include 
//----------------------------------------------------
class TForm1 : public TForm
{
__published:   // IDE-managed Components
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *New1;
TMenuItem *Open1;
TMenuItem *Save1;
TMenuItem *SaveAs1;
TMenuItem *N2;
TMenuItem *Print1;
TMenuItem *PrintSetup1;
TMenuItem *N1;
TMenuItem *Exit1;
TMenuItem *MRUSeparator;
TMenuItem *MRU1;
TMenuItem *MRU2;
TMenuItem *MRU3;
TMenuItem *MRU4;
TMenuItem *MRU5;
TMemo *Memo1;
TOpenDialog *OpenDialog1;
TCheckBox *CheckBox1;
void __fastcall MRUClick(TObject *Sender);
void __fastcall File1Click(TObject *Sender);
void __fastcall FormCreate(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
void __fastcall Open1Click(TObject *Sender);
private:	// User declarations
TStringList* MruList;
public:   // User declarations
__fastcall TForm1(TComponent* Owner);
};
//----------------------------------------------------
extern TForm1 *Form1;
//----------------------------------------------------
#endif
Listing B: MRUUNIT.CPP
//----------------------------------------------------
#include 
#pragma hdrstop

#include "MruUnit.h"
//----------------------------------------------------
#pragma resource "*.dfm"
const int MruCount = 5;
const char* RegKey = "Software\\BCBJournal\MruTestProgram";
TForm1 *Form1;
//----------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//----------------------------------------------------
void __fastcall TForm1::MRUClick(TObject *Sender)
{
// Load a file in the Memo component based on
// the MRU item that was clicked.
TMenuItem* itemClicked = 
dynamic_cast(Sender);
int index = 
itemClicked->MenuIndex - MRU1->MenuIndex;
Memo1->Lines->LoadFromFile(
MruList->Strings[index]);
}
//----------------------------------------------------
void __fastcall 
TForm1::File1Click(TObject *Sender)
{
// This is the OnClick handler for the top
// level File menu. Here we check each string 
// in the MRU list and show the associated MRU
// menu item if the string is not empty.
int index = MRU1->MenuIndex;
for (int i=0;iStrings[i] != "") {
MRUSeparator->Visible = true;
File1->Items[index + i]->Visible = true;
char buff[MAX_PATH];
sprintf(buff, "&%d %s",
i + 1, MruList->Strings[i].c_str());
File1->Items[index + i]->Caption = buff;
    }
  }
}
//----------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
// Create the TStringList class.
MruList = new TStringList;
// Load the strings from the registry. First
// try to open the key. If that fails, then we
// know that the program is being run for the
// first time and that we need to create the
// key and set up the MRU items in the key.
TRegistry* reg = new TRegistry;
if (!reg->OpenKey(RegKey, false)) {
reg->OpenKey(RegKey, true);
reg->WriteString("MRU1", "");
reg->WriteString("MRU2", "");
reg->WriteString("MRU3", "");
reg->WriteString("MRU4", "");
reg->WriteString("MRU5", "");
  }
// Read each string from the registry. Some of
// the strings could be empty, but that’s OK.
MruList->Add(reg->ReadString("MRU1"));
MruList->Add(reg->ReadString("MRU2"));
MruList->Add(reg->ReadString("MRU3"));
MruList->Add(reg->ReadString("MRU4"));
MruList->Add(reg->ReadString("MRU5"));
// Delete the TRegistry object.
delete reg;
}
//----------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
// Save the string list to the registry. If 
// the check box is checked, then delete the 
// key and don’t save the MRU list (duh).
TRegistry* reg = new TRegistry;
if (CheckBox1->Checked)
reg->DeleteKey(RegKey);
else {
reg->OpenKey(RegKey, true);
// Write the list to the registry.
reg->WriteString(
"MRU1", MruList->Strings[0]);
reg->WriteString(
"MRU2", MruList->Strings[1]);
reg->WriteString(
"MRU3", MruList->Strings[2]);
reg->WriteString(
"MRU4", MruList->Strings[3]);
reg->WriteString(
"MRU5", MruList->Strings[4]);
  }
// Clean up.
delete reg;
delete MruList;
}
//----------------------------------------------------
void __fastcall TForm1::Open1Click(TObject *Sender)
{
// Load the selected file into the Memo and 
// update the MRU list. Add the FileName to 
// the top of the string list and delete the 
// last item if the string list is full. We 
// don’t check for duplicate strings, but that 
// would be a good feature to implement.
if (OpenDialog1->Execute()) {
Memo1->Lines->LoadFromFile(
OpenDialog1->FileName);
MruList->Insert(0, OpenDialog1->FileName);
if (MruList->Count > MruCount)
MruList->Delete(MruCount);
  }
}
//----------------------------------------------------