использование SendInput в Node-FFI

Я хотел использовать функцию SendInput из Windows Api в nodejs, используя пакет FFI.

Мои знания C ограничены, поэтому я не могу понять, в чем проблема, я в основном пытаюсь виртуально нажать клавишу на клавиатуре.

Это код, который у меня есть:

var ffi = require('ffi');
var ref = require ('ref');
var struct = require ('ref-struct');

var keyboardInput = struct({
    'type': 'int',
    'wVK': 'int',
    'wScan': 'int',
    'dwFlags': 'int',
    'time': 'int',
    'dwExtraInfo': 'int64'
});

var keyboardInputPtr = ref.refType(keyboardInput);
var keyboard = new keyboardInput();
keyboard.type = 1;
keyboard.wVK = 0x41;
keyboard.wScan = 0;
keyboard.dwFlags = 2;
keyboard.time = 0;
keyboard.dwExtraInfo = 0;

var user32 = ffi.Library('user32', {
    'SendInput': [ 'int', [ 'uint', keyboardInputPtr, 'int' ] ]
});

setInterval(function(){
    var r = user32.SendInput(1, keyboard.ref(), 40);
    console.log(r);
}, 500);

Он регистрирует меня как «1» в консоли, разве это не должно означать, что он работает? Потому что у меня не нажимается клавиша, когда я открываю блокнот.


person Community    schedule 27.12.2016    source источник
comment
SendInput помещает ввод в аппаратную очередь ввода. Любое окно (или поток, на самом деле) находится на переднем плане в момент захвата этого входного события, получает входные данные. Поэтому, когда вы запускаете свое приложение, Блокнот, естественно, не является окном переднего плана. Во всяком случае, то, что вы описали, является предлагаемым вами решением. Какую проблему вы на самом деле пытаетесь решить?   -  person IInspectable    schedule 27.12.2016
comment
Ни у кого никогда не было поддельного ввода в блокнот в качестве конечной цели. Что вы действительно пытаетесь сделать. Вполне возможно, даже если вы сможете подделать ее, ваша истинная цель не будет решена так же.   -  person David Heffernan    schedule 27.12.2016
comment
@IInspectable Я пытаюсь написать программу, которая нажимает кнопку (на клавиатуре) в текущем активном окне. С интервалом это в основном как автоматический кликер.   -  person    schedule 28.12.2016
comment
нажимает кнопку (на клавиатуре) — это не имеет смысла. Вы хотите нажать кнопку, или вы хотите генерировать ввод с клавиатуры.   -  person IInspectable    schedule 28.12.2016
comment
@IInspectable Извините, если я неясно выразился, но я хочу сгенерировать ввод с клавиатуры, точно так же, как нажатие кнопки A на клавиатуре.   -  person    schedule 28.12.2016
comment
Если вы действительно хотите генерировать ввод с клавиатуры (например, экранную клавиатуру), SendInput — правильный инструмент. Вы все еще должны убедиться, что правильный элемент управления находится на переднем плане, когда вы генерируете ввод.   -  person IInspectable    schedule 28.12.2016


Ответы (3)


Наконец-то я нашел способ использовать node-ffi/node-ffi-napi для ввода нажатий клавиш с помощью функции SendInput! (текущий код ниже использует node-ffi-napi, так как node-ffi не поддерживается/сломан; см. историю изменений для версии node-ffi, API почти точно такой же)

Однако обратите внимание, что существует два способа вызова функции SendInput, как показано здесь: https://autohotkey.com/boards/viewtopic.php?p=213617#p213617

В моем случае мне пришлось использовать второй подход (код сканирования), потому что первый подход (виртуальный ключ) не работал в программах, для которых мне нужна была имитация ключа.

Без лишних слов, вот полное решение:

import keycode from "keycode";
import ffi from "ffi-napi";
import ref from "ref-napi";
import os from "os";
import import_Struct from "ref-struct-di";

var arch = os.arch();
const Struct = import_Struct(ref);

var Input = Struct({
    "type": "int",

    // For some reason, the wScan value is only recognized as the wScan value when we add this filler slot.
    // It might be because it's expecting the values after this to be inside a "wrapper" substructure, as seen here:
    //     https://msdn.microsoft.com/en-us/library/windows/desktop/ms646270(v=vs.85).aspx
    "???": "int",
     
    "wVK": "short",
    "wScan": "short",
    "dwFlags": "int",
    "time": "int",
    "dwExtraInfo": "int64"
});

var user32 = ffi.Library("user32", {
    SendInput: ["int", ["int", Input, "int"]],
    //MapVirtualKeyEx: ["uint", ["uint", "uint", intPtr]],
});

const extendedKeyPrefix = 0xe000;
const INPUT_KEYBOARD = 1;
const KEYEVENTF_EXTENDEDKEY = 0x0001;
const KEYEVENTF_KEYUP       = 0x0002;
const KEYEVENTF_UNICODE     = 0x0004;
const KEYEVENTF_SCANCODE    = 0x0008;
//const MAPVK_VK_TO_VSC = 0;

export class KeyToggle_Options {
    asScanCode = true;
    keyCodeIsScanCode = false;
    flags?: number;
    async = false; // async can reduce stutter in your app, if frequently sending key-events
}

let entry = new Input(); // having one persistent native object, and just changing its fields, is apparently faster (from testing)
entry.type = INPUT_KEYBOARD;
entry.time = 0;
entry.dwExtraInfo = 0;
export function KeyToggle(keyCode: number, type = "down" as "down" | "up", options?: Partial<KeyToggle_Options>) {
    const opt = Object.assign({}, new KeyToggle_Options(), options);
    
    // scan-code approach (default)
    if (opt.asScanCode) {
        //let scanCode = user32.MapVirtualKeyEx(keyCode, MAPVK_VK_TO_VSC); // this should work, but it had a Win32 error (code 127) for me
        let scanCode = opt.keyCodeIsScanCode ? keyCode : ConvertKeyCodeToScanCode(keyCode);
        let isExtendedKey = (scanCode & extendedKeyPrefix) == extendedKeyPrefix;

        entry.dwFlags = KEYEVENTF_SCANCODE;
        if (isExtendedKey) {
            entry.dwFlags |= KEYEVENTF_EXTENDEDKEY;
        }

        entry.wVK = 0;
        entry.wScan = isExtendedKey ? scanCode - extendedKeyPrefix : scanCode;
    }
    // (virtual) key-code approach
    else {
        entry.dwFlags = 0;
        entry.wVK = keyCode;
        //info.wScan = 0x0200;
        entry.wScan = 0;
    }

    if (opt.flags != null) {
        entry.dwFlags = opt.flags;
    }
    if (type == "up") {
        entry.dwFlags |= KEYEVENTF_KEYUP;
    }

    if (opt.async) {
        return new Promise((resolve, reject)=> {
            user32.SendInput.async(1, entry, arch === "x64" ? 40 : 28, (error, result)=> {
                if (error) reject(error);
                resolve(result);
            });
        });
    }
    return user32.SendInput(1, entry, arch === "x64" ? 40 : 28);
}

export function KeyTap(keyCode: number, opt?: Partial<KeyToggle_Options>) {
    KeyToggle(keyCode, "down", opt);
    KeyToggle(keyCode, "up", opt);
}

// Scan-code for a char equals its index in this list. List based on: https://qb64.org/wiki/Scancodes, https://www.qbasic.net/en/reference/general/scan-codes.htm
// Not all keys are in this list, of course. You can add a custom mapping for other keys to the function below it, as needed.
let keys = "**1234567890-=**qwertyuiop[]**asdfghjkl;'`*\\zxcvbnm,./".split("");

export function ConvertKeyCodeToScanCode(keyCode: number) {
    let keyChar = String.fromCharCode(keyCode).toLowerCase();
    let result = keys.indexOf(keyChar);
    console.assert(result != -1, `Could not find scan-code for key ${keyCode} (${keycode.names[keyCode]}).`)
    return result;
}

Чтобы использовать его, позвоните:

KeyTap(65); // press the A key

Или, если вы используете пакет keycode npm:

import keycode from "keycode";
KeyTap(keycode.codes.a);
person Venryx    schedule 18.05.2018
comment
@truefusion Похоже, вы правы, хотя я не могу найти подходящей документации для этой области. Страница, на которую я ссылаюсь в комментарии к коду, упоминает поле, но вообще не описывает его и не предоставляет возможные значения. (он просто дает текстовую ссылку DUMMYUNIONNAME.mi) - person Venryx; 08.01.2019
comment
чтобы исправить проблему MapVirtualKeyEx, я использовал вместо нее MapVirtualKeyExA, и это сработало: user32.MapVirtualKeyExA(keyCode, 0, 0); и MapVirtualKeyExA: ["uint", ["uint", "uint", "int"]], - person yaya; 05.10.2020
comment
@yaya Потрясающе! Спасибо за информацию. (Я обновлю свой ответ, как только подтвержу его на своем компьютере.) - person Venryx; 06.10.2020
comment
Конечно. Вот мой полный код, поэтому, если вы столкнетесь с какой-либо ошибкой, вы также можете проверить его: pastebin.pl/view/f527be0e< /а> - person yaya; 07.10.2020
comment
@Venryx Я тоже столкнулся с необходимостью элемента заполнения "???": "int" во время работы в архитектуре x64. Я считаю, что это связано с тем, что указатели внутри структур должны быть выровнены по размеру указателей (8 байтов в архитектуре x64). Добавив этот 4-байтовый интервал, вы фактически выровняли указатель (ULONG_PTR dwExtraInfo). Я нашел другой способ исправить это: пометьте это поле типом pointer, а ref-struct-napi позаботится о выравнивании за вас. Смотрите мой ответ для более подробной информации. - person Jason Fry; 16.10.2020

Вот рабочий пример, который нажимает клавишу a. Он использует ref-struct-napi и ref-union-napi для точного представления структуры INPUT.

const FFI = require('ffi-napi')
const StructType = require('ref-struct-napi')
const UnionType = require('ref-union-napi')
const ref = require('ref-napi')


const user32 = new FFI.Library('user32.dll', {
  // UINT SendInput(
  //   _In_ UINT cInputs,                     // number of input in the array
  //   _In_reads_(cInputs) LPINPUT pInputs,  // array of inputs
  //   _In_ int cbSize);                      // sizeof(INPUT)
  'SendInput': ['uint32', ['int32', 'pointer', 'int32']],
})

// typedef struct tagMOUSEINPUT {
//   LONG    dx;
//   LONG    dy;
//   DWORD   mouseData;
//   DWORD   dwFlags;
//   DWORD   time;
//   ULONG_PTR dwExtraInfo;
// } MOUSEINPUT;
const MOUSEINPUT = StructType({
  dx: 'int32',
  dy: 'int32',
  mouseData: 'uint32',
  flags: 'uint32',
  time: 'uint32',
  extraInfo: 'pointer',
})

// typedef struct tagKEYBDINPUT {
//   WORD    wVk;
//   WORD    wScan;
//   DWORD   dwFlags;
//   DWORD   time;
//   ULONG_PTR dwExtraInfo;
// } KEYBDINPUT;
const KEYBDINPUT = StructType({
  vk: 'uint16',
  scan: 'uint16',
  flags: 'uint32',
  time: 'uint32',
  extraInfo: 'pointer',
})

// typedef struct tagHARDWAREINPUT {
//   DWORD   uMsg;
//   WORD    wParamL;
//   WORD    wParamH;
// } HARDWAREINPUT;
const HARDWAREINPUT = StructType({
  msg: 'uint32',
  paramL: 'uint16',
  paramH: 'uint16',
})

// typedef struct tagINPUT {
//   DWORD   type;
//   union
//   {
//     MOUSEINPUT      mi;
//     KEYBDINPUT      ki;
//     HARDWAREINPUT   hi;
//   } DUMMYUNIONNAME;
// } INPUT;
const INPUT_UNION = UnionType({
  mi: MOUSEINPUT,
  ki: KEYBDINPUT,
  hi: HARDWAREINPUT,
})
const INPUT = StructType({
  type: 'uint32',
  union: INPUT_UNION,
})

const pressKey = (scanCode) => {
  const keyDownKeyboardInput = KEYBDINPUT({vk: 0, extraInfo: ref.NULL_POINTER, time: 0, scan: scanCode, flags: 0x0008})
  const keyDownInput = INPUT({type: 1, union: INPUT_UNION({ki: keyDownKeyboardInput})})
  user32.SendInput(1, keyDownInput.ref(), INPUT.size)

  const keyUpKeyboardInput = KEYBDINPUT({vk: 0, extraInfo: ref.NULL_POINTER, time: 0, scan: scanCode, flags: 0x0008 | 0x0002})
  const keyUpInput = INPUT({type: 1, union: INPUT_UNION({ki: keyUpKeyboardInput})})
  user32.SendInput(1, keyUpInput.ref(), INPUT.size)
}

pressKey(0x1E)

Если вы хотите выполнить один вызов SendInput, включающий несколько нажатий клавиш, создайте массив из INPUT структур:

const pressKey = (scanCode) => {
  const inputCount = 2
  const inputArray = Buffer.alloc(INPUT.size * inputCount)
  const keyDownKeyboardInput = KEYBDINPUT({vk: 0, extraInfo: ref.NULL_POINTER, time: 0, scan: scanCode, flags: 0x0008})
  const keyDownInput = INPUT({type: 1, union: INPUT_UNION({ki: keyDownKeyboardInput})})
  keyDownInput.ref().copy(inputArray, 0)
  const keyUpKeyboardInput = KEYBDINPUT({vk: 0, extraInfo: ref.NULL_POINTER, time: 0, scan: scanCode, flags: 0x0008 | 0x0002})
  const keyUpInput = INPUT({type: 1, union: INPUT_UNION({ki: keyUpKeyboardInput})})
  keyUpInput.ref().copy(inputArray, INPUT.size)
  user32.SendInput(inputCount, inputArray, INPUT.size)
}
person Jason Fry    schedule 16.10.2020
comment
Я читал, что SendInput может отправлять несколько ключевых событий в рамках одного вызова функции (docs.microsoft.com/en-us/windows/win32/api/winuser/). Знаете ли вы, как это сделать, используя приведенный выше код/структуры? (это то, что я не смог понять, как это сделать) - person Venryx; 18.10.2020
comment
@Venryx Посмотрите мой ответ, я обновил его, чтобы включить это. - person Jason Fry; 19.10.2020
comment
Потрясающий! Большое спасибо, пригодится. - person Venryx; 19.10.2020

«1» говорит о том, что было вставлено 1 событие, а не о том, что это за событие на самом деле. Я не знаю о FFI, но мне кажется, что в keyboardInput есть некоторые недопустимые определения типов. wVK и wScan должны быть 16-битными целыми числами (отсюда 'w' для WORD). Поскольку они набираются так же, как dwFlags («int»), это приводит к недопустимым входным значениям.

person Ton Plooij    schedule 27.12.2016