在MSPaint中模拟鼠标单击

我有一个控制台应用程序应该在MSPaint中绘制一个随机图片(鼠标按下 – >让光标随机绘制一些东西 – >鼠标向上。这是我到目前为止(我在Main方法中添加了注释,以便更好地理解我想要的内容)实现):

 [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))); } 

我从这里获得了点击模拟的代码。

点击被模拟,但它不会绘制任何东西。 似乎点击在MSPaint内部不起作用。 光标变为MSPaint的“十字架”,但正如我所提到的……点击似乎不起作用。

FindWindowhwndMain的值hwndMain为值0.将参数mspaint更改为MSPaintApp不会更改任何内容。 hwndMain的值保持为0。

如果有帮助,这是我的OpenPaint()方法:

 private static void OpenPaint() { Process.process = new Process(); process.StartInfo.FileName = "mspaint.exe"; process.StartInfo.WindowStyle = "ProcessWindowStyle.Maximized; process.Start(); } 

我究竟做错了什么?

正如我所承诺的那样,我昨天var hwndMain = FindWindow("mspaint ", null);测试了 – 说实话我的光标刚刚移动,但不在窗口中,并且没有任何影响 – 因为我调试时看到var hwndMain = FindWindow("mspaint ", null); 值为0 。 我虽然这必须是问题,所以我确实看了一下另一个stackoverflow主题,你得到了你的代码。 我认识到解决方案是使用他们在FindWindow()的不同窗口名称 – 所以我尝试了。

 var hwndMain = FindWindow("MSPaintApp", null); 

在更改了方法调用后,它对我有用 – 但是 – 在移动MsPaint之后,光标仍处于原始打开位置 – 您可能想要考虑这一点并向窗口询问它的位置。 是否可以使用Win7 / 8/10更改名称?

编辑:

在Windows 10上,绘画的名称似乎已经改变了 – 所以我猜你在获得正确的窗口句柄时仍然有问题 – 汉斯·帕斯特(Hans Passant)certificate这是错误的,他解释了处理程序的问题(链接如下)。 解决这个问题的一种方法是从流程本身获取处理程序而不是从FindWindow()获取它

我建议你像这样改变你的OpenPaint()

  private IntPtr OpenPaint() { Process process = new Process(); process.StartInfo.FileName = "mspaint.exe"; process.StartInfo.WindowStyle = ProcessWindowStyle.Maximized; process.Start(); // As suggested by Thread Owner Thread.Sleep so we get no probs with the handle not set yet //Thread.Sleep(500); - bad as suggested by @Hans Passant in his post below, // a much better approach would be WaitForInputIdle() as he describes it in his post. process.WaitForInputIdle(); return process.MainWindowHandle; } 

链接汉斯帕斯特解释说明为什么Thread.Sleep()只是一个坏主意。

接下来是电话:

 IntPtr hwndMain = OpenPaint(); // Method that opens MSPaint 

这样你就可以很好地获得正确的窗口,并且你的代码应该正常工作,无论微软如何在win10中调用它

 IntPtr hwndMain = FindWindow("mspaint", null); 

这还不够好。 在pinvoke代码中常见的错误,C#程序员往往过分依赖exception来跳出屏幕并将它们打到脸上以告诉他们出了问题。 .NET Framework确实做得非常好。 但是,当您使用基于C语言的api(如winapi)时,这种方法的工作方式不同。 C是一种恐龙语言,根本不支持例外。 它仍然没有。 当pinvoke管道发生故障时,您只会遇到exception,通常是因为[DllImport]声明错误或DLL丢失。 当函数执行成功但返回失败返回码时,它不会说出来。

这确实使得检测和报告失败完全是您自己的工作。 只需转到MSDN文档 ,它总是告诉您winapi函数如何表示事故。 不完全一致,所以你必须看,在这种情况下,当无法找到窗口时,FindWindow返回null。 所以总是这样编码:

 IntPtr hwndMain = FindWindow("mspaint", null); if (hwndMain == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(); 

对所有其他pinvokes也这样做。 现在你可以获得成功,你可以得到一个例外,而不是用坏数据。 对于糟糕的数据,情况往往不是很糟糕。 NULL实际上是一个有效的窗口句柄,OS会假设你的意思是桌面窗口。 哎哟。 您正在自动化完全错误的过程。


理解FindWindow()失败的原因确实需要一些洞察力,它不是很直观,但良好的错误报告对于实现这一目标至关重要。 Process.Start()方法只确保程序启动,它不会以任何方式等待进程完成初始化。 在这种情况下,它不会等到它创建了它的主窗口。 因此,FindWindow()调用过早地执行大约几十毫秒。 额外令人费解,因为它在您调试和单步执行代码时工作得很好。

也许你认识到这种不幸事件,这是一个线程种族错误 。 最卑鄙的编程错误。 由于比赛与时间有关,臭名昭着不会导致失败并且很难调试。

希望您意识到在接受的答案中提出的解决方案也不够好。 任意添加Thread.Sleep(500)只会提高你在调用FindWindow()之前等待足够长时间的几率。 但你怎么知道500足够好? 它总是足够好吗?

没有 .Thread.Sleep() 永远不是线程竞争错误的正确解决方案。 如果用户的机器运行缓慢或负载太重而缺少可用的未映射RAM,那么几毫秒就会变成几秒钟。 你必须处理最糟糕的情况 ,而且确实是最糟糕的,一般情况下 ,机器开始颠簸时你需要考虑的最小值只需要10秒。 这变得非常不切实际。

可靠地互锁这是一个普遍需求,操作系统具有启发式function。 需要是一个启发式而不是对同步对象的WaitOne()调用,因为进程本身根本不合作。 您通常可以假设GUI程序在开始询问通知时已经充分发展。 用Windows白话“抽取消息循环”。 该启发式也使其成为Process类。 固定:

 private static void OpenPaint() { Process.process = new Process(); process.StartInfo.FileName = "mspaint.exe"; process.StartInfo.WindowStyle = "ProcessWindowStyle.Maximized; process.Start(); process.WaitForInputIdle(); // <=== NOTE: added } 

如果我没有指出你应该使用内置的api,那将是我的疏忽。 称为UI自动化,在System.Windows.Automation命名空间中包含。 处理所有那些讨厌的小细节,比如线程竞赛和将错误代码转换为良好的exception。 最相关的教程可能就在这里 。