# -*- coding: utf-8 -*-

import time
import socket
import subprocess
from mss import mss
from functools import wraps
import pywintypes  # noqa
import win32api

from pywinauto.application import Application
from pywinauto import mouse, keyboard
from pywinauto.win32structures import RECT
from pywinauto.win32functions import SetForegroundWindow, GetSystemMetrics

from import key_press, key_release
from import screenshot

from airtest import aircv
from airtest.core.device import Device

[文档]def require_app(func): @wraps(func) def wrapper(inst, *args, **kwargs): if not raise RuntimeError("Connect to an application first to use %s" % func.__name__) return func(inst, *args, **kwargs) return wrapper
[文档]class Windows(Device): """Windows client.""" def __init__(self, handle=None, dpifactor=1, **kwargs): super(Windows, self).__init__() = None self.handle = int(handle) if handle else None # windows high dpi scale factor, no exact way to auto detect this value for a window # reference: self._dpifactor = float(dpifactor) self._app = Application() self._top_window = None self._focus_rect = (0, 0, 0, 0) self.mouse = mouse self.keyboard = keyboard self._init_connect(handle, kwargs) self.screen = mss() self.monitor = self.screen.monitors[0] # 双屏的时候,self.monitor为整个双屏 self.main_monitor = self.screen.monitors[1] # 双屏的时候,self.main_monitor为主屏 @property def uuid(self): return self.handle def _init_connect(self, handle, kwargs): if handle: self.connect(handle=handle) elif kwargs: self.connect(**kwargs)
[文档] def connect(self, handle=None, **kwargs): """ Connect to window and set it foreground Args: **kwargs: optional arguments Returns: None """ if handle: handle = int(handle) = self._app.connect(handle=handle) self._top_window = else: for k in ["process", "timeout"]: if k in kwargs: kwargs[k] = int(kwargs[k]) = self._app.connect(**kwargs) self._top_window = self.set_foreground()
[文档] def shell(self, cmd): """ Run shell command in subprocess Args: cmd: command to be run Raises: subprocess.CalledProcessError: when command returns non-zero exit status Returns: command output as a byte string """ return subprocess.check_output(cmd, shell=True)
[文档] def snapshot(self, filename=None, quality=10, max_size=None): """ Take a screenshot and save it in ST.LOG_DIR folder Args: filename: name of the file to give to the screenshot, {time}.jpg by default quality: The image quality, integer in range [1, 99] max_size: the maximum size of the picture, e.g 1200 Returns: display the screenshot """ if self.handle: screen = screenshot(filename, self.handle) else: screen = screenshot(filename) if rect = self.get_rect() rect = self._fix_image_rect(rect) screen = aircv.crop_image(screen, [rect.left,, rect.right, rect.bottom]) if not screen.any(): if rect = self.get_rect() rect = self._fix_image_rect(rect) screen = aircv.crop_image(screenshot(filename), [rect.left,, rect.right, rect.bottom]) if self._focus_rect != (0, 0, 0, 0): height, width = screen.shape[:2] rect = (self._focus_rect[0], self._focus_rect[1], width + self._focus_rect[2], height + self._focus_rect[3]) screen = aircv.crop_image(screen, rect) if filename: aircv.imwrite(filename, screen, quality, max_size=max_size) return screen
def _fix_image_rect(self, rect): """Fix rect in image.""" # 将rect 转换为左上角为(0,0), 与图片坐标对齐 rect.left = rect.left - self.monitor["left"] rect.right = rect.right - self.monitor["left"] = - self.monitor["top"] rect.bottom = rect.bottom - self.monitor["top"] return rect
[文档] def keyevent(self, keyname, **kwargs): """ Perform a key event References: Args: keyname: key event **kwargs: optional arguments Returns: None """ self.keyboard.SendKeys(keyname)
[文档] def text(self, text, **kwargs): """ Input text Args: text: text to input **kwargs: optional arguments Returns: None """ self.keyevent(text)
def _fix_op_pos(self, pos): """Fix operation position.""" # 如果是全屏的话,就进行双屏修正,否则就正常即可 if not self.handle: pos = list(pos) pos[0] = pos[0] + self.monitor["left"] pos[1] = pos[1] + self.monitor["top"] return pos
[文档] def key_press(self, key): """Simulates a key press event. Sends a scancode to the computer to report which key has been pressed. Some games use DirectInput devices, and respond only to scancodes, not virtual key codes. You can simulate DirectInput key presses using this method, instead of the keyevent() method, which uses virtual key codes. :param key: A string indicating which key to be pressed. Available key options are: {'ESCAPE', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'BACKSPACE', 'TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', 'ENTER', 'LCTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', "'", '`', 'LSHIFT', 'BACKSLASH', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'RSHIFT', '*', 'LALT', 'SPACE', 'CAPS_LOCK', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'NUM_LOCK', 'SCROLL_LOCK', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_-', 'NUMPAD_4', 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_+', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_0', 'NUMPAD_.', 'F11', 'F12', 'PRINT_SCREEN', 'PAUSE', 'NUMPAD_ENTER', 'RCTRL', 'NUMPAD_/', 'RALT', 'HOME', 'UP', 'PAGE_UP', 'LEFT', 'RIGHT', 'END', 'DOWN', 'PAGE_DOWN', 'INSERT', 'DELETE', 'LWINDOWS', 'RWINDOWS', 'MENU'}. """ key_press(key)
[文档] def key_release(self, key): """Simulates a key release event. Sends a scancode to the computer to report which key has been released. Some games use DirectInput devices, and respond only to scancodes, not virtual key codes. You can simulate DirectInput key releases using this method. A call to the key_release() method usually follows a call to the key_press() method of the same key. :param key: A string indicating which key to be released. """ key_release(key)
[文档] def touch(self, pos, **kwargs): """ Perform mouse click action References: Args: pos: coordinates where to click **kwargs: optional arguments Returns: None """ duration = kwargs.get("duration", 0.01) right_click = kwargs.get("right_click", False) button = "right" if right_click else "left" steps = kwargs.get("steps", 1) offset = kwargs.get("offset", 0) start = self._action_pos(win32api.GetCursorPos()) end = self._action_pos(pos) start_x, start_y = self._fix_op_pos(start) end_x, end_y = self._fix_op_pos(end) interval = float(duration) / steps time.sleep(interval) for i in range(1, steps): x = int(start_x + (end_x-start_x) * i / steps) y = int(start_y + (end_y-start_y) * i / steps) self.mouse.move(coords=(x, y)) time.sleep(interval) self.mouse.move(coords=(end_x, end_y)) for i in range(1, offset+1): self.mouse.move(coords=(end_x+i, end_y+i)) time.sleep(0.01) for i in range(offset): self.mouse.move(coords=(end_x+offset-i, end_y+offset-i)) time.sleep(0.01), coords=(end_x, end_y)) time.sleep(duration) self.mouse.release(button=button, coords=(end_x, end_y))
[文档] def double_click(self, pos): pos = self._fix_op_pos(pos) coords = self._action_pos(pos) self.mouse.double_click(coords=coords)
[文档] def swipe(self, p1, p2, duration=0.8, steps=5): """ Perform swipe (mouse press and mouse release) Args: p1: start point p2: end point duration: time interval to perform the swipe action steps: size of the swipe step Returns: None """ # 设置坐标时相对于整个屏幕的坐标: x1, y1 = self._fix_op_pos(p1) x2, y2 = self._fix_op_pos(p2) from_x, from_y = self._action_pos(p1) to_x, to_y = self._action_pos(p2) interval = float(duration) / (steps + 1), from_y)) time.sleep(interval) for i in range(1, steps): self.mouse.move(coords=( int(from_x + (to_x - from_x) * i / steps), int(from_y + (to_y - from_y) * i / steps), )) time.sleep(interval) for i in range(10): self.mouse.move(coords=(to_x, to_y)) time.sleep(interval) self.mouse.release(coords=(to_x, to_y))
[文档] def mouse_move(self, pos): """Simulates a `mousemove` event. Known bug: Due to a bug in the pywinauto module, users might experience \ off-by-one errors when it comes to the exact coordinates of \ the position on screen. :param pos: A tuple (x, y), where x and y are x and y coordinates of the screen to move the mouse to, respectively. """ if not isinstance(pos, tuple) or len(pos) != 2: # pos is not a 2-tuple raise ValueError('invalid literal for mouse_move: {}'.format(pos)) try: self.mouse.move(coords=self._action_pos(pos)) except ValueError: # in case where x, y are not numbers raise ValueError('invalid literal for mouse_move: {}'.format(pos))
[文档] def mouse_down(self, button='left'): """Simulates a `mousedown` event. :param button: A string indicating which mouse button to be pressed. Available mouse button options are: {'left', 'middle', 'right'}. """ buttons = {'left', 'middle', 'right'} if not isinstance(button, str) or button not in buttons: raise ValueError('invalid literal for mouse_down(): {}'.format(button)) else: coords = self._action_pos(win32api.GetCursorPos()), coords=coords)
[文档] def mouse_up(self, button='left'): """Simulates a `mouseup` event. A call to the mouse_up() method usually follows a call to the mouse_down() method of the same mouse button. :param button: A string indicating which mouse button to be released. """ buttons = {'left', 'middle', 'right'} if not isinstance(button, str) or button not in buttons: raise ValueError('invalid literal for mouse_up(): {}'.format(button)) else: coords = self._action_pos(win32api.GetCursorPos()) self.mouse.release(button=button, coords=coords)
[文档] def start_app(self, path, **kwargs): """ Start the application Args: path: full path to the application kwargs: reference: Returns: None """ = self._app.start(path, **kwargs)
[文档] def stop_app(self, pid): """ Stop the application Args: pid: process ID of the application to be stopped Returns: None """ self._app.connect(process=pid).kill()
[文档] @require_app def set_foreground(self): """ Bring the window foreground Returns: None """ SetForegroundWindow(self._top_window)
[文档] def get_rect(self): """ Get rectangle Returns: win32structures.RECT """ if and self._top_window: return self._top_window.rectangle() else: return RECT(right=GetSystemMetrics(0), bottom=GetSystemMetrics(1))
[文档] @require_app def get_title(self): """ Get the window title Returns: window title """ return self._top_window.texts()
[文档] @require_app def get_pos(self): """ Get the window position coordinates Returns: coordinates of topleft corner of the window (left, top) """ rect = self.get_rect() return (rect.left,
[文档] @require_app def move(self, pos): """ Move window to given coordinates Args: pos: coordinates (x, y) where to move the window Returns: None """ self._top_window.MoveWindow(x=pos[0], y=pos[1])
[文档] @require_app def kill(self): """ Kill the application Returns: None """
def _action_pos(self, pos): if pos = self._windowpos_to_screenpos(pos) pos = (int(pos[0]), int(pos[1])) return pos # @property # def handle(self): # return self._top_window.handle @property def focus_rect(self): return self._focus_rect @focus_rect.setter def focus_rect(self, value): # set focus rect to get rid of window border assert len(value) == 4, "focus rect must be in [left, top, right, bottom]" self._focus_rect = value
[文档] def get_current_resolution(self): rect = self.get_rect() w = (rect.right + self._focus_rect[2]) - (rect.left + self._focus_rect[0]) h = (rect.bottom + self._focus_rect[3]) - ( + self._focus_rect[1]) return w, h
def _windowpos_to_screenpos(self, pos): """ Convert given position relative to window topleft corner to screen coordinates Args: pos: coordinates (x, y) Returns: converted position coordinates """ rect = self.get_rect() pos = (int((pos[0] + rect.left + self._focus_rect[0]) * self._dpifactor), int((pos[1] + + self._focus_rect[1]) * self._dpifactor)) return pos
[文档] def get_ip_address(self): """ Return default external ip address of the windows os. Returns: :py:obj:`str`: ip address """ hostname = socket.getfqdn() return socket.gethostbyname_ex(hostname)[2][0]