ButtonClicking.zip
Update: For Windows 10
(and I assume Windows 8 and above) the Windows Calculator Accesssory cannot
be used for this. See the modification of the attached sample code necessary
for the Windows Character Map Accessory for the ButtonId and fn.
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 or can click the "Select" button for the Windows Charactrer Map
Acesssory.
This article was originally dwritten using Windows 7 but the Windows Calculator Acesssory in
Windows 10 (and I assume Windows 8 and above) cannot be used in the manner it is used here.
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
or Windows Charactrer Map Acesssory.
For 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.
For the Windows Charactrer Map Acesssory, we just need to execute it.
When the "Select" button is clicked the characer is put into the selected
box. We canmove to a different character usign the mouse or keyboard. We can
see that the "Select" button is clicked becasue the specified character is
put into the selected box.
The next thing to do is to determine the identity of the button to be
clicked so our program can click it. With the acesssory
executing, go to Visual Studio. In the "Tools" menu is "Spy++". If your
system is a 64-bit system then you need to use the 64-bit version of Spy++.
The Spy++ toolbar will look like:

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

That will open a "Find Window" dialog. Now first, ensure that the
acesssory is visible; at least the relevant 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:

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 5 tabs. The five tabs are
General, Styles, Windows, Class and Process. The wndow will look like:

Look for "Contol ID" near the botom of the first (General) tab. It will
probably have the value "00000079" for the Windows Calculator Acesssory or
"00000067" for the Windows Charactrer Map Acesssory. It is a hexadecimal value. Whatever the
value is, it will probably be that value every timne 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);
For the Windows Charactrer Map Acesssory use the following for the ButtonId
and fn:
const int ButtonId = 0x67;
const string fn = @"C:\Windows\system32\charmap.exe";
The sample program will first look at the processes to find Calculator.
We will assume the exe is at:
C:\Windows\system32\calc.exe
The Windows Charactrer Map Acesssory is at:
C:\Windows\system32\charmap.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, first execute Calculator and type in "2+2=". Then execute
the sample program. Every time it executes, 2 will be added to the
Calculator value.