You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
372 lines
14 KiB
372 lines
14 KiB
6 years ago
|
# GUI Application automation and testing library
|
||
|
# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors
|
||
|
# https://github.com/pywinauto/pywinauto/graphs/contributors
|
||
|
# http://pywinauto.readthedocs.io/en/latest/credits.html
|
||
|
# All rights reserved.
|
||
|
#
|
||
|
# Redistribution and use in source and binary forms, with or without
|
||
|
# modification, are permitted provided that the following conditions are met:
|
||
|
#
|
||
|
# * Redistributions of source code must retain the above copyright notice, this
|
||
|
# list of conditions and the following disclaimer.
|
||
|
#
|
||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||
|
# this list of conditions and the following disclaimer in the documentation
|
||
|
# and/or other materials provided with the distribution.
|
||
|
#
|
||
|
# * Neither the name of pywinauto nor the names of its
|
||
|
# contributors may be used to endorse or promote products derived from
|
||
|
# this software without specific prior written permission.
|
||
|
#
|
||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
|
||
|
"""Provides functions for iterating and finding windows/elements"""
|
||
|
from __future__ import unicode_literals
|
||
|
|
||
|
import re
|
||
|
import ctypes
|
||
|
import six
|
||
|
|
||
|
from . import win32functions
|
||
|
from . import win32structures
|
||
|
from . import findbestmatch
|
||
|
from . import controls
|
||
|
from .backend import registry
|
||
|
|
||
|
|
||
|
# TODO: we should filter out invalid elements before returning
|
||
|
|
||
|
#=========================================================================
|
||
|
class WindowNotFoundError(Exception):
|
||
|
|
||
|
"""No window could be found"""
|
||
|
pass
|
||
|
|
||
|
|
||
|
#=========================================================================
|
||
|
class WindowAmbiguousError(Exception):
|
||
|
|
||
|
"""There was more then one window that matched"""
|
||
|
pass
|
||
|
|
||
|
|
||
|
#=========================================================================
|
||
|
class ElementNotFoundError(Exception):
|
||
|
|
||
|
"""No element could be found"""
|
||
|
pass
|
||
|
|
||
|
|
||
|
#=========================================================================
|
||
|
class ElementAmbiguousError(Exception):
|
||
|
|
||
|
"""There was more then one element that matched"""
|
||
|
pass
|
||
|
|
||
|
|
||
|
#=========================================================================
|
||
|
def find_element(**kwargs):
|
||
|
"""
|
||
|
Call find_elements and ensure that only one element is returned
|
||
|
|
||
|
Calls find_elements with exactly the same arguments as it is called with
|
||
|
so please see :py:func:`find_elements` for the full parameters description.
|
||
|
"""
|
||
|
elements = find_elements(**kwargs)
|
||
|
|
||
|
if not elements:
|
||
|
raise ElementNotFoundError(kwargs)
|
||
|
|
||
|
if len(elements) > 1:
|
||
|
exception = ElementAmbiguousError(
|
||
|
"There are {0} elements that match the criteria {1}".format(
|
||
|
len(elements),
|
||
|
six.text_type(kwargs),
|
||
|
)
|
||
|
)
|
||
|
|
||
|
exception.elements = elements
|
||
|
raise exception
|
||
|
|
||
|
return elements[0]
|
||
|
|
||
|
|
||
|
#=========================================================================
|
||
|
def find_window(**kwargs):
|
||
|
"""
|
||
|
Call find_elements and ensure that only handle of one element is returned
|
||
|
|
||
|
Calls find_elements with exactly the same arguments as it is called with
|
||
|
so please see :py:func:`find_elements` for the full parameters description.
|
||
|
"""
|
||
|
try:
|
||
|
kwargs['backend'] = 'win32'
|
||
|
element = find_element(**kwargs)
|
||
|
return element.handle
|
||
|
except ElementNotFoundError:
|
||
|
raise WindowNotFoundError
|
||
|
except ElementAmbiguousError:
|
||
|
raise WindowAmbiguousError
|
||
|
|
||
|
|
||
|
#=========================================================================
|
||
|
def find_elements(class_name=None,
|
||
|
class_name_re=None,
|
||
|
parent=None,
|
||
|
process=None,
|
||
|
title=None,
|
||
|
title_re=None,
|
||
|
top_level_only=True,
|
||
|
visible_only=True,
|
||
|
enabled_only=False,
|
||
|
best_match=None,
|
||
|
handle=None,
|
||
|
ctrl_index=None,
|
||
|
found_index=None,
|
||
|
predicate_func=None,
|
||
|
active_only=False,
|
||
|
control_id=None,
|
||
|
control_type=None,
|
||
|
auto_id=None,
|
||
|
framework_id=None,
|
||
|
backend=None,
|
||
|
depth=None
|
||
|
):
|
||
|
"""
|
||
|
Find elements based on criteria passed in
|
||
|
|
||
|
WARNING! Direct usage of this function is not recommended! It's a very low level API.
|
||
|
Better use Application and WindowSpecification objects described in the
|
||
|
Getting Started Guide.
|
||
|
|
||
|
Possible values are:
|
||
|
|
||
|
* **class_name** Elements with this window class
|
||
|
* **class_name_re** Elements whose class matches this regular expression
|
||
|
* **parent** Elements that are children of this
|
||
|
* **process** Elements running in this process
|
||
|
* **title** Elements with this text
|
||
|
* **title_re** Elements whose text matches this regular expression
|
||
|
* **top_level_only** Top level elements only (default=**True**)
|
||
|
* **visible_only** Visible elements only (default=**True**)
|
||
|
* **enabled_only** Enabled elements only (default=False)
|
||
|
* **best_match** Elements with a title similar to this
|
||
|
* **handle** The handle of the element to return
|
||
|
* **ctrl_index** The index of the child element to return
|
||
|
* **found_index** The index of the filtered out child element to return
|
||
|
* **predicate_func** A user provided hook for a custom element validation
|
||
|
* **active_only** Active elements only (default=False)
|
||
|
* **control_id** Elements with this control id
|
||
|
* **control_type** Elements with this control type (string; for UIAutomation elements)
|
||
|
* **auto_id** Elements with this automation id (for UIAutomation elements)
|
||
|
* **framework_id** Elements with this framework id (for UIAutomation elements)
|
||
|
* **backend** Back-end name to use while searching (default=None means current active backend)
|
||
|
"""
|
||
|
if backend is None:
|
||
|
backend = registry.active_backend.name
|
||
|
backend_obj = registry.backends[backend]
|
||
|
|
||
|
# allow a handle to be passed in
|
||
|
# if it is present - just return it
|
||
|
if handle is not None:
|
||
|
return [backend_obj.element_info_class(handle), ]
|
||
|
|
||
|
if isinstance(parent, backend_obj.generic_wrapper_class):
|
||
|
parent = parent.element_info
|
||
|
elif isinstance(parent, six.integer_types):
|
||
|
# check if parent is a handle of element (in case of searching native controls)
|
||
|
parent = backend_obj.element_info_class(parent)
|
||
|
|
||
|
if top_level_only:
|
||
|
# find the top level elements
|
||
|
element = backend_obj.element_info_class()
|
||
|
elements = element.children(process=process,
|
||
|
class_name=class_name,
|
||
|
title=title,
|
||
|
control_type=control_type,
|
||
|
cache_enable=True)
|
||
|
|
||
|
# if we have been given a parent
|
||
|
if parent:
|
||
|
elements = [elem for elem in elements if elem.parent == parent]
|
||
|
|
||
|
# looking for child elements
|
||
|
else:
|
||
|
# if not given a parent look for all children of the desktop
|
||
|
if not parent:
|
||
|
parent = backend_obj.element_info_class()
|
||
|
|
||
|
# look for ALL children of that parent
|
||
|
elements = parent.descendants(class_name=class_name,
|
||
|
title=title,
|
||
|
control_type=control_type,
|
||
|
cache_enable=True,
|
||
|
depth=depth)
|
||
|
|
||
|
# if the ctrl_index has been specified then just return
|
||
|
# that control
|
||
|
if ctrl_index is not None:
|
||
|
return [elements[ctrl_index], ]
|
||
|
|
||
|
# early stop
|
||
|
if not elements:
|
||
|
if found_index is not None:
|
||
|
if found_index > 0:
|
||
|
raise ElementNotFoundError("found_index is specified as {0}, but no windows found".format(
|
||
|
found_index))
|
||
|
return elements
|
||
|
|
||
|
if framework_id is not None and elements:
|
||
|
elements = [elem for elem in elements if elem.framework_id == framework_id]
|
||
|
|
||
|
if control_id is not None and elements:
|
||
|
elements = [elem for elem in elements if elem.control_id == control_id]
|
||
|
|
||
|
if active_only:
|
||
|
# TODO: re-write to use ElementInfo interface
|
||
|
gui_info = win32structures.GUITHREADINFO()
|
||
|
gui_info.cbSize = ctypes.sizeof(gui_info)
|
||
|
|
||
|
# get all the active elements (not just the specified process)
|
||
|
ret = win32functions.GetGUIThreadInfo(0, ctypes.byref(gui_info))
|
||
|
|
||
|
if not ret:
|
||
|
raise ctypes.WinError()
|
||
|
|
||
|
found_active = False
|
||
|
for elem in elements:
|
||
|
if elem.handle == gui_info.hwndActive:
|
||
|
found_active = True
|
||
|
elements = [elem, ]
|
||
|
break
|
||
|
if not found_active:
|
||
|
elements = []
|
||
|
|
||
|
if class_name is not None:
|
||
|
elements = [elem for elem in elements if elem.class_name == class_name]
|
||
|
|
||
|
if class_name_re is not None:
|
||
|
class_name_regex = re.compile(class_name_re)
|
||
|
elements = [elem for elem in elements if class_name_regex.match(elem.class_name)]
|
||
|
|
||
|
if process is not None:
|
||
|
elements = [elem for elem in elements if elem.process_id == process]
|
||
|
|
||
|
if auto_id is not None and elements:
|
||
|
elements = [elem for elem in elements if elem.automation_id == auto_id]
|
||
|
|
||
|
if title is not None:
|
||
|
# TODO: some magic is happenning here
|
||
|
if elements:
|
||
|
elements[0].rich_text
|
||
|
elements = [elem for elem in elements if elem.rich_text == title]
|
||
|
elif title_re is not None:
|
||
|
title_regex = re.compile(title_re)
|
||
|
|
||
|
def _title_match(w):
|
||
|
"""Match a window title to the regexp"""
|
||
|
t = w.rich_text
|
||
|
if t is not None:
|
||
|
return title_regex.match(t)
|
||
|
return False
|
||
|
elements = [elem for elem in elements if _title_match(elem)]
|
||
|
|
||
|
if visible_only:
|
||
|
elements = [elem for elem in elements if elem.visible]
|
||
|
|
||
|
if enabled_only:
|
||
|
elements = [elem for elem in elements if elem.enabled]
|
||
|
|
||
|
if best_match is not None:
|
||
|
# Build a list of wrapped controls.
|
||
|
# Speed up the loop by setting up local pointers
|
||
|
wrapped_elems = []
|
||
|
add_to_wrp_elems = wrapped_elems.append
|
||
|
wrp_cls = backend_obj.generic_wrapper_class
|
||
|
for elem in elements:
|
||
|
try:
|
||
|
add_to_wrp_elems(wrp_cls(elem))
|
||
|
except (controls.InvalidWindowHandle,
|
||
|
controls.InvalidElement):
|
||
|
# skip invalid handles - they have dissapeared
|
||
|
# since the list of elements was retrieved
|
||
|
continue
|
||
|
elements = findbestmatch.find_best_control_matches(best_match, wrapped_elems)
|
||
|
|
||
|
# convert found elements back to ElementInfo
|
||
|
backup_elements = elements[:]
|
||
|
elements = []
|
||
|
for elem in backup_elements:
|
||
|
if hasattr(elem, "element_info"):
|
||
|
elem.element_info.set_cache_strategy(cached=False)
|
||
|
elements.append(elem.element_info)
|
||
|
else:
|
||
|
elements.append(backend_obj.element_info_class(elem.handle))
|
||
|
else:
|
||
|
for elem in elements:
|
||
|
elem.set_cache_strategy(cached=False)
|
||
|
|
||
|
if predicate_func is not None:
|
||
|
elements = [elem for elem in elements if predicate_func(elem)]
|
||
|
|
||
|
# found_index is the last criterion to filter results
|
||
|
if found_index is not None:
|
||
|
if found_index < len(elements):
|
||
|
elements = elements[found_index:found_index + 1]
|
||
|
else:
|
||
|
raise ElementNotFoundError("found_index is specified as {0}, but {1} window/s found".format(
|
||
|
found_index, len(elements)))
|
||
|
|
||
|
return elements
|
||
|
|
||
|
|
||
|
#=========================================================================
|
||
|
def find_windows(**kwargs):
|
||
|
"""
|
||
|
Find elements based on criteria passed in and return list of their handles
|
||
|
|
||
|
Calls find_elements with exactly the same arguments as it is called with
|
||
|
so please see :py:func:`find_elements` for the full parameters description.
|
||
|
"""
|
||
|
try:
|
||
|
kwargs['backend'] = 'win32'
|
||
|
elements = find_elements(**kwargs)
|
||
|
return [elem.handle for elem in elements]
|
||
|
except ElementNotFoundError:
|
||
|
raise WindowNotFoundError
|
||
|
|
||
|
|
||
|
#=========================================================================
|
||
|
def enum_windows():
|
||
|
"""Return a list of handles of all the top level windows"""
|
||
|
windows = []
|
||
|
|
||
|
# The callback function that will be called for each HWND
|
||
|
# all we do is append the wrapped handle
|
||
|
def enum_window_proc(hwnd, lparam):
|
||
|
"""Called for each window - adds handles to a list"""
|
||
|
windows.append(hwnd)
|
||
|
return True
|
||
|
|
||
|
# define the type of the child procedure
|
||
|
enum_win_proc_t = ctypes.WINFUNCTYPE(
|
||
|
ctypes.c_int, ctypes.c_long, ctypes.c_long)
|
||
|
|
||
|
# 'construct' the callback with our function
|
||
|
proc = enum_win_proc_t(enum_window_proc)
|
||
|
|
||
|
# loop over all the children (callback called for each)
|
||
|
win32functions.EnumWindows(proc, 0)
|
||
|
|
||
|
# return the collected wrapped windows
|
||
|
return windows
|