Reader Level:
ARTICLE

Clicking a Button in Another Application

Posted by Sam Hobbs Articles | COM Interop December 12, 2012
This article provides a simple sample of an application that clicks a button in another application.
  • 0
  • 0
  • 20683
Download Files:
 

This article provides a simple sample of an application that clicks a button in another application. The technique could be used for other controls too. For the purpose of a simple sample the program is a console program. The sample will click the "=" button in the Windows Calculator Acesssory.

Note that I am using Windows 7; if the Windows Calculator Acesssory in Windows 8 is different and/or cannot be used in the manner it is used here then I hope you can find another program to use.

The first thing to do is to determine what button to click and the circumstances of when to click it. Execute the Windows Calculator Acesssory. Type "2+2=" into it. The result should be 4 of course. Then click the "=" button. Each time "=" is clicked, the value will be increased by 2. So that is something very simple to automate. We can "manually" execute Calculator and type "2+2=" into it. Then when we execute our sample application it can just click the "=" button and we can see the result.

The next thing to do is to determine the identity of the button to be clicked so our program can click it. With the Windows Calculator Acesssory executing, go to Visual Studio. In the "Tools" menu is "Spy++". If your system is a 64-bit system then you might need to use the 64-bit version of Spy++. The Spy++ toolbar will look like:

Spytoolbar.jpg

Click on the "Find Window" icon; it is a binoculars with a window in the top-left; it looks like:

FindWindowIcon.jpg

That will open a "Find Window" dialog. Now first, ensure that Calculator is visible; at least the "=" button part of it. Then click on the "Finder Tool" icon (the square with a circle in it that looks like a target). The "Find Window" dialog looks like:

FinderToolIcon.jpg

With the mouse button down, drag from the Finder Tool icon to the "=" button and release the mouse. The Find Window dialog will be filled in with the "=" button's handle and other data. Click on "OK". You will then get a "Window Properties" window with a tab control with five tabs. The five tabs are General, Styles, Windows, Class and Process. The wndow will look like:

WindowProperties.jpg

Look for "Contol ID" near the botom of the first (General) tab. It will probably have the value "00000079". It is a hexadecimal value. Whatever the value is, it will probably be that value every time you execute that program. At the Windows API level, controls are often identified by a control id. We can use the Contol ID shown in the Window Properties window in our program.

We are ready to write the application. I assume you can create a console application. If your system is 64-bit then you will need to change the "Platform Target" to "x64". The .Net Process class cannot do everything we need it to do when our application is not the same as the other application in terms of bits (32-bits and 64-bits). The "Platform Target" property is in the Build tab of the project's properties. If you need to automate a 32-bit application from a 64-bit application or 64-bit from 32-bit then you can do that but you will need to use the Windows API directly to do the equivalent of what we are using the .Net Process class to do.

You need to add a "using" for the "System.Diagnostics" and "System.Runtime.InteropServices" namespaces. You will also need to add the following to the "Program" class:

const int WM_COMMAND = 0x0111;
const int BN_CLICKED = 0;
const int ButtonId = 0x79;
const string fn = @"C:\Windows\system32\calc.exe";
[DllImport("user32.dll")]
static extern IntPtr GetDlgItem(IntPtr hWnd, int nIDDlgItem);
[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam);

The sample program will first look at the processes to find Calculator. We will assume the exe is at:

C:\Windows\system32\calc.exe

When we find a process that is executed from there, we know for sure it is the one we are looking for. Note that there are very many processes in a system; many more than most people are aware of. Each Windows Service is a process. Windows Services and other processes that we need to ignore do not have a window, so we can ignore all processes without a window. The following code will do all that:

IntPtr handle = IntPtr.Zero;
Process[] localAll = Process.GetProcesses();
foreach (Process p in localAll)
{
if (p.MainWindowHandle != IntPtr.Zero)
{
ProcessModule pm = GetModule(p);
if (pm != null && p.MainModule.FileName == fn)
handle = p.MainWindowHandle;
}
}
if (handle == IntPtr.Zero)
{
Console.WriteLine("Not found");
return;
}

The GetModule function is shown later in this article.

We are ready to click the button from within our application. To click the button we will send a BN_CLICKED notification message to the button's parent, the Calculator main window. To do that we will need both the button id (as in the preceding) and the handle of the button. The handle varies for each execution but we can determine the handle based on the handle of its parent and the control id. The Windows API function GetDlgItem does that, as in:

IntPtr hWndButton = GetDlgItem(handle, ButtonId);

Windows messages are sent to a window as determined by the window handle. Messages also have a message id that specifies what the message is for. We will send a WM_COMMAND message. The other two parameters we need to send the message is a wParam and a lParam. The value of those depend on what the message is (the message id). For a WM_COMMAND message doing what we are doing, the control id is passed as the high word of wParam and the low word of wParam is the notification code (BN_CLICKED). The lParam is the handle of the control. It seems unnecessary to pass both the control id and the control window's handle but that is the way it works. The following sends the message:

int wParam = (BN_CLICKED << 16) | (ButtonId & 0xffff);
SendMessage(handle, WM_COMMAND, wParam, hWndButton);

When the processes are being searched, since some modules might be restricted from access for security purposes, we catch errors using the GetModule function. It is very simple, as in the following:

private static ProcessModule GetModule(Process p)
{
ProcessModule pm = null;
try { pm = p.MainModule; }
catch
{
return null;
}
return pm;
}

To test the program, execute Calculator and type in "2+2=". Then execute the sample program. Every time it executes, 2 will be added to the Calculator value.

COMMENT USING

Trending up