在 MSPaint 中模拟鼠标点击

Simulate mouse click in 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 的 "cross",但正如我提到的...点击似乎不起作用。

FindWindowhwndMain 的值设置为值 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();
}

我做错了什么?

正如承诺的那样,我昨天自己测试了它 - 老实说,我的光标只是移动了,但没有在 window 中,并且没有任何影响 - 当我调试时,我看到 var hwndMain = FindWindow("mspaint ", null);是值 0。我虽然这一定是问题所在,所以我确实查看了另一个 Whosebug 主题,您从中获得了代码。我认识到解决方案使用的是不同的 window-名称,他们在 FindWindow() 照顾 - 所以我尝试了。

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

更改方法调用后它对我有用 - 虽然 - 在移动 MsPaint 后光标仍处于原始打开位置 - 您可能需要考虑一下并询问 window 它的位置也许. 名称可能随 Win7 / 8 / 10 更改了吗?

编辑:

在 Windows 10 上,油漆的名称似乎已更改 - 所以我猜你在获得正确的 window 手柄方面仍然存在问题 - Hans Passant 证明这是错误的,他解释得很好处理程序有什么问题(下面的 link)。解决这个问题的一种方法是从进程本身获取处理程序,而不是从 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;
 }

Link 到 解释为什么 Thread.Sleep() 只是一个坏主意。

随后调用:

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

这样你就可以得到正确的 window 句柄,你的代码应该可以正常工作,无论微软在 win10 中如何调用它

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

这还不够好。 pinvoke 代码中的常见错误,C# 程序员倾向于完全依赖异常以跳出屏幕并打他们的脸告诉他们出了什么问题。 .NET Framework 在这方面做得非常好。但当您使用基于 C 语言的 api 时, 的工作方式相同,例如 winapi。 C 是一种恐龙语言,根本不支持异常。它仍然没有。只有在 pinvoke 管道失败时才会出现异常,通常是因为 [DllImport] 声明错误或缺少 DLL。当函数执行成功时它不会说话,但 return 是一个失败的 return 代码。

这确实使检测和报告故障完全是您自己的工作。只需转向 MSDN documentation,它总是告诉您 winapi 函数如何指示事故。不完全一致,因此您必须查看,在这种情况下,当找不到 window 时,FindWindow returns null。所以总是这样编码:

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

对所有其他 pinvoke 也执行此操作。现在您可以取得进展,您将可靠地获得异常,而不是继续处理不良数据。与不良数据经常发生的情况一样,这还不够糟糕。 NULL 实际上是一个有效的 window 句柄,OS 会假定您指的是桌面 window。哎哟。您正在自动化完全错误的过程。


理解为什么 FindWindow() 失败确实需要一点洞察力,它不是很直观,但良好的错误报告对于到达那里至关重要。 Process.Start() 方法只确保程序启动,它不会以任何方式等待进程完成初始化。在这种情况下,它不会等到它创建了它的 main window。所以 FindWindow() 调用执行大约,哦,太早了几十毫秒。令人费解的是,当您调试和单步执行代码时它工作得很好。

也许您认识到这种事故,它是一个线程竞争错误。最卑鄙的编程错误。臭名昭著的是不会始终如一地导致故障并且很难调试,因为竞争取决于时间。

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

没有。 Thread.Sleep() 绝不是 线程竞争错误的正确解决方案。如果用户的机器运行缓慢或负载过重且可用的未映射 RAM 不足,则几毫秒会变成几秒。您必须处理 最坏的情况 ,这确实是最糟糕的,通常只有大约 10 秒是机器开始抖动时您需要考虑的最短时间。这变得非常不切实际。

可靠地 互锁是一种普遍的需求,因此 OS 有一个启发式方法。需要启发式而不是对同步对象的 WaitOne() 调用,因为进程本身根本不合作。您通常可以假设 GUI 程序在开始请求通知时已经取得了足够的进展。 "Pumping the message loop" Windows 白话。该启发式也进入了过程 class。修复:

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 命名空间中。处理所有那些讨厌的小细节,比如线程竞争和将错误代码转化为良好的异常。最相关的教程是 probably here