"""This module contains functions to replay a sequence of user actions automatically."""
# print('__file__={0:<35} | __name__={1:<20} | __package__={2:<20}'.format(__file__, __name__, str(__package__)))
from enum import Enum
from typing import Optional, Union, NewType
import time
import re
import pathlib
import pywinauto
from win32api import GetCursorPos as win32api_GetCursorPos
from win32api import GetSystemMetrics as win32api_GetSystemMetrics
from win32api import mouse_event as win32api_mouse_event
from win32gui import LoadCursor as win32gui_LoadCursor
from win32gui import GetCursorInfo as win32gui_GetCursorInfo
from win32gui import GetWindowRect as win32gui_GetWindowRect
from win32gui import MoveWindow as win32gui_MoveWindow
from win32gui import ShowWindow as win32gui_ShowWindow
from win32gui import IsIconic as win32gui_IsIconic
from win32gui import SetWindowPos as win32gui_SetWindowPos
from win32con import IDC_WAIT, MOUSEEVENTF_MOVE, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, \
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL, \
WHEEL_DELTA, SW_RESTORE,HWND_NOTOPMOST, HWND_TOPMOST
from .core import type_separator, path_separator, get_entry, get_entry_list, find_elements, get_sorted_region, \
get_wrapper_path, is_int, is_absolute_path, set_native_window_handle, get_native_window_handle, find_window_candidates
from functools import partial, update_wrapper
from cachetools import func
import math
from .ocr_wrapper import OCRWrapper
import sys
UI_Coordinates = NewType('UI_Coordinates', (float, float))
UI_Path = str
PYWINAUTO_Wrapper = pywinauto.controls.uiawrapper.UIAWrapper
UI_Selector = Union[UI_Path, PYWINAUTO_Wrapper, UI_Coordinates]
class PywinautoRecorderException(Exception):
"""Base class for other exceptions."""
...
class FailedSearch(PywinautoRecorderException):
"""FailedSearch is a subclass of *PywinautoRecorderException* that is raised when a search for a control fails."""
...
__all__ = ['PlayerSettings', 'MoveMode', 'ButtonLocation', 'load_dictionary', 'shortcut', 'full_definition', 'UIPath',
'Window', 'Region', 'find', 'find_all', 'move_window', 'move', 'click', 'left_click', 'right_click',
'double_left_click', 'triple_left_click', 'double_click', 'triple_click',
'drag_and_drop', 'middle_drag_and_drop', 'right_drag_and_drop', 'menu_click',
'mouse_wheel', 'send_keys', 'set_combobox', 'set_text', 'exists', 'select_file', 'playback',
'find_cache_clear', 'UIApplication', 'start_application', 'connect_application', 'focus_on_application',
'find_main_windows', 'kill_application']
# TODO special_char_array in core for recorder.py and player.py (check when to call escape & unescape)
def unescape_special_char(string):
for r in (("\\\\", "\\"), ("\\t", "\t"), ("\\n", "\n"), ("\\r", "\r"), ("\\v", "\v"), ("\\f", "\f"), ('\\"', '"')):
string = string.replace(*r)
return string
[docs]class PlayerSettings:
"""The player settings class contains the default settings."""
typing_pause = 0.1
"""The pause time between characters typed"""
mouse_move_duration = 0.5
"""Mouse move duration (in seconds)."""
timeout = 10
"""Maximum duration (in seconds) to wait for the :func:`find` function to search an element before giving up.
If the element is not found after the given timeout, the search is interrupted."""
use_cache = True
"""If True, the :func:`find` function caches the results of the search.
This is useful if the search is called several times on the same element."""
@staticmethod
def _apply_settings(
typing_pause: Optional[float] = None,
mouse_move_duration: Optional[float] = None,
timeout: Optional[float] = None) -> dict:
"""
If the duration and timeout arguments are None, set them to the default values.
:param typing_duration: The pause time between characters typed
:param mouse_move_duration: The duration of the mouse movement
:param timeout: The maximum duration to wait for the :func:`find` function to find an element before giving up
:return: The duration and timeout are being returned.
"""
if typing_pause is None:
typing_pause = PlayerSettings.typing_pause
if mouse_move_duration is None:
mouse_move_duration = PlayerSettings.mouse_move_duration
if timeout is None:
timeout = PlayerSettings.timeout
return {"typing_pause": typing_pause, "mouse_move_duration": mouse_move_duration, "timeout": timeout}
[docs]class MoveMode(Enum):
"""The MoveMode class is an enumeration of the different ways that the mouse can move."""
linear = 0
"""The mouse cursor moves in a straight line from the start point to the end point with a constant speed."""
y_first = 1
"""The mouse cursor moves at a right angle from the starting point to the ending point at a constant speed.
The first segment of the right angle is vertical and the second is horizontal."""
x_first = 2
"""The mouse cursor moves at a right angle from the starting point to the ending point at a constant speed.
The first segment of the right angle is horizontal and the second is vertical."""
_dictionary = {}
unique_element_old = None
element_path_old = ''
w_rOLD = None
[docs]def load_dictionary(filename_key: str, filename_def: str,encoding: str = 'utf8') -> None:
"""
Loads a dictionary.
:param filename_key: filename of the key file
:param filename_def: filename of the definition file
:param encoding: encoding of the dictionary file
"""
abs_path = [x for x in range(99)]
with open(filename_key, encoding=encoding) as fp_key, open(filename_def, encoding=encoding) as fp_def:
for line_key, line_def in zip(fp_key, fp_def):
words = line_key.split("\t")
word = words[-1].translate(str.maketrans('', '', '\n\t\r'))
words = line_def.split("\t")
definition = words[-1].translate(str.maketrans('', '', '\n\t\r'))
level = len(words) - 1
#print(level)
abs_path[level] = definition
abs_definition = abs_path[0]
for i in range(1, level):
abs_definition += path_separator + abs_path[i]
#print(abs_definition + path_separator + definition)
_dictionary[word] = (abs_definition, definition)
[docs]def shortcut(str_shortcut: str) -> str:
"""
Returns the shortcut path associated to the shortcut defined in the previously loaded dictionary.
:param str_shortcut: shortcut
"""
return _dictionary[str_shortcut][1]
[docs]def full_definition(str_shortcut: str) -> str:
"""
Returns the full element path associated to the shortcut defined in the previously loaded dictionary.
:param str_shortcut: shortcut
"""
return _dictionary[str_shortcut][0] + path_separator + _dictionary[str_shortcut][1]
def wait_is_ready_try1(wrapper, timeout=120):
"""
Waits until element is ready (wait while greyed, not enabled, not visible, not ready, ...) :
So far, I didn't find better than wait_cpu_usage_lower when greyed but must be enhanced.
"""
t0 = time.time()
while not wrapper.is_enabled() or not wrapper.is_visible():
try:
h_wait_cursor = win32gui_LoadCursor(0, IDC_WAIT)
_, h_cursor, _ = win32gui_GetCursorInfo()
app = pywinauto.Application(backend='uia', allow_magic_lookup=False)
app.connect(process=wrapper.element_info.element.CurrentProcessId)
while h_cursor == h_wait_cursor:
app.wait_cpu_usage_lower()
spec = app.window(handle=wrapper.handle, top_level_only=False)
while not wrapper.is_enabled() or not wrapper.is_visible():
spec.wait("exists enabled visible ready")
if (time.time() - t0) > timeout:
break
except Exception:
time.sleep(0.1)
if (time.time() - t0) > timeout:
msg = "Element " + get_wrapper_path(wrapper) + " was not found after " + str(timeout) + " s of searching."
raise TimeoutError("Time out! ", msg)
[docs]class UIPath(object):
"""
UIPath is a context manager used to keep track of the current path in the UI tree.
.. code-block:: python
:caption: Example of code not using a 'UIPath' object::
:emphasize-lines: 3,4
from pywinauto_recorder.player import click
click("Calculator||Window->*->Number pad||Group->One||Button")
click("Calculator||Window->*->Number pad||Group->Two||Button")
The code above clicks on two buttons. On each line that corresponds to a click operation, the whole path is repeated.
A UIPath object will allow to factorize a common path where several operations will be performed.
The following code does the same as the previous example:
.. code-block:: python
:caption: Example of code using a 'UIPath' object::
:emphasize-lines: 3,3
from pywinauto_recorder.player import UIPath, click
with UIPath("Calculator||Window"):
click("*->Number pad||Group->One||Button")
click("*->Number pad||Group->Two||Button")
UIPath objects can be nested.
The following code does the same as the previous example:
.. code-block:: python
:caption: Example of code using nested 'UIPath' objects::
:emphasize-lines: 3,4
from pywinauto_recorder.player import UIPath, click
with UIPath("Calculator||Window"):
with UIPath("*->Number pad||Group"):
click("One||Button")
click("Two||Button")
"""
_stack = []
_path_list = []
_regex_list = [] # UIPath._regex_list must be removed
[docs] @staticmethod
def get_full_path(element_path: Optional[UI_Path] = None) -> UI_Path:
"""
If the element_path is None, return the full path of the current UI element
:param element_path: Optional[UI_Path] = None
:type element_path: Optional[UI_Path]
:return: The full path of the element.
"""
if element_path is None or element_path == "":
return path_separator.join(UIPath._path_list)
elif UIPath._path_list and not is_absolute_path(element_path):
return path_separator.join(UIPath._path_list) + path_separator + element_path
else:
return element_path
def __init__(self, relative_path=None, regex_title=False,absolute_path=False):
self.relative_path = relative_path
self.regex_title = regex_title # UIPath._regex_list must be removed
self.absolute_path = absolute_path
def __enter__(self):
if self.absolute_path:
UIPath._stack.append(UIPath._path_list)
UIPath._path_list = []
if self.relative_path:
UIPath._path_list.append(self.relative_path)
UIPath._regex_list.append(self.regex_title) # UIPath._regex_list must be removed
return self
def __exit__(self, type, value, traceback):
if self.relative_path:
UIPath._path_list = UIPath._path_list[0:-1]
UIPath._regex_list = UIPath._regex_list[0:-1] # UIPath._regex_list must be removed
if UIPath._stack:
UIPath._path_list = UIPath._stack.pop()
Window = UIPath
Region = UIPath
[docs]def find_cache_clear():
"""
Clears the cache of the :func:`find` function.
"""
_cached_find.cache_clear()
@func.ttl_cache(ttl=60)
def _cached_find(
full_element_path: Optional[UI_Selector] = None,
window_handle=None,
timeout: Optional[float] = None) -> PYWINAUTO_Wrapper:
"""
Finds the element defined by the full_element_path.
full_element_path must not contain the relative coordinates.
"""
return _find(full_element_path, timeout)
def _find(
full_element_path: Optional[UI_Selector] = None,
timeout: Optional[float] = None) -> PYWINAUTO_Wrapper:
"""
Finds the element defined by the full_element_path.
When the [] operator is used and only one element is found, the row and column indices are not tested and the element is returned.
"""
print("🔎", end="", file=sys.stdout)
sys.stdout.flush()
_, _, y_x, _ = get_entry(get_entry_list(full_element_path)[-1])
elements = []
t0 = time.time()
while (time.time() - t0) < timeout:
while not elements:
if (time.time() - t0) > timeout:
msg = "No element found with the UIPath '" + full_element_path + "' after " + str(timeout) + " s of searching."
raise FailedSearch(msg)
try:
elements = find_elements(full_element_path)
if not elements:
print("🔴", end="", file=sys.stdout)
sys.stdout.flush()
time.sleep(2.0)
except Exception:
print("🟢", end="", file=sys.stdout)
sys.stdout.flush()
pass
if len(elements) == 1:
return elements[0]
if y_x is not None:
nb_y, _, candidates = get_sorted_region(elements)
if is_int(y_x[0]):
return candidates[int(y_x[0])][int(y_x[1])]
else:
full_smart_element_path = UIPath.get_full_path(y_x[0])
ref_unique_element = find_elements(full_smart_element_path)
if len(ref_unique_element) > 1:
msg = "No element found with the UIPath '" + full_smart_element_path + "' in the array line."
raise FailedSearch(msg)
ref_r = ref_unique_element[0].rectangle()
r_y = 0
while r_y < nb_y:
for candidate in candidates[r_y]:
y_candidate = candidate.rectangle().mid_point()[1]
if ref_r.top < y_candidate < ref_r.bottom:
return candidates[r_y][y_x[1]]
r_y = r_y + 1
time.sleep(0.1)
if len(elements) > 1:
message = "There are " + str(len(elements)) + " undiscriminated elements that match the path '" + full_element_path + "':"
for e in elements:
if isinstance(e, OCRWrapper):
message += "\n" + str(e.result)
else:
message += "\n" + get_wrapper_path(e)
raise FailedSearch(message)
raise FailedSearch("Unique element not found using path '", full_element_path + "'")
[docs]def find(
element_path: Optional[UI_Selector] = None,
regex: bool = False,
timeout: Optional[float] = None) -> PYWINAUTO_Wrapper:
"""
Finds the element matching element_path.
This function is called in all the other functions (:func:`click`, :func:`move`, ...) that require to search an element.
To significantly increase search performance, the user can enable a cache with 'Player.Setting.use_cache = True'.
When the cache is active, it is sometimes necessary to empty it with the :func:`find_cache_clear` function.
When the [] operator is used and only one element is found, the row and column indices are not tested and the element is returned.
.. code-block:: python
:caption: Example of code using the 'find' function::
:emphasize-lines: 3,3
from pywinauto_recorder.player import UIPath, find
with UIPath("RegEx: .* Google Chrome$||Pane"):
find().set_focus() # Set focus to the Google Chrome window.
The code above will set focus to the Google Chrome window.
The :func:`find` function is used to find the Pywinauto wrapper of the window.
It will work only if the window is not minimized.
:param element_path: element path
:param regex: The parameter 'regex' is deprecated. Please use the new RegEx syntax of UIPath!
:param timeout: duration in seconds that will be allowed to find the element
:return: Pywinauto wrapper of found element
:raises FailedSearch: if the element is not found
"""
if regex:
deprecated_msg = """
The parameter 'regex' is deprecated. Please use the new RegEx syntax of UIPath!
For example:
with Window(".* - Notepad||Window", regex=True):
edit = left_click("Text Editor||Edit")
Must be coded with the new syntax:
with UIPath("RegEx: .* - Notepad||Window"):
edit = left_click("Text Editor||Edit")
This parameter will be removed in the next release.
"""
print(deprecated_msg)
timeout = PlayerSettings._apply_settings(timeout=timeout)["timeout"]
if element_path is None or isinstance(element_path, str):
if element_path is not None:
element_path = re.sub(r"%\([+-]?\d*.?\d*,\s?[+-]?\d*.?\d*\)$", "", element_path) # remove "%(?, ?)"
full_element_path = UIPath.get_full_path(element_path)
else:
full_element_path = get_wrapper_path(element_path)
if PlayerSettings.use_cache:
return _cached_find(full_element_path, get_native_window_handle(), timeout=timeout)
else:
return _find(full_element_path, timeout=timeout)
[docs]def find_all(
element_path: Optional[UI_Selector] = None,
timeout: Optional[float] = None) -> PYWINAUTO_Wrapper:
"""
Finds all elements matching element_path.
.. code-block:: python
:caption: Example of code using the 'find_all' function::
:emphasize-lines: 4,4
from pywinauto_recorder.player import UIPath, find, find_all
with UIPath("RegEx: .* Google Chrome$||Pane"):
find().set_focus()
wrapper_tab_list = find_all("*->RegEx: .*||TabItem") # Find all tabs.
for wrapper_tab in wrapper_tab_list:
wrapper_tab.click_input()
wrapper_url = find("*->Address and search bar||Edit")
print(wrapper_url.get_value())
The code above will click on all tabs of Google Chrome and print the URL of each tab.
It will work only if the Google Chrome window is not minimized.
The :func:`find_all` function is used to find all tabs.
:param element_path: element path
:param timeout: period of time in seconds that will be allowed to find the element
:return: Pywinauto wrapper list of found elements
:raises FailedSearch: if no element found
"""
timeout = PlayerSettings._apply_settings(timeout=timeout)["timeout"]
if element_path is None or isinstance(element_path, str):
full_element_path = UIPath.get_full_path(element_path)
else:
full_element_path = get_wrapper_path(element_path)
entry_list = get_entry_list(full_element_path)
_, _, y_x, _ = get_entry(entry_list[-1])
if y_x:
return find(element_path, timeout=timeout)
t0 = time.time()
while (time.time() - t0) < timeout:
try:
elements = find_elements(full_element_path)
if elements:
return elements
time.sleep(2.0)
except Exception:
pass
time.sleep(0.1)
return []
[docs]def move_window(element_path: Optional[UI_Selector] = None,
x: Optional[int] = 0,
y: Optional[int] = 0,
width: Optional[int] = 0,
height: Optional[int] = 0):
"""
Moves and resizes a window
:param element_path: element path
:param x: new x coordinate of the upper left corner of the window
:param y: new y coordinate of the upper left corner of the window
:param width: new width of the window, if 0 then the with and the height are not modified
:param height: new height of the window, if 0 then the with and the height are not modified
:return: Pywinauto wrapper of found window
:raises FailedSearch: if no element found
"""
window = find(element_path)
while window.handle is None:
window = window.parent()
native_window_handle = window.handle
if width == 0 or height == 0:
rect = win32gui_GetWindowRect(native_window_handle)
x = rect[0]
y = rect[1]
width = rect[2] - x
height = rect[3] - y
win32gui_MoveWindow(native_window_handle, x, y, width, height, True)
return window
def _move(x, y, xd, yd, duration=1, refresh_rate=25):
"""
It moves the mouse from (x, y) to (xd, yd) in a straight line, with a duration of duration seconds.
:param x: The x-coordinate of the mouse cursor before the move
:param y: The y-coordinate of the mouse cursor before the move
:param xd: The x-coordinate of the mouse cursor after the move
:param yd: The y-coordinate of the mouse cursor after the move
:param duration: The time (in seconds) it takes to move the mouse from (x, y) to (xd, yd)
:param refresh_rate: 25 Hz is the default refresh rate of the mouse move
"""
x_max = win32api_GetSystemMetrics(0) - 1
y_max = win32api_GetSystemMetrics(1) - 1
if duration > 0:
samples = duration * refresh_rate
dt = 1 / refresh_rate
step_x = (xd - x) / samples
step_y = (yd - y) / samples
t0 = time.time()
for i in range(int(samples)):
x, y = x+step_x, y+step_y
t1 = time.time()
if t1-t0 > i*dt:
continue
time.sleep(dt)
nx = int(x * 65535 / x_max)
ny = int(y * 65535 / y_max)
win32api_mouse_event(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, nx, ny)
nx = round(xd * 65535 / x_max)
ny = round(yd * 65535 / y_max)
win32api_mouse_event(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, nx, ny)
[docs]def move(
element_path: Optional[UI_Selector] = None,
duration: Optional[float] = None,
mode: Enum = MoveMode.linear,
timeout: float = 120) -> PYWINAUTO_Wrapper:
"""
Moves the mouse cursor over the user interface element.
:param element_path: element path
:param duration: duration in seconds of the mouse move (it doesn't take into account the time it takes to find),
if duration is -1 the mouse cursor doesn't move.
:param mode: move mouse mode (see :class:`MoveMode`)
:param timeout: period of time in seconds that will be allowed to find the element
:return: Pywinauto wrapper of clicked element
:raises FailedSearch: if the element is not found
"""
duration = PlayerSettings._apply_settings(mouse_move_duration=duration)["mouse_move_duration"]
if duration == -1:
return
if element_path is None or isinstance(element_path, str):
unique_element = find(element_path, timeout=timeout)
try:
w_r = unique_element.rectangle()
except Exception:
#with _cached_find.cache_lock:
# _cached_find.cache.pop(_cached_find.cache_key(element_path, timeout=timeout), None)
find_cache_clear()
unique_element = find(element_path, timeout=timeout)
w_r = unique_element.rectangle()
xd, yd = w_r.mid_point()
if element_path:
_, _, _, dx_dy = get_entry(get_entry_list(element_path)[-1])
if dx_dy:
dx, dy = dx_dy
xd, yd = round(xd + dx/100.0*(w_r.width()/2-1), 0), round(yd + dy/100.0*(w_r.height()/2-1), 0)
elif isinstance(element_path, pywinauto.base_wrapper.BaseWrapper):
unique_element = element_path
w_r = unique_element.rectangle()
xd, yd = w_r.mid_point()
else:
(xd, yd) = element_path
unique_element = None
x, y = win32api_GetCursorPos()
if (x, y) != (xd, yd):
if mode == MoveMode.linear:
_move(x, y, xd, yd, duration)
elif mode == MoveMode.x_first:
_move(x, y, xd, y, duration / 2)
_move(xd, y, xd, yd, duration / 2)
elif mode == MoveMode.y_first:
_move(x, y, x, yd, duration / 2)
_move(x, yd, xd, yd, duration / 2)
return unique_element
def _win32api_mouse_click(button: ButtonLocation = ButtonLocation.left, click_count: int = 1):
"""
Clicks the mouse.
:param button: The button to click
:param click_count: How many times to click, defaults to 1
"""
if button == ButtonLocation.left:
event_down = MOUSEEVENTF_LEFTDOWN
event_up = MOUSEEVENTF_LEFTUP
elif button == ButtonLocation.middle:
event_down = MOUSEEVENTF_MIDDLEDOWN
event_up = MOUSEEVENTF_MIDDLEUP
elif button == ButtonLocation.right:
event_down = MOUSEEVENTF_RIGHTDOWN
event_up = MOUSEEVENTF_RIGHTUP
for _ in range(click_count):
win32api_mouse_event(event_down, 0, 0)
time.sleep(.01)
win32api_mouse_event(event_up, 0, 0)
time.sleep(.1)
[docs]def click(
element_path: Optional[UI_Selector] = None,
duration: Optional[float] = None,
mode: MoveMode = MoveMode.linear,
button: ButtonLocation = ButtonLocation.left,
click_count: int = 1,
timeout: float = None,
wait_ready: bool = True) -> PYWINAUTO_Wrapper:
"""
Clicks on found element.
.. code-block:: python
:caption: Example of code using the 'click' function::
:emphasize-lines: 3,3
from pywinauto_recorder.player import click, MoveMode
click("Calculator||Window->*->One||Button", mode=MoveMode.x_first, duration=4)
:param element_path: element path
:param duration: duration in seconds of the mouse move (it doesn't take into account the time it takes to find)
(if duration is -1 the mouse cursor doesn't move, it just sends WM_CLICK window message,
useful for minimized or non-active window).
:param mode: move mouse mode: MoveMode.linear, MoveMode.x_first, MoveMode.y_first
:param button: mouse button: ButtonLocation.left, ButtonLocation.middle, ButtonLocation.right
:param click_count: number of clicks
:param timeout: period of time in seconds that will be allowed to find the element
:param wait_ready: if True waits until the element is ready
:return: Pywinauto wrapper of clicked element
:raises FailedSearch: if the element is not found
"""
settings = PlayerSettings._apply_settings(mouse_move_duration=duration, timeout=timeout)
duration = settings["mouse_move_duration"]
timeout = settings["timeout"]
if duration == -1:
wrapper = find(element_path)
has_get_value = getattr(wrapper, "click", None)
if callable(has_get_value):
wrapper.click()
else:
wrapper.click_input()
return wrapper
elif wait_ready and \
(element_path is None or isinstance(element_path, str) or isinstance(element_path, pywinauto.base_wrapper.BaseWrapper)):
use_cache_old = PlayerSettings.use_cache
PlayerSettings.use_cache = False
# if isinstance(element_path, pywinauto.base_wrapper.BaseWrapper):
# wrapper = element_path
wrapper = move(element_path, duration=duration, mode=mode, timeout=timeout)
wait_is_ready_try1(wrapper, timeout=timeout)
PlayerSettings.use_cache = use_cache_old
else:
wrapper = move(element_path, duration=duration, mode=mode, timeout=timeout)
_win32api_mouse_click(button, click_count)
return wrapper
def wrapped_partial(func, *args, **kwargs):
partial_func = partial(func, *args, **kwargs)
update_wrapper(partial_func, func)
partial_func.__doc__ = "This function is a partial function derived from the :func:`" + func.__name__
partial_func.__doc__ += "` general function.\nThe parameters of the function are set with the following values:"
for key in kwargs.keys():
partial_func.__doc__ += "\n - " + str(key) + "=" + str(kwargs[key])
partial_func.__doc__ += "\n\n" + func.__doc__
return partial_func
left_click = wrapped_partial(click, button=ButtonLocation.left)
right_click = wrapped_partial(click, button=ButtonLocation.right)
double_left_click = wrapped_partial(click, button=ButtonLocation.left, click_count=2)
double_click = wrapped_partial(click, button=ButtonLocation.left, click_count=2)
triple_left_click = wrapped_partial(click, button=ButtonLocation.left, click_count=3)
triple_click = wrapped_partial(click, button=ButtonLocation.left, click_count=3)
[docs]def drag_and_drop(
element_path1: UI_Selector,
element_path2: UI_Selector,
duration: Optional[float] = None,
mode: Enum = MoveMode.linear,
button: ButtonLocation = ButtonLocation.left,
timeout: Optional[float] = None) -> PYWINAUTO_Wrapper:
"""
Drags and drops from element_path1 to element_path2.
:param element_path1: source element path
:param element_path2: destination element path
:param duration: duration in seconds of the mouse move (it doesn't take into account the time it takes to find)
(if duration is -1 the mouse cursor doesn't move, it just sends WM_CLICK window message,
useful for minimized or non-active window).
:param mode: move mouse mode: MoveMode.linear, MoveMode.x_first, MoveMode.y_first
:param button: mouse button: ButtonLocation.left, ButtonLocation.middl, ButtonLocation.right
:param timeout: period of time in seconds that will be allowed to find the element
:return: Pywinauto wrapper found with element_path2
:raises FailedSearch: if the element is not found
"""
move(element_path1, duration=duration, mode=mode, timeout=timeout)
if button == ButtonLocation.left:
event_down = MOUSEEVENTF_LEFTDOWN
event_up = MOUSEEVENTF_LEFTUP
elif button == ButtonLocation.middle:
event_down = MOUSEEVENTF_MIDDLEDOWN
event_up = MOUSEEVENTF_MIDDLEUP
elif button == ButtonLocation.right:
event_down = MOUSEEVENTF_RIGHTDOWN
event_up = MOUSEEVENTF_RIGHTUP
win32api_mouse_event(event_down, 0, 0)
unique_element = move(element_path2, duration=duration, timeout=timeout)
win32api_mouse_event(event_up, 0, 0)
return unique_element
left_drag_and_drop = wrapped_partial(drag_and_drop, button=ButtonLocation.left)
middle_drag_and_drop = wrapped_partial(drag_and_drop, button=ButtonLocation.middle)
right_drag_and_drop = wrapped_partial(drag_and_drop, button=ButtonLocation.right)
[docs]def mouse_wheel(steps: int, pause: float = 0.05) -> None:
"""
Turns the mouse wheel up or down.
:param steps: number of wheel steps, if positive the mouse wheel is turned up else it is turned down
:param pause: pause in seconds between each wheel step
"""
if pause == 0:
win32api_mouse_event(MOUSEEVENTF_WHEEL, 0, 0, WHEEL_DELTA * steps, 0)
else:
for _ in range(abs(steps)):
if steps > 0:
win32api_mouse_event(MOUSEEVENTF_WHEEL, 0, 0, WHEEL_DELTA, 0)
else:
win32api_mouse_event(MOUSEEVENTF_WHEEL, 0, 0, -WHEEL_DELTA, 0)
time.sleep(pause)
[docs]def send_keys(
str_keys: str,
pause: Optional[float] = None,
with_spaces: bool = True,
with_tabs: bool = True,
with_newlines: bool = True,
turn_off_numlock: bool = True,
vk_packet: bool = True) -> None:
"""
Parses the keys and type them
You can use any Unicode characters (on Windows) and some special keys.
See https://pywinauto.readthedocs.io/en/latest/code/pywinauto.keyboard.html
:param str_keys: string representing the keys to be typed
:param pause: pause in seconds between each typed key
:param with_spaces: if False spaces are not taken into account
:param with_tabs: if False tabs are not taken into account
:param with_newlines: if False newlines are not taken into account
:param turn_off_numlock: if True numlock is turned off
:param vk_packet: For Windows only, pywinauto defaults to sending a virtual key packet (VK_PACKET) for textual input
"""
typing_pause = PlayerSettings._apply_settings(typing_pause=pause)["typing_pause"]
for r in (('(', '{(}'), (')', '{)}'), ('+', '{+}')):
str_keys = str_keys.replace(*r)
pywinauto.keyboard.send_keys( # lgtm [py/call/wrong-named-argument]
str_keys,
pause=typing_pause,
with_spaces=with_spaces,
with_tabs=with_tabs,
with_newlines=with_newlines,
turn_off_numlock=turn_off_numlock,
vk_packet=vk_packet
)
[docs]def set_combobox(
element_path: UI_Selector,
value: str,
duration: Optional[float] = None,
mode: Enum = MoveMode.linear,
timeout: Optional[float] = None,
wait_ready: bool = True) -> None:
"""
Sets the value of a combobox.
:param element_path: element path
:param value: value of the combobox
:param duration: duration in seconds of the mouse move (it doesn't take into account the time it takes to find)
(if duration is -1 the mouse cursor doesn't move, it just sends WM_CLICK window message,
useful for minimized or non-active window).
:param mode: move mouse mode: MoveMode.linear, MoveMode.x_first, MoveMode.y_first
:param timeout: period of time in seconds that will be allowed to find the element
:raises FailedSearch: if the element is not found
"""
click(element_path, duration=duration, mode=mode, timeout=timeout, wait_ready=wait_ready)
time.sleep(0.9)
send_keys(value + "{ENTER}")
[docs]def set_text(
element_path: UI_Selector,
value: str,
duration: Optional[float] = None,
mode: Enum = MoveMode.linear,
timeout: Optional[float] = None,
pause: float = None,
end_with_enter: bool = True) -> None:
"""
Sets the value of a text field.
:param element_path: element path
:param value: value of the combobox
:param duration: duration in seconds of the mouse move (it doesn't take into account the time it takes to find)
(if duration is -1 the mouse cursor doesn't move, it just sends WM_CLICK window message,
useful for minimized or non-active window)
:param mode: move mouse mode: MoveMode.linear, MoveMode.x_first, MoveMode.y_first
:param timeout: period of time in seconds that will be allowed to find the element
:param pause: pause in seconds between each typed key
:param end_with_enter: if True then Enter is sent after the value is entered
:raises FailedSearch: if the element is not found
"""
typing_pause = PlayerSettings._apply_settings(typing_pause=pause)["typing_pause"]
double_left_click(element_path, duration=duration, mode=mode, timeout=timeout)
send_keys("{VK_CONTROL down}a{VK_CONTROL up}", pause=0)
time.sleep(0.1)
send_keys(value, pause=typing_pause)
if end_with_enter:
send_keys("{ENTER}", pause=typing_pause)
[docs]def exists(
element_path: UI_Selector,
timeout: Optional[float] = None) -> PYWINAUTO_Wrapper:
"""
Tests if the user interface element exists. It returns the element if it exists, None otherwise.
In both cases no exception is thrown as for the :func:`find` function.
:param element_path: element path
:param timeout: period of time in seconds that will be allowed to find the element
:return: Pywinauto wrapper of the found element or None
"""
if timeout is None:
timeout = PlayerSettings.timeout
if element_path is None or isinstance(element_path, str):
if element_path is not None:
element_path = re.sub(r"%\([+-]?\d*.?\d*,\s?[+-]?\d*.?\d*\)$", "", element_path) # remove "%(?, ?)"
full_element_path = UIPath.get_full_path(element_path)
else:
full_element_path = get_wrapper_path(element_path)
try:
wrapper = _find(full_element_path, timeout=timeout)
return wrapper
except FailedSearch:
return None
[docs]def select_file(
window_path: UI_Selector,
full_path: str,
force_slow_path_typing: bool = False) -> None:
"""
Selects a file in an already opened file dialog.
.. code-block:: python
:caption: Example of code using 'select_file'::
:emphasize-lines: 3,3
from pywinauto_recorder.player import select_file
select_file("Document - WordPad||Window->Open||Window", "Documents/file.txt")
To make this code work, you must first launch 'WordPad' and click on 'File->Open'.
:param window_path: window path of the file dialog (e.g. "Untitled - Paint||Window->Save As||Window"
:param full_path: the full path of the file to select
:param force_slow_path_typing: if True it will type the path even if the current path of the dialog box is the same as the file to select
:raises FailedSearch: if an element is not found
"""
p = pathlib.Path(full_path)
folder = p.parent
filename = p.name
with UIPath(window_path):
find().set_focus()
click("*->All locations||SplitButton")
if not force_slow_path_typing:
with UIPath(window_path):
try:
old_folder = find("*->Address||ComboBox->Address||Edit").get_value()
except Exception:
find_cache_clear()
#with _cached_find.cache_lock:
# _cached_find.cache.pop(_cached_find.cache_key("*->Address||ComboBox->Address||Edit"), None)
old_folder = find("*->Address||ComboBox->Address||Edit").get_value()
if force_slow_path_typing or old_folder != folder:
send_keys(str(folder))
send_keys("{ENTER}")
with UIPath(window_path):
double_left_click("*->File name:||ComboBox->File name:||Edit")
send_keys(filename + "{ENTER}")
[docs]def playback(str_code='', filename=''):
"""
This function plays back a string of code or a Python file.
:param str_code: The Python code to be played back
:param filename: The name of the file corresponding to the Python code to be played back
"""
from ctypes import windll
import traceback
import os
import sys
import codecs
if str_code == '' and os.path.isfile(filename):
with codecs.open(filename, "r", encoding='utf-8') as python_file:
str_code = python_file.read()
try:
script_dir = os.path.abspath(os.path.dirname(filename))
os.chdir(os.path.abspath(script_dir))
sys.path.append(script_dir)
compiled_code = compile(str_code, filename, 'exec')
exec(compiled_code)
except Exception:
windll.user32.ShowWindow(windll.kernel32.GetConsoleWindow(), 3)
exc_type, exc_value, exc_traceback = sys.exc_info()
output = traceback.format_exception(exc_type, exc_value, exc_traceback)
i_line = d_line = 0
full_traceback = False
if not full_traceback:
for line in output:
i_line += 1
if "pywinauto_recorder.py" in line:
d_line = i_line
for line in output[d_line:]:
print(line, file=sys.stderr, end='')
input("Press Enter to continue...")
[docs]class UIApplication(object):
def __init__(self, app, native_window_handle=None):
self.app = app
self.native_window_handle = native_window_handle
[docs]def start_application(cmd_line, timeout=10, wait_for_idle=True):
"""
This function starts an application
:param cmd_line: The command line to start the application
:param timeout: timeout of the connection process
:return: UIApplication object
"""
desktop = pywinauto.Desktop(backend='uia', allow_magic_lookup=False)
window_candidates_1 = desktop.windows()
app = pywinauto.Application(backend="win32")
app.start(cmd_line=cmd_line, timeout=timeout, wait_for_idle=wait_for_idle)
time.sleep(2)
window_candidates_2 = desktop.windows()
diff = set(window_candidates_2) - set(window_candidates_1)
while not diff:
time.sleep(1)
window_candidates_2 = desktop.windows()
diff = set(window_candidates_2) - set(window_candidates_1)
native_window_handle = list(diff)[0].handle # We assume that there is only one window
set_native_window_handle(native_window_handle)
return UIApplication(app, native_window_handle)
[docs]def connect_application(**kwargs):
"""Connect to an already running application
The action is performed according to only one of parameters
:param process: a process ID of the target
:param handle: a native window handle of the target
:param path: a path used to launch the target
:param timeout: a timeout for process start (relevant if path is specified)
.. seealso::
:func:`pywinauto.findwindows.find_elements` - the keyword arguments that
are also can be used instead of **process**, **handle** or **path**
"""
if 'exclude_main_windows' in kwargs or 'main_window_uipath' in kwargs:
excluded_main_windows = kwargs['exclude_main_windows'] if 'exclude_main_windows' in kwargs else []
main_window_uipath = kwargs['main_window_uipath'] if 'main_window_uipath' in kwargs else '*'
timeout = kwargs['timeout'] if 'timeout' in kwargs else 20
main_windows = []
t0 = time.time()
t1 = t0
while len(main_windows) != 1 and t1-t0 < timeout:
main_windows = find_main_windows(main_window_uipath)
if main_windows:
excluded_handles = [w.handle for w in excluded_main_windows]
main_windows = [w for w in main_windows if w.handle not in excluded_handles]
time.sleep(1)
t1 = time.time()
if len(main_windows) == 1:
kwargs['handle'] = main_windows[0].handle
else:
raise FailedSearch("Window not found using args '", str(kwargs) + "'")
app = pywinauto.Application(backend="uia")
app.connect(**kwargs)
top_window = app.top_window().wrapper_object()
native_window_handle = top_window.handle
return UIApplication(app, native_window_handle)
[docs]def focus_on_application(application_or_window=None):
"""
Focuses on a specified application by bringing its main window to the foreground.
If 'application_or_window' is None, it clears the focus, allowing subsequent automation commands
to target any window without restrictions.
:param application_or_window: UIApplication object, UIAWrapper object of a main window or None
The object to focus on the main window.
If None, the focus is cleared.
"""
if application_or_window is None:
set_native_window_handle(None)
else:
set_native_window_handle(application_or_window.native_window_handle)
time.sleep(1)
if win32gui_IsIconic(application_or_window.native_window_handle):
win32gui_ShowWindow(application_or_window.native_window_handle, SW_RESTORE)
win32gui_SetWindowPos(application_or_window.native_window_handle, HWND_TOPMOST, 0, 0, 0, 0, 3)
win32gui_SetWindowPos(application_or_window.native_window_handle, HWND_NOTOPMOST, 0, 0, 0, 0, 3)
[docs]def find_main_windows(main_window_uipath='*'):
return find_window_candidates(main_window_uipath, handle=None)
[docs]def kill_application(application, timeout=10):
"""
This function kills an application.
:param application: UIApplication object
:param timeout: timeout of the connection process
"""
application.app.connect(handle=application.native_window_handle, timeout=timeout)
application.app.kill()