INPRISE Online And ZD Journals Present:
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. 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.
Figure A shows a typical list.
Figure A: A typical MRU list looks like this
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:
- A way to show the MRU list when it's needed and hide it when it isn't
- A way to detect when an MRU menu item is clicked
- An event handler that performs some action when an MRU item is clicked
- A routine to manage the list (adding new items and deleting old items)
- A place to store the filename strings while the program is running
- A 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 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.zdjournals.com/cpb; click the Source Code hyperlink to reach the nov97.zip file.) 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 (vcl\Classes.hpp)
#include (vcl\Controls.hpp)
#include (vcl\StdCtrls.hpp)
#include (vcl\Forms.hpp)
#include (vcl\Menus.hpp)
#include (vcl\Dialogs.hpp)
#include (vcl\Registry.hpp)
//----------------------------------------------------
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;i
if (MruList->Strings[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);
}
}
//-------------------------------------------------------------
Kent Reisdorph is a senior software engineer at TurboPower Software and a member of TeamB, Borland's volunteer online support group. He's the author of Teach Yourself C++Builder in 21 Days and Teach Yourself C++Builder in 14 Days. You can contact Kent at kentr@turbopower.com.
Back to Top
Back to 1997 Index of Articles
Copyright © 1998, Ziff-Davis Inc. All rights reserved. ZD Journals and the ZD Journals logo are trademarks of Ziff-Davis Inc. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis is prohibited.