# Windows implementation of PyAutoGUI functions. # BSD license # Al Sweigart al@inventwithpython.com import ctypes import ctypes.wintypes import pyautogui from pyautogui import LEFT, MIDDLE, RIGHT import sys if sys.platform != 'win32': raise Exception('The pyautogui_win module should only be loaded on a Windows system.') # Fixes the scaling issues where PyAutoGUI was reporting the wrong resolution: try: ctypes.windll.user32.SetProcessDPIAware() except AttributeError: pass # Windows XP doesn't support this, so just do nothing. """ A lot of this code is probably repeated from win32 extensions module, but I didn't want to have that dependency. Note: According to http://msdn.microsoft.com/en-us/library/windows/desktop/ms646260(v=vs.85).aspx the ctypes.windll.user32.mouse_event() function has been superceded by SendInput. SendInput() is documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646310(v=vs.85).aspx UPDATE: SendInput() doesn't seem to be working for me. I've switched back to mouse_event().""" # Event codes to be passed to the mouse_event() win32 function. # Documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646273(v=vs.85).aspx MOUSEEVENTF_LEFTDOWN = 0x0002 MOUSEEVENTF_LEFTUP = 0x0004 MOUSEEVENTF_LEFTCLICK = MOUSEEVENTF_LEFTDOWN + MOUSEEVENTF_LEFTUP MOUSEEVENTF_RIGHTDOWN = 0x0008 MOUSEEVENTF_RIGHTUP = 0x0010 MOUSEEVENTF_RIGHTCLICK = MOUSEEVENTF_RIGHTDOWN + MOUSEEVENTF_RIGHTUP MOUSEEVENTF_MIDDLEDOWN = 0x0020 MOUSEEVENTF_MIDDLEUP = 0x0040 MOUSEEVENTF_MIDDLECLICK = MOUSEEVENTF_MIDDLEDOWN + MOUSEEVENTF_MIDDLEUP MOUSEEVENTF_WHEEL = 0x0800 MOUSEEVENTF_HWHEEL = 0x01000 # Documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646304(v=vs.85).aspx KEYEVENTF_KEYUP = 0x0002 # Documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646270(v=vs.85).aspx INPUT_MOUSE = 0 INPUT_KEYBOARD = 1 # This ctypes structure is for a Win32 POINT structure, # which is documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx # The POINT structure is used by GetCursorPos(). class POINT(ctypes.Structure): _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)] # These ctypes structures are for Win32 INPUT, MOUSEINPUT, KEYBDINPUT, and HARDWAREINPUT structures, # used by SendInput and documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/ms646270(v=vs.85).aspx # Thanks to BSH for this StackOverflow answer: https://stackoverflow.com/questions/18566289/how-would-you-recreate-this-windows-api-structure-with-ctypes class MOUSEINPUT(ctypes.Structure): _fields_ = [ ('dx', ctypes.wintypes.LONG), ('dy', ctypes.wintypes.LONG), ('mouseData', ctypes.wintypes.DWORD), ('dwFlags', ctypes.wintypes.DWORD), ('time', ctypes.wintypes.DWORD), ('dwExtraInfo', ctypes.POINTER(ctypes.wintypes.ULONG)), ] class KEYBDINPUT(ctypes.Structure): _fields_ = [ ('wVk', ctypes.wintypes.WORD), ('wScan', ctypes.wintypes.WORD), ('dwFlags', ctypes.wintypes.DWORD), ('time', ctypes.wintypes.DWORD), ('dwExtraInfo', ctypes.POINTER(ctypes.wintypes.ULONG)), ] class HARDWAREINPUT(ctypes.Structure): _fields_ = [ ('uMsg', ctypes.wintypes.DWORD), ('wParamL', ctypes.wintypes.WORD), ('wParamH', ctypes.wintypes.DWORD) ] class INPUT(ctypes.Structure): class _I(ctypes.Union): _fields_ = [ ('mi', MOUSEINPUT), ('ki', KEYBDINPUT), ('hi', HARDWAREINPUT), ] _anonymous_ = ('i', ) _fields_ = [ ('type', ctypes.wintypes.DWORD), ('i', _I), ] # End of the SendInput win32 data structures. """ Keyboard key mapping for pyautogui: Documented at http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx The *KB dictionaries in pyautogui map a string that can be passed to keyDown(), keyUp(), or press() into the code used for the OS-specific keyboard function. They should always be lowercase, and the same keys should be used across all OSes.""" keyboardMapping = dict([(key, None) for key in pyautogui.KEY_NAMES]) keyboardMapping.update({ 'backspace': 0x08, # VK_BACK '\b': 0x08, # VK_BACK 'super': 0x5B, #VK_LWIN 'tab': 0x09, # VK_TAB '\t': 0x09, # VK_TAB 'clear': 0x0c, # VK_CLEAR 'enter': 0x0d, # VK_RETURN '\n': 0x0d, # VK_RETURN 'return': 0x0d, # VK_RETURN 'shift': 0x10, # VK_SHIFT 'ctrl': 0x11, # VK_CONTROL 'alt': 0x12, # VK_MENU 'pause': 0x13, # VK_PAUSE 'capslock': 0x14, # VK_CAPITAL 'kana': 0x15, # VK_KANA 'hanguel': 0x15, # VK_HANGUEL 'hangul': 0x15, # VK_HANGUL 'junja': 0x17, # VK_JUNJA 'final': 0x18, # VK_FINAL 'hanja': 0x19, # VK_HANJA 'kanji': 0x19, # VK_KANJI 'esc': 0x1b, # VK_ESCAPE 'escape': 0x1b, # VK_ESCAPE 'convert': 0x1c, # VK_CONVERT 'nonconvert': 0x1d, # VK_NONCONVERT 'accept': 0x1e, # VK_ACCEPT 'modechange': 0x1f, # VK_MODECHANGE ' ': 0x20, # VK_SPACE 'space': 0x20, 'pgup': 0x21, # VK_PRIOR 'pgdn': 0x22, # VK_NEXT 'pageup': 0x21, # VK_PRIOR 'pagedown': 0x22, # VK_NEXT 'end': 0x23, # VK_END 'home': 0x24, # VK_HOME 'left': 0x25, # VK_LEFT 'up': 0x26, # VK_UP 'right': 0x27, # VK_RIGHT 'down': 0x28, # VK_DOWN 'select': 0x29, # VK_SELECT 'print': 0x2a, # VK_PRINT 'execute': 0x2b, # VK_EXECUTE 'prtsc': 0x2c, # VK_SNAPSHOT 'prtscr': 0x2c, # VK_SNAPSHOT 'prntscrn': 0x2c, # VK_SNAPSHOT 'printscreen': 0x2c, # VK_SNAPSHOT 'insert': 0x2d, # VK_INSERT 'del': 0x2e, # VK_DELETE 'delete': 0x2e, # VK_DELETE 'help': 0x2f, # VK_HELP 'win': 0x5b, # VK_LWIN 'winleft': 0x5b, # VK_LWIN 'winright': 0x5c, # VK_RWIN 'apps': 0x5d, # VK_APPS 'sleep': 0x5f, # VK_SLEEP 'num0': 0x60, # VK_NUMPAD0 'num1': 0x61, # VK_NUMPAD1 'num2': 0x62, # VK_NUMPAD2 'num3': 0x63, # VK_NUMPAD3 'num4': 0x64, # VK_NUMPAD4 'num5': 0x65, # VK_NUMPAD5 'num6': 0x66, # VK_NUMPAD6 'num7': 0x67, # VK_NUMPAD7 'num8': 0x68, # VK_NUMPAD8 'num9': 0x69, # VK_NUMPAD9 'multiply': 0x6a, # VK_MULTIPLY ??? Is this the numpad *? 'add': 0x6b, # VK_ADD ??? Is this the numpad +? 'separator': 0x6c, # VK_SEPARATOR ??? Is this the numpad enter? 'subtract': 0x6d, # VK_SUBTRACT ??? Is this the numpad -? 'decimal': 0x6e, # VK_DECIMAL 'divide': 0x6f, # VK_DIVIDE 'f1': 0x70, # VK_F1 'f2': 0x71, # VK_F2 'f3': 0x72, # VK_F3 'f4': 0x73, # VK_F4 'f5': 0x74, # VK_F5 'f6': 0x75, # VK_F6 'f7': 0x76, # VK_F7 'f8': 0x77, # VK_F8 'f9': 0x78, # VK_F9 'f10': 0x79, # VK_F10 'f11': 0x7a, # VK_F11 'f12': 0x7b, # VK_F12 'f13': 0x7c, # VK_F13 'f14': 0x7d, # VK_F14 'f15': 0x7e, # VK_F15 'f16': 0x7f, # VK_F16 'f17': 0x80, # VK_F17 'f18': 0x81, # VK_F18 'f19': 0x82, # VK_F19 'f20': 0x83, # VK_F20 'f21': 0x84, # VK_F21 'f22': 0x85, # VK_F22 'f23': 0x86, # VK_F23 'f24': 0x87, # VK_F24 'numlock': 0x90, # VK_NUMLOCK 'scrolllock': 0x91, # VK_SCROLL 'shiftleft': 0xa0, # VK_LSHIFT 'shiftright': 0xa1, # VK_RSHIFT 'ctrlleft': 0xa2, # VK_LCONTROL 'ctrlright': 0xa3, # VK_RCONTROL 'altleft': 0xa4, # VK_LMENU 'altright': 0xa5, # VK_RMENU 'browserback': 0xa6, # VK_BROWSER_BACK 'browserforward': 0xa7, # VK_BROWSER_FORWARD 'browserrefresh': 0xa8, # VK_BROWSER_REFRESH 'browserstop': 0xa9, # VK_BROWSER_STOP 'browsersearch': 0xaa, # VK_BROWSER_SEARCH 'browserfavorites': 0xab, # VK_BROWSER_FAVORITES 'browserhome': 0xac, # VK_BROWSER_HOME 'volumemute': 0xad, # VK_VOLUME_MUTE 'volumedown': 0xae, # VK_VOLUME_DOWN 'volumeup': 0xaf, # VK_VOLUME_UP 'nexttrack': 0xb0, # VK_MEDIA_NEXT_TRACK 'prevtrack': 0xb1, # VK_MEDIA_PREV_TRACK 'stop': 0xb2, # VK_MEDIA_STOP 'playpause': 0xb3, # VK_MEDIA_PLAY_PAUSE 'launchmail': 0xb4, # VK_LAUNCH_MAIL 'launchmediaselect': 0xb5, # VK_LAUNCH_MEDIA_SELECT 'launchapp1': 0xb6, # VK_LAUNCH_APP1 'launchapp2': 0xb7, # VK_LAUNCH_APP2 #';': 0xba, # VK_OEM_1 #'+': 0xbb, # VK_OEM_PLUS #',': 0xbc, # VK_OEM_COMMA #'-': 0xbd, # VK_OEM_MINUS #'.': 0xbe, # VK_OEM_PERIOD #'/': 0xbf, # VK_OEM_2 #'~': 0xc0, # VK_OEM_3 #'[': 0xdb, # VK_OEM_4 #'|': 0xdc, # VK_OEM_5 #']': 0xdd, # VK_OEM_6 #"'": 0xde, # VK_OEM_7 #'': 0xdf, # VK_OEM_8 #'': 0xe7, # VK_PACKET #'': 0xf6, # VK_ATTN #'': 0xf7, # VK_CRSEL #'': 0xf8, # VK_EXSEL #'': 0xf9, # VK_EREOF #'': 0xfa, # VK_PLAY #'': 0xfb, # VK_ZOOM #'': 0xfc, # VK_NONAME #'': 0xfd, # VK_PA1 #'': 0xfe, # VK_OEM_CLEAR }) # Populate the basic printable ascii characters. for c in range(32, 128): keyboardMapping[chr(c)] = ctypes.windll.user32.VkKeyScanA(ctypes.wintypes.WCHAR(chr(c))) def _keyDown(key): """Performs a keyboard key press without the release. This will put that key in a held down state. NOTE: For some reason, this does not seem to cause key repeats like would happen if a keyboard key was held down on a text field. Args: key (str): The key to be pressed down. The valid names are listed in pyautogui.KEY_NAMES. Returns: None """ if key not in keyboardMapping or keyboardMapping[key] is None: return needsShift = pyautogui.isShiftCharacter(key) """ # OLD CODE: The new code relies on having all keys be loaded in keyboardMapping from the start. if key in keyboardMapping.keys(): vkCode = keyboardMapping[key] elif len(key) == 1: # note: I could use this case to update keyboardMapping to cache the VkKeyScan results, but I've decided not to just to make any possible bugs easier to reproduce. vkCode = ctypes.windll.user32.VkKeyScanW(ctypes.wintypes.WCHAR(key)) if vkCode == -1: raise ValueError('There is no VK code for key "%s"' % (key)) if vkCode > 0x100: # the vk code will be > 0x100 if it needs shift vkCode -= 0x100 needsShift = True """ mods, vkCode = divmod(keyboardMapping[key], 0x100) for apply_mod, vk_mod in [(mods & 4, 0x12), (mods & 2, 0x11), (mods & 1 or needsShift, 0x10)]: #HANKAKU not suported! mods & 8 if apply_mod: ctypes.windll.user32.keybd_event(vk_mod, 0, 0, 0) # ctypes.windll.user32.keybd_event(vkCode, 0, 0, 0) for apply_mod, vk_mod in [(mods & 1 or needsShift, 0x10), (mods & 2, 0x11), (mods & 4, 0x12)]: #HANKAKU not suported! mods & 8 if apply_mod: ctypes.windll.user32.keybd_event(vk_mod, 0, KEYEVENTF_KEYUP, 0) # def _keyUp(key): """Performs a keyboard key release (without the press down beforehand). Args: key (str): The key to be released up. The valid names are listed in pyautogui.KEY_NAMES. Returns: None """ if key not in keyboardMapping or keyboardMapping[key] is None: return needsShift = pyautogui.isShiftCharacter(key) """ # OLD CODE: The new code relies on having all keys be loaded in keyboardMapping from the start. if key in keyboardMapping.keys(): vkCode = keyboardMapping[key] elif len(key) == 1: # note: I could use this case to update keyboardMapping to cache the VkKeyScan results, but I've decided not to just to make any possible bugs easier to reproduce. vkCode = ctypes.windll.user32.VkKeyScanW(ctypes.wintypes.WCHAR(key)) if vkCode == -1: raise ValueError('There is no VK code for key "%s"' % (key)) if vkCode > 0x100: # the vk code will be > 0x100 if it needs shift vkCode -= 0x100 needsShift = True """ mods, vkCode = divmod(keyboardMapping[key], 0x100) for apply_mod, vk_mod in [(mods & 4, 0x12), (mods & 2, 0x11), (mods & 1 or needsShift, 0x10)]: #HANKAKU not suported! mods & 8 if apply_mod: ctypes.windll.user32.keybd_event(vk_mod, 0, 0, 0) # ctypes.windll.user32.keybd_event(vkCode, 0, KEYEVENTF_KEYUP, 0) for apply_mod, vk_mod in [(mods & 1 or needsShift, 0x10), (mods & 2, 0x11), (mods & 4, 0x12)]: #HANKAKU not suported! mods & 8 if apply_mod: ctypes.windll.user32.keybd_event(vk_mod, 0, KEYEVENTF_KEYUP, 0) # def _position(): """Returns the current xy coordinates of the mouse cursor as a two-integer tuple by calling the GetCursorPos() win32 function. Returns: (x, y) tuple of the current xy coordinates of the mouse cursor. """ cursor = POINT() ctypes.windll.user32.GetCursorPos(ctypes.byref(cursor)) return (cursor.x, cursor.y) def _size(): """Returns the width and height of the screen as a two-integer tuple. Returns: (width, height) tuple of the screen size, in pixels. """ return (ctypes.windll.user32.GetSystemMetrics(0), ctypes.windll.user32.GetSystemMetrics(1)) def _moveTo(x, y): """Send the mouse move event to Windows by calling SetCursorPos() win32 function. Args: button (str): The mouse button, either 'left', 'middle', or 'right' x (int): The x position of the mouse event. y (int): The y position of the mouse event. Returns: None """ ctypes.windll.user32.SetCursorPos(x, y) def _mouseDown(x, y, button): """Send the mouse down event to Windows by calling the mouse_event() win32 function. Args: x (int): The x position of the mouse event. y (int): The y position of the mouse event. button (str): The mouse button, either 'left', 'middle', or 'right' Returns: None """ if button not in (LEFT, MIDDLE, RIGHT): raise ValueError('button arg to _click() must be one of "left", "middle", or "right", not %s' % button) if button == LEFT: EV = MOUSEEVENTF_LEFTDOWN elif button == MIDDLE: EV = MOUSEEVENTF_MIDDLEDOWN elif button == RIGHT: EV = MOUSEEVENTF_RIGHTDOWN try: _sendMouseEvent(EV, x, y) except (PermissionError, OSError): # TODO: We need to figure out how to prevent these errors, see https://github.com/asweigart/pyautogui/issues/60 pass def _mouseUp(x, y, button): """Send the mouse up event to Windows by calling the mouse_event() win32 function. Args: x (int): The x position of the mouse event. y (int): The y position of the mouse event. button (str): The mouse button, either 'left', 'middle', or 'right' Returns: None """ if button not in (LEFT, MIDDLE, RIGHT): raise ValueError('button arg to _click() must be one of "left", "middle", or "right", not %s' % button) if button == LEFT: EV = MOUSEEVENTF_LEFTUP elif button == MIDDLE: EV = MOUSEEVENTF_MIDDLEUP elif button == RIGHT: EV = MOUSEEVENTF_RIGHTUP try: _sendMouseEvent(EV, x, y) except (PermissionError, OSError): # TODO: We need to figure out how to prevent these errors, see https://github.com/asweigart/pyautogui/issues/60 pass def _click(x, y, button): """Send the mouse click event to Windows by calling the mouse_event() win32 function. Args: button (str): The mouse button, either 'left', 'middle', or 'right' x (int): The x position of the mouse event. y (int): The y position of the mouse event. Returns: None """ if button not in (LEFT, MIDDLE, RIGHT): raise ValueError('button arg to _click() must be one of "left", "middle", or "right", not %s' % button) if button == LEFT: EV = MOUSEEVENTF_LEFTCLICK elif button == MIDDLE: EV = MOUSEEVENTF_MIDDLECLICK elif button ==RIGHT: EV = MOUSEEVENTF_RIGHTCLICK try: _sendMouseEvent(EV, x, y) except (PermissionError, OSError): # TODO: We need to figure out how to prevent these errors, see https://github.com/asweigart/pyautogui/issues/60 pass def _sendMouseEvent(ev, x, y, dwData=0): """The helper function that actually makes the call to the mouse_event() win32 function. Args: ev (int): The win32 code for the mouse event. Use one of the MOUSEEVENTF_* constants for this argument. x (int): The x position of the mouse event. y (int): The y position of the mouse event. dwData (int): The argument for mouse_event()'s dwData parameter. So far this is only used by mouse scrolling. Returns: None """ assert x != None and y != None, 'x and y cannot be set to None' # TODO: ARG! For some reason, SendInput isn't working for mouse events. I'm switching to using the older mouse_event win32 function. #mouseStruct = MOUSEINPUT() #mouseStruct.dx = x #mouseStruct.dy = y #mouseStruct.mouseData = ev #mouseStruct.time = 0 #mouseStruct.dwExtraInfo = ctypes.pointer(ctypes.c_ulong(0)) # according to https://stackoverflow.com/questions/13564851/generate-keyboard-events I can just set this. I don't really care about this value. #inputStruct = INPUT() #inputStruct.mi = mouseStruct #inputStruct.type = INPUT_MOUSE #ctypes.windll.user32.SendInput(1, ctypes.pointer(inputStruct), ctypes.sizeof(inputStruct)) # TODO Note: We need to handle additional buttons, which I believe is documented here: # https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-mouse_event width, height = _size() convertedX = 65536 * x // width + 1 convertedY = 65536 * y // height + 1 ctypes.windll.user32.mouse_event(ev, ctypes.c_long(convertedX), ctypes.c_long(convertedY), dwData, 0) # TODO: Too many false positives with this code: See: https://github.com/asweigart/pyautogui/issues/108 #if ctypes.windll.kernel32.GetLastError() != 0: # raise ctypes.WinError() def _scroll(clicks, x=None, y=None): """Send the mouse vertical scroll event to Windows by calling the mouse_event() win32 function. Args: clicks (int): The amount of scrolling to do. A positive value is the mouse wheel moving forward (scrolling up), a negative value is backwards (down). x (int): The x position of the mouse event. y (int): The y position of the mouse event. Returns: None """ startx, starty = _position() width, height = _size() if x is None: x = startx else: if x < 0: x = 0 elif x >= width: x = width - 1 if y is None: y = starty else: if y < 0: y = 0 elif y >= height: y = height - 1 try: _sendMouseEvent(MOUSEEVENTF_WHEEL, x, y, dwData=clicks) except (PermissionError, OSError): # TODO: We need to figure out how to prevent these errors, see https://github.com/asweigart/pyautogui/issues/60 pass def _hscroll(clicks, x, y): """Send the mouse horizontal scroll event to Windows by calling the mouse_event() win32 function. Args: clicks (int): The amount of scrolling to do. A positive value is the mouse wheel moving right, a negative value is moving left. x (int): The x position of the mouse event. y (int): The y position of the mouse event. Returns: None """ return _scroll(clicks, x, y) def _vscroll(clicks, x, y): """A wrapper for _scroll(), which does vertical scrolling. Args: clicks (int): The amount of scrolling to do. A positive value is the mouse wheel moving forward (scrolling up), a negative value is backwards (down). x (int): The x position of the mouse event. y (int): The y position of the mouse event. Returns: None """ return _scroll(clicks, x, y)