在 MSPaint 中模拟鼠标点击

Posted

技术标签:

【中文标题】在 MSPaint 中模拟鼠标点击【英文标题】:Simulate mouse click in MSPaint 【发布时间】:2017-04-17 10:05:09 【问题描述】:

我有一个控制台应用程序,它应该在 MSPaint 中绘制一张随机图片(鼠标向下 -> 让光标随机绘制一些东西 -> 鼠标向上。这就是我到目前为止所拥有的(我在 Main 方法中添加了 cmets更好地理解我想要实现的目标):

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

我从here得到了这个点击模拟的代码。

点击被模拟,但它没有绘制任何东西。似乎单击在 MSPaint 中不起作用。光标变为 MSPaint 的“十字”,但正如我所提到的......点击似乎不起作用。

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();

我做错了什么?

【问题讨论】:

第一步:尝试它是否与 Paint 以外的其他应用程序配合得更好,然后报告! 嗨!我喜欢这个问题并且很好奇 - 你是否已经找到了答案或者这仍然是开放的?如果您现在还没有找到答案,我今晚会亲自尝试。 @TripleEEE 我还没有找到答案。我不能检查,因为我病了.. 嗨@diiN_ 刚刚发布了一个答案;) 【参考方案1】:
IntPtr hwndMain = FindWindow("mspaint", null);

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

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

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

对所有其他 pinvokes 也执行此操作。现在您可以取得成功,您将可靠地获得异常,而不是继续使用不良数据。就像坏数据经常发生的情况一样,这还不够糟糕。 NULL 实际上是一个有效的窗口句柄,操作系统会假设你的意思是桌面窗口。哎哟。您正在自动化完全错误的过程。


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

也许您认识到这种事故,它是一个线程竞争错误。最卑鄙的编程错误。臭名昭著的原因是由于比赛与时间有关,因此不会持续导致失败并且很难调试。

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

没有。 Thread.Sleep() 从不是线程竞争错误的正确解决方案。如果用户的机器速度很慢或者负载太重而缺少可用的未映射 RAM,那么几毫秒就会变成几秒钟。您必须处理最坏情况,而且确实是最坏的情况,通常只有约 10 秒是您在机器开始抖动时需要考虑的最短时间。这变得非常不切实际。

可靠地联锁这个可靠是一种常见的需求,操作系统有一个启发式方法。需要是启发式而不是对同步对象的 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 命名空间中。处理所有那些讨厌的小细节,比如线程竞争和将错误代码变成好的异常。最相关的教程是probably here。

【讨论】:

【参考方案2】:

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

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

在更改方法调用后,它对我有用——尽管——在移动 MsPaint 之后,光标仍处于原来的打开位置——你可能想考虑一下,并询问窗口的位置。 名称可能随着 Win7 / 8 / 10 改变?

编辑:

在 Windows 10 上,paint 的名称似乎已更改 - 所以我猜你在获取正确的窗口句柄方面仍然存在问题 - Hans Passant 证明这是错误的,他很好地解释了处理程序的问题(下面的链接) .解决此问题的一种方法是从进程本身获取处理程序,而不是从 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;
 

链接到Hans Passant decription 以解释为什么 Thread.Sleep() 只是一个坏主意。

接着调用:

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

这样你就可以得到正确的窗口句柄,并且你的代码应该可以正常工作,不管微软如何在 win10 中调用它

【讨论】:

我以前也有过这样的情况,但后来根本没有用。使用Console.WriteLine(process.ProcessName); 时,输出为“mspaint”。光标在油漆内移动,但没有点击... 但是您不要求进程名称 - 您正在寻找 windowName,这是不同的。 如果 lpWindowName 参数不为 NULL,则 FindWindow 调用 GetWindowText 函数来检索窗口名称以进行比较 参见:msdn.microsoft.com/de-de/library/windows/desktop/… @diiN_ 如果您提供 `OpenPaint(); 的代码可能会有所帮助// 打开 MSPaint` 的方法 - 可能有一些错误。顺便说一句:您是否调试过 FindWindow 是否得到句柄,或者它是否为零? @diiN_ 好的 - 我会试试这个 - 你在 Win7/8/10 上吗? @diiN_ 很好,我可以帮你!我将Thread.Sleep() 添加到我的答案中,供路过的其他人使用;)

以上是关于在 MSPaint 中模拟鼠标点击的主要内容,如果未能解决你的问题,请参考以下文章

易语言中!模拟鼠标点击无效

vb后台模拟鼠标点击网页

C#怎么模拟鼠标点击

vbs/vb如何模拟鼠标点击

在后台窗口中模拟鼠标点击

写一个程序,模拟人操作鼠标,进行操作,包括点击,和使用应用程序(如:浏览器),求实现方法。