通过挂钩直接向另一个进程发送按键信息

21

经过尝试SendInput、SendKeys、PostMessage、SendMessage、SendNotifyMessage、keybd_event等各种方法后,我发现尝试向另一个非前景进程发送键盘输入相当棘手且不可靠。

我尝试了一种SendInput的方法,在其中欺骗Z顺序(以保持当前窗口在顶部),并快速将第三方窗口置于前景,发送输入,然后重新将我的窗口置于前景。但这最终失败了,并且还不知道为什么,在没有前景的情况下还成功地在我的窗口上处理了按键(导致两个窗口之间无限循环发送和接收,直到我设法关闭该进程)。

我尝试了不同的SendMessage和PostMessage组合。一个用于按下,一个用于弹起,因为同时使用两个会出现问题,例如两个都用于PostMessage会导致接收窗口上的键重复。或者两个都用于SendMessage,这会导致文本输入出现问题,但其他功能正常运行。对于所有功能,SendMessage用于keydown,而PostMessage用于keyUp可以工作,但可靠性大大降低,并增加了按键事件的延迟。只有PostMessage用于keydown,而SendMessage用于keyup的组合才能做任何有用的事情,但keyup的注册率可能会下降5-10%。SentNotifyMessage也是一样的(在可靠性方面基本上与SendMessage相同)。

因此,我已经无计可施了,并且想知道如何直接将钩子注入目标窗口,并以这种方式发送按键,绕过消息队列等。以一种不会触发全局按键事件,仅影响目标窗口的方式进行。唯一的问题是,当涉及到注入/钩子等方面时,我对此并不太了解。所以我向您,社区求助。

该怎么办?


发送输入后,您需要等待一段时间才能将自己的进程设置回前台,以确保输入已成功传递。尽管名称为SendInput,但实际上它会将输入发布到输入队列中;如果您的窗口立即获得焦点,则当系统处理输入时,它可能最终会发送到您的窗口! - BrendanMcK
说实话,向非前台应用程序发送输入并不受支持;没有干净可靠的方法来实现它。将目标应用程序设置为前台并使用SendInput是唯一可靠的方法。SendMessage和PostMessage都存在问题。你试图解决的更大问题是什么 - 你为什么要首先尝试发送输入? - BrendanMcK
感谢您的回复,BrendanMcK。我可以尝试调整重新前置窗口的时间,但我基本上想避免这种情况。我需要输入的原因是,我正在为LAN上的多个应用程序和物理机器编写键盘/鼠标输入复用器。其中在多重盒子视频游戏中使用(通常是MMO和RTS游戏)。对我来说,最理想的情况是,如果我能找出如何做到这一点,就可以直接钩入第三方进程,并以某种方式发送输入。我应该选择哪些选项?我应该在哪里进行研究? - Hydra
1
有人提到了Spy++,我认为它确实在帮助我解决问题方面给了我一些提升。有了这个工具,我也许能够使用PostMessage()来发送消息,而不会出现键盘注册的问题。然而,我无法弄清楚的是,当我只使用PostMessage(handle, WM_KEYDOWN, key, 0);来发送按键按下消息和PostMessage(handle, WM_KEYUP, key, 0);来发送按键松开消息时,为什么会出现这种情况:链接 - Hydra
伪造按键输入很难做到完美。检查消息的参数,你会发现你“模拟”的消息与“真实”的消息并不完全匹配:特别是在WM_KEYUP消息中,“fUp”标志(位31)在真实消息中为1,表示释放,但在你的消息中为0,表示按下。这可能会让其他代码感到困惑。最重要的是:如果你要“伪造”消息,你需要尽可能地接近原始消息。祝玩得愉快 :) - BrendanMcK
2
我已经成功解决了PostMessage()复制问题;现在我可以将PostMessage()用于keydown和keyup事件,这似乎极大地提高了可靠性。在我有限的初始测试中,我还没有遇到过键盘故障。文本输入很好。问题是我需要将lParam设置为0xC0000001,以便在keyup后不生成WM_CHAR消息。此外,Spy ++非常棒,如果没有它,我永远无法解决这个问题。然而,我仍然对我的最初的问题感兴趣,如何进行注入? - Hydra
2个回答

16

这是一段代码,可以让你向后台应用程序发送消息。例如,要发送字符“A”,只需调用sendKeystroke(Keys.A)函数,并记得使用System.windows.forms命名空间才能使用Keys对象。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace keybound
{
class WindowHook
{
    [DllImport("user32.dll")]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    [DllImport("user32.dll")]
    public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll")]
    public static extern IntPtr PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

    public static void sendKeystroke(ushort k)
    {
        const uint WM_KEYDOWN = 0x100;
        const uint WM_SYSCOMMAND = 0x018;
        const uint SC_CLOSE = 0x053;

        IntPtr WindowToFind = FindWindow(null, "Untitled1 - Notepad++");

        IntPtr result3 = SendMessage(WindowToFind, WM_KEYDOWN, ((IntPtr)k), (IntPtr)0);
        //IntPtr result3 = SendMessage(WindowToFind, WM_KEYUP, ((IntPtr)c), (IntPtr)0);
    }
}
}

5
PostMessage非常适合这个任务。使用Spy++帮助我构建消息,使其看起来与实际键盘输入的消息完全相同。 - Hydra
2
刚在Windows 10上尝试了一下,但没有任何效果。不确定是我做错了什么还是它已经不再起作用了? - Sam
我只想补充一点,这对我已经不再起作用了。我所做的是找到编辑窗口的句柄,它是WindowToFind的子窗口,并使用PostMessage而不是SendMessage。我的代码最终变成了:IntPtr editHandle = (IntPtr)0x00850766; //使用Spy++找到PostMessage(editHandle, 0x100, (IntPtr)0x56, (IntPtr)0); - Victor Chavauty

9

你可能需要尝试一下,但是你可以通过进程发送数据。这可能不适用于你的情况,但这是一个想法。

using System;
using System.Runtime.InteropServices;
using System.Diagnostics;

    [DllImport("user32.dll", EntryPoint = "FindWindowEx")]
    public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter,          string lpszClass, string lpszWindow);
    [DllImport("User32.dll")]
    public static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, string lParam);

     static void Send(string message)
     {
         Process[] notepads = Process.GetProcessesByName("notepad");

         if (notepads.Length == 0)
             return;

         if (notepads[0] != null)
         {
             IntPtr child = FindWindowEx(notepads[0].MainWindowHandle, 
                 new IntPtr(0), "Edit", null);

             SendMessage(child, 0x000C, 0, message);
         }
     }

如果那样不行,你总可以这样做:
System.Threading.Thread.Sleep(1000);
//User clicks on active form.
System.Windows.Forms.Sendkeys.Sendwait("<Message>");

这正是我所需要的。但是,我如何了解我的进程的lpszClass是什么呢? - FinnTheHuman
2
你可以使用Spy++来查找lpClassName是什么。如果lpClassName为NULL,则FindWindow会查找与lpWindowName参数匹配的任何窗口的标题。(https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms633499(v=vs.85).aspx) - Derek Johnson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接