模拟按键 X 秒

6

这是我用来模拟某个进程中的tab键按下的代码:

[DllImport("user32.dll")]
static extern bool PostMessage(IntPtr hWnd, UInt32 Msg, int wParam, int lParam);

public Form1()
{
    PostMessage(MemoryHandler.GetMainWindowHandle(), 
               (int)KeyCodes.WMessages.WM_KEYDOWN, 
               (int)KeyCodes.VKeys.VK_TAB, 0);

    InitializeComponent();
}

有没有办法扩展它,以便按下键(例如)1秒钟,而不仅仅是轻敲它?

请注意,我对阻塞UI线程的Thread.Sleep()解决方案不感兴趣。


2
按住键的目的是触发自动重复,还是有其他目的? - Robert Harvey
2
我会用另一种方式来问我的问题:你为什么需要按住它一秒钟? - Robert Harvey
1
然后只需等待半秒钟左右,然后发送重复按键,就像自动重复一样。 - Robert Harvey
2
@KingKing,我认为这种方法可能过于依赖于假设系统的反应,因此可能会产生意想不到的后果。我没有尝试过像那样使用PostMessage,所以我不太确定可能会有什么副作用。 - keyboardP
2
@Johan,考虑到SystemInformation.KeyboardRepeat,我们不应该使用任意时间周期进行自动重复,否则这不是真正的模拟。 - King King
显示剩余6条评论
6个回答

6
当你按住键盘上的某个键时,重复输入该键是键盘控制器内置的功能。这是键盘自身内置的微处理器。传统上选择的是8042 微控制器,键盘设备驱动程序仍然沿用它的名称。
因此,这不是Windows完成的,PostMessage()也不能为您完成它。不完全是问题,您可以使用定时器来模拟它。

好的,如果不是使用 PostMessage(),那么计时器会做什么? - Johan
为什么您认为计时器的Tick事件处理程序不会使用PostMessage呢?如果您有一个良好的窗口句柄,它通常可以正常工作。如果没有,那么SendInput是更好的选择。嗯,键盘陷阱。SendKeys也往往有效。我不建议特定的方法,因为我不知道目标进程的类型,但我假设这个问题是关于如何重复按键。 - Hans Passant
我指的是你的回答:“Windows和PostMessage()不能为你完成它”。我想知道为什么这不够用,应该使用什么替代。 - Johan
谢谢,但我已经尝试过了。使用计时器和同步循环(使用Thread.Sleep()),同时使用PostMessageSendMessage。我不知道为什么另一个应用程序无法识别它作为按键被按下。你知道其他方法吗? - Johan
抱歉,我回答了你提出的问题。但是我无法帮助你找出这个应用程序为什么不协作的原因,因为你没有告诉我任何相关信息。这是一个相当普遍的问题,所以请务必先搜索答案。如果这样还是没有解决问题,请再次点击“提问”按钮,并确保详细记录你的问题。 - Hans Passant
显示剩余2条评论

3
如果您想要模拟Windows消息的处理方式,那么您可能需要了解按键重复速率有多快。可以在注册表路径HKEY_CURRENT_USER\Control Panel\Keyboard\KeyboardSpeed中找到该值。还有一个名为KeyboardDelay的数值。
Windows会在按键初始按下时发送WM_KEYDOWN和WM_CHAR消息。然后,如果在KeyboardDelay时间段之后仍然按住该键,则每隔KeyboardSpeed时间间隔就会重复发送WM_KEYDOWN和WM_CHAR消息,直到松开该键,此时将发送WM_KEYUP消息。
建议使用计时器以特定频率发送消息。
更新: 例如:
int keyboardDelay, keyboardSpeed;
using (var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Keyboard"))
{
    Debug.Assert(key != null);
    keyboardDelay = 1;
    int.TryParse((String)key.GetValue("KeyboardDelay", "1"), out keyboardDelay);
    keyboardSpeed = 31;
    int.TryParse((String)key.GetValue("KeyboardSpeed", "31"), out keyboardSpeed);
}

maxRepeatedCharacters = 30; // repeat char 30 times
var startTimer = new System.Windows.Forms.Timer {Interval = keyboardSpeed};
startTimer.Tick += startTimer_Tick;
startTimer.Start();
var repeatTimer = new System.Windows.Forms.Timer();
repeatTimer.Interval += keyboardDelay;
repeatTimer.Tick += repeatTimer_Tick;

//...

private static void repeatTimer_Tick(object sender, EventArgs e)
{
    PostMessage(MemoryHandler.GetMainWindowHandle(),
               (int)KeyCodes.WMessages.WM_KEYDOWN,
               (int)KeyCodes.VKeys.VK_TAB, 0);
    PostMessage(MemoryHandler.GetMainWindowHandle(),
                (int)KeyCodes.WMessages.WM_CHAR,
                (int)KeyCodes.VKeys.VK_TAB, 0);

    counter++;
    if (counter > maxRepeatedCharacters)
    {
        Timer timer = sender as Timer;
        timer.Stop();
    }
}

private static void startTimer_Tick(object sender, EventArgs eventArgs)
{
    Timer timer = sender as Timer;
    timer.Stop();
    PostMessage(MemoryHandler.GetMainWindowHandle(),
               (int)KeyCodes.WMessages.WM_KEYDOWN,
               (int)KeyCodes.VKeys.VK_TAB, 0);
    PostMessage(MemoryHandler.GetMainWindowHandle(),
               (int)KeyCodes.WMessages.WM_CHAR,
               (int)KeyCodes.VKeys.VK_TAB, 0);
}

1

我会在一个线程中执行它,以便让UI线程睡眠并不被阻塞。

看这个例子:

System.Threading.Thread KeyThread = new System.Threading.Thread(() => {
       //foreach process
       // press key now
       PostMessage(MemoryHandler.GetMainWindowHandle(), 
              (int)KeyCodes.WMessages.WM_KEYDOWN, 
              (int)KeyCodes.VKeys.VK_TAB, 0);
       System.Threading.Thread.Sleep(1000); // wait 1 second

       //foreach process
       // release keys again
       PostMessage(MemoryHandler.GetMainWindowHandle(), 
              (int)KeyCodes.WMessages.WM_KEYUP, 
              (int)KeyCodes.VKeys.VK_TAB, 0);
});

当然,你必须开始它。

1
我不知道你是否注意到了原帖作者明确表示他不想使用 Thread.Sleep() 解决方案。 - Edper
2
他对阻塞UI线程的睡眠不感兴趣 - 这个解决方案不会阻塞UI线程,而是执行此代码的“工作线程”。只要没有太多类似这样的事件,这就是一个有效的解决方案。 - MasterPlanMan
为什么不直接使用 Timer,这就是它的作用呀? - Peter Ritchie

1

我不确定您想要实现什么,但以下是我使用的函数来模拟文本输入的方法,如果您稍微修改一下,使最后一次调用SendInput函数在一个新的线程中执行,然后再通过计时器分离按下和释放事件,这样是否可以达到您所需的效果?

[DllImport("user32.dll", SetLastError = true)]
static extern UInt32 SendInput(UInt32 numberOfInputs, INPUT[] inputs, Int32 sizeOfInputStructure);

public enum InputType : uint
{
    MOUSE = 0,
    KEYBOARD = 1,
    HARDWARE = 2,
}

struct INPUT
{
    public UInt32 Type;
    public MOUSEKEYBDHARDWAREINPUT Data;
}

struct KEYBDINPUT
{
    public UInt16 Vk;
    public UInt16 Scan;
    public UInt32 Flags;
    public UInt32 Time;
    public IntPtr ExtraInfo;
}

public enum KeyboardFlag : uint // UInt32
{
    EXTENDEDKEY = 0x0001,
    KEYUP = 0x0002,
    UNICODE = 0x0004,
    SCANCODE = 0x0008,
}

public static void SimulateTextEntry(string text)
{
    if (text.Length > UInt32.MaxValue / 2) throw new ArgumentException(string.Format("The text parameter is too long. It must be less than {0} characters.", UInt32.MaxValue / 2), "text");

    var chars = UTF8Encoding.ASCII.GetBytes(text);
    var len = chars.Length;
    INPUT[] inputList = new INPUT[len * 2];
    for (int x = 0; x < len; x++)
    {
        UInt16 scanCode = chars[x];

        var down = new INPUT();
        down.Type = (UInt32)InputType.KEYBOARD;
        down.Data.Keyboard = new KEYBDINPUT();
        down.Data.Keyboard.Vk = 0;
        down.Data.Keyboard.Scan = scanCode;
        down.Data.Keyboard.Flags = (UInt32)KeyboardFlag.UNICODE;
        down.Data.Keyboard.Time = 0;
        down.Data.Keyboard.ExtraInfo = IntPtr.Zero;

        var up = new INPUT();
        up.Type = (UInt32)InputType.KEYBOARD;
        up.Data.Keyboard = new KEYBDINPUT();
        up.Data.Keyboard.Vk = 0;
        up.Data.Keyboard.Scan = scanCode;
        up.Data.Keyboard.Flags = (UInt32)(KeyboardFlag.KEYUP | KeyboardFlag.UNICODE);
        up.Data.Keyboard.Time = 0;
        up.Data.Keyboard.ExtraInfo = IntPtr.Zero;

        // Handle extended keys:
        // If the scan code is preceded by a prefix byte that has the value 0xE0 (224),
        // we need to include the KEYEVENTF_EXTENDEDKEY flag in the Flags property. 
        if ((scanCode & 0xFF00) == 0xE000)
        {
            down.Data.Keyboard.Flags |= (UInt32)KeyboardFlag.EXTENDEDKEY;
            up.Data.Keyboard.Flags |= (UInt32)KeyboardFlag.EXTENDEDKEY;
        }

        inputList[2*x] = down;
        inputList[2*x + 1] = up;

    }

    var numberOfSuccessfulSimulatedInputs = SendInput((UInt32)len*2, inputList, Marshal.SizeOf(typeof(INPUT)));
}

1
当在物理键盘上按住一个键时,重复的击键会传递到活动窗口。这不是键盘内置的功能,而是Windows功能。
您可以通过按以下步骤模拟此操作:
1. 发送键按下消息。 2. 每隔30毫秒(Windows中的默认值,可通过“辅助功能”设置更改),在每个tick时向窗口发送按键消息。 3. 发送键释放消息。

据我所知,在编程中没有“keypress”消息,只有“keyup”和“keydown”。 - Johan

0

不确定您在使用PostMessage做什么,但是可以从这里修改一些代码: SendKey.Send() 无法工作

   [DllImport("user32.dll", SetLastError = true)]
    static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
    public static void PressKey(Keys key, bool up)
    {
        const int KEYEVENTF_EXTENDEDKEY = 0x1;
        const int KEYEVENTF_KEYUP = 0x2;
        if (up)
        {
            keybd_event((byte)key, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, (UIntPtr)0);
        }
        else
        {
            keybd_event((byte)key, 0x45, KEYEVENTF_EXTENDEDKEY, (UIntPtr)0);
        }
    }

    void TestProc()
    {
        PressKey(Keys.Tab, false);
        Thread.Sleep(1000);
        PressKey(Keys.Tab, true);
    }

也许这对你有用。它只是一个按键按下,然后是一个松开按键的操作,之间有一个休眠时间。你甚至可以进一步添加,并传递一个时间值,以确定你希望按键保持按下多长时间。

Thread.Sleep() 同样会阻塞当前线程,但实际的按键操作并非如此。 - King King

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