Ivan Danilov
Ivan Danilov

Reputation: 14777

Is it possible to wait until other thread processes input messages posted to it?

I want to simulate user input to other window reliably. I use SendInput for this, but then I need to wait until target app handles the input before sending more. As far as I know, SendInput, despite its name, really posts messages to the queue and does not wait until they're processed.

My try is based on the idea to wait until message queue is empty at least once. As I can't check other's thread message queue directly (at least I don't know a way to do so), I'm using AttachThreadInput to attach target thread's queue to this thread's one and then PeekMessage to check.

In order to check the function I use small app with one window and a button. When button is clicked I call Thread.Sleep(15000) effectively stopping message processing, thus ensuring that for the next 15s message queue can't be empty.

Code is here:

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

Now, it is not working for some reason. It always returns that the message queue is empty. Does someone know what I'm doing wrong, or maybe some other way to achieve what I need?

EDIT: About why I need to wait in the first place. If other actions are simulated immediately without pause, I encounter situations when text is entered only partially. E.g. I simulated "abcdefgh" via SendInput when focus is at some text box, and right after that - some mouse click. What I get is "abcde" typed and click after that. If I put Thread.Sleep(100) after SendInput - the problem is not reproducible on my machine, but rarely reproducible on VM with low hardware. So I need more reliable way to wait correct amount of time.

My speculation of what might be going on is related to 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.

So, I call SendInput for "abcdefgh" - bunch of input messages posted to the thread queue. Then it starts to process those messages in FIFO order, translating "abcde", and posting messages for each char to queue's tail. Then mouse click is simulated and posted after character messages for "abcde". Then translating finishes, but translated messages for "fgh" happens after the mouse click. And finally app sees "abcde", then click, then "fgh" - obviously going to some wrong place...

Upvotes: 4

Views: 2942

Answers (1)

Hans Passant
Hans Passant

Reputation: 941744

This is a common need in UI Automation. It is in fact implemented in .NET by the WindowPattern.WaitForInputIdle() method.

You'd be well-off using the System.Windows.Automation namespace to implement this. The method is however easy to implement yourself. You can have a look-see from the Reference Source or a decompiler. It surprised me a bit how they did it, it looks solid however. Instead of trying to guess if the message queue is empty, it just looks at the state of the UI thread that owns the window. If it is blocked and it isn't waiting for an internal system operation then you have a very strong signal that the thread is waiting for Windows to deliver the next message. I wrote it like this:

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

Call WaitForInputIdle() in your code before calling SendInput(). The window handle you must pass is pretty flexible, any window handle will do as long as it is owned by the UI thread of the process. The Process.MainWindowHandle is already a very good candidate. Beware that the method will throw an exception if the process terminates.

Upvotes: 5

Related Questions