How can I get notified when some other window is destroyed?


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?

Comments (18)
  1. Damien says:

    Exercise 2: I'd guess that the window isn't destroyed at this point, only hidden, since after you dismiss the error message, the run dialog reappears.

  2. Bob says:

    Exercise 1:  A thread can have more than one window and we can't restrict the callback to only one window.

  3. Niels says:

    Exercise 1: We registered for events for all windows owned by that thread, not one particular window. Better check it really is that event we wanted before acting on it.

  4. parkrrrr says:

    Exercise 1: EVENT_OBJECT_DESTROY is fired for the caret object, so we'll get one when focus leaves the edit box.

  5. aolszowka says:

    Raymond I love these automation related articles! I've been working on a project where we've been doing a bit of this to black box test our application. While the UIAutomation Framework provided by Microsoft is great, it doesn't cover all of our use cases and sometimes we end up having to roll our own.

  6. WS says:

    INDEXID_CONTAINER==INDEXID_OBJECT==CHILDID_SELF, but why so many names for the same thing?

  7. Joshua says:

    Hmmm.

    This seems somewhat of a dangerous procedure if you expect it to work across version upgrades, documented or not.

    Then again, maybe expecting this kind of automation to work across version upgrades is the dangerous idea.

  8. Good ole WinEventHook (LauraBu and I share a couple of patents on that multi-headed monster).  Going places where CBT Hooks would never dare to tread.

  9. Aaron.E says:

    @JamesJohnston:  I think you have the right idea, but the example code you posted doesn't quite make sense to me.  When an error is dismissed by the user, the Run window is displayed again.  Immediately afterward, UnloadRunWindow() is called.  Did you intend to have a return statement at the end of the if block?

    Another clue for this behavior might be that the run window fades in when "Run" is clicked from the start menu (with Aero anyway), but when the error is dismissed, the run window appears immediately.

  10. Exercise 2:  Open Run, type some garbage into edit control and click OK.  Error message appears.  After the error message goes away, Run dialog reappears in *exactly the same position that you left it when you clicked OK.*  It's obvious that the code in Run might look like this:

    void OKClicked() {

       SetRunWindowVisible(false);

       if (!ShellExecuteEx(…)) {

           ShowLastError();

           SetRunWindowVisible(true);

       }

       UnloadRunWindow();

    }

    Therefore, the test program won't unload until you click Cancel, or the Run dialog succeeds in starting a program.  If you want your test program to close, you'd need to check for a different event (maybe EVENT_OBJECT_HIDE ?) in addition to the EVENT_OBJECT_DESTROY event.

  11. lixiong says:

    I think a better and formal way is to use UIAutomation API, which really means accessibility.

    Try WindowPattern.WindowClosedEvent:

    msdn.microsoft.com/…/system.windows.automation.windowpattern.windowclosedevent.aspx

  12. Nick says:

    @JamesJohnston: Raymond correct me if I'm wrong but I believe EVENT_OBJECT_HIDE is only for cursors… (i.e. not fired if a window is hidden)…

  13. @Aaron: "Did you intend to have a return statement at the end of the if block?" – yes, I did mean to have that – oops!

    Good catch with the window fading – I never noticed that subtle behavior before!

    @Nick:  I was only stabbing at suspiciously-named events without reading documentation. :)  (My point being that there's probably something else in there that can be monitored if somebody wants to read through all the docs).

  14. parkrrrr says:

    @JamesJohnston: Don't worry; Nick's wrong: EVENT_OBJECT_HIDE does fire for windows.

    And for the various commenters who are treating SetWinEventHook as if it were some sort of scary undocumented hack: MSAA has been documented since before Windows 98 shipped. It's not new, it's not a hack, and it's not scary. And unlike UIA, it works on every version of NT since NT4 SP6.

  15. Nick says:

    @parkrrr: msdn.microsoft.com/…/dd318066%28v=vs.85%29.aspx

    My interpretation of the documentation says that the system does not send EVENT_OBJECT_HIDE for windows:

    "An object is hidden. The system sends this event for the following user interface elements: caret and cursor."

    Could we get some clarification here Raymond?

  16. Tergiver says:

    Wouldn't the lighter-weight CBTHook be a better choice if all you want to know is when a window is destroyed?

    [CBTHook is really heavy. It gets called for every window, and for many more events than just window destruction, and they are synchronous rather than asynchronous. -Raymond]

  17. parkrrrr says:

    @Nick: regardless of what the documentation says, EVENT_OBJECT_HIDE is indeed sent for windows, and always has been. Microsoft provides a tool called AccEvent that can be used to verify this. Note, too, that EVENT_OBJECT_SHOW is documented to fire for windows. I suspect a documentation error.

    (And I think ChuckOp, above, is the guy who could confirm or deny this better than Raymond.)

  18. parkrrrr says:

    I found a possible reason for the documentation and the reality disagreeing here: the MSAA 1.3 documentation also said "This event is not sent consistently by the system. This is a known problem and is being addressed." So it's possible that they elected to not document the window behavior, since it couldn't be counted on.

Comments are closed.