C++ Q & A -- Microsoft Systems Journal August 1999

来源:互联网 发布:bhuztez 知乎 编辑:程序博客网 时间:2024/04/28 10:44
August 1999

 

Microsoft Systems Journal Homepage
C++ Q&A

 

 

Code for this article: Aug99CQA.exe (26KB)Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.

 

 

Q I have a problem: my MFC application has two CDocument classes. The MRU file list shows all the document files in the same menu. How can I separate the file names into two groups? For example, one for projects and one for documents, like in Visual C++®.
Juan José Núñez

I read your May 1998 answer to a question about adding menus to the toolbar. I also downloaded the source code. I see where you added the list of MRU files to the dropdown menu, but I am not able to find the code for adding the files to the menu. Can you please tell me who is adding file names to the menu?

Mahendra Bhagat

A To make the recent file list appear in a menu, all you have to do is add the special command ID ID_FILE_MRU_ FILE1 to your menu. MFC then magically replaces this menu item with the names of the files the user most recently opened, as shown in Figure 1. MFC even stores the names of the files in your application profile (.INI file or system registry), so your app "remembers" them from one session to the next. This is wonderful, but how does it work? And how can you have more than one MRU list? To answer these questions, I have to take you spelunking. I'll start by asking Mahendra's question: where does MFC convert the menu item ID_ FILE_MRU_FILE1 into a list of recent files?
Figure 1 Most Recently Used FilesFigure 1 Most Recently Used Files

 

Updating menus is a WM_INITMENU thing, so your natural instinct would be to look for a UI update handler, the MFC object-oriented transmutation of WM_INITMENU. Indeed, if you look in appui.cpp you'll discover the following CWinApp message map entry:
 ON_UPDATE_COMMAND_UI(ID_FILE_MRU_FILE1, OnUpdateRecentFileMenu)
When the user invokes a menu, Windows® sends your app a WM_INITMENU message. MFC routes this through your app as a CN_UPDATE_COMMAND_UI event. More specifically, MFC calls the ON_UPDATE_COMMAND_UI handlers for all the items in the menu, giving each one a CCmdUI object that identifies the UI object, in this case a menu item. (In other contexts it could be a button, status bar pane, or any UI object you like.) The handler for ID_ FILE_MRU_FILE1 is CWinApp::OnUpdateRecentFileMenu, so control passes here.
voidCWinApp::OnUpdateRecentFileMenu(CCmdUI* pCmdUI){    if (m_pRecentFileList == NULL)        pCmdUI->Enable(FALSE);    else m_pRecentFileList->UpdateMenu(pCmdUI);}
If there is a recent file list, MFC will call its UpdateMenu function; otherwise, MFC will disable the menu item. But what is m_pRecentFileList anyway? It is a pointer to an object of class—what else?—CRecentFileList. This is the class that implements the MRU list of file names. Figure 2 shows the declaration.
CRecentFileList has functions to add and remove file names, as well as read and write the list to the profile. As you may have guessed, CRecentFileList::UpdateMenu is the magic method that adds the file names to the menu. It's rather long and gnarly, but straightforward, so I won't bore you with the details. UpdateMenu first deletes any file names that appear in the menu, then adds the file names currently in the list. It replaces the single menu item ID_ FILE_MRU_FILE1 with a sequence of menu items ID_ FILE_MRU_FILE1, ID_FILE_MRU_FILE2, and so on for as many file names as there are in the list, up to some maximum you can set (default is four). MFC generates the recent section of the menu afresh every time, and only needs one UI update handler (for the first menu item) to update all the recent file names.
OK, that answers the question of who adds the file names to the menu. (Answer: CRecentFileList::UpdateMenu.) But to make two MRU file lists, there are so many other questions that must be answered: who creates the recent file list? How is it maintained? Can you get it to use some other range of IDs? For the answers, keep reading.
Let's start at the beginning: who creates the recent file list? Surprise!—it's you. I bet you didn't even know it, did you? It's true. The standard AppWizard-generated app contains the following line in your app's InitInstance function:
LoadStdProfileSettings();
This function loads the standard settings from your app's profile, which among other things includes the recent file list. LoadStdProfileSettings even takes an argument: the maximum number of file names. For example, my sample program's InitInstance function has the line
 LoadStdProfileSettings(NMAXMRU);
where NMAXMRU is 8. So my recent file list has up to eight entries. LoadStdProfileSettings is a CWinApp function that creates the recent file list and initializes it by reading from the registry.
// in CWinApp::LoadStdProfileSettings(UINT nMaxMRU)  m_pRecentFileList = new  CRecentFileList(0, szFileSection,                  szFileEntry, nMaxMRU);m_pRecentFileList->ReadList();
szFileSection and szFileEntry describe where the recent file information is stored in the registry. Figure 3 explains it.
Figure 3 An MRU and the RegistryFigure 3 An MRU and the Registry

 

But how do the file names get in the registry in the first place? When the user selects File|Exit or otherwise says bye-bye, control eventually passes to CWinApp::ExitInstance, which does its cleanup stuff. One of the things ExitInstance does is call another function, CWinApp:: SaveStdProfileSettings, which (as you probably guessed) saves the standard profile settings. It calls m_pRecentFileList->WriteList to save the recent file names in the registry. The destructor, CWinApp::~CWinApp, destroys the file list.
So far, so good. You know how the recent file list gets created, loaded, saved, and destroyed, and you know how CWinApp::OnUpdateRecentFileMenu calls CRecentFileList::UpdateMenu to create the menu. All that remains is to figure out how file names get added to the list, and what happens when the user selects one from the menu.
Whenever the user opens a new document, control goes through one of the MFC open-document functions such as CMultiDocTemplate::OpenDocumentFile or CSingleDocTemplate::OpenDocumentFile, both of which will eventually call the function CDocument:: SetPathName to store the fully qualified path in the CDocument object. SetPathName has two arguments:
  void SetPathName(LPCTSTR lpszPathName,                   BOOL bAddToMRU = TRUE);
The second arg is a flag that tells MFC whether to add the name to the MRU list. If it's TRUE, MFC adds the path name to the recent file list.
  // in CDocument::SetPathName  if (bAddToMRU)      AfxGetApp()-> AddToRecentFileList(m_strPathName);
The only place I could find where MFC calls SetPathName with bAddToMRU = FALSE is COleLinkingDoc::XPersistFile:: SaveCompleted, which is to say some totally obscure piece of OLE code you can safely ignore. For all practical purposes, bAddToMRU is always TRUE. Moreover, SetPathName is the only function that calls AddToRecentFileList, so in effect SetPathName is the place where file names get added to the recent file list. CWinApp::AddToRecentFileList is the function that does it.
// CWinApp::AddToRecentFileListm_pRecentFileList->Add(lpszPathName);
This is pretty brainless: AddToRecentFileList calls CRecentFileList::Add. Now the next time the user invokes the menu, the new file name appears at the top of the list.
How does MFC open a recent file when the user selects one from the menu? Through the usual means. CWinApp has a command handler for IDs in the special range ID_ FILE_MRU_FILE1 through ID_FILE_MRU_FILE16:
// CWinAppON_COMMAND_EX_RANGE(ID_FILE_MRU_FILE1,    ID_FILE_MRU_FILE16, OnOpenRecentFile)
When the user selects one of the MRU menu items, control passes to CWinApp::OnOpenRecentFile, which tries to open the file by calling OpenDocumentFile with the name of the document.
// in CWinApp::OnOpenRecentFile(UINT nID)int nIndex = nID - ID_FILE_MRU_FILE1;if (OpenDocumentFile((*m_pRecentFileList)[nIndex]) ==                     NULL)    m_pRecentFileList->Remove(nIndex);
When you use the RANGE version of ON_COMMAND, MFC passes the actual command ID as an argument—else how do you know which command was invoked? OpenDocumentFile converts the command ID to an index, then tries to open the file at that index in its list. If it works, fine; if not, OpenDocumentFile displays an error message (typically the file name is no longer valid—it got deleted or something) and OnOpenRecentFile removes the file name from the MRU list.
Whew! That, in a nutshell, is how MFC does the recent file menu. Armed with all this information, how can you implement two recent file menus, as in the files/projects example?
The first thing to do in all such situations is invent a class. Any time you implement some new behavior or feature, you want to keep all the functionality in one place instead of spread all over your app. This is what OOers call encapsulation. So I invented CMruFileManager to manage multiple recent file lists, and a new version of CRecentFileList, CRecentFileList2, to go with it. I also wrote a sample app, a text editor called MyEdit that uses CMruFileManager to implement three recent file lists: one ordinary MFC-style recent file list, and two special lists for .h and .cpp files, each with its own submenu in the top-level menu (see Figure 4).
Figure 4 Multiple File Lists and SubmenusFigure 4 Multiple File Lists and Submenus

 

To see how it works, let's begin with MyEdit.h (shown in Figure 5), where my app gets a CMruFileManager object.
class CMyApp : public CWinApp {    CMruFileManager m_mruFileMgr;};
To create the recent file lists, CMyApp::InitInstance calls CMruFileManager::Add twice. Here's the first call (from MyEdit.cpp in Figure 5):
m_mruFileMgr.Add(ID_MY_RECENT_CFILE1,    _T("Recent .cpp Files"),    _T("File%d"),    CFileFunc,    NMAXMRU);
This creates a recent file list containing up to NMAXMRU file names, using "Recent .cpp Files" as the registry key and "File%d" as the format (see Figure 6). ID_MY_RECENT_ CFILE1 is a resource ID; there are consecutive IDs, ID_ MY_RECENT_CFILE, ID_MY_RECENT_CFILE2, and so on, up to ID_MY_RECENT_CFILE8. CMruFileManager uses this range for the menu's command IDs.
Figure 6 Recent .cpp FilesFigure 6 Recent .cpp Files

 

Finally, CFileFunc is a static "filter" function that's used to determine which file names belong to this list.
// in MyEdit.cppstatic BOOL CALLBACK CFileFunc(LPCTSTR lpszPathName){    return CompareFilenameSuffix(lpszPathName,                                 _T(".cpp"));}
CompareFilenameSuffix tests whether the string lpszPathName ends in a particular suffix, such as .cpp.
Just like LoadStdProfileSettings, CMruFileManager:: Add creates a CRecentFileList2 and adds it to its own internal list, m_listRfl. CRecentFileList2 is derived from CRecentFileList, and adds a couple of new members: m_ nBaseID holds the base menu ID (for example ID_MY_RECENT_CFILE1) and m_pMruFn holds the filter function (such as CFileFunc). All the MFC code is hardwired with ID_FILE_MRU_FILEx as the command ID; CRecentFile2 lets you specify a different range of IDs. Like CWinApp:: LoadStdProfileSettings, CMruFileManager::Add calls CRecentFileList::ReadList after creating the list, to read the file names from the registry. But now each MRU list must have a different registry key (as in Figure 6), which you supply when you call CMruFileManager::Add.
I said there were two calls to CMruFileManager::Add in CMyApp::InitInstance; the second looks just like the first, only it uses a different group of IDs, registry key names, and a filter function for .h files instead of .cpp files (see Figure 5).
Once you instantiate the CMruFileManager in your app object and call CMruFileManager::Add to add the lists you want, there are two other places you have to hook up the MRU file manager. First, you have to call the MRU file manager's OnCmdMsg function from your app's OnCmdMsg function.
BOOL CMyApp::OnCmdMsg(...){    if (m_mruFileMgr.OnCmdMsg(...))        return TRUE;        return CWinApp::OnCmdMsg(...);}
This joins the MRU menu manager to the MFC message routing infobahn. For more information about how this works, read my article, "Meandering Through the Maze of MFC Message and Command Routing" (MSJ, July 1995), which is still relevant after four years—amazing! Once you hook up the MRU file manager, it receives ON_COMMAND and ON_UPDATE_COMMAND_UI messages. This lets the CMruFileManager handle its own commands, so your app doesn't have to.
The only other place you have to modify your app is in CWinApp::AddToRecentFileList, like so:
void CMyApp::AddToRecentFileList(LPCTSTR lpszPathName){    if (m_mruFileMgr.AddToRecentFileList(lpszPathName))        return;    CWinApp::AddToRecentFileList(lpszPathName);}
Remember from my earlier discussion that CWinApp:: AddToRecentFileList is the function that adds a file name to the recent file list. But now that there are several lists, how do you tell the file manager which one to use? This is where the filter function comes in. CMruFileManager::AddToRecentFileList loops through all its lists, calling a new function, CRecentFileList2::IsMyKindOfFile, to see if the file name belongs in that list.
BOOL CMruFileManager::AddToRecentFileList(    LPCTSTR lpszPathName){    for (prfl = /*each         CRecentFileList2*/) {        if (prfl-> IsMyKindOfFile(lpszPathName)) {            prfl->Add(szTemp);            return TRUE;        }    }    return FALSE;}
IsMyKindOfFile calls the filter function you supplied when you created the recent file list, passing the file name as argument. Your function can do anything it wants. In MyEdit, the filter function compares the file name suffix to .h or .cpp. Whatever. If the filter function returns TRUE, CMruFileManager adds the name to the list. If no filter function returns TRUE, CMruFileManager::AddToRecentFileList returns FALSE. In this case, CMyApp::AddToRecentFileList passes control to the base class CWinApp:: AddToRecentFileList, which adds the file name to the default MFC recent file list, m_pRecentFileList. You can omit this call if you don't want to use the MFC list; I only did it to show how it can be done.
You've probably had about all you can take on recent file menus, but still I must digress to point out another approach. Since CDocument::SetPathName is the place that calls AddToRecentFileList, you could override CDocument:: SetPathName (which is virtual) for each of your document classes, and have it add the file name to a different recent file list. I implemented CMruFileManager::Add so it returns a DWORD that identifies the list. You could implement a new function CMruFileManager::AddToRecentFileList(LPCTSTR, DWORD) that adds the file name to a specific list instead of using callbacks and IsMyKindOfFile.
This might be more elegant, since MFC already knows how to tell from the file name what kind of document to open; you'd just be piggybacking on that information. But for my sample app, I didn't want to bother implementing separate doc types for .h, .cpp, and others. My way requires writing less code, and good programmers are lazy. But if you've already implemented multiple doc types in your app, the lazy way might be to use SetPathName. It's your choice.
In case you're totally lost (which is a good sign), let me quickly review. I've created a CMruFileManager class that holds a list of CRecentFileList2 objects. A single CMruFileManager object is instantiated in the app and hooked up to the command routing system via OnCmdMsg. The app passes AddToRecentFileList to the CMruFileManager, where each CRecentFileList2 uses a callback function to determine whether a particular file name belongs in its list. Very good. Now let's look inside CMruFileManager to see how it works.
The first thing to notice is that CMruFileManager has its own message map to handle ON_COMMAND and ON_ COMMAND_UPDATE_UI. CMruFileManager uses the RANGE version of these macros:
ON_COMMAND_EX_RANGE(0, 0xFFFF, OnOpenRecentFile)
Thus CMruFileManager::OnOpenRecentFile gets control for any command in the range 0 to 0xFFFF, which is to say all commands.
When it gets control, OnOpenRecentFile looks through each CRecentFileList2 to see whether the actual command ID is in the range for that list. If so, it calls the CWinApp:: OpenDocumentFile to open the file. The MFC function that does this, CWinApp::OnOpenRecentFile, has ID_FILE_ MRU_FILEx hardcoded throughout, whereas my CRecentFileList2 objects each use a different base command ID, so it's possible to have multiple MRU lists. In fact, that's the main reason for CRecentFileList2.
To update the menu—that is, convert the base ID into a group of menu items with the recent file names and consecutive IDs starting at the base ID—CMruFileManager uses a similar trick. It has an ON_UPDATE_COMMAND_ UI_RANGE entry to trap all command updates (ID 0 to 0xFFFF), with a handler function OnUpdateRecentFileMenu that checks for IDs that fall within the range of some recent file list.
void CMruFileManager::OnUpdateRecentFileMenu(    CCmdUI* pCmdUI){    CRecentFileList2* prfl = FindRFL(pCmdUI->m_nID);    if (prfl) {        pCmdUI->Enable(prfl->GetSize()>0);        prfl->UpdateMenu(pCmdUI);    } else {      pCmdUI->ContinueRouting();    }}
UpdateMenu is the same MFC function I showed you before; ContinueRouting is a special CCmdUI trick you may or may not know about. It tells MFC to keep routing the CN_UPDATE_COMMAND_UI event. ContinueRouting is required because you don't want CMruFileManager::OnUpdateRecentFileMenu to gobble all IDs, only the ones destined for one of its recent file lists.
Well, believe it or not, I have now explained more or less everything. Really! There's just one minor glitch. It turns out there's a little buggy-boo in the MFC CRecentFileList::UpdateMenu. This function assumes the recent file menu items live in the top-level menu (see Figure 1), not a submenu, as in Figure 4. UpdateMenu uses pCmdUI->m_ pMenu for the menu, but if the item is in a submenu, it should use pCmdUI->m_pSubMenu. Tsk, tsk. Well, not to worry—since this function is virtual, you can fix it. CRecentFileList2::UpdateMenu does so by faking out MFC; if the menu is a submenu, it sets m_pMenu in the CCmdUI object to point to the submenu before calling the base class CRecentFileList::UpdateMenu, then restores it again. Figure 7 shows the final MRU file manager class. Ciao, babe—I'm outta here.

 

Have a question about programming in C or C++? Send it to Paul DiLascia at askpd@pobox.com

 

From the August 1999 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.