Michael ilkanayev
Michael ilkanayev

Reputation: 95

Detect keyboard input with support of other languages from English

I want to detect keystrokes in python in English and also in Hebrew ,I am using Python version 3.7. I've already tried a lot of methods but but nothing works. The problem is that no matter what I do it does not recognize the letters in a language other than English. And if I write in Hebrew it's still appears in the text file in English. I need that even if I press Alt+Shift to changes my input to another language, it detect that language.

IMPORTANT: I need the Windows version.

Suppose this simple example:

# coding: utf-8
from pynput import keyboard
    
def on_press(key):
    with open('keys.txt', 'a',encoding ='utf-8') as file:
        file.write("{0}\n".format( str(key)))
       
with keyboard.Listener(on_press=on_press) as listener:
    listener.join()

Thanks.

Upvotes: 3

Views: 1608

Answers (1)

JosefZ
JosefZ

Reputation: 30113

  1. You need to translate the specified virtual-key code and keyboard state to the corresponding Unicode character or characters using the ToUnicodeEx function.
  2. To do that (see the first part of code below, mainly def ToUn), you need to know the active input locale identifier (formerly called the keyboard layout). This isn't an easy task, and its solution is based on my own research, see the def get_current_keyboard_layout(). The main problem is that the GetKeyboardLayout function returns right value for the current thread merely under some shell/terminal (e.g. under Python IDLE or Visual Studio Code or Visual Studio 2019, …) and fails if launched from the Run dialog box (WinKey+R) or from cmd.exe or from pwsh.exe or from any app which calls the conhost.exe helper process.
  3. The original code is improved for extensively verbose output.

You can easy identify above three parts in the following (partially commented) script:

import os
import sys

if os.name != 'nt':
    raise Exception("IMPORTANT: I need the Windows version.") 
_verbose = ((len(sys.argv) > 1) and bool(sys.argv[1]))

### Translate the specified virtual-key code and keyboard state
#   to the corresponding Unicode character or characters.
#   learn.microsoft.com/en-gb/windows/win32/api/winuser/nf-winuser-tounicodeex
### Adapted from
#   the solution to https://stackoverflow.com/questions/38224277/
#   by https://stackoverflow.com/users/235698/mark-tolonen
###

from ctypes import (
    WinDLL, POINTER, create_string_buffer, create_unicode_buffer,
    c_int32, c_uint, c_uint, c_char, c_wchar, c_int, c_uint, c_void_p
    )
_ToUnicodeEx = WinDLL('user32').ToUnicodeEx
_ToUnicodeEx.argtypes = [
        c_uint,           # wVirtKey   virtual-key code to be translated
        c_uint,           # wScanCode  hardware scan code of ˙wVirtKey˙
        POINTER(c_char),  # lpKeyState current keyboard state (256-byte array)
        POINTER(c_wchar), # pwszBuff   buffer that receives translated chars
        c_int,            # cchBuff    size of the `pwszBuff` buffer (in chars)
        c_uint,           # wFlags     behavior of the function
        c_void_p          # dwhkl      input locale identifier
]
_ToUnicodeEx.restype = c_int


def ToUn(vk,sc,wfl,hkid):
    kst = create_string_buffer(256)
    b = create_unicode_buffer(5)
    if _ToUnicodeEx(vk,sc,kst,b,5,wfl,hkid):
        return b.value
    else:
        return chr( 0xFFFD) # Replacement Character


### Retrieve the active input locale identifier
#   (formerly called the keyboard layout)
#   https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeyboardlayout
###
#   Method based on my own research; non-optimized, debugged on Windows 10… 
###

from ctypes import WinDLL
user32 = WinDLL('user32', use_last_error=True)


def list_parents(pid, proclist):
    '''For verbose output'''
    aux = [_ for _ in proclist if _[0] == pid]
    if len( aux) > 0:
        auxcon = [x for x in proclist if (
                x[1] == aux[0][0] and x[2] == "conhost.exe")]
        list_parents(aux[0][1], proclist)
        print('parent', aux[0], auxcon if (len(auxcon) == 0) else auxcon[0])

def get_servant_conhost(pid, proclist):
    """Find “attendant” host process (conhost.exe)"""
    aux = [_ for _ in proclist if _[0] == pid]
    if len( aux) > 0:
        auxcon = [x for x in proclist if (
                x[1] == aux[0][0] and x[2] == "conhost.exe")]
        if len( auxcon) == 0:
            auxconret = get_servant_conhost(aux[0][1], proclist)
            return auxconret
        else:
            auxconret = auxcon[0]
            auxconret.append( aux[0][2])
            return auxconret
    else:
        return []


def get_conhost_threads():
    if sys.executable.lower().endswith('\\pythonw.exe'):
        return []
    import wmi
    c = wmi.WMI()
    w_where = ' or '.join([
        'Name like "p%.exe"',  # py.exe|python.exe|pwsh.exe|powershell.exe 
        'Name = "conhost.exe"',
        'Name = "cmd.exe"'
    ])
    w_properties = 'ProcessId, ParentProcessId, Name'
    w_wql = f'SELECT {w_properties} FROM Win32_Process where {w_where}'
    w_wqlaux = c.query(w_wql)
    proc_list = [[wqlitem.__getattr__('ProcessId'),
          wqlitem.__getattr__('ParentProcessId'),
          wqlitem.__getattr__('Name')] for wqlitem in w_wqlaux] 
    if _verbose:
        list_parents(os.getpid(), proc_list)
    servant_conhost = get_servant_conhost(os.getpid(), proc_list)
    if len( servant_conhost) == 0:
        return []
    else:
        try:
            w_where = f'ProcessHandle = {servant_conhost[0]}'
            w_wql = f'SELECT Handle FROM Win32_Thread WHERE {w_where}'
            w_wqlHandle = c.query(w_wql)
            wqlthreads = [x.__getattr__('Handle') for x in w_wqlHandle]
        except:
            wqlthreads = []
    return wqlthreads


# required if run from `cmd` or from the `Run` dialog box (`<WinKey>+R`) 
conhost_threads = get_conhost_threads()
if _verbose:
    print( 'threads', conhost_threads)
                                    

def get_current_keyboard_layout():
    foregroundWindow  = user32.GetForegroundWindow();
    foregroundProcess = user32.GetWindowThreadProcessId(int(foregroundWindow), 0);
    keyboardLayout    = user32.GetKeyboardLayout(int(foregroundProcess));
    keyboardLayout0   = user32.GetKeyboardLayout(int(0));
    if keyboardLayout == 0  or len(conhost_threads):                 
        if keyboardLayout == 0:
            keyboardLayout = keyboardLayout0
        for thread in conhost_threads:
            aux = user32.GetKeyboardLayout( int(thread))
            if aux != 0 and aux != keyboardLayout0:
                if _verbose:
                    print('thread', thread)
                keyboardLayout = aux
                break
    return keyboardLayout


### improved original code
#   Detect keyboard input with support of other languages from English
#   https://stackoverflow.com/questions/71431386/
###
#   written for extensively verbose output
###

import unicodedata
from pynput import keyboard
last_key = keyboard.Key.media_next  # improbable last key pressed


def on_press(key):
    global last_key
    if isinstance(key, keyboard.Key):
        if key == keyboard.Key.space:
            c_hkl = get_current_keyboard_layout()
            chklp = f'{(c_hkl & 0xFFFFFFFF):08x}'
            # 0x39 = SpaceBarScanCode https://kbdlayout.info/kbduk/scancodes
            print(key.value.char,
                  f'{key.value.vk} ({key.value.vk:02x})',
                  f'{0x39} ({0x39:02x})',
                  f'{c_hkl:10} ({chklp})',
                  unicodedata.name(key.value.char, '?')
                  )
        else:
            if last_key != key:
                print( f'{"":39}', key.value, key.name )
                last_key = key
    else:
        c_hkl = get_current_keyboard_layout()
        chklp = f'{(c_hkl & 0xFFFFFFFF):08x}'
        c_char = ToUn(key.vk, key._scan, 0, c_hkl)
        print(c_char,
              f'{key.vk} ({key.vk:02x})',
              f'{key._scan} ({key._scan:02x})',
              f'{c_hkl:10} ({chklp})',
              unicodedata.name(c_char[0], '?')
              )


def on_release(key):
    if key == keyboard.Key.esc:     
        return False             # Stop listener


print('\n  vk_code scancode   HKL dec (HKL hexa) character name')
with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
    x=listener.join()

Output (tested mainly the F key for different keyboard layouts (input languages) switched cyclically by LeftAltShift): \SO\71431386.py

  vk_code scancode   HKL dec (HKL hexa) character name
f 70 (46) 33 (21)   67438601 (04050809) LATIN SMALL LETTER F
                                        <164> alt_l
                                        <160> shift
ф 70 (46) 33 (21) -265092071 (f0330419) CYRILLIC SMALL LETTER EF
                                        <164> alt_l
                                        <160> shift
f 70 (46) 33 (21)   67437573 (04050405) LATIN SMALL LETTER F
                                        <164> alt_l
                                        <160> shift
φ 70 (46) 33 (21) -266992632 (f0160408) GREEK SMALL LETTER PHI
                                        <164> alt_l
                                        <160> shift
f 70 (46) 33 (21)   67438601 (04050809) LATIN SMALL LETTER F
  32 (20) 57 (39)   67438601 (04050809) SPACE
                                        <27> esc

Upvotes: 5

Related Questions