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

Iva*_*lov 4 .net c# windows winapi

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

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

为了检查该功能,我使用带有一个窗口和一个按钮的小型应用程序。单击按钮时,我会Thread.Sleep(15000)有效地停止消息处理,从而确保接下来的 15 秒消息队列不会为空。

代码在这里:

    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();
Run Code Online (Sandbox Code Playgroud)

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

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

我对可能发生的情况的猜测与TranslateMessage功能有关:

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

因此,我调用SendInput“abcdefgh” - 一堆发布到线程队列的输入消息。然后它开始按 FIFO 顺序处理这些消息,翻译“abcde”,并将每个字符的消息发布到队列的尾部。然后模拟鼠标单击并在“abcde”的字符消息之后发布。然后翻译完成,但单击鼠标后会出现“fgh”的翻译消息。最后应用程序看到“abcde”,然后单击,然后“fgh” - 显然去到了一些错误的地方......

Han*_*ant 5

这是 UI 自动化中的常见需求。事实上,它是在 .NET 中通过WindowPattern.WaitForInputIdle() 方法实现的。

使用 System.Windows.Automation 命名空间来实现这一点会很顺利。然而,该方法很容易自己实现。您可以从参考源或反编译器中查看。他们的做法让我有点惊讶,但看起来很扎实。它不会尝试猜测消息队列是否为空,而是仅查看拥有该窗口的 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);
Run Code Online (Sandbox Code Playgroud)

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