11

I need to handle another windows application programatically, searching google I found a sample which handles windows calculator using DLLImport Attribute and importing the user32.dll functions into managed ones in C#.

The application is running, I am getting the handle for the main window i.e. Calculator itself, but the afterwards code is not working. The FindWindowEx method is not returning the handles of the children of the Calculator like buttons and textbox.

I have tried using the SetLastError=True on DLLImport and found that I am getting an error code of 127 which is "Procedure not found".

This is the link from where I got sample application:

http://www.codeproject.com/script/Articles/ArticleVersion.aspx?aid=14519&av=34503

Please help if anyone knows how to solve it.

UPDATE: The DLLImport is:

[DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className,  string  windowTitle);

The Code that is not working is:

hwnd=FindWindow(null,"Calculator"); // This is working, I am getting handle of Calculator

// The following is not working, I am getting hwndChild=0 and err = 127
hwndChild = FindWindowEx((IntPtr)hwnd,IntPtr.Zero,"Button","1");

                Int32 err = Marshal.GetLastWin32Error();
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
teenup
  • 7,459
  • 13
  • 63
  • 122

3 Answers3

14

The code you're trying relies on the captions of the individual buttons to identify them. For example, it uses the following code to get a handle to the "1" button:

hwndChild = FindWindowEx((IntPtr)hwnd, IntPtr.Zero, "Button", "1");

Which specifies "Button" for the name of the window class, and "1" for the name of the window (in the case of a button, this is the same as the caption text displayed on the button itself).

This code worked fine under Windows XP (and previous versions), where the calculator buttons were identified with textual captions. The "1" button had a window name of "1", and thus "1" was displayed as the button's caption.

However, it looks like things have changed under Windows 7 (possibly under Vista as well, although I can't verify this because I don't have access to such a system). Using Spy++ to investigate the calculator window confirms that the "1" button no longer has a window name of "1". In fact, it doesn't have a window name at all; the caption is NULL. Presumably, the new fancy look of the calculator required that buttons be custom drawn, thus the captions are no longer necessary to indicate which button corresponds to which function. The custom painting routines take care of drawing the necessary captions.

Since no button can be found with the window text you specified, a value of 0 (NULL) is returned for the window handle.

The documentation for the FindWindowEx function indicates that you can specify NULL for the lpszWindow parameter, but that this will, of course, match all windows of the specified class. Probably not what you want in this case, as the calculator app has a bunch of buttons.

I don't know a good workaround. Calculator wasn't designed to be "automated" this way, and Microsoft never guaranteed that they wouldn't change its internal workings. That's a risk you take in using this approach to mess with the windows of other applications.


EDIT: The code you've linked to is also wrong in another fairly serious way, even on earlier versions of Windows. It declares the hwnd variable as type int, rather than as type IntPtr. Since a window handle is a pointer, you should always store it as an IntPtr type. That also fixes the ugly cast in the FindWindowEx function call that should have sent up red flags.

You'll also need to fix the declaration of SendMessage so that its first parameter is of type IntPtr.

The code should have been written like this:

IntPtr hwnd = IntPtr.Zero;
IntPtr hwndChild = IntPtr.Zero;

//Get a handle for the Calculator Application main window
hwnd = FindWindow(null, "Calculator");
if(hwnd == IntPtr.Zero)
{
    if(MessageBox.Show("Couldn't find the calculator" + 
                       " application. Do you want to start it?", 
                       "TestWinAPI", 
                       MessageBoxButtons.YesNo) == DialogResult.Yes)
    {
        System.Diagnostics.Process.Start("Calc");
    }
}
else
{
    //Get a handle for the "1" button
    hwndChild = FindWindowEx(hwnd, IntPtr.Zero, "Button", "1");

    //send BN_CLICKED message
    SendMessage(hwndChild, BN_CLICKED, 0, IntPtr.Zero);

    //Get a handle for the "+" button
    hwndChild = FindWindowEx(hwnd, IntPtr.Zero, "Button", "+");

    //send BN_CLICKED message
    SendMessage(hwndChild, BN_CLICKED, 0, IntPtr.Zero);

    //Get a handle for the "2" button
    hwndChild = FindWindowEx(hwnd, IntPtr.Zero, "Button", "2");

    //send BN_CLICKED message
    SendMessage(hwndChild, BN_CLICKED, 0, IntPtr.Zero);

    //Get a handle for the "=" button
    hwndChild = FindWindowEx(hwnd, IntPtr.Zero, "Button", "=");

    //send BN_CLICKED message
    SendMessage(hwndChild, BN_CLICKED, 0, IntPtr.Zero);
}
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
  • Do you know any way of enumerating child windows using EnumChildWindows and checking in the callback if this is the window we need to capture? I am using WINAPI for the first time and I don't know how to write code for using EnumChildWindows. – teenup Mar 09 '11 at 06:24
  • @Puneet: To do that, you're going to have to figure out some way of uniquely identifying the buttons on the Calculator. As I explained in my answer, I don't know of a good way to do this. All of the buttons have a window title (caption) of `""`, or `NULL`. Programmatically, it's going to be difficult to find a way to identify individual buttons, given only their handles. – Cody Gray - on strike Mar 09 '11 at 06:29
  • On my Win7 box, Calculator's buttons all have standard Unicode captions according to Spy++ (I tried all 4 modes in the View menu). Also, for 32-bit compatibility, all window handles can fit in 32 bits, so an `HWND` should be convertable to an `int` and back just fine. – Gabe Mar 09 '11 at 06:43
  • @Gabe: Interesting. I checked mine on Server 2008 R2 64-bit, and they definitely don't have standard Unicode captions. I just checked the Standard view, but the others don't seem to have captions, either. And yes, even though using `int` will often work for handles, it's not the right way of doing things. You're supposed to use `IntPtr`, not take advantage of backwards compatibility. :-) – Cody Gray - on strike Mar 09 '11 at 06:45
  • I figured out the problem. I have Themes off, which causes Calc to use Unicode button captions. You must have Themes on (which is not the default for Server), which causes Calc to use themed buttons with blank captions. – Gabe Mar 09 '11 at 06:52
  • @Gabe: Yeah, I run with themes on to make sure my apps look as they should. Turns out most users don't agree with my aesthetic preferences for Classic mode. – Cody Gray - on strike Mar 09 '11 at 06:54
  • It's easy to work out which button is which! Just get their positions! – David Heffernan Mar 09 '11 at 20:39
2

The following code works fine in Caculator of Windows 7 in Classic theme (not working in Basic or Aero theme):

IntPtr hwndFrame = FindWindowEx(hwnd, IntPtr.Zero, "CalcFrame", null); 
IntPtr hwndDialog = FindWindowEx(hwndFrame, IntPtr.Zero, "#32770", null); 
IntPtr hwndDialog2 = FindWindowEx(hwndFrame, (IntPtr)hwndDialog, "#32770", null);

IntPtr hwndThree = FindWindowEx(hwndDialog2, IntPtr.Zero, "Button", "3"); 
SendMessage((int)hwndThree, BN_CLICKED, 0, IntPtr.Zero);

IntPtr hwndPlus = FindWindowEx(hwndDialog2, IntPtr.Zero, "Button", "+");
SendMessage((int)hwndPlus, BN_CLICKED, 0, IntPtr.Zero);

IntPtr hwndOne = FindWindowEx((IntPtr)hwndDialog2, IntPtr.Zero, "Button", "1");
SendMessage((int)hwndOne, BN_CLICKED, 0, IntPtr.Zero);

IntPtr hwndEqual = FindWindowEx(hwndDialog2, IntPtr.Zero, "Button", "=");
SendMessage((int)hwndEqual, BN_CLICKED, 0, IntPtr.Zero);
Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Yanzhi
  • 73
  • 2
  • 7
2

I was able to repro this on Win7 Pro. Your problem is likely that the labels on the buttons are drawn via the theme of the calculator and not as a caption. When the Themes service is running, starting Calculator will cause it to have buttons with no caption.

In order to get proper button captions, you must:

  1. Stop the Themes service (run net stop themes from an elevated command prompt or use the Services administrative tool).
  2. Start Calculator.

If you have Calculator running when you stop the Themes service, you will notice that all of its buttons become blank.

Gabe
  • 84,912
  • 12
  • 139
  • 238
  • Ah, so you were using Classic mode. I certainly agree with that choice. Disabling Themes does indeed appear to be the workaround, but not a very good one. You're simply working around assumptions made in the code, rather than fixing those assumptions. It's difficult to imagine the utility of an application that will only work properly on a non-default configuration of Windows. – Cody Gray - on strike Mar 09 '11 at 06:53
  • 1
    @Cody: Seeing as how it's pointless to automate a calculator from C#, I would have to assume that this is merely an exercise. It's hard to imagine that somebody is actually creating an application that relies on automating Calc! – Gabe Mar 09 '11 at 06:57
  • @Cody: Eventually my need is not to automate the Calculator through C#, I wrote in my question, that i found a sample application for understanding how to do the actual work. Finally, I need to implement it for aother windows application which is a tickets reservation system and reporting tool. – teenup Mar 09 '11 at 07:05
  • 1
    Even after stopping the themes on my Win7, I am not able to automate the Calculator through this application. Still I am getting the handle of child buttons as Zero. – teenup Mar 09 '11 at 07:09