A big little program: Monitoring Internet Explorer and Explorer windows, part 3: Tracking creation and destruction


Last time, we listener for window navigations. Today we'll learn about tracking window creation and destruction.

The events to listen to are the DShell­Windows­Events. The Window­Registered event fires when a new window is created, and the Window­Revoked event fires when a window is destroyed.

The bad news is that the parameter to those events is a cookie, which is not useful for much, so we just use the events to tell us that it's time to kick off a new enumeration to see what changed. This will also catch the case where something fell out of sync because a window closed without unregistering (say, because the application crashed).

Take our program from last time and make these changes:

LONG g_lCounter;

struct ItemInfo
{
 ItemInfo(HWND hwnd, IDispatch *pdisp)
  : hwnd(hwnd), lCounter(g_lCounter) { ... }
 ...

 HWND hwnd;
 CComPtr<CWebBrowserEventsSink> spSink;
 LONG lCounter;
};

The counter is used to detect stale windows when we re-enumerate.

HRESULT BuildWindowList()
{
 CComPtr<IUnknown> spunkEnum;
 HRESULT hr = g_spWindows->_NewEnum(&spunkEnum);
 if (FAILED(hr)) return hr;

 ++g_lCounter;

 CComQIPtr<IEnumVARIANT> spev(spunkEnum);
 for (CComVariant svar;
      spev->Next(1, &svar, nullptr) == S_OK;
      svar.Clear()) {
  if (svar.vt != VT_DISPATCH) continue;

  HWND hwnd;
  CComHeapPtr<WCHAR> spszLocation;
  if (FAILED(GetBrowserInfo(svar.pdispVal,
             &hwnd, &spszLocation))) continue;

  ItemInfo *pii = GetItemByWindow(hwnd, nullptr);
  if (pii) { pii->lCounter = g_lCounter; continue; }
  pii = new(std::nothrow) ItemInfo(hwnd, svar.pdispVal);
  if (!pii) continue;

  LVITEM item;
  item.mask = LVIF_TEXT | LVIF_PARAM;
  item.iItem = MAXLONG;
  item.iSubItem = 0;
  item.pszText = spszLocation;
  item.lParam = reinterpret_cast<LPARAM>(pii);
  int iItem = ListView_InsertItem(g_hwndChild, &item);
  if (iItem < 0) delete pii;
 }

 int iItem = ListView_GetItemCount(g_hwndChild);
 while (--iItem >= 0) {
  ItemInfo *pii = GetItemByIndex(iItem);
  if (pii->lCounter != g_lCounter) {
   ListView_DeleteItem(g_hwndChild, iItem);
  }
 }

 return S_OK;
}

Building the window list is now a two-step process, since what we are really doing is updating the window list. First, we enumerate the contents of the IShell­Windows. For each window, we get its window handle and see if there is already an item for that window. If so, then we update the counter for that item. If there is not already an item for that window, then we create one like we did before.

After we've processed all the windows that exist, we go look for the deletion by walking through all our items and deleting any whose counter was not updated by the previous loop.

Okay, but so far we haven't actually done anything new. Here's the new stuff:

class CShellWindowsEventsSink :
    public CDispInterfaceBase<DShellWindowsEvents>
{
public:
 HRESULT SimpleInvoke(
    DISPID dispid, DISPPARAMS *pdispparams, VARIANT *pvarResult)
 {
  switch (dispid) {
  case DISPID_WINDOWREGISTERED:
  case DISPID_WINDOWREVOKED:
   BuildWindowList();
   break;
  }
  return S_OK;
 }
};

CComPtr<CShellWindowsEventsSink> g_spShellSink;

This is the object that listens for changes to the window list. And whether the change is that a window arrived or a window departed, the response is the same: Refresh the window list.

All that's left to do is hook up this event sink (and clean it up):

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 g_hwndChild = CreateWindow(WC_LISTVIEW, 0,
    LVS_LIST | WS_CHILD | WS_VISIBLE |
    WS_HSCROLL | WS_VSCROLL, 0, 0, 0, 0,
    hwnd, (HMENU)1, g_hinst, 0);
 g_spWindows.CoCreateInstance(CLSID_ShellWindows);
 BuildWindowList();

 g_spShellSink.Attach(new CShellWindowsEventsSink());
 g_spShellSink->Connect(g_spWindows);

 return TRUE;
}

void OnDestroy(HWND hwnd)
{
 g_spWindows.Release();
 if (g_spShellSink) {
  g_spShellSink->Disconnect();
  g_spShellSink.Release();
 }
 PostQuitMessage(0);
}

We now have a program that displays all the Internet Explorer and Explorer windows, updates their locations as you navigate, and adds and removes them as new windows are created or existing ones are closed.

Reminder: This is a Little Program, which means that there is little to no error checking, and the design may be somewhat suboptimal. (For example, I use global variables everywhere because I'm lazy.) But it should give you enough of a head start so you can write a more robust version.

Exercise: There is still a subtle bug in Build­Window­List. Identify it and discuss how you would address it.

Comments (13)
  1. Adam Rosenfield says:

    Thanks for this great series, Raymond.  I never really write code that interacts with COM, Explorer, or Internet Explorer (beyond calling ShellExecute every now and then), so it's interesting to see how this kind of stuff works.

  2. Mordachai says:

    I would use an unsigned instead of LONG, as signed types are not guaranteed to wrap around when you hit maximum value; whereas unsigned types will.

    Not sure if that is the subtle bug of which you speak.

    [That's not what I was thinking. Win32 imposes a lot of requirements beyond the C standard. For example, Win32 is required to be little-endian. Also, Win32 forbids padding bits in integers, requires twos-complement representation, and InterlockedIncrement will not raise an integer overflow exception. -Raymond]
  3. This series has been really great! I'm in the opposite position of Adam: I use COM every day, but from Visual Basic, a language that uses it as a scripting language. This series have helped me understand some of the limitations of VB's COM support, and why some COM libraries don't work at all with VB (I had read about laking of support for "scripting interfaces", but I like to understand what's happening under my feet).

    COM is amazing, but complex enough so that understanding it just with the official documentation (correct but terse) is very hard. Raymond's "COM for dummies" articles shed a lot of light over it. Great work!

  4. John says:

    Totally unrelated, but Raymond is that really you at the bottom of some of those SNPP episode capsules? Specifically AABF12: "This work is dedicated to Raymond Chen, James A. Cherry, Ricardo Lafaurie, Frederic Briere, and all of those who made episode capsules what they are today." My mind was blown to read that haha.

  5. 640k says:

    This can be done using the journal hook. No need to use bloated COM for tracking windows creation/destruction.

    [You're using a global solution to a local problem. Journal hooks force all input to be synchronized. (See last week for a discussion of why this is bad.) Also, how does your journal hook know which windows are Explorer and Internet Explorer windows? And how does it tell you what URL each browser window is looking at? -Raymond]
  6. skSdnW says:

    IShellWindows::FindWindowSW can turn a cookie into a HWND but that still leaves the issue of crashed apps.

    @640k: That is overkill and both the shell hook and WinEvent (Raymond used it recently) are probably better but since all we care about here are shell windows, why not used the tool designed to give you exactly that?

  7. 640k says:

    The hook type which records windows creation/destruction was of course CBT, not journal. This is not more "global" than using a message proc instead of event functions.

    [CBT hooks are awful for system performance, too, and you still have the identification problem. (For example, a CBT-based solution would have been broken when IE added tab windows, and again when IE added the Windows Store version.) Just subscribe to the event. That's what it's for. No global hooks needed or any kind. -Raymond]
  8. 640k says:

    What's bad for performance is to load the whole ole infrastructure into every application which only need to listen to raw messages.

  9. ErikF says:

    @640k: Processing lower-level messages to obtain higher-level information is almost always the wrong method in my experience; doing so usually turns into a screen-scraping exercise and is extremely fragile (and adds to Raymond's workload by adding yet another program that relies on implementation details….)

    I can't think of any realistic program that would only need raw messages anyways, unless all that you're doing is logging or something. In any event, if your program is needs to interact with Explorer/IE windows (or any other COM server!), you'll likely need to pull in COM eventually to do anything useful.

  10. John Doe says:

    Exercise: the ItemInfo of each deleted list-view item (in lParam) is not deleted (freed). This can be fixed by adding "delete pii;" after the ListView_DeleteItem statement.

    I wouldn't discuss further if you wouldn't suggest to. Since BuildWindowList is doing all the bookkeeping, it wouldn't be much more worth e.g. having an LVITEM (to use with ListView_GetItem) or LVFINDINFO (to use with ListView_FindItem) in the ItemInfo itself, in such way that delete'ing an ItemInfo would remove it from the list-view control e.g. in the destructor.

    I mean, would it be worth? Perhaps for a bigger little program, with context menus on the items, etc.

    [It is freed in the LVN_DELETE­ITEM handler. Your design would leak all the Item­Info objects when the listview is destroyed. -Raymond]
  11. John Doe says:

    @Raymond, I completely missed that one.

    Is it then that an existing list-view item's text (location) isn't updated?

  12. John Doe says:

    In the hope that this is the subtle bug, I'd do (excuse any formatting errors, I only have a simple text box):

     // …

     int iItem = MAXLONG; // moved up

     ItemInfo *pii = GetItemByWindow(hwnd, &iItem);

     if (pii && iItem != MAXLONG) { // I hope we never have these many items

       pii->lCounter = g_lCounter;

     }

     else {

       pii = new(std::nothrow) ItemInfo(hwnd, svar.pdispVal);

       if (!pii) continue;

     }

     // …

     item.iItem = iItem;

     // …

     if (iItem != MAXLONG) {

       ListView_SetItem(g_hwndChild, &item);

     }

     else {

       iItem = ListView_InsertItem(g_hwndChild, &item);

       if (iItem < 0) delete pii;

     }

     // …

    In English, pass an int reference to GetItemByWindow, and then use ListView_SetItem if there's an existing item.

    [I think you're saying that the subtle bug is that an existing item may have changed its location and we don't notice it in Build­Window­List. But that's okay. We notice it in our DISPID_NAVIGATE­COMPLETE handler. -Raymond]
  13. foo says:

    I suppose the thing [GetItemByWindow(hwnd…) etc] relies on window handles not being recycled between callbacks? This is very unlikely but.

Comments are closed.

Skip to main content