gpt4 book ai didi

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

转载 作者:可可西里 更新时间:2023-11-01 09:20:30 26 4
gpt4 key购买 nike

我想可靠地模拟用户对其他窗口的输入。我为此使用了 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();

现在,由于某种原因它无法正常工作。它总是返回消息队列为空。有人知道我做错了什么,或者可能有其他方法可以实现我的需要吗?

编辑:关于为什么我首先需要等待。如果不间断地立即模拟其他 Action ,我会遇到只输入部分文本的情况。例如。当焦点位于某个文本框时,我通过 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.

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

最佳答案

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

如果使用 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 已经是一个很好的候选者。请注意,如果进程终止,该方法将抛出异常。

关于c# - 是否可以等到其他线程处理输入消息发布到它?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21028326/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com