Статьи

Отправка нажатий клавиш в другие приложения с помощью Windows API и C #

Недавно мне пришлось решать задачу, в которой мне нужно было отправлять нажатия клавиш в другое приложение, которое запускается из приложения .NET Windows. Очевидно, что есть способ сделать это через WinAPI, и этот способ называется  SendInput  — основная функция, которая может использоваться для имитации нажатия клавиш, действий мыши и нажатий кнопок.

Давайте поговорим о том, что нужно. Прежде всего, вы используете его для отправки нажатий клавиш, а не символов, туда, где находится текущий фокус ввода. Так, например, если бы я захотел напечатать символ { на экране, я не смог бы просто получить его представление кода, потому что фактическая клавиша, которая несет его на стандартной  клавиатуре QWERTY в США,  была бы [ . Проблема, с которой я сталкивался несколько раз, заключается в том, что разработчик пытается использовать  VkKeyScan  для получения необходимого кода виртуальной клавиши для его последующей репликации, вместо этого он создает другой символ.  

Теперь давайте посмотрим на его родную подпись:

UINT WINAPI SendInput(
  _In_  UINT nInputs,
  _In_  LPINPUT pInputs,
  _In_  int cbSize
);

В ограничениях управляемой среды, в моем случае — в приложении C #, это объявление будет выглядеть так:

[DllImport("user32.dll", SetLastError = true)]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

Помните, поскольку вы используете P / Invoke, вам нужно добавить 

public struct INPUT
{
    public int type;
    public InputBatch u;
}

Что именно представляет собой InputBatch? Если вы посмотрите на структуру структуры INPUT, вы заметите, что она опирается на объединение  MOUSEINPUT KEYBDINPUT  и  HARDWAREINPUT  — структур, которые переносят данные, относящиеся к их собственному классу моделирования ввода. Базовая реализация этих в C # выглядит следующим образом:

[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT
{
    public int dx;
    public int dy;
    public uint mouseData;
    public uint dwFlags;
    public uint time;
    public IntPtr dwExtraInfo;
}

[StructLayout(LayoutKind.Sequential)]
public struct KEYBDINPUT
{    
    public ushort wVk;
    public ushort wScan;
    public uint dwFlags;
    public uint time;
    public IntPtr dwExtraInfo;
}

[StructLayout(LayoutKind.Sequential)]
public struct HARDWAREINPUT
{
    public uint uMsg;
    public ushort wParamL;
    public ushort wParamH;
}

В нашем случае мы фокусируемся на клавиатуре, поэтому давайте пройдемся по полям, которые мы используем в структуре KEYBDINPUT.

  • wVk — код виртуального ключа. Не так же, как код символа. Вы можете найти список кодов виртуальных клавиш. 

  • wScan — код сканирования оборудования (см. спецификацию  здесь ).

  • dwFlags — дополнительные флаги, которые предопределяют поведение обработки ввода. Вы можете найти полный список  на MSDN .
  • время — если вам не нужно указывать другую временную метку, позвольте Windows предоставить свою собственную и установите значение 0.
  • dwExtraInfo — будет связан с вызовом  GetMessageExtraInfo .

Давайте предположим, что я хочу смоделировать нажатие клавиши Enter. Для этого мне сначала нужно сгенерировать массив структур INPUT, которые будут действовать как дескриптор ввода:

WindowsAPI.INPUT[] data = new WindowsAPI.INPUT[] {
    new WindowsAPI.INPUT() 
    {
        type = WindowsAPI.INPUT_KEYBOARD,
        u = new WindowsAPI.InputBatch
        {
            ki = new WindowsAPI.KEYBDINPUT
            {
                wVk = 0x0D,
                wScan = 0,
                dwFlags = 0,
                dwExtraInfo = WindowsAPI.GetMessageExtraInfo(),
            }
        }
    } 
};

Обратите внимание, что я использую HEX-представление для Enter (это правильный связанный код виртуальной клавиши). Когда я хочу вызвать SendInput, я могу вызвать это:

WindowsAPI.SendInput((uint)data.Length, data, Marshal.SizeOf(typeof(WindowsAPI.INPUT)));

Marshal.SizeOf  вернет значение размера структуры INPUT . Что произойдет, если мне нужно отправить комбинацию клавиш? Это легко реализовать с помощью нескольких экземпляров INPUT . Например, для Ctrl + F5 вы можете использовать это:

public static WindowsAPI.INPUT[] Find()
{
    WindowsAPI.INPUT[] data = new WindowsAPI.INPUT[] {
        new WindowsAPI.INPUT() 
        {
            type = WindowsAPI.INPUT_KEYBOARD,
            u = new WindowsAPI.InputBatch
            {
                ki = new WindowsAPI.KEYBDINPUT
                {
                    wVk = 0xA2,
                    wScan = 0,
                    dwFlags = 0,
                    dwExtraInfo = WindowsAPI.GetMessageExtraInfo(),
                }
            }
        },
        new WindowsAPI.INPUT() 
        {
            type = WindowsAPI.INPUT_KEYBOARD,
            u = new WindowsAPI.InputBatch
            {
                ki = new WindowsAPI.KEYBDINPUT
                {
                    wVk = (ushort)WindowsAPI.VkKeyScan('f'),
                    wScan = 0,
                    dwFlags = 0,
                    dwExtraInfo = WindowsAPI.GetMessageExtraInfo(),
                }
            }
        }    
    };

    return data;
}

Затем вызовите SendInput так же, как вы вызывали его для клавиши Enter. Здесь есть одна проблема. Когда вы собираетесь вызывать эту функцию, вы заметите, что ОС по- прежнему будет считать нажатой клавишу Ctrl . Чтобы избежать этого, вам нужно будет использовать KEYBDINPUT экземпляр с dwFlags для набора в  KEYEVENTF_KEYUP отпустить клавишу.