在MSPaint中模拟鼠标单击

dii*_*___ 5 c# click mouseevent cursor-position

我有一个控制台应用程序应该在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)));
}
Run Code Online (Sandbox Code Playgroud)

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

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

FindWindow将值设置hwndMain为值0.将参数更改mspaintMSPaintApp不更改任何内容.hwndMain逗留0 的值.

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

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

我究竟做错了什么?

Han*_*ant 8

IntPtr hwndMain = FindWindow("mspaint", null);
Run Code Online (Sandbox Code Playgroud)

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

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

IntPtr hwndMain = FindWindow("mspaint", null);
if (hwndMain == IntPtr.Zero) throw new System.ComponentModel.Win32Exception();
Run Code Online (Sandbox Code Playgroud)

对所有其他的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
}
Run Code Online (Sandbox Code Playgroud)

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


小智 -1

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

var hwndMain = FindWindow("MSPaintApp", null);
Run Code Online (Sandbox Code Playgroud)

更改方法调用后,它对我有用 - 不过 - 在移动 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;
 }
Run Code Online (Sandbox Code Playgroud)

链接到Hans Passant 描述,解释为什么 Thread.Sleep() 是一个坏主意。

接下来是调用:

IntPtr hwndMain = OpenPaint(); // Method that opens MSPaint
Run Code Online (Sandbox Code Playgroud)

这样你就可以得到正确的窗口句柄,并且你的代码应该可以工作,无论微软在 win10 中如何称呼它