A big little program: Monitoring Internet Explorer and Explorer windows, part 2: Tracking navigations


Okay, it’s been a while since we set aside our Little Program to learn a bit about connection points and using dispatch interfaces as connection point interfaces. Now we can put that knowledge to use.

Internet Explorer and Explorer windows fire a group of events known as DWeb­Browser­Events, so we just need to listen on those events to follow the window as it navigates around.

Take our scratch program and make these changes:

#define UNICODE
#define _UNICODE
#define STRICT
#define STRICT_TYPED_ITEMIDS
#include <windows.h>
#include <windowsx.h>
#include <ole2.h>
#include <commctrl.h>
#include <shlwapi.h>

#include <shlobj.h>
#include <atlbase.h>
#include <atlalloc.h>
#include <exdisp.h>
#include <exdispid.h>

...
// DispInterfaceBase incorporated by reference

void UpdateText(HWND hwnd, PCWSTR pszText);

class CWebBrowserEventsSink :
    public CDispInterfaceBase<DWebBrowserEvents>

public:
 CWebBrowserEventsSink(HWND hwnd) : m_hwnd(hwnd) { }

 IFACEMETHODIMP SimpleInvoke(
    DISPID dispid, DISPPARAMS *pdispparams, VARIANT *pvarResult)
 {
  switch (dispid) {
  case DISPID_NAVIGATECOMPLETE:
   UpdateText(m_hwnd, pdispparams->rgvarg[0].bstrVal);
   break;

  case DISPID_QUIT:
   UpdateText(m_hwnd, L"<exited>");
   Disconnect();
   break;
  }
  return S_OK;
 };

private:
 HWND m_hwnd;
};

Our event sink class listens for DISPID_NAVIGATE­COMPLETE and DISPID_QUIT and updates the text with the new navigation location or the string L"<exited>" if the window exited. In the exit case, we also disconnect from the connection point to break the circular reference.

The IDL file for Navigate­Complete says

[id(DISPID_NAVIGATECOMPLETE), helpstring("...")]
void NavigateComplete([in] BSTR URL );

Therefore, we know that the URL parameter arrives as a VT_BSTR in position zero, so we can access it as pdispparams->rgvarg[0].bstrVal.

That class is basically the guts of the program. The rest is scaffolding. Like hooking up this guy to a listview item so it can report its findings somewhere.

struct ItemInfo
{
 ItemInfo(HWND hwnd, IDispatch *pdisp)
  : hwnd(hwnd) {
  spSink.Attach(new(std::nothrow) CWebBrowsrEventsSink(hwnd));
  if (spSink) spSink->Connect(pdisp);
 }
 ~ItemInfo() { if (spSink) spSink->Disconnect(); }

 HWND hwnd;
 CComPtr<CWebBrowserEventsSink> spSink;
};

ItemInfo *GetItemByIndex(int iItem)
{
 LVITEM item;
 item.mask = LVIF_PARAM;
 item.iItem = iItem;
 item.iSubItem = 0;
 item.lParam = 0;
 ListView_GetItem(g_hwndChild, &item);
 return reinterpret_cast<ItemInfo *>(item.lParam);
}

ItemInfo *GetItemByWindow(HWND hwnd, int *piItem)
{
 int iItem = ListView_GetItemCount(g_hwndChild);
 while (--iItem >= 0) {
  ItemInfo *pii = GetItemByIndex(iItem);
  if (pii->hwnd == hwnd) {
   if (piItem) *piItem = iItem;
   return pii;
  }
 }
 return nullptr;
}

void UpdateText(HWND hwnd, PCWSTR pszText)
{
 int iItem;
 if (GetItemByWindow(hwnd, &iItem)) {
  ListView_SetItemText(g_hwndChild, iItem, 0,
                       const_cast<PWSTR>(pszText));
 }
}

Attached to each listview item is an Item­Info structure which remembers the browser window it is associated with and the event sink that is listening for events.

// GetLocationFromView, GetLocationFromBrowser, and GetBrowserInfo
// incorporated by reference

CComPtr<IShellWindows> g_spWindows;

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

 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 =
            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;
 }
 return S_OK;
}

To build the window list, we enumerate the contents of the IShell­Windows. For each window, we get its window handle and current location and create a listview item for it. The reference data for the listview item is the Item­Info.

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();
 return TRUE;
}

Our creation function creates a child listview and fills it with stuff.

And of course we clean up our objects when the items are deleted and when the window is destroyed.

LRESULT OnNotify(HWND hwnd, int idFrom, NMHDR *pnm)
{
 switch (idFrom) {
 case 1:
  switch (pnm->code) {
  case LVN_DELETEITEM:
   {
    auto pnmlv = CONTAINING_RECORD(pnm, NMLISTVIEW, hdr);
    delete reinterpret_cast<ItemInfo *>(pnmlv->lParam);
   }
   break;
  }
 }
 return 0;
}

void OnDestroy(HWND hwnd)
{
 g_spWindows.Release();
 PostQuitMessage(0);
}

 HANDLE_MSG(hwnd, WM_NOTIFY, OnNotify);

And there we have it, a program that displays all the Internet Explorer and Explorer windows and updates their locations as you navigate.

Note, however, that our program doesn’t notice when new windows are created. We’ll hook that up next time.

Comments (8)
  1. GWO says:

    Can you show us how we add the have the capability of forwarding those events directly to the NSA, in order to save them the development costs on their spyware?

    GWO – at least 51% foreign

  2. 12BitSlab says:

    GWO, in my own life, my rule is that when I am a guest in someone elses house, I try my best not to insult the host.  Tha goes double when complaining about something that the host has no knowledge of and can't control.

    Raymond, I understand that you don't need me to defend your writings.  But of all of the blogs that I read on the web, this one is my favorite.  I find that I learn something new almost everyday and I couldn't help myself.  I had to say something.

  3. Adrian Lock says:

    We've tried this technique in the past and had problems where IE doesn't respond to some keystroke input after sinking these events. This is a very subtle bug and we never noticed it for a while. For a long time we just dismissed bug reports as people typing too quickly and therefore mis-typing but we eventually reproduced it and isolated it to these sinks.

    If you look around the internet, other people have had the same issue so it wasn't just us. E.g.

    s398.codeinspot.com/…/488235

    us.generation-nt.com/mshtml-iwebbrowser2-problem-catching-events-causes-missed-keystrokes-help-9735282.html

    Any clues Raymond?

  4. Neil says:

    I'm obviously missing something here. Is it necessary to use a dispatch interface or could you just use a regular interface?

  5. GWO says:

    @12BitSlap I didn't make any accusation about Raymond, or any one else except the NSA.  You're inventing things that aren't there, so chill the hell out.

    The only suggestion I made was that the NSA like tracking people's web use. Which is true.

    [The accusation is "I know that Microsoft has code to send our personal information to the NSA, so you may as well show us how to do that, too." -Raymond]
  6. Doublespeak says:

    "we can neither confirm nor deny the existence of such code"

  7. Mordachai says:

    @Neil – I believe it was necessary because IE and WE publish their events via IDispatch.

  8. leehark says:

    How do u Use CWebBrowserEventsSink , connect to every IWebBrowser2 ?

Comments are closed.