├── MANIFEST.in ├── winput ├── __init__.py ├── vk_codes.py └── winput.py ├── .gitattributes ├── .gitignore ├── setup.cfg ├── .github └── workflows │ └── python-publish.yml ├── examples ├── capture_mouse_and_keyboard.py └── send_input.py ├── LICENSE ├── setup.py └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md -------------------------------------------------------------------------------- /winput/__init__.py: -------------------------------------------------------------------------------- 1 | from .winput import * 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | README.rst 4 | readmelang.py 5 | runreadmelang.bat 6 | build/ 7 | dist/ 8 | winput.egg-info/ 9 | *.bat 10 | track.py 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: '3.x' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip setuptools 21 | python -m pip install build wheel 22 | - name: Build package 23 | run: python -m build -n 24 | - name: Publish package 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | with: 27 | user: __token__ 28 | password: ${{ secrets.PYPI_PASSWORD }} 29 | -------------------------------------------------------------------------------- /examples/capture_mouse_and_keyboard.py: -------------------------------------------------------------------------------- 1 | import winput, time 2 | 3 | def mouse_callback( event ): 4 | if event.action == winput.WM_LBUTTONDOWN: 5 | print("Left mouse button press at {}".format( event.position )) 6 | 7 | def keyboard_callback( event ): 8 | if event.vkCode == winput.VK_ESCAPE: # quit on pressing escape 9 | winput.stop() 10 | 11 | print("Press escape to quit") 12 | 13 | # hook input 14 | winput.hook_mouse( mouse_callback ) 15 | winput.hook_keyboard( keyboard_callback ) 16 | 17 | # enter message loop 18 | try: 19 | while 1: 20 | time.sleep(1./120) 21 | msg = (winput.get_message()) 22 | if msg: 23 | break 24 | except KeyboardInterrupt: 25 | pass 26 | 27 | # remove input hook 28 | winput.unhook_mouse() 29 | winput.unhook_keyboard() 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | zlib/libpng license 2 | 3 | Copyright (c) 2017 Zuzu_Typ 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /examples/send_input.py: -------------------------------------------------------------------------------- 1 | import winput 2 | from winput.vk_codes import * 3 | 4 | import time 5 | 6 | def slow_click(vk_code): # delay each keypress by 1/10 of a second 7 | time.sleep(0.1) 8 | winput.click_key(vk_code) 9 | 10 | # open the RUN menu (WIN + R) 11 | winput.press_key(VK_LWIN) 12 | winput.click_key(VK_R) 13 | winput.release_key(VK_LWIN) 14 | 15 | time.sleep(0.5) 16 | 17 | # enter "notepad.exe" 18 | slow_click(VK_N) 19 | slow_click(VK_O) 20 | slow_click(VK_T) 21 | slow_click(VK_E) 22 | slow_click(VK_P) 23 | slow_click(VK_A) 24 | slow_click(VK_D) 25 | slow_click(VK_OEM_PERIOD) 26 | slow_click(VK_E) 27 | slow_click(VK_X) 28 | slow_click(VK_E) 29 | slow_click(VK_RETURN) 30 | 31 | time.sleep(1) 32 | 33 | # enter "hello world" 34 | slow_click(VK_H) 35 | slow_click(VK_E) 36 | slow_click(VK_L) 37 | slow_click(VK_L) 38 | slow_click(VK_O) 39 | slow_click(VK_SPACE) 40 | slow_click(VK_W) 41 | slow_click(VK_O) 42 | slow_click(VK_R) 43 | slow_click(VK_L) 44 | slow_click(VK_D) 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | See: 3 | https://packaging.python.org/en/latest/distributing.html 4 | https://github.com/pypa/sampleproject 5 | """ 6 | 7 | from setuptools import setup, find_packages 8 | from codecs import open 9 | from os import path 10 | 11 | here = path.abspath(path.dirname(__file__)) 12 | 13 | # Get the long description from the README file 14 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 15 | long_description_list = f.readlines() 16 | 17 | long_description = "" 18 | 19 | for line in long_description_list: 20 | long_description += line 21 | long_description = long_description.replace("\r", "") 22 | 23 | setup( 24 | name='winput', 25 | 26 | version='1.5.0', 27 | 28 | description='Capture and send keyboard and mouse input', 29 | 30 | long_description=open(path.join(here, 'README.md')).read(), 31 | long_description_content_type='text/markdown', 32 | 33 | # The project's main homepage. 34 | url='https://github.com/Zuzu-Typ/winput', 35 | 36 | # Author details 37 | author='Zuzu_Typ', 38 | author_email='zuzu.typ@gmail.com', 39 | 40 | license='zlib/libpng license', 41 | 42 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 43 | classifiers=[ 44 | # How mature is this project? Common values are 45 | # 3 - Alpha 46 | # 4 - Beta 47 | # 5 - Production/Stable 48 | 'Development Status :: 5 - Production/Stable', 49 | 50 | 'Intended Audience :: Developers', 51 | 'Topic :: Games/Entertainment', 52 | 'Topic :: Software Development :: Libraries', 53 | 54 | 'License :: OSI Approved :: zlib/libpng License', 55 | 56 | 'Programming Language :: Python :: 3', 57 | 'Programming Language :: Python :: 3.5', 58 | 'Programming Language :: Python :: 3.6', 59 | 'Programming Language :: Python :: 3.7', 60 | 'Programming Language :: Python :: 3.8', 61 | 'Programming Language :: Python :: 3.9', 62 | 'Programming Language :: Python :: 3.10', 63 | ], 64 | 65 | keywords='record send input cursor mouse keyboard keys hook hooks pyhook windows user32.dll user32', 66 | 67 | packages=["winput"], 68 | ) 69 | -------------------------------------------------------------------------------- /winput/vk_codes.py: -------------------------------------------------------------------------------- 1 | VK_LBUTTON =0x01 2 | VK_RBUTTON =0x02 3 | VK_CANCEL =0x03 4 | VK_MBUTTON =0x04 #/* NOT contiguous with L & RBUTTON */ 5 | 6 | VK_XBUTTON1 =0x05 #/* NOT contiguous with L & RBUTTON */ 7 | VK_XBUTTON2 =0x06 #/* NOT contiguous with L & RBUTTON */ 8 | 9 | 10 | 11 | VK_BACK =0x08 12 | VK_TAB =0x09 13 | 14 | 15 | VK_CLEAR =0x0C 16 | VK_RETURN =0x0D 17 | 18 | VK_SHIFT =0x10 19 | VK_CONTROL =0x11 20 | VK_MENU =0x12 21 | VK_PAUSE =0x13 22 | VK_CAPITAL =0x14 23 | 24 | VK_KANA =0x15 25 | VK_HANGEUL =0x15 #/* old name - should be here for compatibility */ 26 | VK_HANGUL =0x15 27 | 28 | 29 | VK_JUNJA =0x17 30 | VK_FINAL =0x18 31 | VK_HANJA =0x19 32 | VK_KANJI =0x19 33 | 34 | 35 | VK_ESCAPE =0x1B 36 | 37 | VK_CONVERT =0x1C 38 | VK_NONCONVERT =0x1D 39 | VK_ACCEPT =0x1E 40 | VK_MODECHANGE =0x1F 41 | 42 | VK_SPACE =0x20 43 | VK_PRIOR =0x21 44 | VK_NEXT =0x22 45 | VK_END =0x23 46 | VK_HOME =0x24 47 | VK_LEFT =0x25 48 | VK_UP =0x26 49 | VK_RIGHT =0x27 50 | VK_DOWN =0x28 51 | VK_SELECT =0x29 52 | VK_PRINT =0x2A 53 | VK_EXECUTE =0x2B 54 | VK_SNAPSHOT =0x2C 55 | VK_INSERT =0x2D 56 | VK_DELETE =0x2E 57 | VK_HELP =0x2F 58 | 59 | VK_LWIN =0x5B 60 | VK_RWIN =0x5C 61 | VK_APPS =0x5D 62 | 63 | VK_0 =0x30 64 | VK_1 =0x31 65 | VK_2 =0x32 66 | VK_3 =0x33 67 | VK_4 =0x34 68 | VK_5 =0x35 69 | VK_6 =0x36 70 | VK_7 =0x37 71 | VK_8 =0x38 72 | VK_9 =0x39 73 | 74 | VK_A =0x41 75 | VK_B =0x42 76 | VK_C =0x43 77 | VK_D =0x44 78 | VK_E =0x45 79 | VK_F =0x46 80 | VK_G =0x47 81 | VK_H =0x48 82 | VK_I =0x49 83 | VK_J =0x4a 84 | VK_K =0x4b 85 | VK_L =0x4c 86 | VK_M =0x4d 87 | VK_N =0x4e 88 | VK_O =0x4f 89 | VK_P =0x50 90 | VK_Q =0x51 91 | VK_R =0x52 92 | VK_S =0x53 93 | VK_T =0x54 94 | VK_U =0x55 95 | VK_V =0x56 96 | VK_W =0x57 97 | VK_X =0x58 98 | VK_Y =0x59 99 | VK_Z =0x5a 100 | 101 | VK_SLEEP =0x5F 102 | 103 | VK_NUMPAD0 =0x60 104 | VK_NUMPAD1 =0x61 105 | VK_NUMPAD2 =0x62 106 | VK_NUMPAD3 =0x63 107 | VK_NUMPAD4 =0x64 108 | VK_NUMPAD5 =0x65 109 | VK_NUMPAD6 =0x66 110 | VK_NUMPAD7 =0x67 111 | VK_NUMPAD8 =0x68 112 | VK_NUMPAD9 =0x69 113 | VK_MULTIPLY =0x6A 114 | VK_ADD =0x6B 115 | VK_SEPARATOR =0x6C 116 | VK_SUBTRACT =0x6D 117 | VK_DECIMAL =0x6E 118 | VK_DIVIDE =0x6F 119 | VK_F1 =0x70 120 | VK_F2 =0x71 121 | VK_F3 =0x72 122 | VK_F4 =0x73 123 | VK_F5 =0x74 124 | VK_F6 =0x75 125 | VK_F7 =0x76 126 | VK_F8 =0x77 127 | VK_F9 =0x78 128 | VK_F10 =0x79 129 | VK_F11 =0x7A 130 | VK_F12 =0x7B 131 | VK_F13 =0x7C 132 | VK_F14 =0x7D 133 | VK_F15 =0x7E 134 | VK_F16 =0x7F 135 | VK_F17 =0x80 136 | VK_F18 =0x81 137 | VK_F19 =0x82 138 | VK_F20 =0x83 139 | VK_F21 =0x84 140 | VK_F22 =0x85 141 | VK_F23 =0x86 142 | VK_F24 =0x87 143 | 144 | 145 | VK_NAVIGATION_VIEW =0x88 # reserved 146 | VK_NAVIGATION_MENU =0x89 # reserved 147 | VK_NAVIGATION_UP =0x8A # reserved 148 | VK_NAVIGATION_DOWN =0x8B # reserved 149 | VK_NAVIGATION_LEFT =0x8C # reserved 150 | VK_NAVIGATION_RIGHT =0x8D # reserved 151 | VK_NAVIGATION_ACCEPT =0x8E # reserved 152 | VK_NAVIGATION_CANCEL =0x8F # reserved 153 | 154 | 155 | VK_NUMLOCK =0x90 156 | VK_SCROLL =0x91 157 | 158 | VK_OEM_NEC_EQUAL =0x92 # '=' key on numpad 159 | 160 | VK_OEM_FJ_JISHO =0x92 # 'Dictionary' key 161 | VK_OEM_FJ_MASSHOU =0x93 # 'Unregister word' key 162 | VK_OEM_FJ_TOUROKU =0x94 # 'Register word' key 163 | VK_OEM_FJ_LOYA =0x95 # 'Left OYAYUBI' key 164 | VK_OEM_FJ_ROYA =0x96 # 'Right OYAYUBI' key 165 | 166 | VK_LSHIFT =0xA0 167 | VK_RSHIFT =0xA1 168 | VK_LCONTROL =0xA2 169 | VK_RCONTROL =0xA3 170 | VK_LMENU =0xA4 171 | VK_RMENU =0xA5 172 | 173 | VK_BROWSER_BACK =0xA6 174 | VK_BROWSER_FORWARD =0xA7 175 | VK_BROWSER_REFRESH =0xA8 176 | VK_BROWSER_STOP =0xA9 177 | VK_BROWSER_SEARCH =0xAA 178 | VK_BROWSER_FAVORITES =0xAB 179 | VK_BROWSER_HOME =0xAC 180 | 181 | VK_VOLUME_MUTE =0xAD 182 | VK_VOLUME_DOWN =0xAE 183 | VK_VOLUME_UP =0xAF 184 | VK_MEDIA_NEXT_TRACK =0xB0 185 | VK_MEDIA_PREV_TRACK =0xB1 186 | VK_MEDIA_STOP =0xB2 187 | VK_MEDIA_PLAY_PAUSE =0xB3 188 | VK_LAUNCH_MAIL =0xB4 189 | VK_LAUNCH_MEDIA_SELECT =0xB5 190 | VK_LAUNCH_APP1 =0xB6 191 | VK_LAUNCH_APP2 =0xB7 192 | 193 | 194 | VK_OEM_1 =0xBA # ';:' for US 195 | VK_OEM_PLUS =0xBB # '+' any country 196 | VK_OEM_COMMA =0xBC # ',' any country 197 | VK_OEM_MINUS =0xBD # '-' any country 198 | VK_OEM_PERIOD =0xBE # '.' any country 199 | VK_OEM_2 =0xBF # '/?' for US 200 | VK_OEM_3 =0xC0 # '`~' for US 201 | 202 | 203 | VK_GAMEPAD_A =0xC3 # reserved 204 | VK_GAMEPAD_B =0xC4 # reserved 205 | VK_GAMEPAD_X =0xC5 # reserved 206 | VK_GAMEPAD_Y =0xC6 # reserved 207 | VK_GAMEPAD_RIGHT_SHOULDER =0xC7 # reserved 208 | VK_GAMEPAD_LEFT_SHOULDER =0xC8 # reserved 209 | VK_GAMEPAD_LEFT_TRIGGER =0xC9 # reserved 210 | VK_GAMEPAD_RIGHT_TRIGGER =0xCA # reserved 211 | VK_GAMEPAD_DPAD_UP =0xCB # reserved 212 | VK_GAMEPAD_DPAD_DOWN =0xCC # reserved 213 | VK_GAMEPAD_DPAD_LEFT =0xCD # reserved 214 | VK_GAMEPAD_DPAD_RIGHT =0xCE # reserved 215 | VK_GAMEPAD_MENU =0xCF # reserved 216 | VK_GAMEPAD_VIEW =0xD0 # reserved 217 | VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON =0xD1 # reserved 218 | VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON =0xD2 # reserved 219 | VK_GAMEPAD_LEFT_THUMBSTICK_UP =0xD3 # reserved 220 | VK_GAMEPAD_LEFT_THUMBSTICK_DOWN =0xD4 # reserved 221 | VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT =0xD5 # reserved 222 | VK_GAMEPAD_LEFT_THUMBSTICK_LEFT =0xD6 # reserved 223 | VK_GAMEPAD_RIGHT_THUMBSTICK_UP =0xD7 # reserved 224 | VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN =0xD8 # reserved 225 | VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT =0xD9 # reserved 226 | VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT =0xDA # reserved 227 | 228 | 229 | 230 | VK_OEM_4 =0xDB # '[{' for US 231 | VK_OEM_5 =0xDC # '\|' for US 232 | VK_OEM_6 =0xDD # ']}' for US 233 | VK_OEM_7 =0xDE # ''"' for US 234 | VK_OEM_8 =0xDF 235 | 236 | VK_OEM_AX =0xE1 # 'AX' key on Japanese AX kbd 237 | VK_OEM_102 =0xE2 # "<>" or "\|" on RT 102-key kbd. 238 | VK_ICO_HELP =0xE3 # Help key on ICO 239 | VK_ICO_00 =0xE4 # 00 key on ICO 240 | 241 | VK_PROCESSKEY =0xE5 242 | 243 | 244 | VK_ICO_CLEAR =0xE6 245 | 246 | VK_PACKET =0xE7 247 | 248 | VK_OEM_RESET =0xE9 249 | VK_OEM_JUMP =0xEA 250 | VK_OEM_PA1 =0xEB 251 | VK_OEM_PA2 =0xEC 252 | VK_OEM_PA3 =0xED 253 | VK_OEM_WSCTRL =0xEE 254 | VK_OEM_CUSEL =0xEF 255 | VK_OEM_ATTN =0xF0 256 | VK_OEM_FINISH =0xF1 257 | VK_OEM_COPY =0xF2 258 | VK_OEM_AUTO =0xF3 259 | VK_OEM_ENLW =0xF4 260 | VK_OEM_BACKTAB =0xF5 261 | 262 | VK_ATTN =0xF6 263 | VK_CRSEL =0xF7 264 | VK_EXSEL =0xF8 265 | VK_EREOF =0xF9 266 | VK_PLAY =0xFA 267 | VK_ZOOM =0xFB 268 | VK_NONAME =0xFC 269 | VK_PA1 =0xFD 270 | VK_OEM_CLEAR =0xFE 271 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # winput 2 | 3 | winput provides a simple Python interface for capturing or sending keyboard and mouse input on Windows. It wraps functions from `user32.dll` so you can listen to events or automate input. 4 | 5 | > **Warning** 6 | > Do not record a user's input without their permission. 7 | 8 | 9 | ## About winput 10 | **winput must not be used to record the user's input without their consent\!** 11 | **winput** is supposed to **replace** the outdated extension [PyHook](https://pypi.org/project/pyHook/)\. 12 | 13 | ## Installation 14 | 15 | Install from PyPI: 16 | 17 | pip install winput 18 | 19 | ## Basic usage 20 | 21 | To use winput in a script, you have to import the package `winput` using 22 | 23 | import winput 24 | 25 | or a wildcard import: 26 | 27 | from winput import * 28 | 29 | 30 | 31 | Here is a minimal example that prints every pressed key until Escape is hit: 32 | 33 | ```python 34 | import winput 35 | 36 | 37 | def on_key(event: winput.KeyboardEvent) -> int: 38 | print(event.vkCode) 39 | if event.vkCode == winput.VK_ESCAPE: 40 | return winput.WP_STOP 41 | 42 | winput.hook_keyboard(on_key) 43 | winput.wait_messages() 44 | winput.unhook_keyboard() 45 | ``` 46 | 47 | See the [examples](examples/) directory for more scripts. 48 | 49 | 50 | #### Capturing mouse input 51 | There are two ways you can get input from the mouse\. 52 | 1\. You can get the current position of the mouse cursor using 53 | 54 | get_mouse_pos() -> (x, y) 55 | 56 | 57 | 2\. You can hook onto the Windows message system to receive an Event every time 58 | the state of the mouse changes: 59 | 60 | hook_mouse( callback_function ) 61 | 62 | The function will be supplied with a **MouseEvent** as it's only argument\. 63 | 64 | class MouseEvent: 65 | position # [length-2-tuple] the screen coordinates at which the event occured 66 | action # [int] which type of event occured - can be any of the mouse-wParams (see below) 67 | time # [int] time in ms since system startup 68 | additional_data # [int] information for specific mouse events (which X-Button, amount of mouse-wheel-movement) 69 | type # [string] = "MouseEvent" 70 | 71 | You **have to run a message loop** to use a hook\! \(see **\[Running a message loop\]** below\) 72 | 73 | Remember to unhook when you're done capturing by using: 74 | 75 | unhook_mouse() 76 | 77 | 78 | The following mouse\-wParams exist: 79 | 80 | 81 | WM_MOUSEMOVE = 0x0200 # the mouse has been moved 82 | 83 | WM_LBUTTONDOWN = 0x0201 # left mouse button pressed 84 | WM_LBTTONUP = 0x0202 # left mouse button released 85 | 86 | WM_RBUTTONDOWN = 0x0204 # right mouse button pressed 87 | WM_RBUTTONUP = 0x0205 # right mouse button released 88 | 89 | WM_MBUTTONDOWN = 0x0207 # middle mouse button pressed 90 | WM_MBUTTONUP = 0x0208 # middle mouse button released 91 | 92 | WM_MOUSEWHEEL = 0x020A # mouse wheel moved 93 | WM_MOUSEHWHEEL = 0x020E # mouse wheel moved (horizontal) 94 | 95 | WM_XBUTTONDOWN = 0x020B # extra button pressed 96 | WM_XBUTTONUP = 0x020C # extra button released 97 | 98 | 99 | 100 | 101 | #### Capturing keyboard input 102 | If you want to monitor keyboard input you also have to hook onto the Windows message system\. 103 | 104 | hook_keyboard( callback_function ) 105 | 106 | The function will be supplied with a **KeyboardEvent** as it's only argument\. 107 | 108 | class KeyboardEvent: 109 | action # [int] which type of event occured - can be any of the keyboard-wParams 110 | vkCode # [int] virtual key code -- which key has been pressed 111 | time # [int] time in ms since system startup 112 | type # [string] = "KeyboardEvent" 113 | 114 | You **have to run a message loop** to use a hook\! \(see *Running a message loop* below\) 115 | 116 | Again, remember to unhook when you're done capturing by using: 117 | 118 | unhook_keyboard() 119 | 120 | 121 | The following keyboard\-wParams exist: 122 | 123 | 124 | WM_KEYDOWN = 0x0100 # key pressed 125 | WM_KEYUP = 0x0101 # key released 126 | 127 | WM_SYSKEYDOWN = 0x0104 # system-key pressed 128 | WM_SYSKEYUP = 0x0105 # system-key released 129 | 130 | 131 | 132 | #### Return values for callback functions 133 | The callback (hook) functions mentioned above are expected to return a **flag**. 134 | The following flags exist: 135 | 136 | Flag | Value | Meaning 137 | -----|-------|-------- 138 | `WP_CONTINUE` | `0x00` | Continue normally 139 | `WP_UNHOOK` | `0x01` | Remove this hook 140 | `WP_STOP` | `0x02` | Stops the message loop 141 | `WP_DONT_PASS_INPUT_ON` | `0x04` | Does not call any other hooks (i.e. input isn't passed on to other programs) 142 | 143 | If the callback function returns `None`, `WP_CONTINUE` is assumed. 144 | 145 | **WARNING:** Using `WP_DONT_PASS_INPUT_ON` will also prevent the inputs to be passed on to Windows. If you do this for a mouse hook, the mouse will **not move** and you might loose control over your computer. Same goes for keyboard hooks. The keyboard events will **not be passed on** to the rest of your system. You may loose control over your computer. 146 | 147 | 148 | #### Running a message loop 149 | If you're using a hook, you have to keep updating the Windows messages\. 150 | You can either do this by using 151 | 152 | wait_messages() 153 | 154 | to enter an infinite message loop, which you can stop by calling 155 | 156 | stop() 157 | 158 | 159 | Or you can have your own loop that repeatedly \(at least 100x per second\) calls 160 | 161 | get_message() 162 | 163 | 164 | 165 | #### Virtual Key Codes \(VK codes\) 166 | Virtual key codes or vk\_codes are numerical representations of given keys\. 167 | To get a list of all virtual key codes, take a look over [here](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes)\. 168 | All VK codes are members of the main `winput` module and the submodule `winput.vk_codes`\. 169 | If you want to import all the VK codes without performing a package\-wide wildcard import, you can use 170 | 171 | from winput.vk_codes import * 172 | 173 | 174 | You can also convert the virtual key codes to a literal representation using a predefined dict\. 175 | 176 | vk_code_dict.get(vk_code, "VK_UNKNOWN") -> string 177 | 178 | 179 | 180 | #### Sending mouse input 181 | To set the position of the mouse cursor, you can use 182 | 183 | set_mouse_pos(x, y) 184 | 185 | 186 | To make sure this also works with high DPI values, please use the DPI awareness functions below\. 187 | 188 | To move the mouse cursor by a given amount, you can use 189 | 190 | move_mouse(dx, dy) 191 | 192 | 193 | A mouse button can be pressed using 194 | 195 | press_mouse_button(mouse_button) 196 | 197 | and released using 198 | 199 | release_mouse_button(mouse_button) 200 | 201 | or pressed and released using 202 | 203 | click_mouse_button(mouse_button) 204 | 205 | 206 | The following mouse buttons exist: 207 | 208 | 209 | LEFT_MOUSE_BUTTON = LMB = 1 210 | RIGHT_MOUSE_BUTTON = RMB = 2 211 | MIDDLE_MOUSE_BUTTON = MMB = 4 212 | EXTRA_MOUSE_BUTTON1 = XMB1 = 8 213 | EXTRA_MOUSE_BUTTON2 = XMB2 = 16 214 | 215 | 216 | The mousewheel can be moved using 217 | `move_mousewheel(amount[, horizontal = False])` 218 | 219 | #### Sending keyboard input 220 | To press a key, you can use 221 | 222 | press_key(vk_code) 223 | 224 | to release it, you can use 225 | 226 | release_key(vk_code) 227 | 228 | and to press and release it, you can use 229 | 230 | click_key(vk_code) 231 | 232 | 233 | 234 | #### DPI awareness 235 | To make the process running winput DPI aware, use the following function: 236 | 237 | set_DPI_aware(per_monitor=True) 238 | 239 | 240 | To get the DPI scaling factor for a given window handle \(HWND\), use 241 | 242 | get_window_scaling_factor(hwnd) 243 | 244 | 245 | ### Example 246 | #### Capturing the mouse and keyboard 247 | ```Python 248 | import winput 249 | 250 | def mouse_callback( event ): 251 | if event.action == winput.WM_LBUTTONDOWN: 252 | print("Left mouse button press at {}".format( event.position )) 253 | 254 | def keyboard_callback( event ): 255 | if event.vkCode == winput.VK_ESCAPE: # quit on pressing escape 256 | return winput.WP_STOP 257 | # alternatively you could also call: 258 | # winput.stop() 259 | 260 | print("Press escape to quit") 261 | 262 | # hook input 263 | winput.hook_mouse( mouse_callback ) 264 | winput.hook_keyboard( keyboard_callback ) 265 | 266 | # enter message loop 267 | winput.wait_messages() 268 | 269 | # remove input hook 270 | winput.unhook_mouse() 271 | winput.unhook_keyboard() 272 | ``` 273 | 274 | #### Capturing keyboard without passthrough 275 | ```Python 276 | import winput 277 | 278 | def keyboard_callback(event : winput.KeyboardEvent) -> int: 279 | if event.key == winput.VK_ESCAPE: 280 | print("quitting") 281 | return winput.WP_UNHOOK | winput.WP_STOP 282 | 283 | print(winput.vk_code_dict.get(event.key, "VK_UNKNOWN")) 284 | 285 | return winput.WP_DONT_PASS_INPUT_ON 286 | 287 | winput.hook_keyboard(keyboard_callback) 288 | winput.wait_messages() 289 | ``` 290 | 291 | 292 | #### Sending input 293 | 294 | ```Python 295 | import winput 296 | from winput.vk_codes import * 297 | 298 | import time 299 | 300 | def slow_click(vk_code): # delay each keypress by 1/10 of a second 301 | time.sleep(0.1) 302 | winput.click_key(vk_code) 303 | 304 | # open the RUN menu (WIN + R) 305 | winput.press_key(VK_LWIN) 306 | winput.click_key(VK_R) 307 | winput.release_key(VK_LWIN) 308 | 309 | time.sleep(0.5) 310 | 311 | # enter "notepad.exe" 312 | slow_click(VK_N) 313 | slow_click(VK_O) 314 | slow_click(VK_T) 315 | slow_click(VK_E) 316 | slow_click(VK_P) 317 | slow_click(VK_A) 318 | slow_click(VK_D) 319 | slow_click(VK_OEM_PERIOD) 320 | slow_click(VK_E) 321 | slow_click(VK_X) 322 | slow_click(VK_E) 323 | slow_click(VK_RETURN) 324 | 325 | time.sleep(1) 326 | 327 | # enter "hello world" 328 | slow_click(VK_H) 329 | slow_click(VK_E) 330 | slow_click(VK_L) 331 | slow_click(VK_L) 332 | slow_click(VK_O) 333 | slow_click(VK_SPACE) 334 | slow_click(VK_W) 335 | slow_click(VK_O) 336 | slow_click(VK_R) 337 | slow_click(VK_L) 338 | slow_click(VK_D) 339 | ``` 340 | 341 | -------------------------------------------------------------------------------- /winput/winput.py: -------------------------------------------------------------------------------- 1 | """ 2 | winput 3 | 4 | Capture and send keyboard and mouse input on Windows 5 | 6 | --------------------- 7 | LICENSE (zlib/libpng) 8 | --------------------- 9 | zlib/libpng license 10 | 11 | Copyright (c) 2017 Zuzu_Typ 12 | 13 | This software is provided 'as-is', without any express or implied 14 | warranty. In no event will the authors be held liable for any damages 15 | arising from the use of this software. 16 | 17 | Permission is granted to anyone to use this software for any purpose, 18 | including commercial applications, and to alter it and redistribute it 19 | freely, subject to the following restrictions: 20 | 21 | 1. The origin of this software must not be misrepresented; you must not 22 | claim that you wrote the original software. If you use this software 23 | in a product, an acknowledgment in the product documentation would be 24 | appreciated but is not required. 25 | 2. Altered source versions must be plainly marked as such, and must not be 26 | misrepresented as being the original software. 27 | 3. This notice may not be removed or altered from any source distribution. 28 | """ 29 | 30 | import ctypes 31 | from sys import getwindowsversion 32 | from ctypes import wintypes 33 | 34 | from typing import Callable, Optional, Tuple 35 | 36 | try: 37 | from .vk_codes import * 38 | from . import vk_codes 39 | except ImportError: 40 | from vk_codes import * 41 | import vk_codes 42 | 43 | class MouseEvent: 44 | type = "MouseEvent" 45 | def __init__(self, position : Tuple[int, int], action : int, time : int, additional_data = None): 46 | self.position = self.pos = position 47 | self.x, self.y = self.pos 48 | self.action = action 49 | self.time = time 50 | self.additional_data = additional_data 51 | 52 | class KeyboardEvent: 53 | type = "KeyboardEvent" 54 | def __init__(self, action : int, vkCode : int, time : int): 55 | self.action = action 56 | self.key = self.vk_code = self.vkCode = vkCode 57 | self.time = time 58 | 59 | WP_CONTINUE = 0x00 60 | WP_UNHOOK = 0x01 61 | WP_STOP = 0x02 62 | WP_DONT_PASS_INPUT_ON = 0x04 63 | 64 | user32 = ctypes.windll.user32 65 | 66 | LRESULT = ctypes.c_void_p 67 | 68 | ULONG_PTR = ctypes.POINTER(ctypes.c_ulong) 69 | 70 | INPUT_MOUSE = 0 71 | INPUT_KEYBOARD = 1 72 | INPUT_HARDWARE = 2 73 | 74 | KEYEVENTF_EXTENDEDKEY = 0x0001 75 | KEYEVENTF_KEYUP = 0x0002 76 | KEYEVENTF_UNICODE = 0x0004 77 | KEYEVENTF_SCANCODE = 0x0008 78 | 79 | MAPVK_VK_TO_VSC = 0 80 | 81 | WH_MOUSE_LL = (14) 82 | 83 | WH_KEYBOARD_LL = (13) 84 | 85 | WM_MOUSEMOVE =0x0200 86 | 87 | WM_LBUTTONDOWN =0x0201 88 | WM_LBUTTONUP =0x0202 89 | 90 | WM_RBUTTONDOWN =0x0204 91 | WM_RBUTTONUP =0x0205 92 | 93 | WM_MBUTTONDOWN =0x0207 94 | WM_MBUTTONUP =0x0208 95 | 96 | WM_MOUSEWHEEL =0x020A 97 | WM_MOUSEHWHEEL =0x020E 98 | 99 | WM_XBUTTONDOWN =0x020B 100 | WM_XBUTTONUP =0x020C 101 | 102 | XBUTTON1 =0x0001 103 | XBUTTON2 =0x0002 104 | 105 | MB_LEFT = 0x0001 106 | MB_RIGHT = 0x0002 107 | MB_MIDDLE = 0x0004 108 | MB_X1 = 0x0008 109 | MB_X2 = 0x0016 110 | 111 | WM_KEYDOWN =0x0100 112 | WM_KEYUP =0x0101 113 | 114 | WM_SYSKEYDOWN =0x0104 115 | WM_SYSKEYUP =0x0105 116 | 117 | WHEEL_DELTA =120 118 | GET_HWORD =lambda x: ctypes.c_short((x >> 16)).value 119 | 120 | vk_code_dict = {} 121 | 122 | for item in dir(vk_codes): 123 | if item.startswith("__"): 124 | continue 125 | vk_code_dict[getattr(vk_codes, item)] = item 126 | 127 | all_vk_codes = vk_code_dict 128 | 129 | QS_KEY =0x0001 130 | QS_MOUSEMOVE =0x0002 131 | QS_MOUSEBUTTON =0x0004 132 | QS_RAWINPUT =0x0400 133 | QS_TOUCH =0x0800 134 | QS_POINTER =0x1000 135 | 136 | QS_MOUSE = QS_MOUSEMOVE | QS_MOUSEBUTTON 137 | 138 | _WINVER = getwindowsversion() 139 | _WIN32_WINNT = (_WINVER.major << 8) | _WINVER.minor 140 | 141 | if (_WIN32_WINNT >= 0x602): 142 | QS_INPUT = QS_MOUSE | QS_KEY | QS_RAWINPUT | QS_TOUCH | QS_POINTER 143 | elif (_WIN32_WINNT >= 0x0501): 144 | QS_INPUT = QS_MOUSE | QS_KEY | QS_RAWINPUT 145 | else: 146 | QS_INPUT = QS_MOUSE | QS_KEY 147 | 148 | PM_NOREMOVE = 0x0000 149 | PM_REMOVE = 0x0001 150 | PM_QS_INPUT = (QS_INPUT << 16) 151 | 152 | PROCESS_DPI_UNAWARE = 0 153 | PROCESS_SYSTEM_DPI_AWARE = 1 154 | PROCESS_PER_MONITOR_DPI_AWARE = 2 155 | 156 | MDT_EFFECTIVE_DPI = 0 157 | MDT_ANGULAR_DPI = 1 158 | MDT_RAW_DPI = 2 159 | 160 | LOGPIXELSX = 88 161 | LOGPIXELSY = 90 162 | 163 | MONITOR_DEFAULTTONULL = 0x00000000 164 | MONITOR_DEFAULTTOPRIMARY = 0x00000001 165 | MONITOR_DEFAULTTONEAREST = 0x00000002 166 | 167 | class MOUSEINPUT(ctypes.Structure): 168 | _fields_ = (("dx", wintypes.LONG), 169 | ("dy", wintypes.LONG), 170 | ("mouseData", wintypes.DWORD), 171 | ("dwFlags", wintypes.DWORD), 172 | ("time", wintypes.DWORD), 173 | ("dwExtraInfo", ULONG_PTR)) 174 | 175 | class KEYBDINPUT(ctypes.Structure): 176 | _fields_ = (("wVk", wintypes.WORD), 177 | ("wScan", wintypes.WORD), 178 | ("dwFlags", wintypes.DWORD), 179 | ("time", wintypes.DWORD), 180 | ("dwExtraInfo", ULONG_PTR)) 181 | 182 | def __init__(self, *args, **kwds): 183 | super(KEYBDINPUT, self).__init__(*args, **kwds) 184 | # some programs use the scan code even if KEYEVENTF_SCANCODE 185 | # isn't set in dwFflags, so attempt to map the correct code. 186 | if not self.dwFlags & KEYEVENTF_UNICODE: 187 | self.wScan = user32.MapVirtualKeyExW(self.wVk, 188 | MAPVK_VK_TO_VSC, 0) 189 | 190 | class HARDWAREINPUT(ctypes.Structure): 191 | _fields_ = (("uMsg", wintypes.DWORD), 192 | ("wParamL", wintypes.WORD), 193 | ("wParamH", wintypes.WORD)) 194 | 195 | class INPUT(ctypes.Structure): 196 | class _INPUT(ctypes.Union): 197 | _fields_ = (("ki", KEYBDINPUT), 198 | ("mi", MOUSEINPUT), 199 | ("hi", HARDWAREINPUT)) 200 | _anonymous_ = ("_input",) 201 | _fields_ = (("type", wintypes.DWORD), 202 | ("_input", _INPUT)) 203 | 204 | LPINPUT = ctypes.POINTER(INPUT) 205 | 206 | def _check_count(result, func, args): 207 | if result == 0: 208 | raise ctypes.WinError(ctypes.get_last_error()) 209 | return args 210 | 211 | user32.SendInput.errcheck = _check_count 212 | user32.SendInput.argtypes = (wintypes.UINT, # nInputs 213 | LPINPUT, # pInputs 214 | ctypes.c_int) # cbSize 215 | 216 | class POINT(ctypes.Structure): 217 | _fields_ = [("x", ctypes.c_long), 218 | ("y", ctypes.c_long)] 219 | 220 | class MSLLHOOKSTRUCT(ctypes.Structure): 221 | _fields_ = [("pt", POINT), 222 | ("mouseData", wintypes.DWORD), 223 | ("flags", wintypes.DWORD), 224 | ("time", wintypes.DWORD), 225 | ("dwExtraInfo", ULONG_PTR)] 226 | 227 | class KBDLLHOOKSTRUCT(ctypes.Structure): 228 | _fields_ = [("vkCode", wintypes.DWORD), 229 | ("scanCode", wintypes.DWORD), 230 | ("flags", wintypes.DWORD), 231 | ("time", wintypes.DWORD), 232 | ("dwExtraInfo", ULONG_PTR)] 233 | 234 | def _LowLevelMouseProc(nCode : int, wParam : int, lParam : int, cbfunc : Callable[[MouseEvent], Optional[int]]): 235 | if nCode < 0: # error passthrough 236 | return user32.CallNextHookEx(0, nCode, wParam, lParam) 237 | 238 | extra_data = None 239 | 240 | if wParam in (WM_XBUTTONDOWN, WM_XBUTTONUP): # X button changed state 241 | extra_data = GET_HWORD(lParam.contents.mouseData) 242 | 243 | elif wParam == WM_MOUSEWHEEL or wParam == WM_MOUSEHWHEEL: # used scrollwheel 244 | extra_data = GET_HWORD(lParam.contents.mouseData) // WHEEL_DELTA 245 | 246 | result = cbfunc(MouseEvent((lParam.contents.pt.x, lParam.contents.pt.y), wParam, lParam.contents.time, extra_data)) 247 | 248 | if result is None: 249 | result = WP_CONTINUE 250 | 251 | assert type(result) == int 252 | 253 | if result & WP_UNHOOK: 254 | global mouse_hook 255 | user32.UnhookWindowsHookEx(mouse_hook) 256 | 257 | if result & WP_STOP: 258 | user32.PostQuitMessage(0) 259 | 260 | if result & WP_DONT_PASS_INPUT_ON: 261 | return -1 262 | 263 | return user32.CallNextHookEx(0, nCode, wParam, lParam) 264 | 265 | def _LowLevelKeyboardProc(nCode : int, wParam : int, lParam : int, cbfunc : Callable[[KeyboardEvent], Optional[int]]): 266 | if nCode < 0: # error passthrough 267 | return user32.CallNextHookEx(0, nCode, wParam, lParam) 268 | 269 | result = cbfunc(KeyboardEvent(wParam, lParam.contents.vkCode, lParam.contents.time)) 270 | 271 | if result is None: 272 | result = WP_CONTINUE 273 | 274 | assert type(result) == int 275 | 276 | if result & WP_UNHOOK: 277 | global keyboard_hook 278 | user32.UnhookWindowsHookEx(keyboard_hook) 279 | 280 | if result & WP_STOP: 281 | user32.PostQuitMessage(0) 282 | 283 | if result & WP_DONT_PASS_INPUT_ON: 284 | return -1 285 | 286 | return user32.CallNextHookEx(0, nCode, wParam, lParam) 287 | 288 | mouse_hook_func = None 289 | keyboard_hook_func = None 290 | 291 | mouse_hook = None 292 | keyboard_hook = None 293 | 294 | 295 | LLMouseProc = ctypes.CFUNCTYPE(LRESULT, ctypes.c_int, wintypes.WPARAM, ctypes.POINTER(MSLLHOOKSTRUCT)) 296 | LLKeyboardProc = ctypes.CFUNCTYPE(LRESULT, ctypes.c_int, wintypes.WPARAM, ctypes.POINTER(KBDLLHOOKSTRUCT)) 297 | 298 | def _issue_mouse_event(dwFlags = 0x0001, dx = 0, dy = 0, mouseData = 0x000): 299 | me = INPUT(type=INPUT_MOUSE, 300 | mi=MOUSEINPUT(dx = dx, dy = dy, dwFlags = dwFlags, mouseData = mouseData)) 301 | user32.SendInput(1, ctypes.byref(me), ctypes.sizeof(me)) 302 | 303 | LEFT_MOUSE_BUTTON = LMB = 1 304 | MIDDLE_MOUSE_BUTTON = MMB = 2 305 | RIGHT_MOUSE_BUTTON = RMB = 4 306 | EXTRA_MOUSE_BUTTON1 = XMB1 = 8 307 | EXTRA_MOUSE_BUTTON2 = XMB2 = 16 308 | 309 | 310 | 311 | def set_mouse_pos(x : int, y : int) -> bool: 312 | """set_mouse_pos(x, y) -> success 313 | Moves the cursor to the given coordinates.""" 314 | return bool(user32.SetCursorPos(x, y)) 315 | 316 | def get_mouse_pos() -> Tuple[int, int]: 317 | """get_mouse_pos() -> (x, y) 318 | Gets the current cursor position""" 319 | pt = POINT() 320 | user32.GetCursorPos(ctypes.byref(pt)) 321 | return (pt.x, pt.y) 322 | 323 | def press_mouse_button(mouse_button : int = LMB) -> None: # presses the given mouse button 324 | if(not (LMB <= mouse_button <= XMB2)): 325 | raise AssertionError("invalid mouse button") 326 | 327 | dwFlags = 0x0002 if mouse_button == LMB else \ 328 | 0x0008 if mouse_button == RMB else \ 329 | 0x0020 if mouse_button == MMB else \ 330 | 0x0080 331 | 332 | if dwFlags == 0x0080: 333 | mouseData = 0x1 if mouse_button == XMB1 else 0x2 334 | else: 335 | mouseData = 0 336 | 337 | _issue_mouse_event(dwFlags, 0, 0, mouseData) 338 | 339 | def release_mouse_button(mouse_button : int = LMB) -> None:# releases the given mouse button 340 | if(not (LMB <= mouse_button <= XMB2)): 341 | raise AssertionError("invalid mouse button") 342 | 343 | dwFlags = 0x0004 if mouse_button == LMB else \ 344 | 0x0010 if mouse_button == RMB else \ 345 | 0x0040 if mouse_button == MMB else \ 346 | 0x0100 347 | 348 | if dwFlags == 0x0100: 349 | mouseData = 0x1 if mouse_button == XMB1 else 0x2 350 | else: 351 | mouseData = 0 352 | 353 | _issue_mouse_event(dwFlags, 0, 0, mouseData) 354 | 355 | def click_mouse_button(mouse_button : int = LMB) -> None: # presses and releases the given mouse button 356 | press_mouse_button(mouse_button) 357 | release_mouse_button(mouse_button) 358 | 359 | def move_mousewheel(amount : int, horizontal : bool = False) -> None: # moves the mousewheel by the specified amount 360 | assert type(amount) == int, "amount has to be an integer" 361 | 362 | _issue_mouse_event(0x0800 if not horizontal else 0x1000, 0, 0, amount * WHEEL_DELTA) 363 | 364 | def move_mouse(dx : int, dy : int) -> None: # moves the mouse by the specified amount in pixels 365 | assert type(dx) == type(dy) == int, "dx and dy have to be integers" 366 | 367 | _issue_mouse_event(0x0001, dx, dy, 0) 368 | 369 | def press_key(vk_code : int) -> None: # presses the given key 370 | x = INPUT(type=INPUT_KEYBOARD, 371 | ki=KEYBDINPUT(wVk=vk_code)) 372 | user32.SendInput(1, ctypes.byref(x), ctypes.sizeof(x)) 373 | 374 | def release_key(vk_code : int) -> None: # releases the given key 375 | x = INPUT(type=INPUT_KEYBOARD, 376 | ki=KEYBDINPUT(wVk=vk_code, 377 | dwFlags=KEYEVENTF_KEYUP)) 378 | user32.SendInput(1, ctypes.byref(x), ctypes.sizeof(x)) 379 | 380 | def click_key(vk_code : int) -> None: # presses and releases the given key 381 | press_key(vk_code) 382 | release_key(vk_code) 383 | 384 | def hook_mouse(func : Callable[[MouseEvent], Optional[int]]) -> None: # hook onto mouse event queue 385 | global mouse_hook_func, mouse_hook 386 | mouse_hook_func = LLMouseProc(lambda x, y, z: _LowLevelMouseProc(x, y, z, func)) 387 | mouse_hook = user32.SetWindowsHookExA(WH_MOUSE_LL, mouse_hook_func, None, 0) 388 | 389 | def hook_keyboard(func : Callable[[KeyboardEvent], Optional[int]]) -> None: # hook onto keyboard event queue 390 | global keyboard_hook_func, keyboard_hook 391 | keyboard_hook_func = LLKeyboardProc(lambda x, y, z: _LowLevelKeyboardProc(x, y, z, func)) 392 | keyboard_hook = user32.SetWindowsHookExA(WH_KEYBOARD_LL, keyboard_hook_func, None, 0) 393 | 394 | def wait_messages() -> None: # enter message loop 395 | msg = wintypes.MSG() 396 | while user32.GetMessageA(ctypes.pointer(msg), None, 0, 0): 397 | pass 398 | 399 | def get_message() -> bool: # get pending messages 400 | msg = wintypes.MSG() 401 | return bool(user32.PeekMessageA(ctypes.pointer(msg), None, 0, 0, PM_REMOVE)) 402 | 403 | def stop() -> None: # stop message loop 404 | user32.PostQuitMessage(0) 405 | 406 | def unhook_mouse() -> None: # remove hook from mouse event queue 407 | global mouse_hook 408 | user32.UnhookWindowsHookEx(mouse_hook) 409 | 410 | def unhook_keyboard() -> None: # remove hook from keyboard event queue 411 | global keyboard_hook 412 | user32.UnhookWindowsHookEx(keyboard_hook) 413 | 414 | def set_DPI_aware(per_monitor : bool = True) -> None: # make this process DPI aware 415 | shcore = ctypes.windll.shcore 416 | 417 | if hasattr(shcore, "SetProcessDpiAwareness"): 418 | shcore.SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE if per_monitor else PROCESS_SYSTEM_DPI_AWARE) 419 | elif hasattr(shcore, "SetProcessDPIAware"): 420 | shcore.SetProcessDPIAware() 421 | 422 | def get_window_scaling_factor(hwnd : int) -> float: # gets the DPI scaling factor for the given window (may require DPI awareness) 423 | if hasattr(user32, "GetDpiForWindow"): 424 | return user32.GetDpiForWindow(hwnd) / 96.0 425 | if hasattr(ctypes.windll.shcore, "GetDpiForMonitor"): 426 | hmonitor = user32.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) 427 | dpiX = ctypes.c_uint() 428 | dpiY = ctypes.c_uint() 429 | hres = ctypes.windll.shcore.GetDpiForMonitor(hmonitor, MDT_EFFECTIVE_DPI, ctypes.byref(dpiX), ctypes.byref(dpiY)) 430 | 431 | assert hres == 0 432 | 433 | return dpiX.value / 96.0 434 | if hasattr(ctypes.windll.gdi32, "GetDeviceCaps"): 435 | hdc = user32.GetDC(hwnd) 436 | try: 437 | return ctypes.windll.gdi32.GetDeviceCaps(hdc, LOGPIXELSX) / 96.0 438 | finally: 439 | user32.ReleaseDC(hwnd, hdc) 440 | 441 | return 1.0 442 | 443 | 444 | --------------------------------------------------------------------------------