10

So I'm trying to figure out how to register a global keyboard hook using Python. From what I have read, it seems to be okay to not have the callback in a DLL. If you use WH_KEYBOARD_LL. I can't confirm that for sure but I find it encouraging that I don't get a 1428 error like I do if I try to hook into say WH_CBT.

I get a hook handle but nothing shows up when I press buttons on the keyboard as I would expect.

Any idea's on why my callback is not being called? Or is this even possible?

The relevant code :

import time
import string
import ctypes
import functools
import atexit
import pythoncom
from ctypes import windll
hookID = 0
class Keyboard(object):
 KEY_EVENT_DOWN = 0
 KEY_EVENT_UP = 2
 KEY_ENTER = 2
 KEY_SHIFT = 16
 KEY_SPACE = 32
 HOOK_ACTION = 13
 HOOK_KEYBOARD = 13
 HOOK_KEYDOWN = 0x100
 HOOK_KEYUP = 0x101
 class Hook:
 '''Holds general hook information'''
 def __init__(self):
 self.hook = 0
 self.struct = None 
 class HookStruct(ctypes.Structure):
 '''Structure that windows returns for keyboard events'''
 __fields__ = [
 ('keycode', ctypes.c_long),
 ('scancode', ctypes.c_long),
 ('flags', ctypes.c_long),
 ('time', ctypes.c_long),
 ('info', ctypes.POINTER(ctypes.c_ulong))
 ]
 def ascii_to_keycode(self, char):
 return windll.user32.VkKeyScanA(ord(char))
 def inject_key_down(self, keycode):
 scancode = windll.user32.MapVirtualKeyA(keycode, 0)
 windll.user32.keybd_event(keycode, scancode, Keyboard.KEY_EVENT_DOWN, 0)
 def inject_key_up(self, keycode):
 scan = windll.user32.MapVirtualKeyA(keycode, 0)
 windll.user32.keybd_event(keycode, scan, Keyboard.KEY_EVENT_UP, 0)
 def inject_key_press(self, keycode, pause=0.05):
 self.inject_key_down(keycode)
 time.sleep(pause)
 self.inject_key_up(keycode)
 def inject_sequence(self, seq, pause=0.05):
 for key in seq:
 if key == ' ':
 self.inject_key_press(Keyboard.KEY_SPACE, pause)
 elif key == '\n':
 self.inject_key_press(Keyboard.KEY_ENTER, pause)
 else:
 if key in string.ascii_uppercase:
 self.inject_key_down(Keyboard.KEY_SHIFT)
 self.inject_key_press(self.ascii_to_keycode(key), pause)
 self.inject_key_up(Keyboard.KEY_SHIFT)
 else:
 self.inject_key_press(self.ascii_to_keycode(key), pause)
 def _win32_copy_mem(self, dest, src):
 src = ctypes.c_void_p(src)
 windll.kernel32.RtlMoveMemory(ctypes.addressof(dest), src, ctypes.sizeof(dest))
 def _win32_get_last_error(self):
 return windll.kernel32.GetLastError()
 def _win32_get_module(self, mname):
 return windll.kernel32.GetModuleHandleA(mname)
 def _win32_call_next_hook(self, id, code, wparam, lparam):
 return windll.kernel32.CallNextHookEx(id, code, wparam, lparam)
 def _win32_set_hook(self, id, callback, module, thread):
 callback_decl = ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_long, ctypes.c_long, ctypes.c_long)
 return windll.user32.SetWindowsHookExA(id, callback_decl(callback), module, thread)
 def _win32_unhook(self, id):
 return windll.user32.UnhookWindowsHookEx(id)
 def keyboard_event(self, data):
 print data.scancode
 return False
 def capture_input(self):
 self.hook = Keyboard.Hook()
 self.hook.struct = Keyboard.HookStruct()
 def low_level_keyboard_proc(code, event_type, kb_data_ptr):
 # win32 spec says return result of CallNextHookEx if code is less than 0
 if code < 0:
 return self._win32_call_next_hook(self.hook.hook, code, event_type, kb_data_ptr)
 if code == Keyboard.HOOK_ACTION:
 # copy data from struct into Python structure
 self._win32_copy_mem(self.hook.struct, kb_data_ptr)
 # only call other handlers if we return false from our handler - allows to stop processing of keys
 if self.keyboard_event(self.hook.struct):
 return self._win32_call_next_hook(self.hook.hook, code, event_type, kb_data_ptr)
 # register hook 
 try: 
 hookId = self.hook.hook = self._win32_set_hook(Keyboard.HOOK_KEYBOARD, low_level_keyboard_proc, self._win32_get_module(0), 0)
 if self.hook.hook == 0:
 print 'Error - ', self._win32_get_last_error()
 else:
 print 'Hook ID - ', self.hook.hook
 except Exception, error:
 print error
 # unregister hook if python exits
 atexit.register(functools.partial(self._win32_unhook, self.hook.hook))
 def end_capture(self):
 if self.hook.hook:
 return self._win32_unhook(self.hook.hook)
kb = Keyboard()#kb.inject_sequence('This is a test\nand tHis is line 2')
kb.capture_input()
pythoncom.PumpMessages()
kb.end_capture()
Sachin D
1,3867 gold badges29 silver badges44 bronze badges
asked Mar 22, 2012 at 6:35

3 Answers 3

21

I couldn't get your class to work, but I found a similar way to accomplish the same goal in this thread.

Here's the adapted code:

from collections import namedtuple
KeyboardEvent = namedtuple('KeyboardEvent', ['event_type', 'key_code',
 'scan_code', 'alt_pressed',
 'time'])
handlers = []
def listen():
 """
 Calls `handlers` for each keyboard event received. This is a blocking call.
 """
 # Adapted from http://www.hackerthreads.org/Topic-42395
 from ctypes import windll, CFUNCTYPE, POINTER, c_int, c_uint, c_void_p, byref
 import win32con, win32api, win32gui, atexit
 event_types = {win32con.WM_KEYDOWN: 'key down',
 win32con.WM_KEYUP: 'key up',
 0x104: 'key down', # WM_SYSKEYDOWN, used for Alt key.
 0x105: 'key up', # WM_SYSKEYUP, used for Alt key.
 }
 def low_level_handler(nCode, wParam, lParam):
 """
 Processes a low level Windows keyboard event.
 """
 event = KeyboardEvent(event_types[wParam], lParam[0], lParam[1],
 lParam[2] == 32, lParam[3])
 for handler in handlers:
 handler(event)
 # Be a good neighbor and call the next hook.
 return windll.user32.CallNextHookEx(hook_id, nCode, wParam, lParam)
 
 # Our low level handler signature.
 CMPFUNC = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p))
 # add argtypes for 64-bit Python compatibility (per @BaiJiFeiLong)
 windll.user32.SetWindowsHookExW.argtypes = (
 c_int,
 c_void_p, 
 c_void_p,
 c_uint
 )
 # Convert the Python handler into C pointer.
 pointer = CMPFUNC(low_level_handler)
 # Hook both key up and key down events for common keys (non-system).
 hook_id = windll.user32.SetWindowsHookExA(win32con.WH_KEYBOARD_LL, pointer,
 win32api.GetModuleHandle(None), 0)
 # Register to remove the hook when the interpreter exits. Unfortunately a
 # try/finally block doesn't seem to work here.
 atexit.register(windll.user32.UnhookWindowsHookEx, hook_id)
 while True:
 msg = win32gui.GetMessage(None, 0, 0)
 win32gui.TranslateMessage(byref(msg))
 win32gui.DispatchMessage(byref(msg))
if __name__ == '__main__':
 def print_event(e):
 print(e)
 handlers.append(print_event)
 listen()

I've made a high-level library to wrap this: keyboard.

JRiggles
7,2682 gold badges17 silver badges37 bronze badges
answered May 8, 2013 at 0:49
Sign up to request clarification or add additional context in comments.

7 Comments

Nice, concise, answer
After many hours of trying a bunch of different approaches, this seems like the perfect way if you need to listen to keyboard at a low level (without having the console focused). Thanks!
It works great but I get key_codes like 240518168740. I thought it should return values like this. So, how do I convert that to actual character/key being pressed?
Try removing the upper bits: 240518168740 -> 0x38000000A4 & 0xFFFFFFFF = 0xA4. A4 looks like an alt key. Is that correct? Usage example: github.com/boppreh/keyboard
Add from ctypes import c_int, c_void_p, c_uint windll.user32.SetWindowsHookExW.argtypes = (c_int, c_void_p, c_void_p, c_uint) To support x64 python.
|
5

The reason that Tim's original code did not work is because the ctypes function pointer to low_level_keyboard_proc was garbage collected, so his callback became invalid and was not called. It just failed silently.

Windows does not retain Python pointers, so we need to separately retain a reference to the exact callback_decl(callback) ctypes function pointer parameter that is passed to SetWindowsHookEx.

answered Sep 7, 2014 at 23:54

Comments

0

I haven't tried this with Python specifically, but yes, it should be possible for a low-level keyboard or mouse hook. For other hook types, the hook functions must be in a dll.

HOOK_ACTION should be 0, not 13.

answered Mar 23, 2012 at 2:41

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.