简体   繁体   English

在MSPaint中模拟鼠标单击

[英]Simulate mouse click in MSPaint

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): 我有一个控制台应用程序应该在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)));
}

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. 似乎点击在MSPaint内部不起作用。 The cursor changes to the "cross" of MSPaint but as I mentioned...the click doesn't seem to work. 光标变为MSPaint的“十字架”,但正如我所提到的......点击似乎不起作用。

FindWindow sets the value of hwndMain to value 0. Changing the parameter mspaint to MSPaintApp doesn't change anything. FindWindowhwndMain的值hwndMain为值0.将参数mspaint更改为MSPaintApp不会更改任何内容。 The value of hwndMain stays 0. hwndMain的值保持为0。

If it helps, here is my OpenPaint() method: 如果它有帮助,这是我的OpenPaint()方法:

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? 我究竟做错了什么?

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

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. 在pinvoke代码中常见的错误,C#程序员往往过分依赖异常来跳出屏幕并将它们打在脸上以告诉他们出了问题。 The .NET Framework does do that extra-ordinarily well. .NET Framework确实做得非常好。 But that does not work the same way when you use an api that's based on the C language, like the winapi. 但是,当您使用的是基于C语言的API,就像WINAPI 相同的方式工作。 C is a dinosaur language and did not support exceptions at all. C是一种恐龙语言,根本不支持例外。 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. 当pinvoke管道发生故障时,您只会遇到异常,通常是因为[DllImport]声明错误或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. 只需转到MSDN文档 ,它总是告诉您winapi函数如何表示事故。 Not completely consistent, so you do have to look, in this case FindWindow returns null when the window could not be found. 不完全一致,所以你必须要看,在这种情况下,FindWindow在找不到窗口时返回null。 So always code it like this: 所以总是这样编码:

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

Do this for all the other pinvokes as well. 对所有其他的pinvokes也这样做。 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. NULL实际上是一个有效的窗口句柄,操作系统会假设您的意思是桌面窗口。 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. 理解FindWindow()失败的原因确实需要一些洞察力,它不是很直观,但良好的错误报告对于实现这一目标至关重要。 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. Process.Start()方法只确保程序启动,它不会以任何方式等待进程完成初始化。 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. 所以FindWindow()调用过早地执行了几十毫秒。 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(). 任意添加Thread.Sleep(500)只会提高你在调用FindWindow()之前等待足够长的几率。 But how do you know that 500 is good enough? 但你怎么知道500足够好呢? It is always good enough? 总是足够好吗?

No. Thread.Sleep() is never the correct solution for a threading race bug. 没有 .Thread.Sleep() 永远不是线程竞争错误的正确解决方案。 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. 如果用户的机器速度很慢或负载太重而缺少可用的未映射RAM,那么几毫秒就会变成几秒钟。 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. 你必须处理最糟糕的情况 ,而且确实是最糟糕的,通常只有~10秒是机器开始颠簸时需要考虑的最小值。 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. 需要是一个启发式而不是对同步对象的WaitOne()调用,因为进程本身根本不合作。 You can generally assume that a GUI program has progressed sufficiently when it starts asking for notifications. 您通常可以假设GUI程序在开始询问通知时已经取得了足够的进展。 "Pumping the message loop" in Windows vernacular. Windows语言中的“消息循环”。 That heuristic also made it into the Process class. 该启发式也使其成为Process类。 Fix: 固定:

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

I would be remiss if I didn't point out that you should use the built-in api for this. 如果我没有指出你应该使用内置的api,那将是我的疏忽。 Called UI Automation, wrapped ably in the System.Windows.Automation namespace. 称为UI自动化,在System.Windows.Automation命名空间中包含。 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); 正如我所承诺的那样,我昨天var hwndMain = FindWindow("mspaint ", null);测试了 - 说实话我的光标刚刚移动,但不在窗口中,并且没有任何影响 - 因为我调试时看到var hwndMain = FindWindow("mspaint ", null); was value 0 . 值为0 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. 我虽然这是问题,所以我确实看了一下另一个stackoverflow主题,你得到了你的代码。 I recognized the solution was using a different window-name they were looking after at FindWindow() - so I did try. 我认识到解决方案是使用他们在FindWindow()的不同窗口名称 - 所以我尝试了。

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

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. 在更改了方法调用后,它对我有用 - 但是 - 在移动MsPaint之后,光标仍处于原始打开位置 - 您可能想要考虑这个问题并向窗口询问它的位置。 Did the Name maybe changed with Win7 / 8 / 10 ? 是否可以使用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). 在Windows 10上,绘画的名称似乎发生了变化 - 所以我猜你在获得正确的窗口句柄方面仍然存在问题 - Hans Passant证明这是错误的,他很好地解释了处理程序的问题(链接如下)。 One way to solve this would be to get you handler from the process itself instead of getting it from FindWindow() 解决这个问题的一种方法是从流程本身获取处理程序而不是从FindWindow()获取它

I suggest you change your OpenPaint() like this: 我建议你像这样更改你的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 to Hans Passant decription for the explaination why Thread.Sleep() just is a bad idea. 链接汉斯帕斯特解释说明为什么Thread.Sleep()只是一个坏主意。

Followed by the call: 接下来是电话:

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

This way you should be fine getting the right windowhandle, and your code should be working, no matter how microsoft called it in win10 这样你就可以很好地获得正确的窗口,并且你的代码应该正常工作,无论微软如何在win10中调用它

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM