微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

可以等待其他线程处理输入到该消息的消息吗?

我想可靠地模拟用户输入其他窗口.我为此使用sendinput,但随后需要等到目标应用处理输入后再发送更多.据我所知,sendinput,尽管它的名字,实际上将消息发布到队列中,并且不等到消息被处理.

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

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

代码在这里

    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();

现在,由于某种原因,它不起作用.它总是返回消息队列为空.有人知道我在做错什么,或者知道以其他方式实现我所需要的吗?

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

我对可能发生的情况的推测与TranslateMessage function有关:

Translates virtual-key messages into character messages. The character messages are posted to the calling thread’s message queue, to be read the next time the thread calls the GetMessage or PeekMessage function.

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

解决方法:

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

您可以使用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);

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐