MSDN Magazine > Issues and Downloads > 2000 > August >  C++ Q&A: Windows 2000 File Dialog Revisited; Au...
This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MSDN Magazine
C++ Q&A
Windows 2000 File Dialog Revisited; Autocompletion and the ACTest Demo App
Paul DiLascia
Code for this article: CQA0800.exe (88KB)
I
n my April 2000 C++ Q&A column, I answered a question from a reader who asked how to get the Windows® 2000-style File | Open dialog (the one with the Places bar) in his MFC app. I said essentially, you can't do it. I should have heeded one of my own favorite sayings, "In programming you can always do anything."
      To restate the problem, Windows 2000 has a new Open dialog (see Figure 1) with a Places bar on the left side. To invoke the dialog, you must call ::GetOpenFileName, passing it an OPENFILENAME struct. MFC wraps this in the class CFileDialog. The problem is that the most recent OPENFILENAME struct (version 5.0 and later, which means Windows 2000) has additional members that increase its size. Before you (or MFC) call ::GetOpenFileName, you must initialize the first member of OPENFILENAMEâ€"lStructSizeâ€"to sizeof(OPENFILENAME). If you compile for Windows 2000 (_WIN32_WINNT >= 0x0500), you'll get the new size; if you compile for earlier versions of Windows, you'll get the old size.
Figure 1 New Open Dialog
Figure 1 New Open Dialog

      When you run your app, you must have the old size when running on Windows 95, Windows 98, or Windows NT® 4.0. On Windows 2000, you can have either the old or new size. If lStructSize is the new size, Windows 2000 runs the new dialog. If it's the old size, Windows examines other information in OPENFILENAME: if the dialog has a hook function (OFN_ENABLEHOOK) or template (OFN_ENABLETEMPLATE), it runs the old dialog; otherwise it runs the new one. Very confusing. The problem is that whatever you do, MFC42.DLL was compiled with the old size, and MFC uses a hook proc (OFN_ENABLEHOOK) to connect the dialog to your message maps, so it would seem you're stuck.
      But you're not. Reader Michael Lemley sent me his solution to the problem, which works quite well. The essence of Michael's solution is to start with MFC's CFileDialog and add a new member, m_ofnEx, that's the new, larger OPENFILENAME, then fool MFC into using it. To make sure the code runs on all versions of Windows, lStructSize is initialized appropriately depending on which version of Windows is actually running. Michael's solution requires two things I hate doing (which no doubt explains why I repressed them): copying a bunch of MFC code and doing a runtime test for which OS version is running. The modifications are wrapped in a new class, CFileDialogEx.



// Windows 2000 version of OPENFILENAME
struct OPENFILENAMEEX : public OPENFILENAME { 
  void *        pvReserved;
  DWORD         dwReserved;
  DWORD         FlagsEx;
};

// Windows 2000 version of CFileDialog
class CFileDialogEx : public CFileDialog {
protected:
    OPENFILENAMEEX m_ofnEx; // new member
};
      OPENFILENAMEEX is a local definition for OPENFILENAME that mimics the one in commdlg.h, in case you're compiling with older header files or values of _WIN32_WINNT less than 0x500. OPENFILENAMEEX has the three new members. To use this new structure, CFileDialogEx overrides DoModal. You must copy the entire function from CFileDialog to CFileDialogEx, with modifications as per Figure 2.
      As you can see, the basic idea is to copy everything from m_ofn to m_ofnEx before calling GetOpenFileName (or GetSaveFileName), then copy everything back afterwards. The all-critical first member, lStructSize, is initialized according to which operating system is actually running. Figure 3 shows IsWin2000, the function that checks the OS version.
      To make handlers work properly, CFileDialogEx also overrides another virtual function, OnNotify. When something happens in the file dialog, Windows can send a WM_NOTIFY message with updated information in the OPENFILENAME struct. Since the handler functions and any other MFC code expect the information to show up in m_ofn, not m_ofnEx, OnNotify intercepts the notification and performs another copy.


BOOL CFileDialogEx::OnNotify(...)
{
    memcpy(&m_ofn, &m_ofnEx, sizeof(m_ofn));
    m_ofn.lStructSize = sizeof(m_ofn);
    return CFileDialog::OnNotify( wParam, lParam, pResult);
}
       Figure 4 shows the final CFileDialogEx. Once you have it working, you'll discover one more problem: how do you get MFC to use it? If you spelunk through MFC, you'll discover MFC invokes the file dialog in a function called CDocManager::DoPromptFileName. Fortunately, this function is virtual, so you can override it, but that requires deriving a new document manager CDocManagerEx and installing it in your application. Figure 5 shows how I wrote CDocManagerEx; to install it you have to add just one line to your app's InitInstance function:


// Use extended CDocManager.
// Add before you create any doc templates!
m_pDocManager = new CDocManagerEx;
      DocMgrEx.cpp has a couple of functions copied from MFC, with a new DoPromptFileName override that creates a CFileDialogEx instead of a CFileDialog. Actually, CDocManagerEx calls a new virtual function, OnCreateFileDialog, to create the dialog, fixing the original problem created by CDocManager; namely, that there's no virtual function you can override to create a different kind of dialog. Pretty clever.
Q
How can I use the IAutoComplete interface in my Edit boxes? I'd like to use a wrapper class for doing this, if possible.
Cassio Goldschmidt

A
Well, call me Mr. Encapsulation. You've come to the right place. But let me say right now that the solution I'll show you isn't what youâ€"or even Iâ€"expected!
      Just so we all know what autocomplete is, you may have noticed that when you type something in the Address Bar in Microsoft® Internet Explorer, a combobox drops down showing URLs that match the characters you've typed so far, with the first match highlighted so all you have to do is press Enter to choose it (see Figure 6). The same thing happens in the File | Open dialog and other places in Windows. Autocompletion is a great thing, and now Windows 2000 has caught up to 1970s technology, only 30 years late!
Figure 6 Autocomplete
Figure 6 Autocomplete

      When I first saw your question, I had never heard of IAutoComplete (see Figure 7)â€"do you think I pay attention to every new COM interface that's released from Redmond?â€"but it seemed like a good idea to me. Figure 8 describes the options available with IAutoComplete. IAutoComplete works with IEnumString, a general-purpose interface for enumerating a list of strings. You give the IAutoComplete object a pointer to your string enumerator and a window handle to an edit control or combobox, and it does the rest. If you want to set fancy options, there's IAutoComplete2. No COM interface is ever complete without a number 2 version, even if it only has two methods.
      There's just one problem with IAutoComplete. It only exists in Windows 2000. Specifically, the COM object that implements IAutoComplete (CLSID_IAutoComplete) lives in version 5.0 of shell32.dll, which ships with Windows 2000, not Windows 95, Windows 98, or Windows NT 4.0. Assuming that's OK with you, the first thing you have to do to use IAutoComplete is implement IEnumString, which I set out to do. While I was in the trenches fretting over QueryInterface, AddRef, and Release (how many times have I done this before?), worrying about CLSIDs, CoInitialize, and whether to set m_dwRef to 0 or 1 in my constructor, I thought there has to be a better way. If I'm going through all this pain to encapsulate IAutoComplete, and the class I end up with will only work on Windows 2000, what about all those poor bozosâ€"myself includedâ€"who still run Windows 98 or Windows NT 4.0?
      Then it hit me: what am I doing? All we're talking about here is searching a list of strings for ones that match what the user typed. How hard could it be to write the code myself? One of the problems with modern programming is that no one wants to write code any more. Don't get me wrongâ€"COM is great. But unless you already have an IEnumString handy, it seems way too much bother for autocompletion.
       Figure 9 shows a C++ class I wrote, CAutoComplete, that implements autocompletion from scratch. No COM, no shell32.dll. Just a simple class and a .cpp file you can add to your favorite app, DLL, or extension library. It works in any version of Windows, maybe even Windows 3.1, for all I know.
      CAutoComplete doesn't implement all the features IAutoComplete does. For example, IAutoComplete has a quick complete format string it uses when the user presses Ctrl+Enter. The format string is a sprintf string that Windows uses to translate the user's input. If the format string is "http://www.%s.com" and the user types "woowoo", IAutoComplete will complete to http://www.woowoo.com. (To be exact, I should say the implementation of IAutoComplete in shell32.dll does this, since IAutoComplete is just an interface, not an implementation.) Likewise, IAutoComplete lets you specify a string that names a registry key where the format string is stored. These features are clever, but they're so Internet Explorer-specific they don't seem to belong in a general autocompletion interface, so I left them out of CAutoComplete. Instead, CAutoComplete provides a more general way to alter the way it performs autocompletion.
      To show how it works, I wrote a mini app called ACTest. Figure 10 contains the code, while Figure 11 shows ACTest in action. ACTest is a dialog-based application with a main dialog that has an edit control, a combobox, and two instances of CAutoComplete.



class CMyDialog : public CDialog {
protected:
    CAutoComplete m_acEdit;  // for edit box
    CAutoComplete m_acCombo; // for combobox
���
};
      To use CAutoComplete, you have to initialize each instance with a pointer to a window (edit control or combobox), then add some strings. CMyDialog does it in OnInitDialog.


// in CMyDialog::OnInitDialog
m_acCombo.Init(GetDlgItem(IDC_COMBO1));
m_acEdit.Init(GetDlgItem(IDC_EDIT1));
static LPCTSTR STRINGS[] = {
    "alpha",
    "alphabet",
    ���
    NULL
};
for (int i=0; STRINGS[i]; i++) {
    m_acCombo.GetStringList().Add(STRINGS[i]);
    m_acEdit.GetStringList().Add(STRINGS[i]);
}
      Since ACTest doesn't use the edit control or combobox for anything else (this is just a dopey demo app), I call CWnd::GetDlgItem to get the dialog controls. In a real app, you'd be more likely to have class members m_wndEdit and m_wndCombo in your dialog class, in which case you'd pass the address of these members after subclassing them with SubclassDlgItem.
Figure 11 ACTest
Figure 11 ACTest

      That's it, that's all you have to do. No IEnumString, no COM gyrations. Just initialize and add the strings. Now when the user types a letter like b, as in Figure 11, CAutoComplete shows the choices that match b and adds the text to complete the entryâ€"in this case, beta.
      How does CAutoComplete work? The basic idea is to derive CAutoComplete from my all-purpose window-subclasser class, CSubclassWnd, which appears so often in these columns. CSubclassWnd lets any object intercept messages sent to a window. It works using ordinary windows subclassing; that is, by installing a window proc. This all-important foundational capability is so often overlooked in MFC programming because MFC is not designed to exploit it. When the app calls CAuto-Complete::Init, CAutoComplete calls CSubclassWnd::HookWindow, which subclasses the window. CSubclass-Wnd::HookWindow "attaches" (using a mechanism similar to MFC's) the CSubclassWnd object to the window so any messages sent to the window are now routed first through the virtual function CSubclassWnd::WindowProc. CAutoComplete overrides this function to process the messages it's interested in.



// override CSubclassWnd::WindowProc
LRESULT CAutoComplete::WindowProc(...)
{
    if (/* EN_CHANGE or CBN_EDITCHANGE */))) {
        // try to complete
    }
    return CSubclassWnd::WindowProc(...);
}
      Please note that CAutoComplete is not a CWnd-derived object. It's derived from CSubclassWnd, which is derived from CObject. After processing the message (or not), CAutoComplete calls CSubclassWnd::WindowProc, which passes the message along its merry way via the original window proc to any message maps with handlers waiting to handle it.
      Once you understand how CSubclassWnd intercepts messages, the rest is just a matter of doing the actual autocompletionâ€"that is, comparing what the user typed against a list of strings. Yada yada yada. CAutoComplete does it in a virtual function OnComplete. The default implementation (see AutoCompl.cpp in Figure 9) compares the user's input against the internal list and, if there's a match, adjusts the input to show it. For comboboxes, CAutoComplete also shows the dropdown list of choices that match.
      There are, as always, just a few tricks. When CAutoComplete intercepts EN_CHANGE (edit control changed) or CBN_ EDITCHANGE, it must turn itself off before calling SetWindowText to set the new, completed text. Otherwise SetWindowText would trigger another CHANGE notification and control would spiral off the stack in an infinitely recursive chain of self-generated EN_ or CBN_CHANGE messages. That's trick number one.
      Trick number two is a little more subtle. Suppose the user types "al", which CAutoComplete completes by changing to "alpha" with "pha" highlighted. Now the user presses Backspace to delete "pha". You don't want to turn around and complete it back to alpha again! The poor user will get frustrated wondering why Backspace didn't work. The solution is to ignore completion if the user's entry is shrinking, not growing. This is equivalent to ignoring completion if what's been typed so far matches what was previously typed. It's confusing to explain in prose, but it makes sense when you get into the code. I added a virtual function IgnoreCompletion to test for this condition; CAutoComplete only performs completion if this function returns FALSE. In case my algorithm isn't totally correct (it's been known to happen), I made the function virtual so you can override my ignorance.
      Finally, trick number three is the addition of virtual functions to break OnComplete into more atomic operations that make it easier for you to alter basic behavior. For example, the first thing OnComplete does is call a virtual function, GetMatches, to get a list (CStringArray) of matches that match what the user typed so far.


void CAutoComplete::OnComplete(CWnd* pWnd, CString s)
{
    CStringArray arMatches; // strings that match
    if (s.GetLength()>0 && 
      GetMatches(s, arMatches, m_iType==Edit)>0) {
            DoCompletion(...);
    }
    m_sPrevious=s; // remember current string
}
      GetMatches in turn navigates the strings by calling more virtual functions: OnFirstString, OnNextString, and OnMatchString. The default implementations navigate an internal CStringArrayâ€"the same array that gets filled when you call CAutoComplete::Add. (Remember, CMyDialog::OnInitDialog called Add to supply the strings.) The upshot is this: you can either call Add to add the strings, or you can derive a new class and override OnFirstString and the others to do something more complex. For example, you may not want to store the strings in CAutoComplete's CStringArray, or maybe you want to override OnMatchString to do some fancy matching. Yet another virtual function, DoCompletion, performs the actual completion (sets the window text and combo dropdown) once the matches are found. You could override DoCompletion to support some other kind of control that's neither an edit control nor a combobox.
      The point of all this is to design an API that lets you override just the functions you need to alter specific behaviors. This is the essence of all good programming. Whether you write a class in C, C++, or COM (of course, in C it wouldn't be called a class), you still have to design the API. Bang! That's the most important part. One reason so many interfaces in Windows seem insufficient is that they're designed to support only the features required by Windows or Internet Explorer, or whatever Microsoft product it happens to use themâ€"and nothing else. Other objects and interfaces are more general. In any case, it's often easier to get exactly what you want by writing it yourself.
       Figure 12 compares CAutoComplete with Microsoft's implementation of IAutoComplete in shell32.dll. The question isn't which is better, it's which fits your needs.

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

From the August 2000 issue of MSDN Magazine.

Page view tracker