I have a console application which should paint a random picture in MSPaint (mouse down -> let the cursor randomly paint something -> mouse up. This is what I have so far (I added comments to the Main
method for better understanding what I want to achieve):
[DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
public static extern void mouse_event(long dwFlags, uint dx, uint dy, long cButtons, long dwExtraInfo);
private const int MOUSEEVENTF_LEFTDOWN = 0x201;
private const int MOUSEEVENTF_LEFTUP = 0x202;
private const uint MK_LBUTTON = 0x0001;
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr parameter);
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowTitle);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);
static IntPtr childWindow;
private static bool EnumWindow(IntPtr handle, IntPtr pointer)
{
childWindow = handle;
return false;
}
public static void Main(string[] args)
{
OpenPaint(); // Method that opens MSPaint
IntPtr hwndMain = FindWindow("mspaint", null);
IntPtr hwndView = FindWindowEx(hwndMain, IntPtr.Zero, "MSPaintView", null);
// Getting the child windows of MSPaintView because it seems that the class name of the child isn't constant
EnumChildWindows(hwndView, new EnumWindowsProc(EnumWindow), IntPtr.Zero);
Random random = new Random();
Thread.Sleep(500);
// Simulate a left click without releasing it
SendMessage(childWindow, MOUSEEVENTF_LEFTDOWN, new IntPtr(MK_LBUTTON), CreateLParam(random.Next(10, 930), random.Next(150, 880)));
for (int counter = 0; counter < 50; counter++)
{
// Change the cursor position to a random point in the paint area
Cursor.Position = new Point(random.Next(10, 930), random.Next(150, 880));
Thread.Sleep(100);
}
// Release the left click
SendMessage(childWindow, MOUSEEVENTF_LEFTUP, new IntPtr(MK_LBUTTON), CreateLParam(random.Next(10, 930), random.Next(150, 880)));
}
I got this code for the click simulation from here.
The click gets simulated but it doesn't paint anything. It seems that the click doesn't work inside MSPaint. The cursor changes to the "cross" of MSPaint but as I mentioned...the click doesn't seem to work.
FindWindow
sets the value of hwndMain
to value 0. Changing the parameter mspaint
to MSPaintApp
doesn't change anything. The value of hwndMain
stays 0.
If it helps, here is my OpenPaint()
method:
private static void OpenPaint()
{
Process.process = new Process();
process.StartInfo.FileName = "mspaint.exe";
process.StartInfo.WindowStyle = "ProcessWindowStyle.Maximized;
process.Start();
}
What am I doing wrong?
That isn't good enough. Common mistake in pinvoke code, C# programmers tend to rely entirely too much on an exception to jump off the screen and slap them in the face to tell them that something went wrong. The .NET Framework does do that extra-ordinarily well. But that does not work the same way when you use an api that's based on the C language, like the winapi. C is a dinosaur language and did not support exceptions at all. It still doesn't. You'll only get an exception when the pinvoke plumbing failed, usually because of the bad [DllImport] declaration or a missing DLL. It does not speak up when the function executed successfully but returns a failure return code.
That does make it entirely your own job to detect and report failure. Just turn to the MSDN documentation, it always tells you how a winapi function indicates a mishap. Not completely consistent, so you do have to look, in this case FindWindow returns null when the window could not be found. So always code it like this:
Do this for all the other pinvokes as well. Now you can get ahead, you'll reliably get an exception instead of plowing on with bad data. Which, as is so often the case with bad data, isn't quite bad enough. NULL is actually a valid window handle, the OS will assume you meant the desktop window. Ouch. You are automating the completely wrong process.
Understanding why FindWindow() fails does require a bit of insight, it is not very intuitive, but good error reporting is crucial to get there. The Process.Start() method only ensures that the program got started, it does not in any way wait until the process has completed its initialization. And in this case, it does not wait until it has created its main window. So the FindWindow() call executes about, oh, a couple of dozen milliseconds too early. Extra puzzling since it works just fine when you debug and single-step through the code.
Perhaps you recognize this kind of mishap, it is a threading race bug. The most dastardly kind of programming bug. Infamous for not causing failure consistently and very hard to debug since races are timing dependent.
Hopefully you realize that the proposed solution in the accepted answer is not good enough either. Arbitrarily adding Thread.Sleep(500) merely improves the odds that you now wait long enough before calling FindWindow(). But how do you know that 500 is good enough? It is always good enough?
No. Thread.Sleep() is never the correct solution for a threading race bug. If the user's machine is slow or is too heavily loaded with a shortage of available unmapped RAM then a couple of milliseconds turns into seconds. You have to deal with the worst case, and it is worst indeed, only ~10 seconds is in general the minimum you need to consider when the machine start thrashing. That's getting very unpractical.
Interlocking this reliably is such a common need that the OS has a heuristic for it. Needs to be a heuristic instead of a WaitOne() call on a synchronization object since the process itself doesn't cooperate at all. You can generally assume that a GUI program has progressed sufficiently when it starts asking for notifications. "Pumping the message loop" in Windows vernacular. That heuristic also made it into the Process class. Fix:
I would be remiss if I didn't point out that you should use the built-in api for this. Called UI Automation, wrapped ably in the System.Windows.Automation namespace. Takes care of all those nasty little details, like threading races and turning error codes into good exceptions. Most relevant tutorial is probably here.
As is promised I tested it myself yesterday - to be honest my cursor just moved, but not in the window, and without any affect - as I did debug I saw that the
var hwndMain = FindWindow("mspaint ", null);
was value0
. I though that this has to be the problem so I did take a look at the other stackoverflow topic, you got your code from. I recognized the solution was using a different window-name they were looking after atFindWindow()
- so I did try.After changing the methodcall it worked out for me - though - after moving the MsPaint there cursor was still in the original open Position - you might want to think about that and ask the window for it's position maybe. Did the Name maybe changed with Win7 / 8 / 10 ?
Edit:
On Windows 10 the name for paint seems to be changed - so I guess you still have problems on getting the right window handle - this was proved wrong by Hans Passant, who explained nicely whats the Problem with the handler (link below). One way to solve this would be to get you handler from the process itself instead of getting it from
FindWindow()
I suggest you change your
OpenPaint()
like this:Link to Hans Passant decription for the explaination why Thread.Sleep() just is a bad idea.
Followed by the call:
This way you should be fine getting the right windowhandle, and your code should be working, no matter how microsoft called it in win10