# -*- 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 airtest.core.win.ctypesinput import key_press, key_release
from airtest.core.win.screen import screenshot
from airtest import aircv
from airtest.core.device import Device
[docs]def require_app(func):
@wraps(func)
def wrapper(inst, *args, **kwargs):
if not inst.app:
raise RuntimeError("Connect to an application first to use %s" % func.__name__)
return func(inst, *args, **kwargs)
return wrapper
[docs]class Windows(Device):
"""Windows client."""
def __init__(self, handle=None, dpifactor=1, **kwargs):
super(Windows, self).__init__()
self.app = 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: https://msdn.microsoft.com/en-us/library/windows/desktop/mt843498(v=vs.85).aspx
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, **kwargs)
elif kwargs:
self.connect(**kwargs)
[docs] 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 = self._app.connect(handle=handle)
self._top_window = self.app.window(handle=handle).wrapper_object()
else:
for k in ["process", "timeout"]:
if k in kwargs:
kwargs[k] = int(kwargs[k])
self.app = self._app.connect(**kwargs)
self._top_window = self.app.top_window().wrapper_object()
if kwargs.get("foreground", True) in (True, "True", "true"):
self.set_foreground()
[docs] 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)
[docs] 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 self.app:
rect = self.get_rect()
rect = self._fix_image_rect(rect)
screen = aircv.crop_image(screen, [rect.left, rect.top, rect.right, rect.bottom])
if not screen.any():
if self.app:
rect = self.get_rect()
rect = self._fix_image_rect(rect)
screen = aircv.crop_image(screenshot(filename), [rect.left, rect.top, 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"]
rect.top = rect.top - self.monitor["top"]
rect.bottom = rect.bottom - self.monitor["top"]
return rect
[docs] def keyevent(self, keyname, **kwargs):
"""
Perform a key event
References:
https://pywinauto.readthedocs.io/en/latest/code/pywinauto.keyboard.html
Args:
keyname: key event
**kwargs: optional arguments
Returns:
None
"""
self.keyboard.SendKeys(keyname)
[docs] 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
[docs] 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)
[docs] 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)
[docs] def touch(self, pos, **kwargs):
"""
Perform mouse click action
References:
https://pywinauto.readthedocs.io/en/latest/code/pywinauto.mouse.html
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)
self.mouse.press(button=button, coords=(end_x, end_y))
time.sleep(duration)
self.mouse.release(button=button, coords=(end_x, end_y))
[docs] def double_click(self, pos):
pos = self._fix_op_pos(pos)
coords = self._action_pos(pos)
self.mouse.double_click(coords=coords)
[docs] 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)
self.mouse.press(coords=(from_x, 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))
[docs] 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))
[docs] 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())
self.mouse.press(button=button, coords=coords)
[docs] 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)
[docs] def start_app(self, path, **kwargs):
"""
Start the application
Args:
path: full path to the application
kwargs: reference: https://pywinauto.readthedocs.io/en/latest/code/pywinauto.application.html#pywinauto.application.Application.start
Returns:
None
"""
self.app = self._app.start(path, **kwargs)
[docs] 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()
[docs] @require_app
def set_foreground(self):
"""
Bring the window foreground
Returns:
None
"""
SetForegroundWindow(self._top_window)
[docs] def get_rect(self):
"""
Get rectangle
Returns:
win32structures.RECT
"""
if self.app and self._top_window:
return self._top_window.rectangle()
else:
return RECT(right=GetSystemMetrics(0), bottom=GetSystemMetrics(1))
[docs] @require_app
def get_title(self):
"""
Get the window title
Returns:
window title
"""
return self._top_window.texts()
[docs] @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, rect.top)
[docs] @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])
[docs] @require_app
def kill(self):
"""
Kill the application
Returns:
None
"""
self.app.kill()
def _action_pos(self, pos):
if self.app:
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
[docs] 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]) - (rect.top + 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] + rect.top + self._focus_rect[1]) * self._dpifactor))
return pos
[docs] 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]