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.
ORPA-pyOpenRPA/3rdParty/pywinauto/controls/uiawrapper.py

787 lines
30 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.
"""Basic wrapping of UI Automation elements"""
from __future__ import unicode_literals
from __future__ import print_function
import six
import time
import warnings
import comtypes
from .. import backend
from ..timings import Timings
from ..base_wrapper import BaseWrapper
from ..base_wrapper import BaseMeta
from ..uia_defines import IUIA
from .. import uia_defines as uia_defs
from ..uia_element_info import UIAElementInfo, elements_from_uia_array
# region PATTERNS
AutomationElement = IUIA().ui_automation_client.IUIAutomationElement
DockPattern = IUIA().ui_automation_client.IUIAutomationDockPattern
ExpandCollapsePattern = IUIA().ui_automation_client.IUIAutomationExpandCollapsePattern
GridItemPattern = IUIA().ui_automation_client.IUIAutomationGridItemPattern
GridPattern = IUIA().ui_automation_client.IUIAutomationGridPattern
InvokePattern = IUIA().ui_automation_client.IUIAutomationInvokePattern
ItemContainerPattern = IUIA().ui_automation_client.IUIAutomationItemContainerPattern
LegacyIAccessiblePattern = IUIA().ui_automation_client.IUIAutomationLegacyIAccessiblePattern
MultipleViewPattern = IUIA().ui_automation_client.IUIAutomationMultipleViewPattern
RangeValuePattern = IUIA().ui_automation_client.IUIAutomationRangeValuePattern
ScrollItemPattern = IUIA().ui_automation_client.IUIAutomationScrollItemPattern
ScrollPattern = IUIA().ui_automation_client.IUIAutomationScrollPattern
SelectionItemPattern = IUIA().ui_automation_client.IUIAutomationSelectionItemPattern
SelectionPattern = IUIA().ui_automation_client.IUIAutomationSelectionPattern
SynchronizedInputPattern = IUIA().ui_automation_client.IUIAutomationSynchronizedInputPattern
TableItemPattern = IUIA().ui_automation_client.IUIAutomationTableItemPattern
TablePattern = IUIA().ui_automation_client.IUIAutomationTablePattern
TextPattern = IUIA().ui_automation_client.IUIAutomationTextPattern
TogglePattern = IUIA().ui_automation_client.IUIAutomationTogglePattern
TransformPattern = IUIA().ui_automation_client.IUIAutomationTransformPattern
ValuePattern = IUIA().ui_automation_client.IUIAutomationValuePattern
VirtualizedItemPattern = IUIA().ui_automation_client.IUIAutomationVirtualizedItemPattern
WindowPattern = IUIA().ui_automation_client.IUIAutomationWindowPattern
# endregion
# =========================================================================
_friendly_classes = {
'Custom': None,
'DataGrid': 'ListView',
'DataItem': 'DataItem',
'Document': None, # TODO: this is RichTextBox
'Group': 'GroupBox',
'Header': None,
'HeaderItem': None,
'Hyperlink': None,
'Image': None,
'List': 'ListBox',
'ListItem': 'ListItem',
'MenuBar': 'Menu',
'Menu': 'Menu',
'MenuItem': 'MenuItem',
'Pane': None,
'ProgressBar': 'Progress',
'ScrollBar': None,
'Separator': None,
'Slider': None,
'Spinner': 'UpDown',
'SplitButton': None,
'Tab': 'TabControl',
'Table': None,
'Text': 'Static',
'Thumb': None,
'TitleBar': None,
'ToolBar': 'Toolbar',
'ToolTip': 'ToolTips',
'Tree': 'TreeView',
'TreeItem': 'TreeItem',
'Window': 'Dialog',
}
# =========================================================================
class LazyProperty(object):
"""
A lazy evaluation of an object attribute.
The property should represent immutable data, as it replaces itself.
Provided by: http://stackoverflow.com/a/6849299/1260742
"""
def __init__(self, fget):
"""Init the property name and method to calculate the property"""
self.fget = fget
self.func_name = fget.__name__
def __get__(self, obj, cls):
"""Replace the property itself on a first access"""
if obj is None:
return None
value = self.fget(obj)
setattr(obj, self.func_name, value)
return value
lazy_property = LazyProperty
# =========================================================================
class UiaMeta(BaseMeta):
"""Metaclass for UiaWrapper objects"""
control_type_to_cls = {}
def __init__(cls, name, bases, attrs):
"""Register the control types"""
BaseMeta.__init__(cls, name, bases, attrs)
for t in cls._control_types:
UiaMeta.control_type_to_cls[t] = cls
@staticmethod
def find_wrapper(element):
"""Find the correct wrapper for this UIA element"""
# Set a general wrapper by default
wrapper_match = UIAWrapper
# Check for a more specific wrapper in the registry
if element.control_type in UiaMeta.control_type_to_cls:
wrapper_match = UiaMeta.control_type_to_cls[element.control_type]
return wrapper_match
# =========================================================================
@six.add_metaclass(UiaMeta)
class UIAWrapper(BaseWrapper):
"""
Default wrapper for User Interface Automation (UIA) controls.
All other UIA wrappers are derived from this.
This class wraps a lot of functionality of underlying UIA features
for working with windows.
Most of the methods apply to every single element type. For example
you can click() on any element.
"""
_control_types = []
# ------------------------------------------------------------
def __new__(cls, element_info):
"""Construct the control wrapper"""
return super(UIAWrapper, cls)._create_wrapper(cls, element_info, UIAWrapper)
# -----------------------------------------------------------
def __init__(self, element_info):
"""
Initialize the control
* **element_info** is either a valid UIAElementInfo or it can be an
instance or subclass of UIAWrapper.
If the handle is not valid then an InvalidWindowHandle error
is raised.
"""
BaseWrapper.__init__(self, element_info, backend.registry.backends['uia'])
# ------------------------------------------------------------
def __hash__(self):
"""Return a unique hash value based on the element's Runtime ID"""
return hash(self.element_info.runtime_id)
# ------------------------------------------------------------
@lazy_property
def iface_expand_collapse(self):
"""Get the element's ExpandCollapse interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "ExpandCollapse")
# ------------------------------------------------------------
@lazy_property
def iface_selection(self):
"""Get the element's Selection interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Selection")
# ------------------------------------------------------------
@lazy_property
def iface_selection_item(self):
"""Get the element's SelectionItem interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "SelectionItem")
# ------------------------------------------------------------
@lazy_property
def iface_invoke(self):
"""Get the element's Invoke interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Invoke")
# ------------------------------------------------------------
@lazy_property
def iface_toggle(self):
"""Get the element's Toggle interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Toggle")
# ------------------------------------------------------------
@lazy_property
def iface_text(self):
"""Get the element's Text interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Text")
# ------------------------------------------------------------
@lazy_property
def iface_value(self):
"""Get the element's Value interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Value")
# ------------------------------------------------------------
@lazy_property
def iface_range_value(self):
"""Get the element's RangeValue interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "RangeValue")
# ------------------------------------------------------------
@lazy_property
def iface_grid(self):
"""Get the element's Grid interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Grid")
# ------------------------------------------------------------
@lazy_property
def iface_grid_item(self):
"""Get the element's GridItem interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "GridItem")
# ------------------------------------------------------------
@lazy_property
def iface_table(self):
"""Get the element's Table interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Table")
# ------------------------------------------------------------
@lazy_property
def iface_table_item(self):
"""Get the element's TableItem interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "TableItem")
# ------------------------------------------------------------
@lazy_property
def iface_scroll_item(self):
"""Get the element's ScrollItem interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "ScrollItem")
# ------------------------------------------------------------
@lazy_property
def iface_scroll(self):
"""Get the element's Scroll interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Scroll")
# ------------------------------------------------------------
@lazy_property
def iface_transform(self):
"""Get the element's Transform interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Transform")
# ------------------------------------------------------------
@lazy_property
def iface_transformV2(self):
"""Get the element's TransformV2 interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "TransformV2")
# ------------------------------------------------------------
@lazy_property
def iface_window(self):
"""Get the element's Window interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "Window")
# ------------------------------------------------------------
@lazy_property
def iface_item_container(self):
"""Get the element's ItemContainer interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "ItemContainer")
# ------------------------------------------------------------
@lazy_property
def iface_virtualized_item(self):
"""Get the element's VirtualizedItem interface pattern"""
elem = self.element_info.element
return uia_defs.get_elem_interface(elem, "VirtualizedItem")
# ------------------------------------------------------------
@property
def writable_props(self):
"""Extend default properties list."""
props = super(UIAWrapper, self).writable_props
props.extend(['is_keyboard_focusable',
'has_keyboard_focus',
'automation_id',
])
return props
# ------------------------------------------------------------
def legacy_properties(self):
"""Get the element's LegacyIAccessible control pattern interface properties"""
elem = self.element_info.element
impl = uia_defs.get_elem_interface(elem, "LegacyIAccessible")
property_name_identifier = 'Current'
interface_properties = [prop for prop in dir(LegacyIAccessiblePattern)
if (isinstance(getattr(LegacyIAccessiblePattern, prop), property)
and property_name_identifier in prop)]
return {prop.replace(property_name_identifier, '') : getattr(impl, prop) for prop in interface_properties}
# ------------------------------------------------------------
def friendly_class_name(self):
"""
Return the friendly class name for the control
This differs from the class of the control in some cases.
class_name() is the actual 'Registered' window class of the control
while friendly_class_name() is hopefully something that will make
more sense to the user.
For example Checkboxes are implemented as Buttons - so the class
of a CheckBox is "Button" - but the friendly class is "CheckBox"
"""
if self.friendlyclassname is None:
if self.element_info.control_type not in IUIA().known_control_types.keys():
self.friendlyclassname = self.element_info.control_type
else:
ctrl_type = self.element_info.control_type
if (ctrl_type not in _friendly_classes) or (_friendly_classes[ctrl_type] is None):
self.friendlyclassname = ctrl_type
else:
self.friendlyclassname = _friendly_classes[ctrl_type]
return self.friendlyclassname
#------------------------------------------------------------
def automation_id(self):
"""Return the Automation ID of the control"""
return self.element_info.automation_id
# -----------------------------------------------------------
def is_keyboard_focusable(self):
"""Return True if the element can be focused with keyboard"""
return self.element_info.element.CurrentIsKeyboardFocusable == 1
# -----------------------------------------------------------
def has_keyboard_focus(self):
"""Return True if the element is focused with keyboard"""
return self.element_info.element.CurrentHasKeyboardFocus == 1
# -----------------------------------------------------------
def set_focus(self):
"""Set the focus to this element"""
try:
if self.is_minimized():
if self.was_maximized():
self.maximize()
else:
self.restore()
except uia_defs.NoPatternInterfaceError:
pass
try:
self.element_info.element.SetFocus()
except comtypes.COMError as exc:
warnings.warn('The window has not been focused due to ' \
'COMError: {}'.format(exc), RuntimeWarning)
return self
# TODO: figure out how to implement .has_focus() method (if no handle available)
# -----------------------------------------------------------
def close(self):
"""
Close the window
Only a control supporting Window pattern should answer.
If it doesn't (menu shadows, tooltips,...), try to send "Esc" key
"""
try:
name = self.element_info.name
control_type = self.element_info.control_type
iface = self.iface_window
iface.Close()
if name and control_type:
self.actions.log("Closed " + control_type.lower() + ' "' + name + '"')
except(uia_defs.NoPatternInterfaceError):
self.type_keys("{ESC}")
# -----------------------------------------------------------
def minimize(self):
"""
Minimize the window
Only controls supporting Window pattern should answer
"""
iface = self.iface_window
if iface.CurrentCanMinimize:
iface.SetWindowVisualState(uia_defs.window_visual_state_minimized)
return self
# -----------------------------------------------------------
def maximize(self):
"""
Maximize the window
Only controls supporting Window pattern should answer
"""
iface = self.iface_window
if iface.CurrentCanMaximize:
iface.SetWindowVisualState(uia_defs.window_visual_state_maximized)
return self
# -----------------------------------------------------------
def restore(self):
"""
Restore the window to normal size
Only controls supporting Window pattern should answer
"""
iface = self.iface_window
iface.SetWindowVisualState(uia_defs.window_visual_state_normal)
return self
# -----------------------------------------------------------
def get_show_state(self):
"""Get the show state and Maximized/minimzed/restored state
Returns values as following
window_visual_state_normal = 0
window_visual_state_maximized = 1
window_visual_state_minimized = 2
"""
iface = self.iface_window
ret = iface.CurrentWindowVisualState
return ret
# -----------------------------------------------------------
def is_minimized(self):
"""Indicate whether the window is minimized or not"""
return self.get_show_state() == uia_defs.window_visual_state_minimized
# -----------------------------------------------------------
def is_maximized(self):
"""Indicate whether the window is maximized or not"""
return self.get_show_state() == uia_defs.window_visual_state_maximized
# -----------------------------------------------------------
def is_normal(self):
"""Indicate whether the window is normal (i.e. not minimized and not maximized)"""
return self.get_show_state() == uia_defs.window_visual_state_normal
# -----------------------------------------------------------
def invoke(self):
"""An interface to the Invoke method of the Invoke control pattern"""
name = self.element_info.name
control_type = self.element_info.control_type
self.iface_invoke.Invoke()
if name and control_type:
self.actions.log("Invoked " + control_type.lower() + ' "' + name + '"')
# Return itself to allow action chaining
return self
# -----------------------------------------------------------
def expand(self):
"""
Displays all child nodes, controls, or content of the control
An interface to Expand method of the ExpandCollapse control pattern.
"""
self.iface_expand_collapse.Expand()
# Return itself to allow action chaining
return self
# -----------------------------------------------------------
def collapse(self):
"""
Displays all child nodes, controls, or content of the control
An interface to Collapse method of the ExpandCollapse control pattern.
"""
self.iface_expand_collapse.Collapse()
# Return itself to allow action chaining
return self
# -----------------------------------------------------------
def get_expand_state(self):
"""
Indicates the state of the control: expanded or collapsed.
An interface to CurrentExpandCollapseState property of the ExpandCollapse control pattern.
Values for enumeration as defined in uia_defines module:
expand_state_collapsed = 0
expand_state_expanded = 1
expand_state_partially = 2
expand_state_leaf_node = 3
"""
return self.iface_expand_collapse.CurrentExpandCollapseState
# -----------------------------------------------------------
def is_expanded(self):
"""Test if the control is expanded"""
state = self.get_expand_state()
return state == uia_defs.expand_state_expanded
# -----------------------------------------------------------
def is_collapsed(self):
"""Test if the control is collapsed"""
state = self.get_expand_state()
return state == uia_defs.expand_state_collapsed
# -----------------------------------------------------------
def get_selection(self):
"""
An interface to GetSelection of the SelectionProvider pattern
Retrieves a UI Automation provider for each child element
that is selected. Builds a list of UIAElementInfo elements
from all retrieved providers.
"""
ptrs_array = self.iface_selection.GetCurrentSelection()
return elements_from_uia_array(ptrs_array)
# -----------------------------------------------------------
def selected_item_index(self):
"""Return the index of a selected item"""
# Go through all children and look for an index
# of an item with the same text.
# Maybe there is another and more efficient way to do it
selection = self.get_selection()
if selection:
for i, c in enumerate(self.children()):
if c.window_text() == selection[0].name:
return i
return None
# -----------------------------------------------------------
def select(self):
"""Select the item
Only items supporting SelectionItem pattern should answer.
Raise NoPatternInterfaceError if the pattern is not supported
Usually applied for controls like: a radio button, a tree view item
or a list item.
"""
self.iface_selection_item.Select()
name = self.element_info.name
control_type = self.element_info.control_type
if name and control_type:
self.actions.log("Selected " + control_type.lower() + ' "' + name + '"')
# Return itself so that action can be chained
return self
# -----------------------------------------------------------
def is_selected(self):
"""Indicate that the item is selected or not.
Only items supporting SelectionItem pattern should answer.
Raise NoPatternInterfaceError if the pattern is not supported
Usually applied for controls like: a radio button, a tree view item,
a list item.
"""
return self.iface_selection_item.CurrentIsSelected
# -----------------------------------------------------------
def children_texts(self):
"""Get texts of the control's children"""
return [c.window_text() for c in self.children()]
# -----------------------------------------------------------
def can_select_multiple(self):
"""
An interface to CanSelectMultiple of the SelectionProvider pattern
Indicates whether the UI Automation provider allows more than one
child element to be selected concurrently.
"""
return self.iface_selection.CurrentCanSelectMultiple
# -----------------------------------------------------------
def is_selection_required(self):
"""
An interface to IsSelectionRequired property of the SelectionProvider pattern.
This property can be dynamic. For example, the initial state of
a control might not have any items selected by default,
meaning that IsSelectionRequired is FALSE. However,
after an item is selected the control must always have
at least one item selected.
"""
return self.iface_selection.CurrentIsSelectionRequired
# -----------------------------------------------------------
def _select(self, item=None):
"""
Find a child item by the name or index and select
The action can be applied for dirrent controls with items:
ComboBox, TreeView, Tab control
"""
if isinstance(item, six.integer_types):
item_index = item
title = None
elif isinstance(item, six.string_types):
item_index = 0
title = item
else:
err_msg = u"unsupported {0} for item {1}".format(type(item), item)
raise ValueError(err_msg)
list_ = self.children(title=title)
if item_index < len(list_):
wrp = list_[item_index]
wrp.iface_selection_item.Select()
else:
raise IndexError("item not found")
# -----------------------------------------------------------
def is_active(self):
"""Whether the window is active or not"""
ae = IUIA().get_focused_element()
focused_wrap = UIAWrapper(UIAElementInfo(ae))
return (focused_wrap.top_level_parent() == self.top_level_parent())
# -----------------------------------------------------------
def is_dialog(self):
"""Return true if the control is a dialog window (WindowPattern interface is available)"""
try:
return self.iface_window is not None
except uia_defs.NoPatternInterfaceError:
return False
# -----------------------------------------------------------
def menu_select(self, path, exact=False, ):
"""Select a menu item specified in the path
The full path syntax is specified in:
:py:meth:`pywinauto.menuwrapper.Menu.get_menu_path`
There are usually at least two menu bars: "System" and "Application"
System menu bar is a standard window menu with items like:
'Restore', 'Move', 'Size', 'Minimize', e.t.c.
This menu bar usually has a "Title Bar" control as a parent.
Application menu bar is often what we look for. In most cases,
its parent is the dialog itself so it should be found among the direct
children of the dialog. Notice that we don't use "Application"
string as a title criteria because it couldn't work on applications
with a non-english localization.
If there is no menu bar has been found we fall back to look up
for Menu control. We try to find the control through all descendants
of the dialog
"""
self.verify_actionable()
cc = self.children(control_type="MenuBar")
if not cc:
cc = self.descendants(control_type="Menu")
if not cc:
raise AttributeError
menu = cc[0]
menu.item_by_path(path, exact).select()
# -----------------------------------------------------------
_scroll_types = {
"left": {
"line": (uia_defs.scroll_small_decrement, uia_defs.scroll_no_amount),
"page": (uia_defs.scroll_large_decrement, uia_defs.scroll_no_amount),
},
"right": {
"line": (uia_defs.scroll_small_increment, uia_defs.scroll_no_amount),
"page": (uia_defs.scroll_large_increment, uia_defs.scroll_no_amount),
},
"up": {
"line": (uia_defs.scroll_no_amount, uia_defs.scroll_small_decrement),
"page": (uia_defs.scroll_no_amount, uia_defs.scroll_large_decrement),
},
"down": {
"line": (uia_defs.scroll_no_amount, uia_defs.scroll_small_increment),
"page": (uia_defs.scroll_no_amount, uia_defs.scroll_large_increment),
},
}
def scroll(self, direction, amount, count=1, retry_interval=Timings.scroll_step_wait):
"""Ask the control to scroll itself
**direction** can be any of "up", "down", "left", "right"
**amount** can be only "line" or "page"
**count** (optional) the number of times to scroll
**retry_interval** (optional) interval between scroll actions
"""
def _raise_attrib_err(details):
control_type = self.element_info.control_type
name = self.element_info.name
msg = "".join([control_type.lower(), ' "', name, '" ', details])
raise AttributeError(msg)
try:
scroll_if = self.iface_scroll
if direction.lower() in ("up", "down"):
if not scroll_if.CurrentVerticallyScrollable:
_raise_attrib_err('is not vertically scrollable')
elif direction.lower() in ("left", "right"):
if not scroll_if.CurrentHorizontallyScrollable:
_raise_attrib_err('is not horizontally scrollable')
h, v = self._scroll_types[direction.lower()][amount.lower()]
# Scroll as often as we have been asked to
for _ in range(count, 0, -1):
scroll_if.Scroll(h, v)
time.sleep(retry_interval)
except uia_defs.NoPatternInterfaceError:
_raise_attrib_err('is not scrollable')
except KeyError:
raise ValueError("""Wrong arguments:
direction can be any of "up", "down", "left", "right"
amount can be only "line" or "page"
""")
return self
backend.register('uia', UIAElementInfo, UIAWrapper)