Providing a custom autocomplete source for an edit control


Today's Little Program shows a custom source for autocomplete. It's nothing exciting, but at least's it's something you can use as a starting point for your own customizations.

We start with a dialog template, whose edit control will be the target of a custom autocomplete.

// scratch.rc
#include <windows.h>

1 DIALOGEX DISCARDABLE  32, 32, 200, 56
STYLE DS_MODALFRAME |  WS_POPUP |
      WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Sample"
FONT 8, "MS Shell Dlg"
BEGIN
 LTEXT "What is your favorite Seattle restaurant?",-1,7,8,184,10
 EDITTEXT 100,7,18,184,14
 PUSHBUTTON "OK",IDOK,146,38,50,14
END

Just for fun, I wrote the program in ATL. Instead of complaining that my code is hard to understand because I didn't use an application framework, people can now complain that my code is hard to understand because I used the wrong application framework.

// scratch.cpp

#include <windows.h>
#include <ole2.h>
#include <windowsx.h>
#include <shlobj.h>
#include <atlbase.h>
#include <atlcom.h>

CComModule _Module;

To save some typing, I define a shorthand name for "the predefined ATL object for enumerating strings via IEnum­String."

typedef CComEnum<IEnumString,
                 &IID_IEnumString,
                 LPOLESTR,
                 _Copy<LPOLESTR> > CComEnumString;

To initialize the dialog, we do the following things:

  • Create a predefined ATL object for implementing IEnum­String.

  • Tell the predefined ATL object to enumerate a hard-coded list of restaurant suggestions.

  • Create an autocomplete object.
  • Connect the autocomplete object to the edit control in the dialog and to the IEnum­String object.

  • Just for fun, change some of the default settings for autocomplete.
LPOLESTR c_rgpszSuggestions[] = {
    L"Brave Horse Tavern",
    L"Cuoco",
    L"Dahlia Bakery",
    L"Dahlia Lounge",
    L"Etta's",
    L"Lola",
    L"Palace Kitchen",
    L"Seatown",
    L"Serious Pie",
    L"Ting Momo",
};

void OnInitDialog(HWND hdlg)
{
  CComPtr<IAutoComplete2> spac;
  CComObject<CComEnumString> *pes;
  HRESULT hr = CComObject<CComEnumString>::CreateInstance(&pes);
  CComPtr<IEnumString> spes(pes);
  if (SUCCEEDED(hr) &&
      SUCCEEDED(pes->Init(&c_rgpszSuggestions[0],
                          &c_rgpszSuggestions[ARRAYSIZE(c_rgpszSuggestions)],
                          NULL)) &&
      SUCCEEDED(spac.CoCreateInstance(CLSID_AutoComplete)) &&
      SUCCEEDED(spac->Init(GetDlgItem(hdlg, 100), spes, NULL, NULL)) &&
      SUCCEEDED(spac->SetOptions(ACO_AUTOSUGGEST | ACO_UPDOWNKEYDROPSLIST))) {
  }
}

The rest is just boilerplate.

INT_PTR CALLBACK DlgProc(HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg) {
  case WM_INITDIALOG:
    OnInitDialog(hdlg);
    return TRUE;

  case WM_COMMAND:
    switch (GET_WM_COMMAND_ID(wParam, lParam)) {
    case IDOK:
      EndDialog(hdlg, 0);
      break;
    }
  }
  return FALSE;
}

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                   LPSTR lpCmdLine, int nShowCmd)
{
  if (SUCCEEDED(CoInitialize(NULL))) {
    DialogBox(hinst, MAKEINTRESOURCE(1), NULL, DlgProc);
    CoUninitialize();
  }
  return 0;
}

Now, one of the reasons for using a framework is that it hides a lot of details from you. But if you are trying to understand how to port code from one framework to another, those hidden details become an obstacle to progress rather than a convenience. You may port the overall structure from one framework to another, but if the two frameworks behave differently in the hidden parts, your conversion was incorrect.

For example, one subtlety hidden in the above code is how the strings are returned by the IEnum­String::Next method. Recall that COM interfaces use the task allocator to pass memory between objects, so the string returned by IEnum­String::Next is allocated by Co­Task­Mem­Alloc, with the expectation that the caller will call Co­Task­Mem­Free to free it.

Unless you happen to be familiar with this detail of ATL, you would never have guessed it from the code above. You might have thought that the enumerator handed out the literal string pointers used to initialize it, and then you'll start wondering why your program crashes at random times (because you introduced a heap corruption bug).

Comments (15)
  1. John Doe says:

    About the frameworks, now that's a lesson learned the hard way. No one prepares you for that. No matter how smart you are, it's hard to port every little detail.

    It's "easy" (but not necessarily without work and a bit of research) to port the hard-to-miss details. Hell, it's just a matter of time to replicate the general actions of another system. The greater problems are in the details, and then you have to know whether you need them, and if not, whether you care about them.

    Slightly off-topic (I was unable to answer in the original post): is the answer to the exercise in this post (A big little program: Monitoring Internet Explorer and Explorer windows, part 3: Tracking creation and destruction blogs.msdn.com/…/10425806.aspx ) that spev is not checked for NULL? It happens again in this example, although it's very unlikely that the actual QueryInterface() fails for a reason other than E_OUTOFMEMORY.

    [That's not what I was thinking of. Little Programs may generally assume that all API calls and memory allocations succeed. -Raymond]
  2. John Doe says:

    Actually, it doesn't happen again, I misread the example and spes is not a CComQIPtr.

  3. Joker_vD says:

    What is a correct way of doing COM? Aside from never touching it?

  4. Joe Doe says:

    WinRT is the correct way of doing COM, no?

  5. Joshua says:

    @Joker_VD: Pay a COM guru to wrap it in a more sane API.

  6. Joker_vD says:

    @Joe Doe: Too bad Microsoft shipped ISA and TMG before WinRT was conceived.

    @Joshua: Guess who is an appointed COM guru at the place where I work? ;) Really though, extending TMG Forefront's console (just an instance of MMC) with a new property page for rules, and a new context menu item has already required a stunningly large amount of work.

  7. Nick says:

    Come now, you can't leave us hanging!  What *is* your favorite Seattle restaurant?

  8. Mostly unrelated, but the contact page/suggestion box is closed, so… since the introduction of the "little programs" series, I added syntax highlighting to the blog on my machine with a 3-lines GreaseMonkey script, and (as expected) it does improve readability of the code snippets quite a bit (img689.imageshack.us/…/737a.png). It would be nice to have something like this working "out of the box"; I know that you don't manage the blog software, that including JS may be a problem and everything, so take this just as an item for the wishlist.

    [On the other hand, it conflicts with my use of color to highlight changes. But worse, I would have to have a meeting with some lawyers about including external libraries. -Raymond]
  9. cheong00 says:

    On the other hand, porting web applications are usually easier.

    If you code can emit the exact HTML code as the original, your port is a success. Virtually no detail can be hidden with the aid of Fiddler like tools.

  10. Jonathan says:

    @Joker_vD: As you appear to already know, Forefront TMG's console was originally written in the late 90's for ISA2000, and has never been re-written since. So its unsurprising that extending it is a non-trivial task – I'm surprised anyone managed to do it at all. There were talks about re-writing it in Managed code, which surely would've made for a much more pleasant to work with, but that never happened. Also, since the product is EOL'd, not much will happen with it either.

    – Former TMG team member

  11. @Joker_vD:

    From my own experiences, smart pointers and other helper classes make doing COM a lot less annoying.

    For the correct way, it is programming, , it is not easy to say that there is one correct way of doing something.

  12. ulric says:

    I use ATL all the time but 99.9% just for CComPtr.  I find always find CComObject a bit of a head scratcher.

    I don't understand the lifespan of the Autocomplete object here.  It's created and then released automatically at the end of OnInitDialog. Who keeps the object alive, the Edit control by some undocumented interface between it and the CLSID_AutoComplete object?

  13. Mike Dimmick says:

    @ulric: msdn.microsoft.com/…/hh127437(v=vs.85).aspx says:

    "6.Release the objects.

    "Note  The autocomplete object remains attached to the edit control even after you release it. If you foresee a need to access these objects later—if you want to change the autocomplete options at a later time, for example—it is not required that you release them at this point."

    At a guess, the autocomplete object subclasses the HWND that it's passed in Init, and deletes itself when WM_DESTROY is sent to the control.

    The autocomplete object holds a reference to the string-enumeration object.

  14. John Doe says:

    While analysing the full sample (by stiching the various code snippets together, but still without compiling), I found that the LVN_DELETEITEM notification's documentation ( msdn.microsoft.com/…/bb774812(v=vs.85).aspx ) states that only the iItem field is set, the other fields are zero. So, this:

       auto pnmlv = CONTAINING_RECORD(pnm, NMLISTVIEW, hdr);

       delete reinterpret_cast<ItemInfo *>(pnmlv->lParam);

    should be:

       auto pnmlv = CONTAINING_RECORD(pnm, NMLISTVIEW, hdr);

       delete GetItemByIndex(pnmlv->iItem);

    I was going for properly freeing the text of each listview item, but (although undocumented) listviews make a copy of the string sent in ListView_SetItemText/LVM_SETITEMTEXT et al.

    BTW, this would make a good blog post [bling bling bling, suggestion uncalled for]: what's the expected behavior of some window messages if nothing else is told (e.g. in this case, to copy the string).

    I guess I quit finding the subtle bug in BuildWindowList, perhaps some day when I actually compile it. I don't have VS at hand right now.

    PS: Commenter foo also states a potential problem, handle recycling, that seems plausible enough to be the subtle bug. To solve it, we could keep the IUnknown interface pointer of either the Explorer or IE object in ItemInfo, to later check if both the hwnd and the object remain the same.

    [The LVN_DELETEITEM thing is a doc error. I'll submit a fix request. The general rule for controls is that strings are copied. (See: List box, combo box, button, static, you get the idea.) -Raymond]
  15. John Doe says:

    @Raymond, thanks for the documentation fix request.

    About the general rule, it's just that WM_GETTEXT states it copies the text, and WM_SETTEXT states it returns a control-specific error "if insufficient space is available to set the text in the edit control". The confident reader will assume it'll be copied too.

    In many other occasions, nothing is told. Only a seasoned Win32 programmer will rely on guess that it's copied just like WM_*TEXT, the rest would need clarification, and meanwhile come up with a SSCCE ( http://sscce.org/ ) to check if it's actually copied or not, or rather, if it leaks or not.

Comments are closed.

Skip to main content