- You need to translate the specified virtual-key code and keyboard state to the corresponding Unicode character or characters using the
ToUnicodeEx
function.
- 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.
- 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