在MSPaint中模拟鼠标点击

5

我有一个控制台应用程序,它应该在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的值设置为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();
}

我做错了什么?


第一步:尝试在除了画图之外的其他应用程序中运行,然后报告! - TaW
你好!我喜欢这个问题,很好奇 - 你已经找到答案了还是仍然未解决?如果你现在还没有找到答案,我今晚会尝试自己解决。 - TripleEEE
@TripleEEE 我还没有找到答案。我现在生病了,无法检查。 - diiN__________
嗨,@diiN_ 刚刚发布了一个答案 ;) - TripleEEE
2个回答

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

这还不够好。在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();

对于所有其他的pinvoke,同样都要做这个处理。现在你可以继续前进了,在有坏数据时可靠地获得异常。通常情况下,坏数据并不太糟糕。NULL实际上是一个有效的窗口句柄,操作系统会认为你指的是桌面窗口。糟糕极了,你正在自动化完全错误的过程。
理解FindWindow()失败的原因需要一些洞察力,它并不是很直观,但良好的错误报告对于达到目标至关重要。Process.Start()方法仅确保程序已启动,它不会以任何方式等待进程完成其初始化。在这种情况下,它不会等待进程创建其主窗口。因此,FindWindow()调用大约提前了几十毫秒。这让人感到非常困惑,因为当你通过代码进行调试和单步执行时,它可以正常工作。
也许你会认识到这种错误,这是线程竞争错误。最卑鄙的编程错误类型。因为竞争取决于时间,所以臭名昭著,不会始终导致失败,并且非常难以调试。
希望你意识到所接受答案中提出的解决方案也不够好。任意添加Thread.Sleep(500)只是提高了在调用FindWindow()之前等待足够长时间的几率。但是,你怎么知道500足够好?它总是足够好吗?
不。对于线程竞争错误,Thread.Sleep()永远不是正确的解决方案。如果用户的计算机速度慢或可用未映射RAM不足,则几毫秒可能变成几秒钟。你必须处理最坏情况,而且最坏情况确实很糟糕,通常最少需要考虑约10秒,当机器开始抖动时就会变得非常不切实际。
OS有一种启发式方法来可靠地进行此类交错。它需要是一种启发式方法,而不是对同步对象的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,我会感到遗漏。 这个API被称为UI Automation,巧妙地包装在System.Windows.Automation命名空间中。 它会处理所有那些讨厌的小细节,比如线程竞争和将错误代码转换为良好的异常。 最相关的教程在这里


-1

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

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

更改方法调用后,它对我起作用了 - 但是 - 将MsPaint移动后,光标仍然在原始打开位置 - 您可能需要考虑一下并询问窗口其位置。

编辑:

在Windows 10上,油漆的名称似乎已更改 - 因此,我想您仍然无法正确获取窗口句柄 - 这被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描述,了解为什么Thread.Sleep()只是一个坏主意。

接着进行调用:

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

这种方式,你应该能够正确获取窗口句柄,并且你的代码应该可以工作,无论微软在 win10 中如何称呼它。


我以前也这样做过,但是那时候根本不起作用。当使用 Console.WriteLine(process.ProcessName); 时,输出为“mspaint”。光标在画图中移动,但不会点击... - diiN__________
但您没有要求进程名称 - 您正在寻找windowName,这是不同的。如果lpWindowName参数不为NULL,则FindWindow调用GetWindowText函数以检索窗口名称进行比较请参阅:https://msdn.microsoft.com/de-de/library/windows/desktop/ms633499(v=vs.85).aspx - TripleEEE
@diiN_ 也许如果您提供“OpenPaint(); //打开MSPaint的方法”的代码,会有所帮助——也许存在某些错误。顺便问一下:您是否进行了调试以查看FindWindow是否返回句柄,或者是否为零? - TripleEEE
@diiN_ 好的 - 这可能是为什么名称不起作用的原因 - 我会编辑我的答案 - 我有另一种(我认为更好的方法)来做这件事。 - TripleEEE
1
@diiN_ 很高兴能帮到你!我在我的答案中添加了 Thread.Sleep(),以便其他人经过时可以参考 ;) - TripleEEE
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接