A customer wanted to know whether there was a method (other than polling) to monitor another window and find out when it gets destroyed. The goal was to automate some operation, and one of the steps was to wait until some program closed its XYZ window before moving on to the next step. Finding the XYZ window could be done with a Find­Window, but since the window belongs to another process, you can't subclass it to find out when it gets destroyed.

Enter accessibility.

The Set­Win­Event­Hook function lets you monitor accessibility events, and you can do it globally, for a particular process, or for a particular thread. Since we're interested in just one specific window, we can restrict our monitoring to a specific process and thread. (You don't want to monitor too much or you end up getting spammed with notifications you don't care about, which will annoy both you and the end users who are wondering why all their CPU is being consumed on pointless activity.)

Let's take our scratch program and have it monitor an arbitrary window whose name is passed on the command line.

HWND g_hwnd; /* our main window */
HWND g_hwndTarget; /* the window we are monitoring */
HWINEVENTHOOK g_hweh;

void CALLBACK WinEventProc(
    HWINEVENTHOOK hWinEventHook,
    DWORD         event,
    HWND          hwnd,
    LONG          idObject,
    LONG          idChild,
    DWORD         idEventThread,
    DWORD         dwmsEventTime)
{
 if (event == EVENT_OBJECT_DESTROY &&
     hwnd == g_hwndTarget &&
     idObject == OBJID_WINDOW &&
     idChild == INDEXID_CONTAINER) {
  PostMessage(g_hwnd, WM_CLOSE, 0, 0);
 }
}

The Win­Event­Hook function is where it all happens. If our callback is told that a window was destroyed, and the window handle matches the one we are monitoring, then post ourselves a WM_CLOSE message, which will close the window and exit the program.

The rest is just scaffolding to get to the point where our Win­Event­Hook gets called.

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 DWORD dwProcessId;
 DWORD dwThreadId = GetWindowThreadProcessId(g_hwndTarget,
                                            &dwProcessId);
 if (dwThreadId)
 g_hweh = SetWinEventHook(
     EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY,
     NULL, WinEventProc,
     dwProcessId, dwThreadId, WINEVENT_OUTOFCONTEXT);
 return g_hweh != NULL;
}

To register the hook, we obtain the thread ID and process ID of the window we are interested in tracking, then use the Set­Win­Event­Hook function to register our callback function, saying that we want to receive only EVENT_OBJECT_DESTROY notifications by passing it as both the event­Min and event­Max. We give it our callback function, and since we ask for WIN­EVENT_OUT­OF­CONTEXT, we don't need to pass a module handle since we are not requesting injection.

Notice that we restrict our hook as much as we can. We specify that we care only about one event, and we are interested in only one process and only one thread. It's generally a good idea to restrict the hook as much as possible.

Of course, we also have to unregister the hook when we're done.

void
OnDestroy(HWND hwnd)
{
 if (g_hweh) UnhookWinEvent(g_hweh);
 PostQuitMessage(0);
}

And finally, we use our command line to specify the title of the window we are monitoring.

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                   LPSTR lpCmdLine, int nShowCmd)
{
 ...
  g_hwndTarget = FindWindowA(lpCmdLine);
  g_hwnd =
  hwnd = CreateWindow(
 ...
}

With the Run dialog open, run this program with the command line argument Run. The program window opens, and when you click Cancel in the Run dialog, the program window closes. Wow that was exciting.

Bonus chatter: Remember that the window manager needs a message pump in order to call you back unexpectedly.

Exercise: Since we registered for only one thing, why did we have to perform the tests in Win­Event­Proc? Why not just simplify the function to this?

void CALLBACK WinEventProc(
    HWINEVENTHOOK hWinEventHook,
    DWORD         event,
    HWND          hwnd,
    LONG          idObject,
    LONG          idChild,
    DWORD         idEventThread,
    DWORD         dwmsEventTime)
{
 PostMessage(g_hwnd, WM_CLOSE, 0, 0);
}

Exercise: With the Run dialog open, run this program with the command line argument Run. Now instead of clicking Cancel in the Run dialog, type some garbage into the edit control and then click OK. The Run dialog goes away and an error message appears instead. Why is the scratch program still running?