我有一个控制台应用程序应该在MSPaint中绘制一个随机图片(鼠标按下 - >让光标随机绘制一些东西 - >鼠标向上。这是我到目前为止(我向{{1添加了评论)方法可以更好地理解我想要实现的目标):
Main
我从here获得了点击模拟的代码。
点击模拟,但它不会绘制任何内容。似乎点击在MSPaint内部不起作用。光标变为"交叉" MSPaint,但正如我所提到的......点击似乎不起作用。
[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)));
}
将FindWindow
的值设置为值0.将参数hwndMain
更改为mspaint
不会改变任何内容。 MSPaintApp
的值保持为0。
如果有帮助,这是我的hwndMain
方法:
OpenPaint()
我做错了什么?
答案 0 :(得分:8)
IntPtr hwndMain = FindWindow("mspaint", null);
这还不够好。在pinvoke代码中常见的错误,C#程序员往往过分依赖异常来跳出屏幕并将它们打在脸上以告诉他们出了问题。 .NET Framework确实做得非常好。但是当你使用基于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程序在开始询问通知时已经取得了足够的进展。 &#34;抽取消息循环&#34;用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。
答案 1 :(得分:-1)
按照承诺,我昨天自己测试了 - 说实话我的光标刚刚移动,但不在窗口中,并且没有任何影响 - 因为我做了调试,我看到var hwndMain = FindWindow("mspaint ", null);
是值{{1} }。我虽然这是问题,所以我确实看了一下另一个stackoverflow主题,你得到了你的代码。我认识到解决方案是在0
使用他们正在关注的不同窗口名称 - 所以我尝试了。
FindWindow()
更改方法调用之后,它对我有用 - 但是 - 在移动MsPaint之后,光标仍处于原始打开位置 - 您可能想要考虑这一点并向窗口询问它的位置。 是否可以使用Win7 / 8/10更改名称?
修改强>
在Windows 10上,绘画的名称似乎发生了变化 - 所以我猜你在获得正确的窗口句柄时仍然遇到问题 - Hans Passant证明这是错误的,他很好地解释了处理程序的问题(链接如下) 。解决这个问题的一种方法是从流程本身获取处理程序,而不是从var hwndMain = FindWindow("MSPaintApp", null);
我建议您像这样更改FindWindow()
:
OpenPaint()
链接到Hans Passant decription以解释为什么Thread.Sleep()只是一个坏主意。
接下来是电话:
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;
}
这样你就可以很好地获得正确的窗口,并且你的代码应该正常工作,无论微软如何在win10中调用它