Why can’t FindWindowEx find another program’s window by name?


A customer had a test app that did something. They then wanted to write a test harness for their test app. The idea is that the test harness launches the test app, finds the test app's Start button, waits for it to become enabled, and then presses it, and then does some more stuff. The customer was stuck at the second step: Find the Start button.

  Console.WriteLine($"Main window handle: {hwnd}");
  IntPtr button = IntPtr.Zero;
  while (true) {
    button = FindWindowEx(hwnd, IntPtr.Zero, null, "Start");
    Console.WriteLine($"Start button window handle: {button}");
    if (button != IntPtr.Zero) break;
    Thread.Sleep(TimeSpan.FromSeconds(1));
  }

The retry loop is to cover the race condition where the test harness looks for the Start button before the test app creates it.

The main window handle is the handle we expect from the test app. But even after a minute, with the Start button right there on the screen, the test harness can't find it.

Okay, so what would prevent Find­Window­Ex from finding a window? Here are some possibilities:

  • The thing you want isn't actually a window. It could be a windowless control. (The fact that their test harness is written in C# suggests that maybe their test app is a WinForms or WPF program, both of which use windowless controls.)
  • The window title isn't exactly what you specified. Maybe there's an accelerator on it: &Start.
  • The thing you want isn't a direct child of that window. Maybe it's a grandchild.
  • The thing you want isn't a descendent of that window. Maybe you're passing the wrong parent window.

Basically, all of these options boil down to "Find­Window­Ex is not finding your window because there is in fact no window that meets all the criteria you specified." Find­Window­Ex is working exactly as defined. You need to check that the window you want really does satisfy the criteria you passed.

Double-checking with Spy++ showed that the customer was in the last case: The wrong parent window was being passed. They got the main window from the Process.MainWindowHandle property. But that property is synthetic. Windows doesn't have a formal concept of a "main window"; a program can create multiple top-level visible windows, and as far as Windows is concerned, they are all of equal importance.

My guess as to what happened is that the test app created a splash screen, and that got detected as "the main window". Too bad the splash screen doesn't have a Start button in it.

The customer updated their test harness to perform an Enum­Windows to find the top-level window of the test app whose title indicates that it is the window with the Start button.

But this is really a case of answering the question without solving the problem.

If you have a test harness that wants to communicate with a test app, you'd be better off having a formal mechanism for the test harness to tell the test app what to do, and a formal mechanism for the test app to report results. Because I suspect the customer's test harness also polls the test app waiting for the phrase Test passed or Test failed to appear.

For example, the test harness can pass a /auto command line option to the test app, and the test app goes into automated mode, where it auto-presses the Start button, auto-fills in the text boxes with the appropriate test values, and so on. You might even have /auto:config.xml that configures what the test app will do. And then the test app signals whether the test was successful, say by using special exit codes or printing a specific message to stdout.

Because what's going to happen sooner or later is that somebody's going to make changes to the test app, maybe rename or rearrange some buttons, or add a new text box that needs to be filled in, and inadvertently break the test harness.

The test harness and the test app are in cahoots. Make the most of it.

Comments (11)
  1. Joshua says:

    “and inadvertently break the test harness.”

    That’s not a bug, that’s a feature.

    Anyway I took “in cahoots with” a step farther and compiled the test harness into the binary. It’s started with /3.

  2. Paul Topping says:

    Unfortunately, the -auto recommendation means they are testing a different code path than their users. Also, sooner or later somebody’s going to make changes to the app and forget to update the -auto mechanism. Of course, there are always going to be dependencies between an app under test and its test rig. The goal is to minimize them which is a pretty deep subject.

  3. Martin Bonner says:

    Yeah, driving a test app like this is crazy. On the other hand, driving a real application like this is not so crazy, and would have the same problems.

  4. Gee Law says:

    You might want to create your app like Office team does — GUI is merely another way to manipulate COM objects. If you don’t do that, you might want to use Automation API and get accessibility at the same time.

    1. The_Assimilator says:

      You must be new here… Raymond’s blog is all about the customers who purposefully ignore the simplest, most well-documented way of achieving something in favour of the most convoluted, fragile, worst practices possible… and then bother Microsoft support, and eventually Raymond, when their terrible “solutions” fail terribly.

      1. Gee Law says:

        I’d not say I’m new. What I meant in the previous comment is that Raymond could’ve given a suggestion (instead of /auto command-line parameter) closer to what the customer was doing/asking for.

        1. An /auto flag probably involves the least change to the customer’s existing infrastructure. Better would be to factor the test into a separate object, and have the test app be a GUI front-end to the test object, and the test harness be a programmatic front-end to the test object.

  5. DWalker07 says:

    “sooner or later … somebody’s going to make changes to the test app, maybe rename or rearrange some buttons, or add a new text box that needs to be filled in,”

    Or someone needs to test a localized version, where the Start button isn’t labeled Start but the translated Start.

  6. Zarat says:

    Slight misinformation I think, WinForms doesn’t use windowless controls as far as I can tell. The base class of every WinForms control (“Control”) is creating a HWND for each and every control. I don’t know of any way to suppress that and still staying inside the WinForms framework. (You can of course build your own windowless control framework on top of WinForms, but then its no longer WinForms controls you are using.)

  7. SomeGuyOnTheInternet says:

    Shoulda used DDE instead :-D

  8. Robert Bennett says:

    More importantly, why can’t it set extended error information like the documentation promises?

    ref: https://msdn.microsoft.com/en-us/library/windows/desktop/ms633500

Comments are closed.

Skip to main content