Simplifying Ctype union in Python (to send Keyboard events in Windows)
Question:
I have the following script that sends a keyboard key every xx seconds (F15 by default), inspired by some code found online
I want to remove the types and union related to Mouse events but cannot make it to work. In particular, I don’t know how to remove the class MOUSEINPUT
and the union of types _INPUTunion
(removing them will stop sending the keyboard key).
Any suggestion on how to trim the script to a minimum (i.e. only keep code related to Keyboard)?
The following code will send the key “C” to be able to debug.
#!/python
import ctypes
import sys
import time
LONG = ctypes.c_long
DWORD = ctypes.c_ulong
ULONG_PTR = ctypes.POINTER(DWORD)
WORD = ctypes.c_ushort
class MOUSEINPUT(ctypes.Structure):
_fields_ = (
('dx', LONG), ('dy', LONG), ('mouseData', DWORD),
('dwFlags', DWORD), ('time', DWORD),
('dwExtraInfo', ULONG_PTR)
)
class KEYBDINPUT(ctypes.Structure):
_fields_ = (
('wVk', WORD), ('wScan', WORD),
('dwFlags', DWORD), ('time', DWORD),
('dwExtraInfo', ULONG_PTR)
)
class _INPUTunion(ctypes.Union):
_fields_ = (('mi', MOUSEINPUT), ('ki', KEYBDINPUT))
class INPUT(ctypes.Structure):
_fields_ = (('type', DWORD), ('union', _INPUTunion))
def SendInput(*inputs):
print(inputs[0].union.mi)
nInputs = len(inputs)
LPINPUT = INPUT * nInputs
pInputs = LPINPUT(*inputs)
cbSize = ctypes.c_int(ctypes.sizeof(INPUT))
return ctypes.windll.user32.SendInput(nInputs, pInputs, cbSize)
INPUT_KEYBOARD = 1
def Input(structure):
if isinstance(structure, KEYBDINPUT):
return INPUT(INPUT_KEYBOARD, _INPUTunion(ki=structure))
else:
raise TypeError('Cannot create INPUT structure (keyboard)!')
def Keyboard(code, flags=0):
return Input(KEYBDINPUT(code, code, flags, 0, None))
if __name__ == '__main__':
nb_cycles = 10
while nb_cycles != 0:
time.sleep(2) # 3 seconds
# Key "c" for debug, but ideally use 0x7E for "F15"
SendInput(Keyboard(ord("C")))
sys.stdout.write(".")
nb_cycles -= 1
Answers:
"The Bible" for tasks like this: [Python.Docs]: ctypes – A foreign function library for Python.
I modified your code (found a bunch of problems, out of which some were critical).
code00.py:
#!/usr/bin/env python
import ctypes as cts
import sys
import time
from ctypes import wintypes as wts
wts.BYTE = cts.c_ubyte
class KEYBDINPUT(cts.Structure):
_fields_ = (
("wVk", wts.WORD),
("wScan", wts.WORD),
("dwFlags", wts.DWORD),
("time", wts.DWORD),
("dwExtraInfo", wts.PULONG),
)
class INPUT(cts.Structure):
_fields_ = (
("type", wts.DWORD),
("ki", KEYBDINPUT),
("padding", wts.BYTE * 8)
)
INPUT_KEYBOARD = 1 # Also defined by win32con if you have pywin32 installed
INPUT_LEN = cts.sizeof(INPUT)
LPINPUT = cts.POINTER(INPUT)
user32_dll = cts.windll.user32
SendInput = user32_dll.SendInput
SendInput.argtypes = (wts.UINT, LPINPUT, cts.c_int)
SendInput.restype = wts.UINT
def send_input(_input):
return SendInput(1, cts.byref(_input), INPUT_LEN)
def keyboard(code, flags=0):
return INPUT(INPUT_KEYBOARD, (KEYBDINPUT(code, code, flags, 0, None)))
def main(*argv):
time.sleep(2)
nb_cycles = 3
for _ in range(nb_cycles):
time.sleep(0.5) # 3 seconds
# Key "c" for debug, but ideally use 0x7E for "F15"
ret = send_input(keyboard(ord("C")))
#print(ret)
sys.stdout.write(".")
sys.stdout.flush()
if __name__ == "__main__":
print("Python {:s} {:03d}bit on {:s}n".format(" ".join(elem.strip() for elem in sys.version.split("n")),
64 if sys.maxsize > 0x100000000 else 32, sys.platform))
rc = main(*sys.argv[1:])
print("nDone.n")
sys.exit(rc)
Notes:
-
Initially, I defined the INPUT structure having only its 1st 2 members, but took a look at [MS.Learn]: INPUT structure (winuser.h) and noted that the union consists of:
-
MOUSEINPUT
-
KEYBDINPUT
-
HARDWAREINPUT
Did some tests and noted that MOUSEINPUT has the largest size out of the 3 structs, and it’s 8 bytes greater than KEYBDINPUT (for both 032bit and 064bit), so I added the (dummy) (padding) member. Normally, I wouldn’t expect that SendInput to go beyond KEYBDINPUT size when receiving an INPUT structure with type set to INPUT_KEYBOARD, but the INPUT size is required by [MS.Docs]: SendInput function (winuser.h)‘s cbSize arg
-
def SendInput(*inputs):
– in Python, Asterisk (*) before an argument does something totally different than in C. Check [SO]: Expanding tuples into arguments (I didn’t find the official .doc). I modified the function so that it only sends one such structure
-
Check [SO]: C function called from Python via ctypes returns incorrect value (@CristiFati’s answer) for a common pitfall when working with CTypes (calling functions)
-
Use ctypes.wintypes for Win specific types, don’t reinvent the wheel. Not to mention that sometimes mismatches might occur (most recent one that I saw, was ctypes.c_bool vs. wintypes.BOOL)
- wts.BYTE redefinition (not necessary in this case) – a CTypes bug: [SO]: Why ctypes.wintypes.BYTE is signed, but native windows BYTE is unsigned? (@CristiFati’s answer)
-
I renamed your functions to be [Python]: PEP 8 – Style Guide for Python Code compliant. I also removed some of them, that were no longer needed
-
Other small changes that don’t worth being mentioned individually
As for testing, I used a Notepad window (or any other, "sensitive" to user input):
-
Launch the script
-
Alt + Tab to the test window (I added the 1st instruction (time.sleep(2)) from main just to give user time to switch windows), and notice the cs "magically" appearing
… Or just launch the script from console, press (and hold) Ctrl, then notice the KeyboardInterrupt (Ctrl + C) occurrence.
I have the following script that sends a keyboard key every xx seconds (F15 by default), inspired by some code found online
I want to remove the types and union related to Mouse events but cannot make it to work. In particular, I don’t know how to remove the class MOUSEINPUT
and the union of types _INPUTunion
(removing them will stop sending the keyboard key).
Any suggestion on how to trim the script to a minimum (i.e. only keep code related to Keyboard)?
The following code will send the key “C” to be able to debug.
#!/python
import ctypes
import sys
import time
LONG = ctypes.c_long
DWORD = ctypes.c_ulong
ULONG_PTR = ctypes.POINTER(DWORD)
WORD = ctypes.c_ushort
class MOUSEINPUT(ctypes.Structure):
_fields_ = (
('dx', LONG), ('dy', LONG), ('mouseData', DWORD),
('dwFlags', DWORD), ('time', DWORD),
('dwExtraInfo', ULONG_PTR)
)
class KEYBDINPUT(ctypes.Structure):
_fields_ = (
('wVk', WORD), ('wScan', WORD),
('dwFlags', DWORD), ('time', DWORD),
('dwExtraInfo', ULONG_PTR)
)
class _INPUTunion(ctypes.Union):
_fields_ = (('mi', MOUSEINPUT), ('ki', KEYBDINPUT))
class INPUT(ctypes.Structure):
_fields_ = (('type', DWORD), ('union', _INPUTunion))
def SendInput(*inputs):
print(inputs[0].union.mi)
nInputs = len(inputs)
LPINPUT = INPUT * nInputs
pInputs = LPINPUT(*inputs)
cbSize = ctypes.c_int(ctypes.sizeof(INPUT))
return ctypes.windll.user32.SendInput(nInputs, pInputs, cbSize)
INPUT_KEYBOARD = 1
def Input(structure):
if isinstance(structure, KEYBDINPUT):
return INPUT(INPUT_KEYBOARD, _INPUTunion(ki=structure))
else:
raise TypeError('Cannot create INPUT structure (keyboard)!')
def Keyboard(code, flags=0):
return Input(KEYBDINPUT(code, code, flags, 0, None))
if __name__ == '__main__':
nb_cycles = 10
while nb_cycles != 0:
time.sleep(2) # 3 seconds
# Key "c" for debug, but ideally use 0x7E for "F15"
SendInput(Keyboard(ord("C")))
sys.stdout.write(".")
nb_cycles -= 1
"The Bible" for tasks like this: [Python.Docs]: ctypes – A foreign function library for Python.
I modified your code (found a bunch of problems, out of which some were critical).
code00.py:
#!/usr/bin/env python
import ctypes as cts
import sys
import time
from ctypes import wintypes as wts
wts.BYTE = cts.c_ubyte
class KEYBDINPUT(cts.Structure):
_fields_ = (
("wVk", wts.WORD),
("wScan", wts.WORD),
("dwFlags", wts.DWORD),
("time", wts.DWORD),
("dwExtraInfo", wts.PULONG),
)
class INPUT(cts.Structure):
_fields_ = (
("type", wts.DWORD),
("ki", KEYBDINPUT),
("padding", wts.BYTE * 8)
)
INPUT_KEYBOARD = 1 # Also defined by win32con if you have pywin32 installed
INPUT_LEN = cts.sizeof(INPUT)
LPINPUT = cts.POINTER(INPUT)
user32_dll = cts.windll.user32
SendInput = user32_dll.SendInput
SendInput.argtypes = (wts.UINT, LPINPUT, cts.c_int)
SendInput.restype = wts.UINT
def send_input(_input):
return SendInput(1, cts.byref(_input), INPUT_LEN)
def keyboard(code, flags=0):
return INPUT(INPUT_KEYBOARD, (KEYBDINPUT(code, code, flags, 0, None)))
def main(*argv):
time.sleep(2)
nb_cycles = 3
for _ in range(nb_cycles):
time.sleep(0.5) # 3 seconds
# Key "c" for debug, but ideally use 0x7E for "F15"
ret = send_input(keyboard(ord("C")))
#print(ret)
sys.stdout.write(".")
sys.stdout.flush()
if __name__ == "__main__":
print("Python {:s} {:03d}bit on {:s}n".format(" ".join(elem.strip() for elem in sys.version.split("n")),
64 if sys.maxsize > 0x100000000 else 32, sys.platform))
rc = main(*sys.argv[1:])
print("nDone.n")
sys.exit(rc)
Notes:
-
Initially, I defined the INPUT structure having only its 1st 2 members, but took a look at [MS.Learn]: INPUT structure (winuser.h) and noted that the union consists of:
-
MOUSEINPUT
-
KEYBDINPUT
-
HARDWAREINPUT
Did some tests and noted that MOUSEINPUT has the largest size out of the 3 structs, and it’s 8 bytes greater than KEYBDINPUT (for both 032bit and 064bit), so I added the (dummy) (padding) member. Normally, I wouldn’t expect that SendInput to go beyond KEYBDINPUT size when receiving an INPUT structure with type set to INPUT_KEYBOARD, but the INPUT size is required by [MS.Docs]: SendInput function (winuser.h)‘s cbSize arg
-
-
def SendInput(*inputs):
– in Python, Asterisk (*) before an argument does something totally different than in C. Check [SO]: Expanding tuples into arguments (I didn’t find the official .doc). I modified the function so that it only sends one such structure -
Check [SO]: C function called from Python via ctypes returns incorrect value (@CristiFati’s answer) for a common pitfall when working with CTypes (calling functions)
-
Use ctypes.wintypes for Win specific types, don’t reinvent the wheel. Not to mention that sometimes mismatches might occur (most recent one that I saw, was ctypes.c_bool vs. wintypes.BOOL)
- wts.BYTE redefinition (not necessary in this case) – a CTypes bug: [SO]: Why ctypes.wintypes.BYTE is signed, but native windows BYTE is unsigned? (@CristiFati’s answer)
-
I renamed your functions to be [Python]: PEP 8 – Style Guide for Python Code compliant. I also removed some of them, that were no longer needed
-
Other small changes that don’t worth being mentioned individually
As for testing, I used a Notepad window (or any other, "sensitive" to user input):
-
Launch the script
-
Alt + Tab to the test window (I added the 1st instruction (time.sleep(2)) from main just to give user time to switch windows), and notice the cs "magically" appearing
… Or just launch the script from console, press (and hold) Ctrl, then notice the KeyboardInterrupt (Ctrl + C) occurrence.