Ключевое эхо в Python в отдельном потоке не отображает первый ход ключа

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

Прежде всего, я использую простой script, который имитирует нажатие клавиш через вызов функции. Это изменено из здесь.

import ctypes

SendInput = ctypes.windll.user32.SendInput

PUL = ctypes.POINTER(ctypes.c_ulong)

class KeyBdInput(ctypes.Structure):
    _fields_ = [("wVk", ctypes.c_ushort),
                ("wScan", ctypes.c_ushort),
                ("dwFlags", ctypes.c_ulong),
                ("time", ctypes.c_ulong),
                ("dwExtraInfo", PUL)]

class HardwareInput(ctypes.Structure):
    _fields_ = [("uMsg", ctypes.c_ulong),
                ("wParamL", ctypes.c_short),
                ("wParamH", ctypes.c_ushort)]

class MouseInput(ctypes.Structure):
    _fields_ = [("dx", ctypes.c_long),
                ("dy", ctypes.c_long),
                ("mouseData", ctypes.c_ulong),
                ("dwFlags", ctypes.c_ulong),
                ("time",ctypes.c_ulong),
                ("dwExtraInfo", PUL)]

class Input_I(ctypes.Union):
    _fields_ = [("ki", KeyBdInput),
                 ("mi", MouseInput),
                 ("hi", HardwareInput)]

class Input(ctypes.Structure):
    _fields_ = [("type", ctypes.c_ulong),
                ("ii", Input_I)]

def getKeyCode(unicodeKey):
    k = unicodeKey
    curKeyCode = 0
    if k == "up": curKeyCode = 0x26
    elif k == "down": curKeyCode = 0x28
    elif k == "left": curKeyCode = 0x25
    elif k == "right": curKeyCode = 0x27
    elif k == "home": curKeyCode = 0x24
    elif k == "end": curKeyCode = 0x23
    elif k == "insert": curKeyCode = 0x2D
    elif k == "pgup": curKeyCode = 0x21
    elif k == "pgdn": curKeyCode = 0x22
    elif k == "delete": curKeyCode = 0x2E
    elif k == "\n": curKeyCode = 0x0D

    if curKeyCode == 0:
        return 0, int(unicodeKey.encode("hex"), 16), 0x0004
    else:
        return curKeyCode, 0, 0

def PressKey(unicodeKey):
    key, unikey, uniflag = getKeyCode(unicodeKey)

    extra = ctypes.c_ulong(0)
    ii_ = Input_I()
    ii_.ki = KeyBdInput( key, unikey, uniflag, 0, ctypes.pointer(extra) )
    x = Input( ctypes.c_ulong(1), ii_ )
    ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))

def ReleaseKey(unicodeKey):
    key, unikey, uniflag = getKeyCode(unicodeKey)
    extra = ctypes.c_ulong(0)
    ii_ = Input_I()
    ii_.ki = KeyBdInput( key, unikey, uniflag + 0x0002, 0, ctypes.pointer(extra) )
    x = Input( ctypes.c_ulong(1), ii_ )
    ctypes.windll.user32.SendInput(1, ctypes.pointer(x), ctypes.sizeof(x))

Я сохранил это в файле с именем keyPress.py.

Используя это, я хотел создать простую программу, которая могла бы обнаружить, что пользователь вводил, когда они вводили ее в оболочку python. Идея заключалась в том, что я использовал бы msvcrt.getch(), чтобы нажать клавишу, а затем script, чтобы было похоже, что она все еще нажата (и "эхо" нажатие клавиши в некотором смысле ")

Вот код:

import keyPress
import msvcrt        
import threading

def getKey():

    k = msvcrt.getch()

    # Escaped Key: 224 is on the keyboard, 0 is on the numpad
    if int(k.encode("hex"), 16) == 224 or int(k.encode("hex"), 16) == 0:
        k = msvcrt.getch()
        if k == "H": k = "up"
        elif k == "P": k = "down"
        elif k == "K": k = "left"
        elif k == "M": k = "right"
        elif k == "G": k = "home"
        elif k == "O": k = "end"
        elif k == "R": k = "insert"
        elif k == "I": k = "pgup"
        elif k == "Q": k = "pgdn"
        elif k == "S": k = "delete"

    # Fix weird linebreak
    if k == "\r":
        k = "\n"

    return k


def actualGetKeys():
    while True:
        k = getKey()
        keyPress.PressKey(k)
        keyPress.ReleaseKey(k)

def getKeys():
    p = threading.Thread(target=actualGetKeys)
    p.daemon = True
    p.start()   

Я сохранил это в файле с именем keyGet.py.

Все это работает очень хорошо, за исключением того, что всякий раз, когда пользователь нажимает кнопку ввода, первый ключ не отображается на экране. Консоль все еще знает, что вы ее набрали, она просто не появляется там. Что-то вроде этого:

Не отображать все входные данные

Что происходит? Я пробовал много вещей, и я не могу заставить это поведение меняться.

Теперь я могу получить эту работу по существу, так как в ней можно асинхронно захватывать ввод ключей, а script выполняется и выполнять с текстом каждой команды, которую вы вводите в командной строке (так что вы могли бы, скажем, сохраните их в массиве). Единственная проблема, с которой я сталкиваюсь, - это что-то вроде этого:

Проблема перепечатки

Я знаю, что это связано с тем, что, по сути, необходимо, чтобы робот повторно вводил свой ввод после того, как он набирал его, мне просто интересно, есть ли способ сделать это, который предотвращает фактическое отображение этого ввода, когда робот его вводит, поэтому он действует так же, как ожидал пользователь.

Ответ 1

Вот результирующий код, в основном написанный комментариями eryksun, потому что как-то он все знает.

Это называется readcmd.py

# Some if this is from http://nullege.com/codes/show/[email protected]@[email protected]@[email protected]@subprocess.py/380/win32api.GetStdHandle
# and
# http://nullege.com/codes/show/[email protected]@[email protected]@[email protected]@winpexpect.py/901/win32console.GetStdHandle.PeekConsoleInput

from ctypes import *
import time
import threading

from win32api import STD_INPUT_HANDLE, STD_OUTPUT_HANDLE

from win32console import GetStdHandle, KEY_EVENT, ENABLE_WINDOW_INPUT, ENABLE_MOUSE_INPUT, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT

import keyPress


class CaptureLines():
    def __init__(self):
        self.stopLock = threading.Lock()

        self.isCapturingInputLines = False

        self.inputLinesHookCallback = CFUNCTYPE(c_int)(self.inputLinesHook)
        self.pyosInputHookPointer = c_void_p.in_dll(pythonapi, "PyOS_InputHook")
        self.originalPyOsInputHookPointerValue = self.pyosInputHookPointer.value

        self.readHandle = GetStdHandle(STD_INPUT_HANDLE)
        self.readHandle.SetConsoleMode(ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT|ENABLE_PROCESSED_INPUT)

    def inputLinesHook(self):

        self.readHandle.SetConsoleMode(ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT|ENABLE_PROCESSED_INPUT)
        inputChars = self.readHandle.ReadConsole(10000000)
        self.readHandle.SetConsoleMode(ENABLE_LINE_INPUT|ENABLE_PROCESSED_INPUT)

        if inputChars == "\r\n":
            keyPress.KeyPress("\n")
            return 0

        inputChars = inputChars[:-2]

        inputChars += "\n"

        for c in inputChars:
            keyPress.KeyPress(c)

        self.inputCallback(inputChars)

        return 0


    def startCapture(self, inputCallback):
        self.stopLock.acquire()

        try:
            if self.isCapturingInputLines:
                raise Exception("Already capturing keystrokes")

            self.isCapturingInputLines = True
            self.inputCallback = inputCallback

            self.pyosInputHookPointer.value = cast(self.inputLinesHookCallback, c_void_p).value
        except Exception as e:
            self.stopLock.release()
            raise

        self.stopLock.release()

    def stopCapture(self):
        self.stopLock.acquire()

        try:
            if not self.isCapturingInputLines:
                raise Exception("Keystrokes already aren't being captured")

            self.readHandle.SetConsoleMode(ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT|ENABLE_PROCESSED_INPUT)

            self.isCapturingInputLines = False
            self.pyosInputHookPointer.value = self.originalPyOsInputHookPointerValue

        except Exception as e:
            self.stopLock.release()
            raise

        self.stopLock.release()

И вот keyPress.py

# Modified from http://stackoverflow.com/a/13615802/2924421

import ctypes
from ctypes import wintypes
import time

user32 = ctypes.WinDLL('user32', use_last_error=True)

INPUT_MOUSE    = 0
INPUT_KEYBOARD = 1
INPUT_HARDWARE = 2

KEYEVENTF_EXTENDEDKEY = 0x0001
KEYEVENTF_KEYUP       = 0x0002
KEYEVENTF_UNICODE     = 0x0004
KEYEVENTF_SCANCODE    = 0x0008

MAPVK_VK_TO_VSC = 0

# C struct definitions
wintypes.ULONG_PTR = wintypes.WPARAM

SendInput = ctypes.windll.user32.SendInput

PUL = ctypes.POINTER(ctypes.c_ulong)

class KEYBDINPUT(ctypes.Structure):
    _fields_ = (("wVk",         wintypes.WORD),
                ("wScan",       wintypes.WORD),
                ("dwFlags",     wintypes.DWORD),
                ("time",        wintypes.DWORD),
                ("dwExtraInfo", wintypes.ULONG_PTR))

class MOUSEINPUT(ctypes.Structure):
    _fields_ = (("dx",          wintypes.LONG),
                ("dy",          wintypes.LONG),
                ("mouseData",   wintypes.DWORD),
                ("dwFlags",     wintypes.DWORD),
                ("time",        wintypes.DWORD),
                ("dwExtraInfo", wintypes.ULONG_PTR))

class HARDWAREINPUT(ctypes.Structure):
    _fields_ = (("uMsg",    wintypes.DWORD),
                ("wParamL", wintypes.WORD),
                ("wParamH", wintypes.WORD))

class INPUT(ctypes.Structure):
    class _INPUT(ctypes.Union):
        _fields_ = (("ki", KEYBDINPUT),
                    ("mi", MOUSEINPUT),
                    ("hi", HARDWAREINPUT))
    _anonymous_ = ("_input",)
    _fields_ = (("type",   wintypes.DWORD),
                ("_input", _INPUT))

LPINPUT = ctypes.POINTER(INPUT)

def _check_count(result, func, args):
    if result == 0:
        raise ctypes.WinError(ctypes.get_last_error())
    return args

user32.SendInput.errcheck = _check_count
user32.SendInput.argtypes = (wintypes.UINT, # nInputs
                             LPINPUT,       # pInputs
                             ctypes.c_int)  # cbSize

def KeyDown(unicodeKey):
    key, unikey, uniflag = GetKeyCode(unicodeKey)
    x = INPUT( type=INPUT_KEYBOARD, ki= KEYBDINPUT( key, unikey, uniflag, 0))
    user32.SendInput(1, ctypes.byref(x), ctypes.sizeof(x))

def KeyUp(unicodeKey):
    key, unikey, uniflag = GetKeyCode(unicodeKey)
    extra = ctypes.c_ulong(0)
    x = INPUT( type=INPUT_KEYBOARD, ki= KEYBDINPUT( key, unikey, uniflag | KEYEVENTF_KEYUP, 0))
    user32.SendInput(1, ctypes.byref(x), ctypes.sizeof(x))

def KeyPress(unicodeKey):
    time.sleep(0.0001)
    KeyDown(unicodeKey)
    time.sleep(0.0001)
    KeyUp(unicodeKey)
    time.sleep(0.0001)


def GetKeyCode(unicodeKey):
    k = unicodeKey
    curKeyCode = 0
    if k == "up": curKeyCode = 0x26
    elif k == "down": curKeyCode = 0x28
    elif k == "left": curKeyCode = 0x25
    elif k == "right": curKeyCode = 0x27
    elif k == "home": curKeyCode = 0x24
    elif k == "end": curKeyCode = 0x23
    elif k == "insert": curKeyCode = 0x2D
    elif k == "pgup": curKeyCode = 0x21
    elif k == "pgdn": curKeyCode = 0x22
    elif k == "delete": curKeyCode = 0x2E
    elif k == "\n": curKeyCode = 0x0D

    if curKeyCode == 0:
        return 0, int(unicodeKey.encode("hex"), 16), KEYEVENTF_UNICODE
    else:
        return curKeyCode, 0, 0