是否有可能等到其他线程处理输入发布到它的消息?

时间:2014-01-09 18:32:38

标签: c# .net windows winapi

我想可靠地模拟用户对其他窗口的输入。我为此使用SendInput,但之后我需要等到目标应用程序处理输入之后再发送更多内容。据我所知,SendInput,尽管名称如此,但确实会将消息发布到队列中,并且不会等到它们被处理完毕。

我的尝试是基于等待消息队列至少为空一次的想法。因为我无法直接检查其他线程消息队列(至少我不知道这样做的方法),我使用AttachThreadInput将目标线程的队列附加到该线程的队列然后{{1}检查。

为了检查功能,我使用带有一个窗口和一个按钮的小应用程序。单击按钮时,我调用PeekMessage有效地停止了消息处理,从而确保在接下来的15秒内消息队列不能为空。

代码在这里:

Thread.Sleep(15000)

现在,由于某种原因它不起作用。它始终返回消息队列为空。有人知道我做错了什么,或者是否有其他方法来实现我的需要?

编辑:关于为什么我需要先等待。 如果立即模拟其他操作而没有暂停,我会遇到仅部分输入文本的情况。例如。当焦点在某个文本框时,我通过SendInput模拟“abcdefgh”,然后在那之后 - 点击一些鼠标。我得到的是“abcde”键入并在此之后单击。如果我在SendInput之后放置Thread.Sleep(100) - 问题在我的机器上不可重现,但很少在具有低硬件的VM上可重现。所以我需要更可靠的方式来等待正确的时间。

我对可能发生的事情的猜测与TranslateMessage function

有关
  

将虚拟密钥消息转换为字符消息。字符消息将发布到调用线程的消息队列,以便在线程下次调用GetMessage或PeekMessage函数时读取。

因此,我将 public static void WaitForWindowInputIdle(IntPtr hwnd) { var currentThreadId = GetCurrentThreadId(); var targetThreadId = GetWindowThreadProcessId(hwnd, IntPtr.Zero); Func<bool> checkIfMessageQueueIsEmpty = () => { bool queueEmpty; bool threadsAttached = false; try { threadsAttached = AttachThreadInput(targetThreadId, currentThreadId, true); if (threadsAttached) { NativeMessage nm; queueEmpty = !PeekMessage(out nm, hwnd, 0, 0, RemoveMsg.PM_NOREMOVE | RemoveMsg.PM_NOYIELD); } else throw new ThreadStateException("AttachThreadInput failed."); } finally { if (threadsAttached) AttachThreadInput(targetThreadId, currentThreadId, false); } return queueEmpty; }; var timeout = TimeSpan.FromMilliseconds(15000); var retryInterval = TimeSpan.FromMilliseconds(500); var start = DateTime.Now; while (DateTime.Now - start < timeout) { if (checkIfMessageQueueIsEmpty()) return; Thread.Sleep(retryInterval); } } [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool PeekMessage(out NativeMessage lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, RemoveMsg wRemoveMsg); [StructLayout(LayoutKind.Sequential)] public struct NativeMessage { public IntPtr handle; public uint msg; public IntPtr wParam; public IntPtr lParam; public uint time; public System.Drawing.Point p; } [Flags] private enum RemoveMsg : uint { PM_NOREMOVE = 0x0000, PM_REMOVE = 0x0001, PM_NOYIELD = 0x0002, } [DllImport("user32.dll", SetLastError = true)] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId); [DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId(); 称为“abcdefgh” - 将大量输入消息发布到线程队列中。然后它开始以FIFO顺序处理这些消息,翻译“abcde”,并将每个char的消息发布到队列的尾部。然后模拟鼠标点击并在“abcde”的字符消息后发布。然后翻译完成,但在鼠标点击后,“fgh”的翻译消息发生。最后app看到“abcde”,然后点击,然后“fgh” - 显然是去了一个错误的地方......

1 个答案:

答案 0 :(得分:4)

这是UI自动化中的常见需求。它实际上是由WindowPattern.WaitForInputIdle() method在.NET中实现的。

使用System.Windows.Automation命名空间来实现这一目标你会很富裕。然而,该方法易于实现。您可以从Reference Source或反编译器中查看。令我惊讶的是他们是如何做到的,但它看起来很稳固。它不是试图猜测消息队列是否为空,而只是查看拥有该窗口的UI线程的状态。如果它被阻止而且它没有等待内部系统操作,那么你有一个非常强烈的信号,即该线程正在等待Windows发送下一条消息。我是这样写的:

using namespace System.Diagnostics;
...
    public static bool WaitForInputIdle(IntPtr hWnd, int timeout = 0) {
        int pid;
        int tid = GetWindowThreadProcessId(hWnd, out pid);
        if (tid == 0) throw new ArgumentException("Window not found");
        var tick = Environment.TickCount;
        do {
            if (IsThreadIdle(pid, tid)) return true;
            System.Threading.Thread.Sleep(15);
        }  while (timeout > 0 && Environment.TickCount - tick < timeout);
        return false;
    }

    private static bool IsThreadIdle(int pid, int tid) {
        Process prc = System.Diagnostics.Process.GetProcessById(pid);
        var thr = prc.Threads.Cast<ProcessThread>().First((t) => tid == t.Id);
        return thr.ThreadState == ThreadState.Wait &&
               thr.WaitReason == ThreadWaitReason.UserRequest;
    }

    [System.Runtime.InteropServices.DllImport("User32.dll")]
    private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int pid);

在调用SendInput()之前在代码中调用WaitForInputIdle()。您必须传递的窗口句柄非常灵活,任何窗口句柄都可以执行,只要它由进程的UI线程拥有即可。 Process.MainWindowHandle已经是一个非常好的候选人。请注意,如果进程终止,该方法将抛出异常。