first commit
This commit is contained in:
598
myenv/lib/python3.10/site-packages/pynput/_util/win32.py
Normal file
598
myenv/lib/python3.10/site-packages/pynput/_util/win32.py
Normal file
@@ -0,0 +1,598 @@
|
||||
# coding=utf-8
|
||||
# pynput
|
||||
# Copyright (C) 2015-2024 Moses Palmér
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Lesser General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
Utility functions and classes for the *win32* backend.
|
||||
"""
|
||||
|
||||
# pylint: disable=C0103
|
||||
# We want to make it obvious how structs are related
|
||||
|
||||
# pylint: disable=R0903
|
||||
# This module contains a number of structs
|
||||
|
||||
import contextlib
|
||||
import ctypes
|
||||
import itertools
|
||||
import threading
|
||||
|
||||
from ctypes import (
|
||||
windll,
|
||||
wintypes)
|
||||
|
||||
from . import AbstractListener, win32_vks as VK
|
||||
|
||||
|
||||
# LPDWORD is not in ctypes.wintypes on Python 2
|
||||
if not hasattr(wintypes, 'LPDWORD'):
|
||||
wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
|
||||
|
||||
|
||||
class MOUSEINPUT(ctypes.Structure):
|
||||
"""Contains information about a simulated mouse event.
|
||||
"""
|
||||
MOVE = 0x0001
|
||||
LEFTDOWN = 0x0002
|
||||
LEFTUP = 0x0004
|
||||
RIGHTDOWN = 0x0008
|
||||
RIGHTUP = 0x0010
|
||||
MIDDLEDOWN = 0x0020
|
||||
MIDDLEUP = 0x0040
|
||||
XDOWN = 0x0080
|
||||
XUP = 0x0100
|
||||
WHEEL = 0x0800
|
||||
HWHEEL = 0x1000
|
||||
ABSOLUTE = 0x8000
|
||||
|
||||
XBUTTON1 = 0x0001
|
||||
XBUTTON2 = 0x0002
|
||||
|
||||
_fields_ = [
|
||||
('dx', wintypes.LONG),
|
||||
('dy', wintypes.LONG),
|
||||
('mouseData', wintypes.DWORD),
|
||||
('dwFlags', wintypes.DWORD),
|
||||
('time', wintypes.DWORD),
|
||||
('dwExtraInfo', ctypes.c_void_p)]
|
||||
|
||||
|
||||
class KEYBDINPUT(ctypes.Structure):
|
||||
"""Contains information about a simulated keyboard event.
|
||||
"""
|
||||
EXTENDEDKEY = 0x0001
|
||||
KEYUP = 0x0002
|
||||
SCANCODE = 0x0008
|
||||
UNICODE = 0x0004
|
||||
|
||||
_fields_ = [
|
||||
('wVk', wintypes.WORD),
|
||||
('wScan', wintypes.WORD),
|
||||
('dwFlags', wintypes.DWORD),
|
||||
('time', wintypes.DWORD),
|
||||
('dwExtraInfo', ctypes.c_void_p)]
|
||||
|
||||
|
||||
class HARDWAREINPUT(ctypes.Structure):
|
||||
"""Contains information about a simulated message generated by an input
|
||||
device other than a keyboard or mouse.
|
||||
"""
|
||||
_fields_ = [
|
||||
('uMsg', wintypes.DWORD),
|
||||
('wParamL', wintypes.WORD),
|
||||
('wParamH', wintypes.WORD)]
|
||||
|
||||
|
||||
class INPUT_union(ctypes.Union):
|
||||
"""Represents the union of input types in :class:`INPUT`.
|
||||
"""
|
||||
_fields_ = [
|
||||
('mi', MOUSEINPUT),
|
||||
('ki', KEYBDINPUT),
|
||||
('hi', HARDWAREINPUT)]
|
||||
|
||||
|
||||
class INPUT(ctypes.Structure):
|
||||
"""Used by :attr:`SendInput` to store information for synthesizing input
|
||||
events such as keystrokes, mouse movement, and mouse clicks.
|
||||
"""
|
||||
MOUSE = 0
|
||||
KEYBOARD = 1
|
||||
HARDWARE = 2
|
||||
|
||||
_fields_ = [
|
||||
('type', wintypes.DWORD),
|
||||
('value', INPUT_union)]
|
||||
|
||||
|
||||
LPINPUT = ctypes.POINTER(INPUT)
|
||||
|
||||
VkKeyScan = windll.user32.VkKeyScanW
|
||||
VkKeyScan.argtypes = (
|
||||
wintypes.WCHAR,)
|
||||
|
||||
MapVirtualKey = windll.user32.MapVirtualKeyW
|
||||
MapVirtualKey.argtypes = (
|
||||
wintypes.UINT,
|
||||
wintypes.UINT)
|
||||
MapVirtualKey.MAPVK_VK_TO_VSC = 0
|
||||
|
||||
SendInput = windll.user32.SendInput
|
||||
SendInput.argtypes = (
|
||||
wintypes.UINT,
|
||||
ctypes.c_voidp, # Really LPINPUT
|
||||
ctypes.c_int)
|
||||
|
||||
GetCurrentThreadId = windll.kernel32.GetCurrentThreadId
|
||||
GetCurrentThreadId.restype = wintypes.DWORD
|
||||
|
||||
|
||||
class MessageLoop(object):
|
||||
"""A class representing a message loop.
|
||||
"""
|
||||
#: The message that signals this loop to terminate
|
||||
WM_STOP = 0x0401
|
||||
|
||||
_LPMSG = ctypes.POINTER(wintypes.MSG)
|
||||
|
||||
_GetMessage = windll.user32.GetMessageW
|
||||
_GetMessage.argtypes = (
|
||||
ctypes.c_voidp, # Really _LPMSG
|
||||
wintypes.HWND,
|
||||
wintypes.UINT,
|
||||
wintypes.UINT)
|
||||
_PeekMessage = windll.user32.PeekMessageW
|
||||
_PeekMessage.argtypes = (
|
||||
ctypes.c_voidp, # Really _LPMSG
|
||||
wintypes.HWND,
|
||||
wintypes.UINT,
|
||||
wintypes.UINT,
|
||||
wintypes.UINT)
|
||||
_PostThreadMessage = windll.user32.PostThreadMessageW
|
||||
_PostThreadMessage.argtypes = (
|
||||
wintypes.DWORD,
|
||||
wintypes.UINT,
|
||||
wintypes.WPARAM,
|
||||
wintypes.LPARAM)
|
||||
|
||||
PM_NOREMOVE = 0
|
||||
|
||||
def __init__(self):
|
||||
self._threadid = None
|
||||
self._event = threading.Event()
|
||||
self.thread = None
|
||||
|
||||
def __iter__(self):
|
||||
"""Initialises the message loop and yields all messages until
|
||||
:meth:`stop` is called.
|
||||
|
||||
:raises AssertionError: if :meth:`start` has not been called
|
||||
"""
|
||||
assert self._threadid is not None
|
||||
|
||||
try:
|
||||
# Pump messages until WM_STOP
|
||||
while True:
|
||||
msg = wintypes.MSG()
|
||||
lpmsg = ctypes.byref(msg)
|
||||
r = self._GetMessage(lpmsg, None, 0, 0)
|
||||
if r <= 0 or msg.message == self.WM_STOP:
|
||||
break
|
||||
else:
|
||||
yield msg
|
||||
|
||||
finally:
|
||||
self._threadid = None
|
||||
self.thread = None
|
||||
|
||||
def start(self):
|
||||
"""Starts the message loop.
|
||||
|
||||
This method must be called before iterating over messages, and it must
|
||||
be called from the same thread.
|
||||
"""
|
||||
self._threadid = GetCurrentThreadId()
|
||||
self.thread = threading.current_thread()
|
||||
|
||||
# Create the message loop
|
||||
msg = wintypes.MSG()
|
||||
lpmsg = ctypes.byref(msg)
|
||||
self._PeekMessage(lpmsg, None, 0x0400, 0x0400, self.PM_NOREMOVE)
|
||||
|
||||
# Set the event to signal to other threads that the loop is created
|
||||
self._event.set()
|
||||
|
||||
def stop(self):
|
||||
"""Stops the message loop.
|
||||
"""
|
||||
self._event.wait()
|
||||
if self._threadid:
|
||||
self.post(self.WM_STOP, 0, 0)
|
||||
|
||||
def post(self, msg, wparam, lparam):
|
||||
"""Posts a message to this message loop.
|
||||
|
||||
:param ctypes.wintypes.UINT msg: The message.
|
||||
|
||||
:param ctypes.wintypes.WPARAM wparam: The value of ``wParam``.
|
||||
|
||||
:param ctypes.wintypes.LPARAM lparam: The value of ``lParam``.
|
||||
"""
|
||||
self._PostThreadMessage(self._threadid, msg, wparam, lparam)
|
||||
|
||||
|
||||
class SystemHook(object):
|
||||
"""A class to handle Windows hooks.
|
||||
"""
|
||||
#: The hook action value for actions we should check
|
||||
HC_ACTION = 0
|
||||
|
||||
_HOOKPROC = ctypes.WINFUNCTYPE(
|
||||
wintypes.LPARAM,
|
||||
ctypes.c_int32, wintypes.WPARAM, wintypes.LPARAM)
|
||||
|
||||
_SetWindowsHookEx = windll.user32.SetWindowsHookExW
|
||||
_SetWindowsHookEx.argtypes = (
|
||||
ctypes.c_int,
|
||||
_HOOKPROC,
|
||||
wintypes.HINSTANCE,
|
||||
wintypes.DWORD)
|
||||
_UnhookWindowsHookEx = windll.user32.UnhookWindowsHookEx
|
||||
_UnhookWindowsHookEx.argtypes = (
|
||||
wintypes.HHOOK,)
|
||||
_CallNextHookEx = windll.user32.CallNextHookEx
|
||||
_CallNextHookEx.argtypes = (
|
||||
wintypes.HHOOK,
|
||||
ctypes.c_int,
|
||||
wintypes.WPARAM,
|
||||
wintypes.LPARAM)
|
||||
|
||||
#: The registered hook procedures
|
||||
_HOOKS = {}
|
||||
|
||||
class SuppressException(Exception):
|
||||
"""An exception raised by a hook callback to suppress further
|
||||
propagation of events.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __init__(self, hook_id, on_hook=lambda code, msg, lpdata: None):
|
||||
self.hook_id = hook_id
|
||||
self.on_hook = on_hook
|
||||
self._hook = None
|
||||
|
||||
def __enter__(self):
|
||||
key = threading.current_thread().ident
|
||||
assert key not in self._HOOKS
|
||||
|
||||
# Add ourself to lookup table and install the hook
|
||||
self._HOOKS[key] = self
|
||||
self._hook = self._SetWindowsHookEx(
|
||||
self.hook_id,
|
||||
self._handler,
|
||||
None,
|
||||
0)
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, value, traceback):
|
||||
key = threading.current_thread().ident
|
||||
assert key in self._HOOKS
|
||||
|
||||
if self._hook is not None:
|
||||
# Uninstall the hook and remove ourself from lookup table
|
||||
self._UnhookWindowsHookEx(self._hook)
|
||||
del self._HOOKS[key]
|
||||
|
||||
@staticmethod
|
||||
@_HOOKPROC
|
||||
def _handler(code, msg, lpdata):
|
||||
key = threading.current_thread().ident
|
||||
self = SystemHook._HOOKS.get(key, None)
|
||||
if self:
|
||||
# pylint: disable=W0702; we want to silence errors
|
||||
try:
|
||||
self.on_hook(code, msg, lpdata)
|
||||
except self.SuppressException:
|
||||
# Return non-zero to stop event propagation
|
||||
return 1
|
||||
except:
|
||||
# Ignore any errors
|
||||
pass
|
||||
# pylint: enable=W0702
|
||||
return SystemHook._CallNextHookEx(0, code, msg, lpdata)
|
||||
|
||||
|
||||
class ListenerMixin(object):
|
||||
"""A mixin for *win32* event listeners.
|
||||
|
||||
Subclasses should set a value for :attr:`_EVENTS` and implement
|
||||
:meth:`_handle_message`.
|
||||
|
||||
Subclasses must also be decorated with a decorator compatible with
|
||||
:meth:`pynput._util.NotifierMixin._receiver` or implement the method
|
||||
``_receive()``.
|
||||
"""
|
||||
#: The Windows hook ID for the events to capture.
|
||||
_EVENTS = None
|
||||
|
||||
#: The window message used to signal that an even should be handled.
|
||||
_WM_PROCESS = 0x410
|
||||
|
||||
#: Additional window messages to propagate to the subclass handler.
|
||||
_WM_NOTIFICATIONS = []
|
||||
|
||||
def suppress_event(self):
|
||||
"""Causes the currently filtered event to be suppressed.
|
||||
|
||||
This has a system wide effect and will generally result in no
|
||||
applications receiving the event.
|
||||
|
||||
This method will raise an undefined exception.
|
||||
"""
|
||||
raise SystemHook.SuppressException()
|
||||
|
||||
def _run(self):
|
||||
self._message_loop = MessageLoop()
|
||||
with self._receive():
|
||||
self._mark_ready()
|
||||
self._message_loop.start()
|
||||
|
||||
# pylint: disable=W0702; we want to silence errors
|
||||
try:
|
||||
with SystemHook(self._EVENTS, self._handler):
|
||||
# Just pump messages
|
||||
for msg in self._message_loop:
|
||||
if not self.running:
|
||||
break
|
||||
if msg.message == self._WM_PROCESS:
|
||||
self._process(msg.wParam, msg.lParam)
|
||||
elif msg.message in self._WM_NOTIFICATIONS:
|
||||
self._on_notification(
|
||||
msg.message, msg.wParam, msg.lParam)
|
||||
except:
|
||||
# This exception will have been passed to the main thread
|
||||
pass
|
||||
# pylint: enable=W0702
|
||||
|
||||
def _stop_platform(self):
|
||||
try:
|
||||
self._message_loop.stop()
|
||||
except AttributeError:
|
||||
# The loop may not have been created
|
||||
pass
|
||||
|
||||
@AbstractListener._emitter
|
||||
def _handler(self, code, msg, lpdata):
|
||||
"""The callback registered with *Windows* for events.
|
||||
|
||||
This method will post the message :attr:`_WM_PROCESS` to the message
|
||||
loop started with this listener using :meth:`MessageLoop.post`. The
|
||||
parameters are retrieved with a call to :meth:`_handle`.
|
||||
"""
|
||||
try:
|
||||
converted = self._convert(code, msg, lpdata)
|
||||
if converted is not None:
|
||||
self._message_loop.post(self._WM_PROCESS, *converted)
|
||||
except NotImplementedError:
|
||||
self._handle_message(code, msg, lpdata)
|
||||
|
||||
if self.suppress:
|
||||
self.suppress_event()
|
||||
|
||||
def _convert(self, code, msg, lpdata):
|
||||
"""The device specific callback handler.
|
||||
|
||||
This method converts a low-level message and data to a
|
||||
``WPARAM`` / ``LPARAM`` pair.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _process(self, wparam, lparam):
|
||||
"""The device specific callback handler.
|
||||
|
||||
This method performs the actual dispatching of events.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _handle_message(self, code, msg, lpdata):
|
||||
"""The device specific callback handler.
|
||||
|
||||
This method calls the appropriate callback registered when this
|
||||
listener was created based on the event.
|
||||
|
||||
This method is only called if :meth:`_convert` is not implemented.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_notification(self, code, wparam, lparam):
|
||||
"""An additional notification handler.
|
||||
|
||||
This method will be called for every message in
|
||||
:attr:`_WM_NOTIFICATIONS`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class KeyTranslator(object):
|
||||
"""A class to translate virtual key codes to characters.
|
||||
"""
|
||||
_GetAsyncKeyState = ctypes.windll.user32.GetAsyncKeyState
|
||||
_GetAsyncKeyState.argtypes = (
|
||||
ctypes.c_int,)
|
||||
_GetKeyboardLayout = ctypes.windll.user32.GetKeyboardLayout
|
||||
_GetKeyboardLayout.argtypes = (
|
||||
wintypes.DWORD,)
|
||||
_GetKeyboardState = ctypes.windll.user32.GetKeyboardState
|
||||
_GetKeyboardState.argtypes = (
|
||||
ctypes.c_voidp,)
|
||||
_GetKeyState = ctypes.windll.user32.GetAsyncKeyState
|
||||
_GetKeyState.argtypes = (
|
||||
ctypes.c_int,)
|
||||
_MapVirtualKeyEx = ctypes.windll.user32.MapVirtualKeyExW
|
||||
_MapVirtualKeyEx.argtypes = (
|
||||
wintypes.UINT,
|
||||
wintypes.UINT,
|
||||
wintypes.HKL)
|
||||
_ToUnicodeEx = ctypes.windll.user32.ToUnicodeEx
|
||||
_ToUnicodeEx.argtypes = (
|
||||
wintypes.UINT,
|
||||
wintypes.UINT,
|
||||
ctypes.c_voidp,
|
||||
ctypes.c_voidp,
|
||||
ctypes.c_int,
|
||||
wintypes.UINT,
|
||||
wintypes.HKL)
|
||||
|
||||
_MAPVK_VK_TO_VSC = 0
|
||||
_MAPVK_VSC_TO_VK = 1
|
||||
_MAPVK_VK_TO_CHAR = 2
|
||||
|
||||
def __init__(self):
|
||||
self.update_layout()
|
||||
|
||||
def __call__(self, vk, is_press):
|
||||
"""Converts a virtual key code to a string.
|
||||
|
||||
:param int vk: The virtual key code.
|
||||
|
||||
:param bool is_press: Whether this is a press.
|
||||
|
||||
:return: parameters suitable for the :class:`pynput.keyboard.KeyCode`
|
||||
constructor
|
||||
|
||||
:raises OSError: if a call to any *win32* function fails
|
||||
"""
|
||||
# Get a string representation of the key
|
||||
layout_data = self._layout_data[self._modifier_state()]
|
||||
scan = self._to_scan(vk, self._layout)
|
||||
character, is_dead = layout_data[scan]
|
||||
|
||||
return {
|
||||
'char': character,
|
||||
'is_dead': is_dead,
|
||||
'vk': vk,
|
||||
'_scan': scan}
|
||||
|
||||
def update_layout(self):
|
||||
"""Updates the cached layout data.
|
||||
"""
|
||||
self._layout, self._layout_data = self._generate_layout()
|
||||
|
||||
def char_from_scan(self, scan):
|
||||
"""Translates a scan code to a character, if possible.
|
||||
|
||||
:param int scan: The scan code to translate.
|
||||
|
||||
:return: maybe a character
|
||||
:rtype: str or None
|
||||
"""
|
||||
return self._layout_data[(False, False, False)][scan][0]
|
||||
|
||||
def _generate_layout(self):
|
||||
"""Generates the keyboard layout.
|
||||
|
||||
This method will call ``ToUnicodeEx``, which modifies kernel buffers,
|
||||
so it must *not* be called from the keyboard hook.
|
||||
|
||||
The return value is the tuple ``(layout_handle, layout_data)``, where
|
||||
``layout_data`` is a mapping from the tuple ``(shift, ctrl, alt)`` to
|
||||
an array indexed by scan code containing the data
|
||||
``(character, is_dead)``, and ``layout_handle`` is the handle of the
|
||||
layout.
|
||||
|
||||
:return: a composite layout
|
||||
"""
|
||||
layout_data = {}
|
||||
|
||||
state = (ctypes.c_ubyte * 255)()
|
||||
with self._thread_input() as active_thread:
|
||||
layout = self._GetKeyboardLayout(active_thread)
|
||||
vks = [
|
||||
self._to_vk(scan, layout)
|
||||
for scan in range(len(state))]
|
||||
|
||||
for shift, ctrl, alt in itertools.product(
|
||||
(False, True), (False, True), (False, True)):
|
||||
current = [(None, False)] * len(state)
|
||||
layout_data[(shift, ctrl, alt)] = current
|
||||
|
||||
# Update the keyboard state based on the modifier state
|
||||
state[VK.SHIFT] = 0x80 if shift else 0x00
|
||||
state[VK.CONTROL] = 0x80 if ctrl else 0x00
|
||||
state[VK.MENU] = 0x80 if alt else 0x00
|
||||
|
||||
# For each virtual key code...
|
||||
out = (ctypes.wintypes.WCHAR * 5)()
|
||||
for (scan, vk) in enumerate(vks):
|
||||
# ...translate it to a unicode character
|
||||
count = self._ToUnicodeEx(
|
||||
vk, scan, ctypes.byref(state), ctypes.byref(out),
|
||||
len(out), 0, layout)
|
||||
|
||||
# Cache the result if a key is mapped
|
||||
if count != 0:
|
||||
character = out[0]
|
||||
is_dead = count < 0
|
||||
current[scan] = (character, is_dead)
|
||||
|
||||
# If the key is dead, flush the keyboard state
|
||||
if is_dead:
|
||||
self._ToUnicodeEx(
|
||||
vk, scan, ctypes.byref(state),
|
||||
ctypes.byref(out), len(out), 0, layout)
|
||||
|
||||
return (layout, layout_data)
|
||||
|
||||
def _to_scan(self, vk, layout):
|
||||
"""Retrieves the scan code for a virtual key code.
|
||||
|
||||
:param int vk: The virtual key code.
|
||||
|
||||
:param layout: The keyboard layout.
|
||||
|
||||
:return: the scan code
|
||||
"""
|
||||
return self._MapVirtualKeyEx(
|
||||
vk, self._MAPVK_VK_TO_VSC, layout)
|
||||
|
||||
def _to_vk(self, scan, layout):
|
||||
"""Retrieves the virtual key code for a scan code.
|
||||
|
||||
:param int vscan: The scan code.
|
||||
|
||||
:param layout: The keyboard layout.
|
||||
|
||||
:return: the virtual key code
|
||||
"""
|
||||
return self._MapVirtualKeyEx(
|
||||
scan, self._MAPVK_VSC_TO_VK, layout)
|
||||
|
||||
def _modifier_state(self):
|
||||
"""Returns a key into :attr:`_layout_data` for the current modifier
|
||||
state.
|
||||
|
||||
:return: the current modifier state
|
||||
"""
|
||||
shift = bool(self._GetAsyncKeyState(VK.SHIFT) & 0x8000)
|
||||
ctrl = bool(self._GetAsyncKeyState(VK.CONTROL) & 0x8000)
|
||||
alt = bool(self._GetAsyncKeyState(VK.MENU) & 0x8000)
|
||||
return (shift, ctrl, alt)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _thread_input(self):
|
||||
"""Yields the current thread ID.
|
||||
"""
|
||||
yield GetCurrentThreadId()
|
||||
Reference in New Issue
Block a user