Hey,
I would like to automate some actions I do regularly with some programs.
I would like to do something like to:
1. open a program
2. generate a click
3. check for text within the output of the program
4. when all is finished, close the program.
Is it hard to do? Do I have to interact with the program's memory?
Thanks a lot for any help.
Loading
Sam HobbsPosted Sep 29, 2011, 10:15 PM
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace _141784_automating_programs_using_C_Sharp
{
class Program
{
const string AVIReCompPath = @"C:\Program Files (x86)\AVI ReComp\AVIReComp.exe";
const int BN_CLICKED = 0;
const string AVIReCompClassName = "TMain_Form";
[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam);
delegate bool EnumWindowsCB(int hwnd, int lparam);
[DllImport("user32")]
static extern int EnumWindows(EnumWindowsCB cb, int lparam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int GetWindowThreadProcessId(int handle, out int processId);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetClassName(int hWnd, StringBuilder lpClassName, int nMaxCount);
static int lid = 0;
static void Main(string[] args)
{
System.Diagnostics.Process AVIReComp = new System.Diagnostics.Process();
AVIReComp.StartInfo.FileName = AVIReCompPath;
AVIReComp.Start();
AVIReComp.WaitForInputIdle();
Thread.Sleep(250);
Console.WriteLine("AVIReComp.Id: {0:X}", AVIReComp.Id);
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
EnumWindows(cb, AVIReComp.Id);
}
public static string GetWindowClassName(int hWnd)
{
StringBuilder buffer = new StringBuilder(128);
GetClassName(hWnd, buffer, buffer.Capacity);
return buffer.ToString();
}
public static bool MyEWP(int hwnd, int lparam)
{
int processId;
GetWindowThreadProcessId(hwnd, out processId);
if (processId == lid)
return true;
lid = processId;
if (processId != lparam)
return true;
string ClassName = GetWindowClassName(hwnd);
if (ClassName != AVIReCompClassName)
return true;
Console.WriteLine("Found {0:X} {1}", hwnd, ClassName);
// int lviCount = (int)SendMessage(hListView, LVM_GETITEMCOUNT, 0, IntPtr.Zero); // get number of items in ListView
return true;
}
}
}
Imri BarrPosted Sep 30, 2011, 10:36 AM
I've done some research in google and found out that this command doesn't send the WM_NOTIFY command so I need to send it myself right after I call TCM_SETCURSEL.
So, even after I called the WM_NOTIFY command myself, it still didn't do anything and the tab stayed the same.
Am I missing something?
Imri BarrPosted Sep 30, 2011, 6:05 AM
I was just wondering if there's a reason why the tab 'Additions' isn't showing up in Spy++ just until I click on it manually?
Also, in my program when I try to find the tab I get an empty handler but if I click on it first and then run the program it finds it with no problem.
Sam HobbsPosted Sep 30, 2011, 4:23 AM
if (JobsHandle != IntPtr.Zero)
Console.WriteLine("Found Jobs control panel: {0}", JobsHandle);
//
//IntPtr PageHandle = FindWindowEx(hwnd, IntPtr.Zero, "TPageControl", null);
IntPtr PageHandle = GetWindow(JobsHandle.ToInt32(), GW_CHILD);
if (PageHandle != IntPtr.Zero)
Console.WriteLine("Found Page: {0}", PageHandle);
//
IntPtr SourceAndOutputHandle = FindWindowEx(PageHandle.ToInt32(), IntPtr.Zero, "TTabSheet", "Source && Output");
if (SourceAndOutputHandle != IntPtr.Zero)
Console.WriteLine("Found SourceAndOutput: {0}", SourceAndOutputHandle);
//
IntPtr SourceFileHandle = FindWindowEx(SourceAndOutputHandle.ToInt32(), IntPtr.Zero, "TGroupBox", "Source file");
if (SourceFileHandle != IntPtr.Zero)
Console.WriteLine("Found Button: {0}", SourceFileHandle);
//
IntPtr ButtonHandle = FindWindowEx(SourceFileHandle.ToInt32(), IntPtr.Zero, "TButton", "Open AVI");
if (ButtonHandle != IntPtr.Zero)
Console.WriteLine("Found Button: {0}", ButtonHandle);
That code is not ideal. For one thing, it should quit when it gets a null (zero) for the handle. I also am not sure when to use int and when to use IntPtr. I would check that if it were me doing the development, but since it is you I will leave it for you to research.
Imri BarrPosted Sep 30, 2011, 3:44 AM
Eventually, I managed to solve this problem by accessing the button using a method much like a DOM in javascript.
What I did was to access the main window, then I accessed the child of it and so on till I got the button.
What I was missing this whole time was an empty container that blocked the FindWindowEx function from finding the correct children.
Here's my final code:
IntPtr hwnd = (IntPtr)FindWindow(null, "AVI ReComp 1.5.3");
IntPtr hwnd_sourceFile = IntPtr.Zero;
IntPtr hwnd_targetFile = IntPtr.Zero;
IntPtr parent = IntPtr.Zero;
IntPtr container = IntPtr.Zero;
parent = FindWindowEx((IntPtr)hwnd, IntPtr.Zero, null, "Job Control Panel");
parent = FindWindowEx((IntPtr)parent, IntPtr.Zero, null, null);
container = FindWindowEx((IntPtr)parent, IntPtr.Zero, null, "Source && Output");
hwnd_sourceFile = FindWindowEx((IntPtr)container, IntPtr.Zero, null, "Source File");
hwnd_sourceFile = FindWindowEx((IntPtr)hwnd_sourceFile, IntPtr.Zero, null, "");
hwnd_targetFile = FindWindowEx((IntPtr)container, IntPtr.Zero, null, "Output File");
hwnd_targetFile = FindWindowEx((IntPtr)hwnd_targetFile, IntPtr.Zero, "TEdit", "");
Now I can finally begin working on my automation program.
Thanks a lot for the help!
Sam HobbsPosted Sep 29, 2011, 9:01 PM
So you can try using FindWindow and SendKeys, but I cannot help with that.
Sam HobbsPosted Sep 29, 2011, 7:04 PM
Do the following. Execute AVIReComp, then execute Spy++. Apparently it has two Finder tools. So go to the Spy menu and choose "Find Window..." from there. Drag the finder tool to the button and then release the mouse. You should get a Find Window box. The caption is "Open AVI" and the class is TButton. Note that at the bottom are radio buttons for Properties and for Messages; the default is Properties. Click OK. When I do that, the Control ID is 000707F8. I notice that the 000707F8 is also the window handle of the button. So when I exit from AVI ReComp and execute it again, the Control ID is different from before but the button's control id is still the same as it's window handle. So as I said, the technique does not always work; you must analyze the application. In this case, the control id is the same as the window handle, so that is easy enough to deal with; you just need to know about it.
Imri BarrPosted Sep 29, 2011, 2:29 PM
The control Id is 00010380, so how am I supposed to accomplish that? This is what I came up with:
hwnd = (IntPtr)FindWindow(null, "AVI ReComp 1.5.3");
SendMessage((int)hwnd, BN_CLICKED, 0x00010380, 0);
Thanks again!
Sam HobbsPosted Sep 29, 2011, 2:19 PM
Imri BarrPosted Sep 29, 2011, 12:59 PM
Thanks again for your reply!
You said that in the function SendMessage there's a parameter for the parent window and a parameter for the button id. Where are they located? The syntax for the function is this:
public static extern int SendMessage(int hWnd, uint Msg, int wParam, int lParam);
so I don't really know what is where since every time I searched for an example of this function I got something like this:
SendMessage(hWnd,BN_CLICKED,0,0);
No id was passed, nor the parent window handle, only the button's handler.
I'm trying to automate the program avi ReComp by the way.
Just trying to make it click the button Open AVI on the first screen.
Thanks a lot again!
Sam HobbsPosted Sep 29, 2011, 12:42 PM
Also, be sure you use the WaitForInputIdle Method after starting the process and before using the window handle.
I will be gone for a few hours.
Imri BarrPosted Sep 29, 2011, 4:16 AM
Is there more to it?
Do you have an example of how to use the winAPI to find a button by its control id rather than it's text?
Thanks a lot.
Sam HobbsPosted Sep 28, 2011, 8:51 PM
I suggest looking at the documentation of the Process class; become familiar with it. Note that it has a MainWindowHandle Property; that will not work for all applications but it will work for most. Be sure to read the documentation, especially the part about using the WaitForInputIdle Method.
Note that the Spy++ tool has a "Finder" tool. You can use that to drag the mouse to a window. In this case, a button is a window. So drag the mouse to the button in the application and then release the mouse then click Ok in the box that appears after that. Then the information shown in the window information box will include the control id. Then you can use that in a message you send.
I assume you are familiar with use of DllImport to use the Windows API. I hope that with the information above, you are able to create a SendMessage for a BN_CLICKED notification message.
Imri BarrPosted Sep 28, 2011, 6:15 PM
Thanks a lot for the help!
I've been trying to do what you said and so far I found the window I was trying to find and found a window inside it but that's about it.
I used the FindWindow function to locate the first window, then used the FindWindowEx to locate a panel within the window but when I try to go even farther to find the desired button, I simply can't (I get 0 for the IntPtr).
This is my code so far:
System.Diagnostics.Process.Start(@"C:\Program Files\AVI ReComp\AVIReComp.exe");
//System.Threading.Thread.Sleep(3000);
hwnd = (IntPtr)FindWindow(null, "AVI ReComp 1.5.3");
//if (SetForegroundWindow(hwnd))
//{
IntPtr parent = IntPtr.Zero;
parent = FindWindowEx((IntPtr)hwnd, IntPtr.Zero, null, "Control Panel");
hwndChild = FindWindowEx((IntPtr)hwnd, IntPtr.Zero, "Button", "Open Movie");
But the hwndChild always is 0, no matter what I try there:
hwndChild = FindWindowEx((IntPtr)hwnd, IntPtr.Zero, null, "Open Movie");
hwndChild = FindWindowEx((IntPtr)parent, IntPtr.Zero, null, "Open Movie");
hwndChild = FindWindowEx((IntPtr)parent, IntPtr.Zero, "Button", "Open Movie");
Am I missing anything?
Thanks again!
Sam HobbsPosted Sep 28, 2011, 1:24 PM
The best way to click a button in another wondow is to send a BN_CLICKED notification message to the button's parent window. To do that, you need to have the window handle and you need to have the control id of the button. To get the control id, usually we can use the Spy++ tool and the control id is usually always the same. Look for the Spy++ tool in the Visual Studio tools menu. I think you can use the Process class to get the window handle, but if you need help after you read articles about the Process class, then create a new thread to ask about it. I assume you will use a Windows Forms application and if you do, then please ask your questions in the Windows Forms forum.
In a manner similar sending a notification message, you probably can send a WM_GETTEXT message to the other application's window to get text from a textbox. Actually, what is a TextBox in .Net is called an Edit Control in the Windows API.
Then when you are done, send a WM_CLOSE message. If sending a WM_CLOSE message to the application does not work, then you need to determine what does work for that specific application. Please do not "kill" the application.
If you need details of any of this, then it will be better to ask specific questions in separate threads.