Skip to content

Instantly share code, notes, and snippets.

@ethanhs
Created October 14, 2015 00:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ethanhs/10ac383a3a5b39d75251 to your computer and use it in GitHub Desktop.
Save ethanhs/10ac383a3a5b39d75251 to your computer and use it in GitHub Desktop.
example to get a tuple of the mouse coordinates when one clicks ctrl+a
This gist is for pywinauto. I would suggest that you `print(e)` above `if type(e)==MouseEvents:`, as this will be particlarly enlightening for looking at the low level info gained by this function.
import ctypes
from ctypes import wintypes
from collections import namedtuple
import platform
__version__="0.0.6"
KeyEvents=namedtuple("KeyEvents",(['event_type', 'key_code',
'scan_code', 'alt_pressed',
'time']))
MouseEvents=namedtuple("MouseEvents",(['event_type','mouse_x','mouse_y']))
class hook:
"""Main class to create and track hotkeys. Use hook.Hotkey to make a new hotkey"""
def __init__(self):
self.fhot=[]
self.list=[]
self.handlers=[]
self.IDs=[]
self.oldID=0
self.current_keys=[]
self.mouse=()
#Scancodes and a few key codes
self.keylist=["Null","Esc","1","2","3","4","5","6","7","8","9","0","-","=","Backspace","Tab","Q","W","E","R","T","Y","U","I","O","P","[","]","Return","LCtrl","A","S","D","F","G","H","J","K","L",";","'","`","LShift","\\","Z","X","C","V","B","N","M",",",".","/","RShift","Key*","LAlt","Space","Capslock","F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","Numlock","ScrollLock","KeyHome","Up","KeyPgUp","Key-","Left","Key5","Right","Key+","End","Down","KeyPgDn","KeyIns","KeyDel","SYSRQ","","","F11","F12","","","LWin","RWin","MenuKey","RAlt","RCtrl","PrtSc"]
def print_event(self,e):
"""This parses through the keyboard events. You shouldn't ever need this. Actually, don't mess with this; you may break your computer."""
if type(e)==MouseEvents:
self.mouse=(e.mouse_x,e.mouse_y)
if platform.python_implementation()=="PyPy":
if "scan_code" in str(e):
self.estr=str(e)
start=self.estr.index("scan_code=")
#PyPy seems to append an 'L' so search until that
end=self.estr.index("L",start)
scancode=int(str(e)[(start+10):end])
start2=self.estr.index("key_code=")
end2=self.estr.index("L",start2)
try:
keycode=int(str(e)[(start2+9):end2])
except:
keycode=0
try:
key=self.keylist[scancode]
except:
key=str(e)
elif platform.python_implementation()=="CPython" or platform.python_implementation()=="IronPython":
if "scan_code" in str(e):
self.estr=str(e)
start=self.estr.index("scan_code=")
end=self.estr.index(",",start)
scancode=int(str(e)[(start+10):end])
start2=self.estr.index("key_code=")
end2=self.estr.index(",",start2)
try:
keycode=int(str(e)[(start2+9):end2])
except:
keycode=0
try:
key=self.keylist[scancode]
except:
key=str(e)
if str(e.event_type)=="move":
key=[e.mouse_x,e.mouse_y]
elif not ("scan_code" in str(e)):
if e.event_type[0]=="l":
key="LMouse"
elif e.event_type[0]=="r":
key="RMouse"
elif e.event_type[:2]=="mi":
key="MMouse"
elif "wheel" in str(e.event_type):
key="Wheel"
#alt keys
if key=="LAlt":
if keycode==64:
key=self.keylist[56]
elif keycode==65:
key=self.keylist[94]
#Ctrl keys
if key=="LCtrl":
if keycode==62:
key=self.keylist[29]
elif keycode==63:
key=self.keylist[95]
#Workaround for PrtSc
if key == "Key*":
if keycode == 44:
key = self.keylist[96]
#append to current_keys when down
if str(e.event_type)=="key down" or str(e.event_type)=="left down" or str(e.event_type)=="right down" or "wheel" in str(e.event_type):
self.current_keys.append(key)
for id in self.IDs:
#This next bit is complex. Basically it checks all the hotkeys provided to see if they are in self.current_keys
if all([(i in self.current_keys) for i in id[1]]):
if id[2]:
if len(id)==4:
id[2](id[3])
else:
id[2]()
#remove key when released
elif str(e.event_type)=="key up" or str(e.event_type)=="left up" or str(e.event_type)=="right up":
try:
while key in self.current_keys:
self.current_keys.remove(key)
except:
pass
#append location for motion
elif str(e.event_type)=="move":
for i in self.current_keys:
if len(i)>1:
self.current_keys.remove(i)
self.current_keys.append(key)
for id in self.IDs:
#This next bit is complex. Basically it checks all the hotkeys provided to see if they are in self.current_keys
if all([(i in self.current_keys) for i in id[1]]):
if id[2]:
if len(id)==4:
id[2](id[3])
else:
id[2]()
else:
print(e)
def Hotkey(self,list=[],fhot=None,args=None):
"""Adds a new hotkey. Definition: Hotkey(list=[],fhot=None) where list is the list of
keys and fhot is the callback function"""
if not (args is None):
self.IDs.append([self.oldID,list,fhot,args])
else:
self.IDs.append([self.oldID,list,fhot])
self.oldID+=1
if self.list is [] or self.fhot is None:
raise Exception("Error: Empty key list or no callback function.")
elif len(self.IDs)==1:
self.handlers.append(self.print_event)
return (self.oldID-1)
def RemHotKey(self,hkey):
"""Remove a hotkey. Specify the id, the key list, or the function to remove the hotkey."""
if str(type(hkey))=="<type 'int'>":
for hotk in self.IDs:
if hotk[0]==hkey:
self.IDs.remove(hotk)
elif str(type(hkey))=="<type 'list'>":
for hotk in self.IDs:
if hotk[1]==hkey:
self.IDs.remove(hotk)
elif str(type(hkey))=="<type 'function'>":
for hotk in self.IDs:
if hotk[2]==hkey:
self.IDs.remove(hotk)
def listener(self):
"""The listener listens to events and adds them to handlers"""
from ctypes import windll, CFUNCTYPE, POINTER, c_int, c_void_p, byref, Structure
import atexit
event_types = {0x100: 'key down', #WM_KeyDown for normal keys
0x101: 'key up', #WM_KeyUp for normal keys
0x104: 'key down', # WM_SYSKEYDOWN, used for Alt key.
0x105: 'key up', # WM_SYSKEYUP, used for Alt key.
}
mouse_types={0x200: 'move', #WM_MOUSEMOVE
0x20A: 'wheel', #WM_MOUSEWHEEL
0x20E: 'H_wheel', #WM_MOUSEHWHEEL
0x204: 'right down', #WM_RBUTTONDOWN
0x205: 'right up', #WM_RBUTTONUP
0x201: 'left down', #WM_LBUTTONDOWN
0x202: 'left up', #WM_LBUTTONUP
0x207: 'middle down', #WM_MBUTTONDOWN
0x208: 'middle up'} #WM_MBUTTONUP
def low_level_handler(nCode, wParam, lParam):
"""
Processes a low level Windows keyboard event.
"""
event = KeyEvents(event_types[wParam], lParam[0], lParam[1],
lParam[2] == 32, lParam[3])
for h in self.handlers:
h(event)
#return next hook
return windll.user32.CallNextHookEx(hook_id, nCode, wParam, lParam)
CMPFUNC = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p))
#Make a C pointer
pointer = CMPFUNC(low_level_handler)
windll.kernel32.GetModuleHandleW.restype = wintypes.HMODULE
windll.kernel32.GetModuleHandleW.argtypes = [wintypes.LPCWSTR]
hook_id = windll.user32.SetWindowsHookExA(0x00D, pointer,
windll.kernel32.GetModuleHandleW(None), 0)
def low_level_handler_mouse(nCode, wParam, lParam):
"""
Processes a low level Windows mouse event.
"""
event = MouseEvents(mouse_types[wParam], lParam[0], lParam[1])
for h in self.handlers:
h(event)
#return next hook
return windll.user32.CallNextHookEx(hook_id, nCode, wParam, lParam)
pointer2 = CMPFUNC(low_level_handler_mouse)
hook_id = windll.user32.SetWindowsHookExA(0x0E, pointer2,
windll.kernel32.GetModuleHandleW(None), 0)
#Remove hook when done
atexit.register(windll.user32.UnhookWindowsHookEx, hook_id)
while True:
msg = windll.user32.GetMessageW(None, 0, 0,0)
windll.user32.TranslateMessage(byref(msg))
windll.user32.DispatchMessageW(byref(msg))
def listen(self):
"""Start listening for hooks"""
self.listener()
def get_mouse(self):
return self.mouse
def foo():
print(hk.get_mouse())
def foobar():
"""For the example, it prints 'foobar'."""
print("foobar")
def exiter():
raise SystemExit
if __name__ == '__main__':
hk=hook()
hk.Hotkey(["LCtrl","A"],foo)
hk.listen()
@ethanhs
Copy link
Author

ethanhs commented Oct 14, 2015

License is LGPL btw.

@vasily-v-ryabov
Copy link

Method print_event looks too complicated, it would be great to re-structure that code. For example, PyPy and CPython branches have only one line difference, but all the code is duplicated for both.

Magic numbers like 0x00D or 0x0E is not a good practice. Pseudo-constants WH_KEYBOARD_LL and WH_MOUSE_LL should be used instead.

It's also not clear why you're using the same variable hook_id for 2 different calls to SetWindowsHookExA. I believe these 2 calls should be in different methods (when setting HotKey handler or mouse handler - separately).

low_level_handler_mouse should pass wParam and lParam to higher level callback so that the callback decides whether to get the info about coordinates and hwnd where the event happened.

@ethanhs
Copy link
Author

ethanhs commented Oct 27, 2015

All good sugestions. And no, I didn't get a notification.

@redstoneleo
Copy link

According to the MSDN doc,

If the keystroke is held long enough, multiple messages are sent.

Because of the autorepeat feature, more than one WM_KEYDOWN message may be posted before a WM_KEYUP message is posted. The previous key state (bit 30) can be used to determine whether the WM_KEYDOWN message indicates the first down transition or a repeated down transition.

I wonder how to get the lParam parameter to implement the feature ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment