From 376c4fe8c0e0a5fcb5dd81eca6988fc1ec15ccba Mon Sep 17 00:00:00 2001 From: eltio Date: Sun, 13 Mar 2022 13:53:51 +0300 Subject: [PATCH 01/59] Add initial WPF backend implemented: children(), class_name, name (temporary works like auto_id) --- pywinauto/controls/__init__.py | 2 + pywinauto/controls/wpfwrapper.py | 67 ++++++++++++ pywinauto/windows/injector/__init__.py | 0 pywinauto/windows/injector/channel.py | 63 +++++++++++ pywinauto/windows/injector/main.py | 113 +++++++++++++++++++ pywinauto/windows/wpf_element_info.py | 143 +++++++++++++++++++++++++ 6 files changed, 388 insertions(+) create mode 100644 pywinauto/controls/wpfwrapper.py create mode 100644 pywinauto/windows/injector/__init__.py create mode 100644 pywinauto/windows/injector/channel.py create mode 100644 pywinauto/windows/injector/main.py create mode 100644 pywinauto/windows/wpf_element_info.py diff --git a/pywinauto/controls/__init__.py b/pywinauto/controls/__init__.py index 9bafa46a0..3aa6fdacf 100644 --- a/pywinauto/controls/__init__.py +++ b/pywinauto/controls/__init__.py @@ -48,5 +48,7 @@ from . import common_controls from . import win32_controls + from . import wpfwrapper + from ..base_wrapper import InvalidElement diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py new file mode 100644 index 000000000..f8867505b --- /dev/null +++ b/pywinauto/controls/wpfwrapper.py @@ -0,0 +1,67 @@ +"""Basic wrapping of WPF elements""" + +from __future__ import unicode_literals +from __future__ import print_function + +import six +import time +import warnings +import comtypes +import threading + +from .. import backend +from .. import WindowNotFoundError # noqa #E402 +from ..timings import Timings +from .win_base_wrapper import WinBaseWrapper +from .hwndwrapper import HwndWrapper +from ..base_wrapper import BaseMeta + +from ..windows.wpf_element_info import WPFElementInfo + +class WpfMeta(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: + WpfMeta.control_type_to_cls[t] = cls + + @staticmethod + def find_wrapper(element): + """Find the correct wrapper for this UIA element""" + + # Check for a more specific wrapper in the registry + try: + wrapper_match = WpfMeta.control_type_to_cls[element.control_type] + except KeyError: + # Set a general wrapper by default + wrapper_match = WPFWrapper + + return wrapper_match + +@six.add_metaclass(WpfMeta) +class WPFWrapper(WinBaseWrapper): + _control_types = [] + + def __new__(cls, element_info): + """Construct the control wrapper""" + return super(WPFWrapper, cls)._create_wrapper(cls, element_info, WPFWrapper) + + # ----------------------------------------------------------- + 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. + """ + WinBaseWrapper.__init__(self, element_info, backend.registry.backends['wpf']) + +backend.register('wpf', WPFElementInfo, WPFWrapper) \ No newline at end of file diff --git a/pywinauto/windows/injector/__init__.py b/pywinauto/windows/injector/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pywinauto/windows/injector/channel.py b/pywinauto/windows/injector/channel.py new file mode 100644 index 000000000..0481df576 --- /dev/null +++ b/pywinauto/windows/injector/channel.py @@ -0,0 +1,63 @@ +import pywintypes +import time +import win32file +import win32pipe +import winerror +import sys + + +class Pipe: + def __init__(self, name): + self.name = name + self.handle = None + + def connect(self, n_attempts=100, delay=1): + for i in range(n_attempts): + try: + self.handle = win32file.CreateFile( + r'\\.\pipe\{}'.format(self.name), + win32file.GENERIC_READ | win32file.GENERIC_WRITE, + 0, + None, + win32file.OPEN_EXISTING, + 0, + None + ) + ret = win32pipe.SetNamedPipeHandleState( + self.handle, + win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT, + None, + None, + ) + # if ret != winerror.S_OK: + # print('SetNamedPipeHandleState exit code is {}'.format(ret)) + + #print(f'Connected to the pipe {self.name}, {self.handle=}') + break + except pywintypes.error as e: + if e.args[0] == winerror.ERROR_FILE_NOT_FOUND: + #print("Attempt {}: connection failed, trying again".format(i + 1)) + time.sleep(delay) + else: + print('Unexpected pipe error: {}'.format(e)) + if self.handle is not None: + return True + return False + + def transact(self, string): + try: + win32file.WriteFile(self.handle, string.encode('utf-8')) + win32file.FlushFileBuffers(self.handle) + resp = win32file.ReadFile(self.handle, 64 * 1024) + #print('message: {}'.format(resp[1].decode(sys.getdefaultencoding()))) + return resp[1].decode(sys.getdefaultencoding()) + except pywintypes.error as e: + if e.args[0] == winerror.ERROR_BROKEN_PIPE: + print("Broken pipe") + else: + print('Unexpected pipe error: {}'.format(e)) + return '' + + def close(self): + win32file.CloseHandle(self.handle) + diff --git a/pywinauto/windows/injector/main.py b/pywinauto/windows/injector/main.py new file mode 100644 index 000000000..0673ee3f2 --- /dev/null +++ b/pywinauto/windows/injector/main.py @@ -0,0 +1,113 @@ +import json +import os +import time +import sys +from pywinauto.application import Application + +from ctypes import windll, c_int, c_ulong, byref + +from .channel import Pipe + +PAGE_READWRITE = 0x04 +PROCESS_ALL_ACCESS = ( 0x000F0000 | 0x00100000 | 0xFFF ) +VIRTUAL_MEM = ( 0x1000 | 0x2000 ) + +kernel32 = windll.kernel32 + + +def inject(pid): + dll_path = (os.path.dirname(__file__) + os.sep + r'x64\bootstrap.dll').encode('utf-8') + #dll_path = rb'D:\repo\pywinauto\dotnetguiauto\Debug\bootstrap.dll' + + h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, int(pid)) + #print(f'{h_process=}') + if not h_process: + #print('') + sys.exit(-1) + + arg_address = kernel32.VirtualAllocEx(h_process, 0, len(dll_path), VIRTUAL_MEM, PAGE_READWRITE) + #print(f'{hex(arg_address)=}') + + written = c_int(0) + kernel32.WriteProcessMemory(h_process, arg_address, dll_path, len(dll_path), byref(written)) + #print(f'{written=}') + + h_kernel32 = kernel32.GetModuleHandleA(b"kernel32.dll") + #print(f'{h_kernel32=}') + h_loadlib = kernel32.GetProcAddress(h_kernel32, b"LoadLibraryA") + #print(f'{h_loadlib=}') + + thread_id = c_ulong(0) + + if not kernel32.CreateRemoteThread( + h_process, + None, + 0, + h_loadlib, + arg_address, + 0, + byref(thread_id) + ): + #print('err inject') + sys.exit(-2) + + #print(f'{thread_id=}') + +def create_pipe(pid): + pipe = Pipe(f'pywinauto_{pid}') + if pipe.connect(n_attempts=1): + return pipe + else: + inject(pid) + #print('Inject successful, connecting to the server...') + pipe.connect() + return pipe + +if __name__ == '__main__': + app = Application().connect(path='WpfApplication1.exe') + inject(app.process) + #print('Inject successful, connecting to the server...') + + pipe = Pipe(f'pywinauto_{app.process}') + pipe.connect() + + #command = f"{json.dumps({'action': 'getChildren'})}\n" + #pipe.send_and_recv(command) + + #command = f"{json.dumps({'action': 'getChildren', 'element_id': 1})}\n" + #pipe.send_and_recv(command) + + # command = f"{json.dumps({'action': 'getChildren', 'element_id': 2})}\n" + # pipe.send_and_recv(command) + # + # command = f"{json.dumps({'action': 'getChildren', 'element_id': 3})}\n" + # pipe.send_and_recv(command) + # + # command = f"{json.dumps({'action': 'getChildren', 'element_id': 4})}\n" + # pipe.send_and_recv(command) + # + # command = f"{json.dumps({'action': 'getChildren', 'element_id': 6})}\n" + # pipe.send_and_recv(command) + # + # command = f"{json.dumps({'action': 'getChildren', 'element_id': 8})}\n" + # pipe.send_and_recv(command) + # + # command = f"{json.dumps({'action': 'getChildren', 'element_id': 9})}\n" + # pipe.send_and_recv(command) + + while True: + action = input('Enter child ID or action: ') + if action.isnumeric(): + currentElement = action + command = f"{json.dumps({'action': 'GetChildren', 'element_id': int(action)})}\n" + pipe.transact(command) + elif action == 'name': + command = f"{json.dumps({'action': 'GetProperty', 'element_id': int(currentElement), 'name': 'Name'})}\n" + pipe.transact(command) + elif action == 'q': + break + else: + print('err') + + pipe.close() + #app.kill() diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py new file mode 100644 index 000000000..d21e61711 --- /dev/null +++ b/pywinauto/windows/wpf_element_info.py @@ -0,0 +1,143 @@ +"""Implementation of the class to deal with an UI element of WPF via injected DLL""" +import json + +from comtypes import COMError +from six import integer_types, text_type, string_types +from ctypes.wintypes import tagPOINT +import warnings + +from pywinauto.handleprops import dumpwindow, controlid +from pywinauto.element_info import ElementInfo +from .win32structures import RECT +from .injector import main, channel + +pipes= {} + +class WPFElementInfo(ElementInfo): + re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "access_key", "accelerator", + "value"] + exact_only_props = ["handle", "pid", "control_id", "enabled", "visible", "rectangle", "framework_id", "runtime_id"] + search_order = ["handle", "control_type", "class_name", "pid", "control_id", "visible", "enabled", "name", + "access_key", "accelerator", "auto_id", "full_control_type", "rectangle", "framework_id", + "runtime_id", "value"] + assert set(re_props + exact_only_props) == set(search_order) + + def __init__(self, elem_id=None, cache_enable=False, pid=None): + """ + Create an instance of WPFElementInfo from an ID of the element (int or long). + + If elem_id is None create an instance for UI root element. + """ + self._pid=pid + if elem_id is not None: + if isinstance(elem_id, integer_types): + # Create instance of WPFElementInfo from a handle + self._element = elem_id + else: + raise TypeError("WPFElementInfo object can be initialized " + \ + "with integer instance only!") + else: + self._element = 0 + + self.set_cache_strategy(cached=cache_enable) + + @property + def pipe(self): + if self._pid is not None and self._pid not in pipes: + pipes[self._pid] = main.create_pipe(self._pid) + return pipes[self._pid] + + def set_cache_strategy(self, cached): + pass + + @property + def handle(self): + return 0 + + @property + def name(self): + command = f"{json.dumps({'action': 'GetProperty', 'element_id': self._element, 'name': 'Name'})}\n" + reply = self.pipe.transact(command) + reply = json.loads(reply) + return reply['value'] + + @property + def rich_text(self): + return self.name + #return '' + + @property + def control_id(self): + return self._element + + @property + def process_id(self): + return self._pid + + pid = process_id + + @property + def framework_id(self): + return "WPF" + + @property + def class_name(self): + command = f"{json.dumps({'action': 'GetTypeName', 'element_id': self._element})}\n" + reply = self.pipe.transact(command) + reply = json.loads(reply) + return reply['value'] + + @property + def enabled(self): + return True + + @property + def visible(self): + return True + + @property + def parent(self): + return None + + def children(self, **kwargs): + return list(self.iter_children(**kwargs)) + + @property + def control_type(self): + """Return control type of element""" + return None + + def iter_children(self, **kwargs): + if 'process' in kwargs: + self._pid = kwargs['process'] + command = f"{json.dumps({'action': 'GetChildren', 'element_id': self._element})}\n" + reply = self.pipe.transact(command) + reply = json.loads(reply) + for elem in reply['elements']: + yield WPFElementInfo(elem, pid=self._pid) + + + def descendants(self, **kwargs): + return list(self.iter_descendants(**kwargs)) + + def iter_descendants(self, **kwargs): + cache_enable = kwargs.pop('cache_enable', False) + depth = kwargs.pop("depth", None) + if not isinstance(depth, (integer_types, type(None))) or isinstance(depth, integer_types) and depth < 0: + raise Exception("Depth must be an integer") + + if depth == 0: + return + for child in self.iter_children(**kwargs): + yield child + if depth is not None: + kwargs["depth"] = depth - 1 + for c in child.iter_descendants(**kwargs): + yield c + + @property + def rectangle(self): + return RECT() + + def dump_window(self): + return {} \ No newline at end of file From 43440a677a182b3d874bbe72e981a39bde217912 Mon Sep 17 00:00:00 2001 From: eltio Date: Sun, 13 Mar 2022 21:15:01 +0300 Subject: [PATCH 02/59] attempt to fix tests failure --- pywinauto/controls/wpfwrapper.py | 1 - pywinauto/windows/wpf_element_info.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index f8867505b..184eb6bcd 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -6,7 +6,6 @@ import six import time import warnings -import comtypes import threading from .. import backend diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index d21e61711..f77485b83 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -1,7 +1,6 @@ """Implementation of the class to deal with an UI element of WPF via injected DLL""" import json -from comtypes import COMError from six import integer_types, text_type, string_types from ctypes.wintypes import tagPOINT import warnings From 93c050b99e77154be429d9592f6f4771c89ec3f0 Mon Sep 17 00:00:00 2001 From: eltio Date: Fri, 25 Mar 2022 20:54:36 +0300 Subject: [PATCH 03/59] improve WpfElementInfo --- pywinauto/windows/wpf_element_info.py | 50 ++++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index f77485b83..3013647a9 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -1,4 +1,5 @@ """Implementation of the class to deal with an UI element of WPF via injected DLL""" +import enum import json from six import integer_types, text_type, string_types @@ -10,7 +11,16 @@ from .win32structures import RECT from .injector import main, channel -pipes= {} +pipes = {} + +# backend exit codes enum +OK=0 +PARSE_ERROR=1, +UNSUPPORTED_ACTION=2, +MISSING_PARAM=3, +RUNTIME_ERROR=4, +NOT_FOUND=5, +UNSUPPORTED_TYPE=6, class WPFElementInfo(ElementInfo): re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "access_key", "accelerator", @@ -51,19 +61,21 @@ def set_cache_strategy(self, cached): @property def handle(self): - return 0 + # TODO + return -1 + + @property + def auto_id(self): + """Return AutomationId of the element""" + return self.get_field('Name') or '' @property def name(self): - command = f"{json.dumps({'action': 'GetProperty', 'element_id': self._element, 'name': 'Name'})}\n" - reply = self.pipe.transact(command) - reply = json.loads(reply) - return reply['value'] + return self.get_field('Content') or '' @property def rich_text(self): return self.name - #return '' @property def control_id(self): @@ -81,21 +93,22 @@ def framework_id(self): @property def class_name(self): - command = f"{json.dumps({'action': 'GetTypeName', 'element_id': self._element})}\n" + command = json.dumps({'action': 'GetTypeName', 'element_id': self._element}) reply = self.pipe.transact(command) reply = json.loads(reply) return reply['value'] @property def enabled(self): - return True + return self.get_field('IsEnabled') or False @property def visible(self): - return True + return self.get_field('IsVisible') or False @property def parent(self): + # TODO return None def children(self, **kwargs): @@ -104,12 +117,13 @@ def children(self, **kwargs): @property def control_type(self): """Return control type of element""" + # TODO return None def iter_children(self, **kwargs): if 'process' in kwargs: self._pid = kwargs['process'] - command = f"{json.dumps({'action': 'GetChildren', 'element_id': self._element})}\n" + command = json.dumps({'action': 'GetChildren', 'element_id': self._element}) reply = self.pipe.transact(command) reply = json.loads(reply) for elem in reply['elements']: @@ -136,7 +150,19 @@ def iter_descendants(self, **kwargs): @property def rectangle(self): + # TODO return RECT() def dump_window(self): - return {} \ No newline at end of file + # TODO + return {} + + def get_field(self, name): + # TODO if no such prop - raise exception or return None? + # Scenarios: OK, cannot serialize value, value is null, property not exist + command = json.dumps({'action': 'GetProperty', 'element_id': self._element, 'name': name}) + reply = self.pipe.transact(command) + reply = json.loads(reply) + if reply['status_code'] == OK: + return reply['value'] + return None \ No newline at end of file From 8c0bcbf8701049e38a6a2b399a4ea56e6540eb98 Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 26 Mar 2022 00:45:49 +0300 Subject: [PATCH 04/59] add rectangle property --- pywinauto/windows/wpf_element_info.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 3013647a9..f0c6d43d4 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -71,7 +71,8 @@ def auto_id(self): @property def name(self): - return self.get_field('Content') or '' + val = self.get_field('Content') or self.get_field('Header') + return val or '' @property def rich_text(self): @@ -150,8 +151,15 @@ def iter_descendants(self, **kwargs): @property def rectangle(self): - # TODO - return RECT() + rect = RECT() + command = json.dumps({'action': 'GetRectangle', 'element_id': self._element}) + reply = self.pipe.transact(command) + reply = json.loads(reply) + rect.left = reply['left'] + rect.right = reply['right'] + rect.top = reply['top'] + rect.bottom = reply['bottom'] + return rect def dump_window(self): # TODO From 154ab505f7d642ea02749df73bc0d2edf0478efc Mon Sep 17 00:00:00 2001 From: eltio Date: Sun, 27 Mar 2022 01:32:00 +0300 Subject: [PATCH 05/59] minor change in WpfElement info --- pywinauto/windows/{injector => injected}/__init__.py | 0 pywinauto/windows/{injector => injected}/channel.py | 0 .../windows/{injector/main.py => injected/injector.py} | 0 pywinauto/windows/wpf_element_info.py | 7 ++++--- 4 files changed, 4 insertions(+), 3 deletions(-) rename pywinauto/windows/{injector => injected}/__init__.py (100%) rename pywinauto/windows/{injector => injected}/channel.py (100%) rename pywinauto/windows/{injector/main.py => injected/injector.py} (100%) diff --git a/pywinauto/windows/injector/__init__.py b/pywinauto/windows/injected/__init__.py similarity index 100% rename from pywinauto/windows/injector/__init__.py rename to pywinauto/windows/injected/__init__.py diff --git a/pywinauto/windows/injector/channel.py b/pywinauto/windows/injected/channel.py similarity index 100% rename from pywinauto/windows/injector/channel.py rename to pywinauto/windows/injected/channel.py diff --git a/pywinauto/windows/injector/main.py b/pywinauto/windows/injected/injector.py similarity index 100% rename from pywinauto/windows/injector/main.py rename to pywinauto/windows/injected/injector.py diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index f0c6d43d4..4bc685109 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -9,7 +9,7 @@ from pywinauto.handleprops import dumpwindow, controlid from pywinauto.element_info import ElementInfo from .win32structures import RECT -from .injector import main, channel +from .injected import injector, channel pipes = {} @@ -53,7 +53,7 @@ def __init__(self, elem_id=None, cache_enable=False, pid=None): @property def pipe(self): if self._pid is not None and self._pid not in pipes: - pipes[self._pid] = main.create_pipe(self._pid) + pipes[self._pid] = injector.create_pipe(self._pid) return pipes[self._pid] def set_cache_strategy(self, cached): @@ -71,7 +71,8 @@ def auto_id(self): @property def name(self): - val = self.get_field('Content') or self.get_field('Header') + # TODO rewrite as action to avoid: "System.Windows.Controls.Label: ListBox and Grid" + val = self.get_field('Title') or self.get_field('Header') or self.get_field('Content') return val or '' @property From 9d8df4753cb721a907c49354ba3d66b8ec4c0327 Mon Sep 17 00:00:00 2001 From: eltio Date: Sun, 27 Mar 2022 12:33:28 +0300 Subject: [PATCH 06/59] WPF backend: add pid to criteria in .by() method --- pywinauto/base_application.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pywinauto/base_application.py b/pywinauto/base_application.py index af738ea1e..d18da2604 100644 --- a/pywinauto/base_application.py +++ b/pywinauto/base_application.py @@ -478,6 +478,7 @@ def by(self, **criteria): new_item = WindowSpecification(self.criteria[0], allow_magic_lookup=self.allow_magic_lookup) new_item.criteria.extend(self.criteria[1:]) + criteria['pid'] = self.app.process new_item.criteria.append(criteria) return new_item From abd3b84f6e58d2fc8560627c20fef7e3f675d26b Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 2 Apr 2022 23:28:55 +0300 Subject: [PATCH 07/59] add handle property, move pipe data to singleton --- pywinauto/windows/injected/channel.py | 9 ++-- pywinauto/windows/injected/defines.py | 51 ++++++++++++++++++++ pywinauto/windows/wpf_element_info.py | 69 +++++++++++---------------- 3 files changed, 86 insertions(+), 43 deletions(-) create mode 100644 pywinauto/windows/injected/defines.py diff --git a/pywinauto/windows/injected/channel.py b/pywinauto/windows/injected/channel.py index 0481df576..302144c56 100644 --- a/pywinauto/windows/injected/channel.py +++ b/pywinauto/windows/injected/channel.py @@ -6,6 +6,9 @@ import sys +class BrokenPipeError(Exception): + pass + class Pipe: def __init__(self, name): self.name = name @@ -39,7 +42,7 @@ def connect(self, n_attempts=100, delay=1): #print("Attempt {}: connection failed, trying again".format(i + 1)) time.sleep(delay) else: - print('Unexpected pipe error: {}'.format(e)) + raise BrokenPipeError('Unexpected pipe error: {}'.format(e)) if self.handle is not None: return True return False @@ -53,9 +56,9 @@ def transact(self, string): return resp[1].decode(sys.getdefaultencoding()) except pywintypes.error as e: if e.args[0] == winerror.ERROR_BROKEN_PIPE: - print("Broken pipe") + raise BrokenPipeError("Broken pipe") else: - print('Unexpected pipe error: {}'.format(e)) + raise BrokenPipeError('Unexpected pipe error: {}'.format(e)) return '' def close(self): diff --git a/pywinauto/windows/injected/defines.py b/pywinauto/windows/injected/defines.py new file mode 100644 index 000000000..1bd945fe7 --- /dev/null +++ b/pywinauto/windows/injected/defines.py @@ -0,0 +1,51 @@ +import json +import six + +from . import injector +from ...backend import Singleton + +# backend exit codes enum +OK=0 +PARSE_ERROR=1 +UNSUPPORTED_ACTION=2 +MISSING_PARAM=3 +RUNTIME_ERROR=4 +NOT_FOUND=5 +UNSUPPORTED_TYPE=6 + +class InjectedBackendError(Exception): + """Base class for exceptions on the injected backend side""" + pass + +class UnsupportedActionError(InjectedBackendError): + pass + +class BackendRuntimeError(InjectedBackendError): + pass + +class NotFoundError(InjectedBackendError): + pass + +@six.add_metaclass(Singleton) +class ConnectionManager(object): + def __init__(self): + self._pipes = {} + + def _get_pipe(self, pid): + if pid not in self._pipes: + self._pipes[pid] = injector.create_pipe(pid) + return self._pipes[pid] + + def call_action(self, action_name, pid, **params): + command = json.dumps({'action': action_name, **params}) + reply = self._get_pipe(pid).transact(command) + reply = json.loads(reply) + + if reply['status_code'] == UNSUPPORTED_ACTION: + raise UnsupportedActionError(reply['message']) + elif reply['status_code'] == RUNTIME_ERROR: + raise BackendRuntimeError(reply['message']) + elif reply['status_code'] == NOT_FOUND: + raise NotFoundError(reply['message']) + + return reply diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 4bc685109..1606c2663 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -9,18 +9,7 @@ from pywinauto.handleprops import dumpwindow, controlid from pywinauto.element_info import ElementInfo from .win32structures import RECT -from .injected import injector, channel - -pipes = {} - -# backend exit codes enum -OK=0 -PARSE_ERROR=1, -UNSUPPORTED_ACTION=2, -MISSING_PARAM=3, -RUNTIME_ERROR=4, -NOT_FOUND=5, -UNSUPPORTED_TYPE=6, +from .injected.defines import * class WPFElementInfo(ElementInfo): re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "access_key", "accelerator", @@ -50,27 +39,27 @@ def __init__(self, elem_id=None, cache_enable=False, pid=None): self.set_cache_strategy(cached=cache_enable) - @property - def pipe(self): - if self._pid is not None and self._pid not in pipes: - pipes[self._pid] = injector.create_pipe(self._pid) - return pipes[self._pid] - def set_cache_strategy(self, cached): pass @property def handle(self): - # TODO - return -1 + if self._element == 0: + return None + reply = ConnectionManager().call_action('GetHandle', self._pid, element_id=self._element) + return reply['value'] @property def auto_id(self): """Return AutomationId of the element""" + if self._element == 0: + return '' return self.get_field('Name') or '' @property def name(self): + if self._element == 0: + return '--root--' # TODO rewrite as action to avoid: "System.Windows.Controls.Label: ListBox and Grid" val = self.get_field('Title') or self.get_field('Header') or self.get_field('Content') return val or '' @@ -95,17 +84,21 @@ def framework_id(self): @property def class_name(self): - command = json.dumps({'action': 'GetTypeName', 'element_id': self._element}) - reply = self.pipe.transact(command) - reply = json.loads(reply) + if self._element == 0: + return '' + reply = ConnectionManager().call_action('GetTypeName', self._pid, element_id=self._element) return reply['value'] @property def enabled(self): + if self._element == 0: + return True return self.get_field('IsEnabled') or False @property def visible(self): + if self._element == 0: + return True return self.get_field('IsVisible') or False @property @@ -125,9 +118,7 @@ def control_type(self): def iter_children(self, **kwargs): if 'process' in kwargs: self._pid = kwargs['process'] - command = json.dumps({'action': 'GetChildren', 'element_id': self._element}) - reply = self.pipe.transact(command) - reply = json.loads(reply) + reply = ConnectionManager().call_action('GetChildren', self._pid, element_id=self._element) for elem in reply['elements']: yield WPFElementInfo(elem, pid=self._pid) @@ -153,25 +144,23 @@ def iter_descendants(self, **kwargs): @property def rectangle(self): rect = RECT() - command = json.dumps({'action': 'GetRectangle', 'element_id': self._element}) - reply = self.pipe.transact(command) - reply = json.loads(reply) - rect.left = reply['left'] - rect.right = reply['right'] - rect.top = reply['top'] - rect.bottom = reply['bottom'] + if self._element != 0: + reply = ConnectionManager().call_action('GetRectangle', self._pid, element_id=self._element) + rect.left = reply['left'] + rect.right = reply['right'] + rect.top = reply['top'] + rect.bottom = reply['bottom'] return rect def dump_window(self): # TODO return {} - def get_field(self, name): - # TODO if no such prop - raise exception or return None? - # Scenarios: OK, cannot serialize value, value is null, property not exist - command = json.dumps({'action': 'GetProperty', 'element_id': self._element, 'name': name}) - reply = self.pipe.transact(command) - reply = json.loads(reply) - if reply['status_code'] == OK: + def get_field(self, name, error_if_not_exists=False): + try: + reply = ConnectionManager().call_action('GetProperty', self._pid, element_id=self._element, name=name) return reply['value'] + except NotFoundError as e: + if error_if_not_exists: + raise e return None \ No newline at end of file From 0aa3f993b8d67e6f05b8f6f3888aa6e3aacf0af2 Mon Sep 17 00:00:00 2001 From: eltio Date: Wed, 6 Apr 2022 01:16:23 +0300 Subject: [PATCH 08/59] parent property --- pywinauto/windows/wpf_element_info.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 1606c2663..84b225e9d 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -12,11 +12,10 @@ from .injected.defines import * class WPFElementInfo(ElementInfo): - re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "access_key", "accelerator", - "value"] + re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "value"] exact_only_props = ["handle", "pid", "control_id", "enabled", "visible", "rectangle", "framework_id", "runtime_id"] search_order = ["handle", "control_type", "class_name", "pid", "control_id", "visible", "enabled", "name", - "access_key", "accelerator", "auto_id", "full_control_type", "rectangle", "framework_id", + "auto_id", "full_control_type", "rectangle", "framework_id", "runtime_id", "value"] assert set(re_props + exact_only_props) == set(search_order) @@ -82,6 +81,10 @@ def process_id(self): def framework_id(self): return "WPF" + @property + def runtime_id(self): + return self._element + @property def class_name(self): if self._element == 0: @@ -103,8 +106,10 @@ def visible(self): @property def parent(self): - # TODO - return None + if self._element == 0: + return None + reply = ConnectionManager().call_action('GetParent', self._pid, element_id=self._element) + return WPFElementInfo(reply['value'], pid=self._pid) def children(self, **kwargs): return list(self.iter_children(**kwargs)) From 66570c4b14c1ddfcb155864f41e08c41ca37827a Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 9 Apr 2022 20:23:42 +0300 Subject: [PATCH 09/59] move injected part to the submodule --- .gitmodules | 4 + pywinauto/windows/injected | 1 + pywinauto/windows/injected/__init__.py | 0 pywinauto/windows/injected/channel.py | 66 --------------- pywinauto/windows/injected/defines.py | 51 ----------- pywinauto/windows/injected/injector.py | 113 ------------------------- 6 files changed, 5 insertions(+), 230 deletions(-) create mode 100644 .gitmodules create mode 160000 pywinauto/windows/injected delete mode 100644 pywinauto/windows/injected/__init__.py delete mode 100644 pywinauto/windows/injected/channel.py delete mode 100644 pywinauto/windows/injected/defines.py delete mode 100644 pywinauto/windows/injected/injector.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..081946d1b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "pywinauto/windows/injected"] + path = pywinauto/windows/injected + url = https://github.com/eltimen/injected.git + branch = dotnet diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected new file mode 160000 index 000000000..b2bbc2d4f --- /dev/null +++ b/pywinauto/windows/injected @@ -0,0 +1 @@ +Subproject commit b2bbc2d4f5ef9d7905027cf3b612e001b4b61965 diff --git a/pywinauto/windows/injected/__init__.py b/pywinauto/windows/injected/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pywinauto/windows/injected/channel.py b/pywinauto/windows/injected/channel.py deleted file mode 100644 index 302144c56..000000000 --- a/pywinauto/windows/injected/channel.py +++ /dev/null @@ -1,66 +0,0 @@ -import pywintypes -import time -import win32file -import win32pipe -import winerror -import sys - - -class BrokenPipeError(Exception): - pass - -class Pipe: - def __init__(self, name): - self.name = name - self.handle = None - - def connect(self, n_attempts=100, delay=1): - for i in range(n_attempts): - try: - self.handle = win32file.CreateFile( - r'\\.\pipe\{}'.format(self.name), - win32file.GENERIC_READ | win32file.GENERIC_WRITE, - 0, - None, - win32file.OPEN_EXISTING, - 0, - None - ) - ret = win32pipe.SetNamedPipeHandleState( - self.handle, - win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT, - None, - None, - ) - # if ret != winerror.S_OK: - # print('SetNamedPipeHandleState exit code is {}'.format(ret)) - - #print(f'Connected to the pipe {self.name}, {self.handle=}') - break - except pywintypes.error as e: - if e.args[0] == winerror.ERROR_FILE_NOT_FOUND: - #print("Attempt {}: connection failed, trying again".format(i + 1)) - time.sleep(delay) - else: - raise BrokenPipeError('Unexpected pipe error: {}'.format(e)) - if self.handle is not None: - return True - return False - - def transact(self, string): - try: - win32file.WriteFile(self.handle, string.encode('utf-8')) - win32file.FlushFileBuffers(self.handle) - resp = win32file.ReadFile(self.handle, 64 * 1024) - #print('message: {}'.format(resp[1].decode(sys.getdefaultencoding()))) - return resp[1].decode(sys.getdefaultencoding()) - except pywintypes.error as e: - if e.args[0] == winerror.ERROR_BROKEN_PIPE: - raise BrokenPipeError("Broken pipe") - else: - raise BrokenPipeError('Unexpected pipe error: {}'.format(e)) - return '' - - def close(self): - win32file.CloseHandle(self.handle) - diff --git a/pywinauto/windows/injected/defines.py b/pywinauto/windows/injected/defines.py deleted file mode 100644 index 1bd945fe7..000000000 --- a/pywinauto/windows/injected/defines.py +++ /dev/null @@ -1,51 +0,0 @@ -import json -import six - -from . import injector -from ...backend import Singleton - -# backend exit codes enum -OK=0 -PARSE_ERROR=1 -UNSUPPORTED_ACTION=2 -MISSING_PARAM=3 -RUNTIME_ERROR=4 -NOT_FOUND=5 -UNSUPPORTED_TYPE=6 - -class InjectedBackendError(Exception): - """Base class for exceptions on the injected backend side""" - pass - -class UnsupportedActionError(InjectedBackendError): - pass - -class BackendRuntimeError(InjectedBackendError): - pass - -class NotFoundError(InjectedBackendError): - pass - -@six.add_metaclass(Singleton) -class ConnectionManager(object): - def __init__(self): - self._pipes = {} - - def _get_pipe(self, pid): - if pid not in self._pipes: - self._pipes[pid] = injector.create_pipe(pid) - return self._pipes[pid] - - def call_action(self, action_name, pid, **params): - command = json.dumps({'action': action_name, **params}) - reply = self._get_pipe(pid).transact(command) - reply = json.loads(reply) - - if reply['status_code'] == UNSUPPORTED_ACTION: - raise UnsupportedActionError(reply['message']) - elif reply['status_code'] == RUNTIME_ERROR: - raise BackendRuntimeError(reply['message']) - elif reply['status_code'] == NOT_FOUND: - raise NotFoundError(reply['message']) - - return reply diff --git a/pywinauto/windows/injected/injector.py b/pywinauto/windows/injected/injector.py deleted file mode 100644 index 0673ee3f2..000000000 --- a/pywinauto/windows/injected/injector.py +++ /dev/null @@ -1,113 +0,0 @@ -import json -import os -import time -import sys -from pywinauto.application import Application - -from ctypes import windll, c_int, c_ulong, byref - -from .channel import Pipe - -PAGE_READWRITE = 0x04 -PROCESS_ALL_ACCESS = ( 0x000F0000 | 0x00100000 | 0xFFF ) -VIRTUAL_MEM = ( 0x1000 | 0x2000 ) - -kernel32 = windll.kernel32 - - -def inject(pid): - dll_path = (os.path.dirname(__file__) + os.sep + r'x64\bootstrap.dll').encode('utf-8') - #dll_path = rb'D:\repo\pywinauto\dotnetguiauto\Debug\bootstrap.dll' - - h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, int(pid)) - #print(f'{h_process=}') - if not h_process: - #print('') - sys.exit(-1) - - arg_address = kernel32.VirtualAllocEx(h_process, 0, len(dll_path), VIRTUAL_MEM, PAGE_READWRITE) - #print(f'{hex(arg_address)=}') - - written = c_int(0) - kernel32.WriteProcessMemory(h_process, arg_address, dll_path, len(dll_path), byref(written)) - #print(f'{written=}') - - h_kernel32 = kernel32.GetModuleHandleA(b"kernel32.dll") - #print(f'{h_kernel32=}') - h_loadlib = kernel32.GetProcAddress(h_kernel32, b"LoadLibraryA") - #print(f'{h_loadlib=}') - - thread_id = c_ulong(0) - - if not kernel32.CreateRemoteThread( - h_process, - None, - 0, - h_loadlib, - arg_address, - 0, - byref(thread_id) - ): - #print('err inject') - sys.exit(-2) - - #print(f'{thread_id=}') - -def create_pipe(pid): - pipe = Pipe(f'pywinauto_{pid}') - if pipe.connect(n_attempts=1): - return pipe - else: - inject(pid) - #print('Inject successful, connecting to the server...') - pipe.connect() - return pipe - -if __name__ == '__main__': - app = Application().connect(path='WpfApplication1.exe') - inject(app.process) - #print('Inject successful, connecting to the server...') - - pipe = Pipe(f'pywinauto_{app.process}') - pipe.connect() - - #command = f"{json.dumps({'action': 'getChildren'})}\n" - #pipe.send_and_recv(command) - - #command = f"{json.dumps({'action': 'getChildren', 'element_id': 1})}\n" - #pipe.send_and_recv(command) - - # command = f"{json.dumps({'action': 'getChildren', 'element_id': 2})}\n" - # pipe.send_and_recv(command) - # - # command = f"{json.dumps({'action': 'getChildren', 'element_id': 3})}\n" - # pipe.send_and_recv(command) - # - # command = f"{json.dumps({'action': 'getChildren', 'element_id': 4})}\n" - # pipe.send_and_recv(command) - # - # command = f"{json.dumps({'action': 'getChildren', 'element_id': 6})}\n" - # pipe.send_and_recv(command) - # - # command = f"{json.dumps({'action': 'getChildren', 'element_id': 8})}\n" - # pipe.send_and_recv(command) - # - # command = f"{json.dumps({'action': 'getChildren', 'element_id': 9})}\n" - # pipe.send_and_recv(command) - - while True: - action = input('Enter child ID or action: ') - if action.isnumeric(): - currentElement = action - command = f"{json.dumps({'action': 'GetChildren', 'element_id': int(action)})}\n" - pipe.transact(command) - elif action == 'name': - command = f"{json.dumps({'action': 'GetProperty', 'element_id': int(currentElement), 'name': 'Name'})}\n" - pipe.transact(command) - elif action == 'q': - break - else: - print('err') - - pipe.close() - #app.kill() From 158abbe65048d5f000d310b72aac9c88e72112a3 Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 12 Apr 2022 23:05:35 +0300 Subject: [PATCH 10/59] improve injected module structure --- pywinauto/windows/wpf_element_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 84b225e9d..772775826 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -9,7 +9,7 @@ from pywinauto.handleprops import dumpwindow, controlid from pywinauto.element_info import ElementInfo from .win32structures import RECT -from .injected.defines import * +from .injected.api import * class WPFElementInfo(ElementInfo): re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "value"] From f23604bddab72a65abbe4f82c2eb7e148f872cdf Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 12 Apr 2022 23:56:45 +0300 Subject: [PATCH 11/59] add tests for WPFElementInfo --- appveyor.yml | 9 +- ci/build_injected_dlls.bat | 11 ++ pywinauto/unittests/test_wpf_element_info.py | 107 +++++++++++++++++++ pywinauto/windows/wpf_element_info.py | 17 ++- 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 ci/build_injected_dlls.bat create mode 100644 pywinauto/unittests/test_wpf_element_info.py diff --git a/appveyor.yml b/appveyor.yml index b74b28562..171ca450d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,7 @@ # # fetch repository as a zip archive -shallow_clone: true # default is "false" +shallow_clone: false # default is "false" environment: @@ -67,6 +67,10 @@ install: # as well as pywin32, pillow and coverage - "powershell ./ci/install.ps1" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - cd %APPVEYOR_BUILD_FOLDER% + - git submodule init + - git submodule update + # Install the build dependencies of the project. If some dependencies contain # compiled extensions and are not provided as pre-built wheel packages, @@ -76,11 +80,12 @@ install: # Enable desktop (for correct screenshots). #- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-desktop.ps1')) - + - build: false # Not a C# project, build stuff at the test step instead. test_script: # run the tests + - ./ci/build_injected_dlls.bat - "powershell ./ci/runTestsuite.ps1" diff --git a/ci/build_injected_dlls.bat b/ci/build_injected_dlls.bat new file mode 100644 index 000000000..3873192aa --- /dev/null +++ b/ci/build_injected_dlls.bat @@ -0,0 +1,11 @@ +pushd pywinauto\windows\injected\backends\dotnet + +mkdir build32 +cmake -G "Visual Studio 14 2015" -A Win32 -B .\build32 +cmake --build build32 --target install --config Release + +mkdir build64 +cmake -G "Visual Studio 14 2015" -A x64 -B .\build64 +cmake --build build64 --target install --config Release + +popd \ No newline at end of file diff --git a/pywinauto/unittests/test_wpf_element_info.py b/pywinauto/unittests/test_wpf_element_info.py new file mode 100644 index 000000000..7aca2f47e --- /dev/null +++ b/pywinauto/unittests/test_wpf_element_info.py @@ -0,0 +1,107 @@ +import unittest +import os +import sys +import mock + +sys.path.append(".") +from pywinauto.windows.application import Application # noqa: E402 +from pywinauto.handleprops import processid # noqa: E402 +from pywinauto.sysinfo import is_x64_Python # noqa: E402 +from pywinauto.timings import Timings # noqa: E402 + +from pywinauto.windows.wpf_element_info import WPFElementInfo + +wpf_samples_folder = os.path.join( + os.path.dirname(__file__), r"..\..\apps\WPF_samples") +if is_x64_Python(): + wpf_samples_folder = os.path.join(wpf_samples_folder, 'x64') +wpf_app_1 = os.path.join(wpf_samples_folder, u"WpfApplication1.exe") + +class WPFElementInfoTests(unittest.TestCase): + """Unit tests for the WPFlementInfo class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + Timings.slow() + + self.app = Application(backend="wpf") + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + self.handle = self.dlg.handle + self.ctrl = self.dlg.find().element_info + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def testHash(self): + """Test element info hashing""" + d = {self.ctrl: "elem"} + self.assertEqual(d[self.ctrl], "elem") + + def testProcessId(self): + """Test process_id equals""" + self.assertEqual(self.ctrl.process_id, processid(self.handle)) + + def testName(self): + """Test application name equals""" + self.assertEqual(self.ctrl.name, "WPF Sample Application") + + def testHandle(self): + """Test application handle equals""" + self.assertEqual(self.ctrl.handle, self.handle) + + def testEnabled(self): + """Test whether the element is enabled""" + self.assertEqual(self.ctrl.enabled, True) + + def testVisible(self): + """Test whether the element is visible""" + self.assertEqual(self.ctrl.visible, True) + + def testChildren(self): + """Test whether a list of only immediate children of the element is equal""" + self.assertEqual(len(self.ctrl.children()), 1) + self.assertEqual(len(self.ctrl.children()[0].children()), 2) + + def test_children_generator(self): + """Test whether children generator iterates over correct elements""" + children = [child for child in self.ctrl.iter_children()] + self.assertSequenceEqual(self.ctrl.children(), children) + + def test_default_depth_descendants(self): + """Test whether a list of descendants with default depth of the element is equal""" + self.assertEqual(len(self.ctrl.descendants(depth=None)), len(self.ctrl.descendants())) + + def test_depth_level_one_descendants(self): + """Test whether a list of descendants with depth=1 of the element is equal to children set""" + self.assertEqual(len(self.ctrl.descendants(depth=1)), len(self.ctrl.children())) + + def test_depth_level_three_descendants(self): + """Test whether a list of descendants with depth=3 of the element is equal""" + descendants = self.ctrl.children() + + level_two_children = [] + for element in descendants: + level_two_children.extend(element.children()) + descendants.extend(level_two_children) + + level_three_children = [] + for element in level_two_children: + level_three_children.extend(element.children()) + descendants.extend(level_three_children) + + self.assertEqual(len(self.ctrl.descendants(depth=3)), len(descendants)) + + def test_invalid_depth_descendants(self): + """Test whether a list of descendants with invalid depth raises exception""" + self.assertRaises(Exception, self.ctrl.descendants, depth='qwerty') + + def test_descendants_generator(self): + """Test whether descendant generator iterates over correct elements""" + descendants = [desc for desc in self.ctrl.iter_descendants(depth=3)] + self.assertSequenceEqual(self.ctrl.descendants(depth=3), descendants) + +if __name__ == "__main__": + unittest.main() diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 772775826..486a30063 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -1,5 +1,4 @@ """Implementation of the class to deal with an UI element of WPF via injected DLL""" -import enum import json from six import integer_types, text_type, string_types @@ -168,4 +167,18 @@ def get_field(self, name, error_if_not_exists=False): except NotFoundError as e: if error_if_not_exists: raise e - return None \ No newline at end of file + return None + + def __hash__(self): + """Return a unique hash value based on the element's ID""" + return hash(self._element) + + def __eq__(self, other): + """Check if 2 UIAElementInfo objects describe 1 actual element""" + if not isinstance(other, WPFElementInfo): + return False + return self._element == other._element + + def __ne__(self, other): + """Check if 2 UIAElementInfo objects describe 2 different elements""" + return not (self == other) \ No newline at end of file From b8e8eb393698592af9d30fc16a641b266d015007 Mon Sep 17 00:00:00 2001 From: eltio Date: Wed, 13 Apr 2022 12:52:51 +0300 Subject: [PATCH 12/59] remove injected dll building before test --- appveyor.yml | 2 -- ci/build_injected_dlls.bat | 11 ----------- pywinauto/windows/injected | 2 +- 3 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 ci/build_injected_dlls.bat diff --git a/appveyor.yml b/appveyor.yml index 171ca450d..3e91c16c9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -80,12 +80,10 @@ install: # Enable desktop (for correct screenshots). #- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-desktop.ps1')) - - build: false # Not a C# project, build stuff at the test step instead. test_script: # run the tests - - ./ci/build_injected_dlls.bat - "powershell ./ci/runTestsuite.ps1" diff --git a/ci/build_injected_dlls.bat b/ci/build_injected_dlls.bat deleted file mode 100644 index 3873192aa..000000000 --- a/ci/build_injected_dlls.bat +++ /dev/null @@ -1,11 +0,0 @@ -pushd pywinauto\windows\injected\backends\dotnet - -mkdir build32 -cmake -G "Visual Studio 14 2015" -A Win32 -B .\build32 -cmake --build build32 --target install --config Release - -mkdir build64 -cmake -G "Visual Studio 14 2015" -A x64 -B .\build64 -cmake --build build64 --target install --config Release - -popd \ No newline at end of file diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index b2bbc2d4f..ed853da75 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit b2bbc2d4f5ef9d7905027cf3b612e001b4b61965 +Subproject commit ed853da75be6d1ca15fce786de3d39c3b2fccf00 From df7d637572c275a57088cbb374114104bb8720f7 Mon Sep 17 00:00:00 2001 From: eltio Date: Wed, 13 Apr 2022 14:07:58 +0300 Subject: [PATCH 13/59] fix errors from ci --- pywinauto/base_application.py | 3 ++- pywinauto/windows/injected | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pywinauto/base_application.py b/pywinauto/base_application.py index d18da2604..170e7c9d4 100644 --- a/pywinauto/base_application.py +++ b/pywinauto/base_application.py @@ -478,7 +478,8 @@ def by(self, **criteria): new_item = WindowSpecification(self.criteria[0], allow_magic_lookup=self.allow_magic_lookup) new_item.criteria.extend(self.criteria[1:]) - criteria['pid'] = self.app.process + if self.app is not None: + criteria['pid'] = self.app.process new_item.criteria.append(criteria) return new_item diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index ed853da75..e06d3a622 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit ed853da75be6d1ca15fce786de3d39c3b2fccf00 +Subproject commit e06d3a622ee7cfbff138e1e2529e70379aa659c2 From cf316a25434fb2faca656487ba0500c4ac14b897 Mon Sep 17 00:00:00 2001 From: eltio Date: Wed, 13 Apr 2022 23:41:29 +0300 Subject: [PATCH 14/59] add tests for wpf wrapper, click_input works --- pywinauto/unittests/test_wpfwrapper.py | 408 +++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 pywinauto/unittests/test_wpfwrapper.py diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py new file mode 100644 index 000000000..9e955b648 --- /dev/null +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -0,0 +1,408 @@ +"""Tests for WPFWrapper""" +from __future__ import print_function +from __future__ import unicode_literals + +import time +import os +import sys +import collections +import unittest +import mock +import six + +sys.path.append(".") +from pywinauto.windows.application import Application # noqa: E402 +from pywinauto.base_application import WindowSpecification # noqa: E402 +from pywinauto.sysinfo import is_x64_Python # noqa: E402 +from pywinauto.timings import Timings, wait_until # noqa: E402 +from pywinauto.actionlogger import ActionLogger # noqa: E402 +from pywinauto import Desktop +from pywinauto import mouse # noqa: E402 +from pywinauto import WindowNotFoundError # noqa: E402 + +import pywinauto.controls.wpf_controls as wpf_ctls +from pywinauto.controls.wpfwrapper import WPFWrapper +from pywinauto.windows.wpf_element_info import WPFElementInfo + +wpf_samples_folder = os.path.join( + os.path.dirname(__file__), r"..\..\apps\WPF_samples") +if is_x64_Python(): + wpf_samples_folder = os.path.join(wpf_samples_folder, 'x64') +wpf_app_1 = os.path.join(wpf_samples_folder, u"WpfApplication1.exe") + +def _set_timings(): + """Setup timings for WPF related tests""" + Timings.defaults() + Timings.window_find_timeout = 20 + + +class WPFWrapperTests(unittest.TestCase): + + """Unit tests for the WPFWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + mouse.move((-500, 500)) # remove the mouse from the screen to avoid side effects + + # start the application + self.app = Application(backend='uia') + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + + def test_get_active_uia(self): + focused_element = self.dlg.get_active() + self.assertTrue(type(focused_element) is UIAWrapper or issubclass(type(focused_element), UIAWrapper)) + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_issue_278(self): + """Test that statement menu = app.MainWindow.Menu works for 'uia' backend""" + menu_spec = self.dlg.Menu + self.assertTrue(isinstance(menu_spec, WindowSpecification)) + # Also check the app binding + self.assertTrue(menu_spec.app, self.app) + + def test_find_nontop_ctl_by_class_name_and_title(self): + """Test getting a non-top control by a class name and a title""" + # Look up for a non-top button control with 'Apply' caption + self.dlg.wait('ready') + caption = 'Apply' + wins = self.app.windows(top_level_only=False, + class_name='Button', + name=caption) + + # Verify the number of found wrappers + self.assertEqual(len(wins), 1) + + # Verify the caption of the found wrapper + self.assertEqual(wins[0].texts()[0], caption) + + def test_find_top_win_by_class_name_and_title(self): + """Test getting a top window by a class name and a title""" + # Since the top_level_only is True by default + # we don't specify it as a criteria argument + self.dlg.wait('ready') + caption = 'WPF Sample Application' + wins = self.app.windows(class_name='Window', name=caption) + + # Verify the number of found wrappers + self.assertEqual(len(wins), 1) + + # Verify the caption of the found wrapper + self.assertEqual(wins[0].texts()[0], caption) + + def test_class(self): + """Test getting the classname of the dialog""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.class_name(), "Button") + + def test_window_text(self): + """Test getting the window Text of the dialog""" + label = self.dlg.TestLabel.find() + self.assertEqual(label.window_text(), u"TestLabel") + self.assertEqual(label.can_be_label, True) + + def test_control_id(self): + """Test getting control ID""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.control_id(), None) + + def test_automation_id(self): + """Test getting automation ID""" + alpha_toolbar = self.dlg.by(name="Alpha", control_type="ToolBar") + button = alpha_toolbar.by(control_type="Button", + auto_id="OverflowButton").find() + self.assertEqual(button.automation_id(), "OverflowButton") + + def test_value(self): + """Test find element by value""" + edit = self.dlg.by(auto_id="edit1").find() + edit.set_edit_text("Test string") + + edit_by_value = self.dlg.by(value="Test string").find() + self.assertEqual("edit1", edit_by_value.element_info.auto_id) + + def test_is_visible(self): + """Test is_visible method of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.is_visible(), True) + + def test_is_enabled(self): + """Test is_enabled method of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.is_enabled(), True) + + def test_process_id(self): + """Test process_id method of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.process_id(), self.dlg.process_id()) + self.assertNotEqual(button.process_id(), 0) + + def test_is_dialog(self): + """Test is_dialog method of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.is_dialog(), False) + self.assertEqual(self.dlg.is_dialog(), True) + + def test_move_window(self): + """Test move_window without any parameters""" + + # move_window with default parameters + prevRect = self.dlg.rectangle() + self.dlg.move_window() + self.assertEqual(prevRect, self.dlg.rectangle()) + + # move_window call for a not supported control + button = self.dlg.by(class_name="Button", name="OK") + self.assertRaises(AttributeError, button.move_window) + + # Make RECT stub to avoid import win32structures + Rect = collections.namedtuple('Rect', 'left top right bottom') + prev_rect = self.dlg.rectangle() + new_rect = Rect._make([i + 5 for i in prev_rect]) + + self.dlg.move_window( + new_rect.left, + new_rect.top, + new_rect.right - new_rect.left, + new_rect.bottom - new_rect.top + ) + time.sleep(0.1) + logger = ActionLogger() + logger.log("prev_rect = %s", prev_rect) + logger.log("new_rect = %s", new_rect) + logger.log("self.dlg.rectangle() = %s", self.dlg.rectangle()) + self.assertEqual(self.dlg.rectangle(), new_rect) + + self.dlg.move_window(prev_rect) + self.assertEqual(self.dlg.rectangle(), prev_rect) + + def test_close(self): + """Test close method of a control""" + wrp = self.dlg.find() + + # mock a failure in get_elem_interface() method only for 'Window' param + orig_get_elem_interface = uia_defs.get_elem_interface + with mock.patch.object(uia_defs, 'get_elem_interface') as mock_get_iface: + def side_effect(elm_info, ptrn_name): + if ptrn_name == "Window": + raise uia_defs.NoPatternInterfaceError() + else: + return orig_get_elem_interface(elm_info, ptrn_name) + mock_get_iface.side_effect=side_effect + # also mock a failure in type_keys() method + with mock.patch.object(UIAWrapper, 'type_keys') as mock_type_keys: + exception_err = comtypes.COMError(-2147220991, 'An event was unable to invoke any of the subscribers', ()) + mock_type_keys.side_effect = exception_err + self.assertRaises(WindowNotFoundError, self.dlg.close) + + self.dlg.close() + self.assertEqual(self.dlg.exists(), False) + + def test_parent(self): + """Test getting a parent of a control""" + button = self.dlg.Alpha.find() + self.assertEqual(button.parent(), self.dlg.find()) + + def test_top_level_parent(self): + """Test getting a top-level parent of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.top_level_parent(), self.dlg.find()) + + def test_texts(self): + """Test getting texts of a control""" + self.assertEqual(self.dlg.texts(), ['WPF Sample Application']) + + def test_children(self): + """Test getting children of a control""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(len(button.children()), 1) + self.assertEqual(button.children()[0].class_name(), "TextBlock") + + def test_children_generator(self): + """Test iterating children of a control""" + button = self.dlg.by(class_name="Button", name="OK").find() + children = [child for child in button.iter_children()] + self.assertEqual(len(children), 1) + self.assertEqual(children[0].class_name(), "TextBlock") + + def test_descendants(self): + """Test iterating descendants of a control""" + toolbar = self.dlg.by(name="Alpha", control_type="ToolBar").find() + descendants = toolbar.descendants() + self.assertEqual(len(descendants), 7) + + def test_descendants_generator(self): + toolbar = self.dlg.by(name="Alpha", control_type="ToolBar").find() + descendants = [desc for desc in toolbar.iter_descendants()] + self.assertSequenceEqual(toolbar.descendants(), descendants) + + def test_is_child(self): + """Test is_child method of a control""" + button = self.dlg.Alpha.find() + self.assertEqual(button.is_child(self.dlg.find()), True) + + def test_equals(self): + """Test controls comparisons""" + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertNotEqual(button, self.dlg.find()) + self.assertEqual(button, button.element_info) + self.assertEqual(button, button) + + @unittest.skip("To be solved with issue #790") + def test_scroll(self): + """Test scroll""" + # Check an exception on a non-scrollable control + button = self.dlg.by(class_name="Button", + name="OK").find() + six.assertRaisesRegex(self, AttributeError, "not scrollable", + button.scroll, "left", "page") + + # Check an exception on a control without horizontal scroll bar + tab = self.dlg.Tree_and_List_Views.set_focus() + listview = tab.children(class_name=u"ListView")[0] + six.assertRaisesRegex(self, AttributeError, "not horizontally scrollable", + listview.scroll, "right", "line") + + # Check exceptions on wrong arguments + self.assertRaises(ValueError, listview.scroll, "bbbb", "line") + self.assertRaises(ValueError, listview.scroll, "up", "aaaa") + + # Store a cell position + cell = listview.cell(3, 0) + orig_rect = cell.rectangle() + self.assertEqual(orig_rect.left > 0, True) + + # Trigger a horizontal scroll bar on the control + hdr = listview.get_header_control() + hdr_itm = hdr.children()[1] + trf = hdr_itm.iface_transform + trf.resize(1000, 20) + listview.scroll("right", "page", 2) + self.assertEqual(cell.rectangle().left < 0, True) + + # Check an exception on a control without vertical scroll bar + tab = self.dlg.ListBox_and_Grid.set_focus() + datagrid = tab.children(class_name=u"DataGrid")[0] + six.assertRaisesRegex(self, AttributeError, "not vertically scrollable", + datagrid.scroll, "down", "page") + + def test_is_keyboard_focusable(self): + """Test is_keyboard focusable method of several controls""" + edit = self.dlg.TestLabelEdit.find() + label = self.dlg.TestLabel.find() + button = self.dlg.by(class_name="Button", + name="OK").find() + self.assertEqual(button.is_keyboard_focusable(), True) + self.assertEqual(edit.is_keyboard_focusable(), True) + self.assertEqual(label.is_keyboard_focusable(), False) + + def test_set_focus(self): + """Test setting a keyboard focus on a control""" + edit = self.dlg.TestLabelEdit.find() + edit.set_focus() + self.assertEqual(edit.has_keyboard_focus(), True) + + def test_get_active_desktop_uia(self): + focused_element = Desktop(backend="uia").get_active() + self.assertTrue(type(focused_element) is UIAWrapper or issubclass(type(focused_element), UIAWrapper)) + + def test_type_keys(self): + """Test sending key types to a control""" + edit = self.dlg.TestLabelEdit.find() + edit.type_keys("t") + self.assertEqual(edit.window_text(), "t") + edit.type_keys("e") + self.assertEqual(edit.window_text(), "te") + edit.type_keys("s") + self.assertEqual(edit.window_text(), "tes") + edit.type_keys("t") + self.assertEqual(edit.window_text(), "test") + edit.type_keys("T") + self.assertEqual(edit.window_text(), "testT") + edit.type_keys("y") + self.assertEqual(edit.window_text(), "testTy") + + def test_minimize_maximize(self): + """Test window minimize/maximize operations""" + wrp = self.dlg.minimize() + self.dlg.wait_not('active') + self.assertEqual(wrp.is_minimized(), True) + wrp.maximize() + self.dlg.wait('active') + self.assertEqual(wrp.is_maximized(), True) + wrp.minimize() + self.dlg.wait_not('active') + wrp.restore() + self.dlg.wait('active') + self.assertEqual(wrp.is_normal(), True) + + def test_capture_as_image_multi_monitor(self): + with mock.patch('win32api.EnumDisplayMonitors') as mon_device: + mon_device.return_value = (1, 2) + rect = self.dlg.rectangle() + expected = (rect.width(), rect.height()) + result = self.dlg.capture_as_image().size + self.assertEqual(expected, result) + + def test_set_value(self): + """Test for UIAWrapper.set_value""" + edit = self.dlg.by(control_type='Edit', auto_id='edit1').find() + self.assertEqual(edit.get_value(), '') + edit.set_value('test') + self.assertEqual(edit.get_value(), 'test') + + +class WPFWrapperMouseTests(unittest.TestCase): + + """Unit tests for mouse actions of the WPFWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + dlg = self.app.WPFSampleApplication + self.button = dlg.by(class_name="System.Windows.Controls.Button", + name="OK").find() + + self.label = dlg.by(class_name="System.Windows.Controls.Label", name="TestLabel").find() + self.app.wait_cpu_usage_lower(threshold=1.5, timeout=30, usage_interval=1.0) + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_click_input(self): + """Test click_input method of a control""" + self.button.click_input() + self.assertEqual(self.label.window_text(), "LeftClick") + + def test_double_click_input(self): + """Test double_click_input method of a control""" + self.button.double_click_input() + self.assertEqual(self.label.window_text(), "DoubleClick") + + def test_right_click_input(self): + """Test right_click_input method of a control""" + self.button.right_click_input() + self.assertEqual(self.label.window_text(), "RightClick") + + +if __name__ == "__main__": + unittest.main() From abcbc1e56659583ceb2f465b090124760bf7b7e1 Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 14 Apr 2022 20:42:24 +0300 Subject: [PATCH 15/59] control_type works --- pywinauto/windows/wpf_element_info.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 486a30063..ccaa4c505 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -116,8 +116,8 @@ def children(self, **kwargs): @property def control_type(self): """Return control type of element""" - # TODO - return None + reply = ConnectionManager().call_action('GetControlType', self._pid, element_id=self._element) + return reply['value'] def iter_children(self, **kwargs): if 'process' in kwargs: @@ -126,7 +126,6 @@ def iter_children(self, **kwargs): for elem in reply['elements']: yield WPFElementInfo(elem, pid=self._pid) - def descendants(self, **kwargs): return list(self.iter_descendants(**kwargs)) @@ -157,8 +156,7 @@ def rectangle(self): return rect def dump_window(self): - # TODO - return {} + return dumpwindow(self.handle) def get_field(self, name, error_if_not_exists=False): try: @@ -174,11 +172,11 @@ def __hash__(self): return hash(self._element) def __eq__(self, other): - """Check if 2 UIAElementInfo objects describe 1 actual element""" + """Check if 2 WPFElementInfo objects describe 1 actual element""" if not isinstance(other, WPFElementInfo): return False return self._element == other._element def __ne__(self, other): - """Check if 2 UIAElementInfo objects describe 2 different elements""" + """Check if 2 WPFElementInfo objects describe 2 different elements""" return not (self == other) \ No newline at end of file From 202666c8ae2656f4062554bbdbf3910eb5b6089b Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 14 Apr 2022 23:48:34 +0300 Subject: [PATCH 16/59] fix best_match for wpf backend --- pywinauto/base_application.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pywinauto/base_application.py b/pywinauto/base_application.py index 170e7c9d4..d842963ad 100644 --- a/pywinauto/base_application.py +++ b/pywinauto/base_application.py @@ -223,6 +223,8 @@ def __find_base(self, criteria_, timeout, retry_interval): if 'backend' not in ctrl_criteria: ctrl_criteria['backend'] = self.backend.name + if self.app is not None: + ctrl_criteria['pid'] = self.app.process ctrl = self.backend.generic_wrapper_class(findwindows.find_element(**ctrl_criteria)) previous_parent = ctrl.element_info ctrls.append(ctrl) @@ -576,7 +578,12 @@ def __getattribute__(self, attr_name): # FIXME - I don't get this part at all, why is it win32-specific and why not keep the same logic as above? # if we have been asked for an attribute of the dialog # then resolve the window and return the attribute - desktop_wrapper = self.backend.generic_wrapper_class(self.backend.element_info_class()) + if self.backend.name in ('wpf'): + desktop_wrapper = self.backend.generic_wrapper_class( + self.backend.element_info_class(pid=self.app.process) + ) + else: + desktop_wrapper = self.backend.generic_wrapper_class(self.backend.element_info_class()) need_to_resolve = (len(self.criteria) == 1 and hasattr(desktop_wrapper, attr_name)) if hasattr(self.backend, 'dialog_class'): need_to_resolve = need_to_resolve and hasattr(self.backend.dialog_class, attr_name) From 1fe8f8395f99ab6c2f461f1291f9fc28181642d6 Mon Sep 17 00:00:00 2001 From: eltio Date: Fri, 15 Apr 2022 00:42:25 +0300 Subject: [PATCH 17/59] wrapper: focus-related methods, initial tests --- pywinauto/controls/wpfwrapper.py | 23 +++- pywinauto/unittests/test_wpfwrapper.py | 154 +++---------------------- pywinauto/windows/wpf_element_info.py | 26 ++++- 3 files changed, 55 insertions(+), 148 deletions(-) diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index 184eb6bcd..c13844322 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -12,9 +12,8 @@ from .. import WindowNotFoundError # noqa #E402 from ..timings import Timings from .win_base_wrapper import WinBaseWrapper -from .hwndwrapper import HwndWrapper from ..base_wrapper import BaseMeta - +from ..windows.injected.api import * from ..windows.wpf_element_info import WPFElementInfo class WpfMeta(BaseMeta): @@ -63,4 +62,24 @@ def __init__(self, element_info): """ WinBaseWrapper.__init__(self, element_info, backend.registry.backends['wpf']) + def get_property(self, name, error_if_not_exists=False): + return self.element_info.get_property(name, error_if_not_exists) + + def is_keyboard_focusable(self): + """Return True if the element can be focused with keyboard""" + return self.get_property('Focusable') or False + + # ----------------------------------------------------------- + def has_keyboard_focus(self): + """Return True if the element is focused with keyboard""" + return self.get_property('IsKeyboardFocused') or False + + def set_focus(self): + reply = ConnectionManager().call_action('SetFocus', self.element_info.pid, + element_id=self.element_info.runtime_id) + return self + + + + backend.register('wpf', WPFElementInfo, WPFWrapper) \ No newline at end of file diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 9e955b648..b5de357a4 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -46,21 +46,21 @@ def setUp(self): mouse.move((-500, 500)) # remove the mouse from the screen to avoid side effects # start the application - self.app = Application(backend='uia') + self.app = Application(backend='wpf') self.app = self.app.start(wpf_app_1) self.dlg = self.app.WPFSampleApplication - def test_get_active_uia(self): + def test_get_active_wpf(self): focused_element = self.dlg.get_active() - self.assertTrue(type(focused_element) is UIAWrapper or issubclass(type(focused_element), UIAWrapper)) + self.assertTrue(type(focused_element) is WPFWrapper or issubclass(type(focused_element), WPFWrapper)) def tearDown(self): """Close the application after tests""" self.app.kill() def test_issue_278(self): - """Test that statement menu = app.MainWindow.Menu works for 'uia' backend""" + """Test that statement menu = app.MainWindow.Menu works for 'wpf' backend""" menu_spec = self.dlg.Menu self.assertTrue(isinstance(menu_spec, WindowSpecification)) # Also check the app binding @@ -87,7 +87,7 @@ def test_find_top_win_by_class_name_and_title(self): # we don't specify it as a criteria argument self.dlg.wait('ready') caption = 'WPF Sample Application' - wins = self.app.windows(class_name='Window', name=caption) + wins = self.app.windows(class_name='MainWindow', name=caption) # Verify the number of found wrappers self.assertEqual(len(wins), 1) @@ -111,7 +111,7 @@ def test_control_id(self): """Test getting control ID""" button = self.dlg.by(class_name="Button", name="OK").find() - self.assertEqual(button.control_id(), None) + self.assertNotEqual(button.control_id(), None) def test_automation_id(self): """Test getting automation ID""" @@ -120,14 +120,6 @@ def test_automation_id(self): auto_id="OverflowButton").find() self.assertEqual(button.automation_id(), "OverflowButton") - def test_value(self): - """Test find element by value""" - edit = self.dlg.by(auto_id="edit1").find() - edit.set_edit_text("Test string") - - edit_by_value = self.dlg.by(value="Test string").find() - self.assertEqual("edit1", edit_by_value.element_info.auto_id) - def test_is_visible(self): """Test is_visible method of a control""" button = self.dlg.by(class_name="Button", @@ -154,65 +146,10 @@ def test_is_dialog(self): self.assertEqual(button.is_dialog(), False) self.assertEqual(self.dlg.is_dialog(), True) - def test_move_window(self): - """Test move_window without any parameters""" - - # move_window with default parameters - prevRect = self.dlg.rectangle() - self.dlg.move_window() - self.assertEqual(prevRect, self.dlg.rectangle()) - - # move_window call for a not supported control - button = self.dlg.by(class_name="Button", name="OK") - self.assertRaises(AttributeError, button.move_window) - - # Make RECT stub to avoid import win32structures - Rect = collections.namedtuple('Rect', 'left top right bottom') - prev_rect = self.dlg.rectangle() - new_rect = Rect._make([i + 5 for i in prev_rect]) - - self.dlg.move_window( - new_rect.left, - new_rect.top, - new_rect.right - new_rect.left, - new_rect.bottom - new_rect.top - ) - time.sleep(0.1) - logger = ActionLogger() - logger.log("prev_rect = %s", prev_rect) - logger.log("new_rect = %s", new_rect) - logger.log("self.dlg.rectangle() = %s", self.dlg.rectangle()) - self.assertEqual(self.dlg.rectangle(), new_rect) - - self.dlg.move_window(prev_rect) - self.assertEqual(self.dlg.rectangle(), prev_rect) - - def test_close(self): - """Test close method of a control""" - wrp = self.dlg.find() - - # mock a failure in get_elem_interface() method only for 'Window' param - orig_get_elem_interface = uia_defs.get_elem_interface - with mock.patch.object(uia_defs, 'get_elem_interface') as mock_get_iface: - def side_effect(elm_info, ptrn_name): - if ptrn_name == "Window": - raise uia_defs.NoPatternInterfaceError() - else: - return orig_get_elem_interface(elm_info, ptrn_name) - mock_get_iface.side_effect=side_effect - # also mock a failure in type_keys() method - with mock.patch.object(UIAWrapper, 'type_keys') as mock_type_keys: - exception_err = comtypes.COMError(-2147220991, 'An event was unable to invoke any of the subscribers', ()) - mock_type_keys.side_effect = exception_err - self.assertRaises(WindowNotFoundError, self.dlg.close) - - self.dlg.close() - self.assertEqual(self.dlg.exists(), False) - def test_parent(self): """Test getting a parent of a control""" - button = self.dlg.Alpha.find() - self.assertEqual(button.parent(), self.dlg.find()) + toolbar = self.dlg.Alpha.find() + self.assertEqual(toolbar.parent(), self.dlg.ToolBarTray.find()) def test_top_level_parent(self): """Test getting a top-level parent of a control""" @@ -251,8 +188,8 @@ def test_descendants_generator(self): def test_is_child(self): """Test is_child method of a control""" - button = self.dlg.Alpha.find() - self.assertEqual(button.is_child(self.dlg.find()), True) + toolbar = self.dlg.Alpha.find() + self.assertEqual(toolbar.is_child(self.dlg.ToolBarTray.find()), True) def test_equals(self): """Test controls comparisons""" @@ -262,47 +199,9 @@ def test_equals(self): self.assertEqual(button, button.element_info) self.assertEqual(button, button) - @unittest.skip("To be solved with issue #790") - def test_scroll(self): - """Test scroll""" - # Check an exception on a non-scrollable control - button = self.dlg.by(class_name="Button", - name="OK").find() - six.assertRaisesRegex(self, AttributeError, "not scrollable", - button.scroll, "left", "page") - - # Check an exception on a control without horizontal scroll bar - tab = self.dlg.Tree_and_List_Views.set_focus() - listview = tab.children(class_name=u"ListView")[0] - six.assertRaisesRegex(self, AttributeError, "not horizontally scrollable", - listview.scroll, "right", "line") - - # Check exceptions on wrong arguments - self.assertRaises(ValueError, listview.scroll, "bbbb", "line") - self.assertRaises(ValueError, listview.scroll, "up", "aaaa") - - # Store a cell position - cell = listview.cell(3, 0) - orig_rect = cell.rectangle() - self.assertEqual(orig_rect.left > 0, True) - - # Trigger a horizontal scroll bar on the control - hdr = listview.get_header_control() - hdr_itm = hdr.children()[1] - trf = hdr_itm.iface_transform - trf.resize(1000, 20) - listview.scroll("right", "page", 2) - self.assertEqual(cell.rectangle().left < 0, True) - - # Check an exception on a control without vertical scroll bar - tab = self.dlg.ListBox_and_Grid.set_focus() - datagrid = tab.children(class_name=u"DataGrid")[0] - six.assertRaisesRegex(self, AttributeError, "not vertically scrollable", - datagrid.scroll, "down", "page") - def test_is_keyboard_focusable(self): """Test is_keyboard focusable method of several controls""" - edit = self.dlg.TestLabelEdit.find() + edit = self.dlg.by(auto_id='edit1').find() label = self.dlg.TestLabel.find() button = self.dlg.by(class_name="Button", name="OK").find() @@ -312,14 +211,10 @@ def test_is_keyboard_focusable(self): def test_set_focus(self): """Test setting a keyboard focus on a control""" - edit = self.dlg.TestLabelEdit.find() + edit = self.dlg.by(auto_id='edit1').find() edit.set_focus() self.assertEqual(edit.has_keyboard_focus(), True) - def test_get_active_desktop_uia(self): - focused_element = Desktop(backend="uia").get_active() - self.assertTrue(type(focused_element) is UIAWrapper or issubclass(type(focused_element), UIAWrapper)) - def test_type_keys(self): """Test sending key types to a control""" edit = self.dlg.TestLabelEdit.find() @@ -336,20 +231,6 @@ def test_type_keys(self): edit.type_keys("y") self.assertEqual(edit.window_text(), "testTy") - def test_minimize_maximize(self): - """Test window minimize/maximize operations""" - wrp = self.dlg.minimize() - self.dlg.wait_not('active') - self.assertEqual(wrp.is_minimized(), True) - wrp.maximize() - self.dlg.wait('active') - self.assertEqual(wrp.is_maximized(), True) - wrp.minimize() - self.dlg.wait_not('active') - wrp.restore() - self.dlg.wait('active') - self.assertEqual(wrp.is_normal(), True) - def test_capture_as_image_multi_monitor(self): with mock.patch('win32api.EnumDisplayMonitors') as mon_device: mon_device.return_value = (1, 2) @@ -358,13 +239,6 @@ def test_capture_as_image_multi_monitor(self): result = self.dlg.capture_as_image().size self.assertEqual(expected, result) - def test_set_value(self): - """Test for UIAWrapper.set_value""" - edit = self.dlg.by(control_type='Edit', auto_id='edit1').find() - self.assertEqual(edit.get_value(), '') - edit.set_value('test') - self.assertEqual(edit.get_value(), 'test') - class WPFWrapperMouseTests(unittest.TestCase): @@ -378,10 +252,10 @@ def setUp(self): self.app = self.app.start(wpf_app_1) dlg = self.app.WPFSampleApplication - self.button = dlg.by(class_name="System.Windows.Controls.Button", + self.button = dlg.by(class_name="Button", name="OK").find() - self.label = dlg.by(class_name="System.Windows.Controls.Label", name="TestLabel").find() + self.label = dlg.by(class_name="Label", name="TestLabel").find() self.app.wait_cpu_usage_lower(threshold=1.5, timeout=30, usage_interval=1.0) def tearDown(self): diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index ccaa4c505..87c49e9de 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -52,14 +52,14 @@ def auto_id(self): """Return AutomationId of the element""" if self._element == 0: return '' - return self.get_field('Name') or '' + return self.get_property('Name') or '' @property def name(self): if self._element == 0: return '--root--' # TODO rewrite as action to avoid: "System.Windows.Controls.Label: ListBox and Grid" - val = self.get_field('Title') or self.get_field('Header') or self.get_field('Content') + val = self.get_property('Title') or self.get_property('Header') or self.get_property('Content') return val or '' @property @@ -95,13 +95,13 @@ def class_name(self): def enabled(self): if self._element == 0: return True - return self.get_field('IsEnabled') or False + return self.get_property('IsEnabled') or False @property def visible(self): if self._element == 0: return True - return self.get_field('IsVisible') or False + return self.get_property('IsVisible') or False @property def parent(self): @@ -116,6 +116,8 @@ def children(self, **kwargs): @property def control_type(self): """Return control type of element""" + if self._element == 0: + return None reply = ConnectionManager().call_action('GetControlType', self._pid, element_id=self._element) return reply['value'] @@ -158,7 +160,7 @@ def rectangle(self): def dump_window(self): return dumpwindow(self.handle) - def get_field(self, name, error_if_not_exists=False): + def get_property(self, name, error_if_not_exists=False): try: reply = ConnectionManager().call_action('GetProperty', self._pid, element_id=self._element, name=name) return reply['value'] @@ -179,4 +181,16 @@ def __eq__(self, other): def __ne__(self, other): """Check if 2 WPFElementInfo objects describe 2 different elements""" - return not (self == other) \ No newline at end of file + return not (self == other) + + @classmethod + def get_active(cls, app): + """Return current active element""" + try: + reply = ConnectionManager().call_action('GetFocusedElement', app.process) + if reply['value'] > 0: + return cls(reply['value'], pid=app.process) + else: + return None + except UnsupportedActionError: + return None \ No newline at end of file From 2832566d5aee502e6acfa01e5de329c91892ffdc Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 16 Apr 2022 11:53:24 +0300 Subject: [PATCH 18/59] make WPFWrapper tests green --- pywinauto/controls/wpfwrapper.py | 10 +++++++-- pywinauto/unittests/test_wpfwrapper.py | 28 +++++++++++++------------- pywinauto/windows/wpf_element_info.py | 17 +++++++++++++--- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index c13844322..cc55b1690 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -65,11 +65,14 @@ def __init__(self, element_info): def get_property(self, name, error_if_not_exists=False): return self.element_info.get_property(name, error_if_not_exists) + def automation_id(self): + """Return the Automation ID of the control""" + return self.element_info.auto_id + def is_keyboard_focusable(self): """Return True if the element can be focused with keyboard""" return self.get_property('Focusable') or False - # ----------------------------------------------------------- def has_keyboard_focus(self): """Return True if the element is focused with keyboard""" return self.get_property('IsKeyboardFocused') or False @@ -79,7 +82,10 @@ def set_focus(self): element_id=self.element_info.runtime_id) return self - + def get_active(self): + """Return wrapper object for current active element""" + element_info = self.backend.element_info_class.get_active(self.element_info._pid) + return self.backend.generic_wrapper_class(element_info) backend.register('wpf', WPFElementInfo, WPFWrapper) \ No newline at end of file diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index b5de357a4..7bb4c6ab8 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -105,7 +105,8 @@ def test_window_text(self): """Test getting the window Text of the dialog""" label = self.dlg.TestLabel.find() self.assertEqual(label.window_text(), u"TestLabel") - self.assertEqual(label.can_be_label, True) + # TODO + # self.assertEqual(label.can_be_label, True) def test_control_id(self): """Test getting control ID""" @@ -117,8 +118,8 @@ def test_automation_id(self): """Test getting automation ID""" alpha_toolbar = self.dlg.by(name="Alpha", control_type="ToolBar") button = alpha_toolbar.by(control_type="Button", - auto_id="OverflowButton").find() - self.assertEqual(button.automation_id(), "OverflowButton") + auto_id="toolbar_button1").find() + self.assertEqual(button.automation_id(), "toolbar_button1") def test_is_visible(self): """Test is_visible method of a control""" @@ -163,23 +164,22 @@ def test_texts(self): def test_children(self): """Test getting children of a control""" - button = self.dlg.by(class_name="Button", - name="OK").find() - self.assertEqual(len(button.children()), 1) - self.assertEqual(button.children()[0].class_name(), "TextBlock") + tab_ctrl = self.dlg.by(class_name="TabControl").find() + self.assertEqual(len(tab_ctrl.children()), 3) + self.assertEqual(tab_ctrl.children()[0].class_name(), "TabItem") def test_children_generator(self): """Test iterating children of a control""" - button = self.dlg.by(class_name="Button", name="OK").find() - children = [child for child in button.iter_children()] - self.assertEqual(len(children), 1) - self.assertEqual(children[0].class_name(), "TextBlock") + tab_ctrl = self.dlg.by(class_name="TabControl").find() + children = [child for child in tab_ctrl.iter_children()] + self.assertEqual(len(children), 3) + self.assertEqual(children[0].class_name(), "TabItem") def test_descendants(self): """Test iterating descendants of a control""" - toolbar = self.dlg.by(name="Alpha", control_type="ToolBar").find() + toolbar = self.dlg.by(class_name="RichTextBox").find() descendants = toolbar.descendants() - self.assertEqual(len(descendants), 7) + self.assertEqual(len(descendants), 11) def test_descendants_generator(self): toolbar = self.dlg.by(name="Alpha", control_type="ToolBar").find() @@ -217,7 +217,7 @@ def test_set_focus(self): def test_type_keys(self): """Test sending key types to a control""" - edit = self.dlg.TestLabelEdit.find() + edit = self.dlg.by(auto_id='edit1').find() edit.type_keys("t") self.assertEqual(edit.window_text(), "t") edit.type_keys("e") diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 87c49e9de..8604368e5 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -7,6 +7,7 @@ from pywinauto.handleprops import dumpwindow, controlid from pywinauto.element_info import ElementInfo +from .application import Application from .win32structures import RECT from .injected.api import * @@ -64,6 +65,8 @@ def name(self): @property def rich_text(self): + if self.control_type=='Edit': + return self.get_property('Text') or '' return self.name @property @@ -184,12 +187,20 @@ def __ne__(self, other): return not (self == other) @classmethod - def get_active(cls, app): + def get_active(cls, app_or_pid): """Return current active element""" + if isinstance(app_or_pid, integer_types): + pid = app_or_pid + elif isinstance(handle_or_elem, Application): + pid = app.process + else: + raise TypeError("UIAElementInfo object can be initialized " + \ + "with integer or IUIAutomationElement instance only!") + try: - reply = ConnectionManager().call_action('GetFocusedElement', app.process) + reply = ConnectionManager().call_action('GetFocusedElement', pid) if reply['value'] > 0: - return cls(reply['value'], pid=app.process) + return cls(reply['value'], pid=pid) else: return None except UnsupportedActionError: From aa9434aaa007da202bfd594900c7c705c2fb6c2c Mon Sep 17 00:00:00 2001 From: eltio Date: Mon, 18 Apr 2022 14:49:49 +0300 Subject: [PATCH 19/59] use GetName action and update submodule --- .gitmodules | 2 +- pywinauto/windows/injected | 2 +- pywinauto/windows/wpf_element_info.py | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.gitmodules b/.gitmodules index 081946d1b..8275e83de 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "pywinauto/windows/injected"] path = pywinauto/windows/injected url = https://github.com/eltimen/injected.git - branch = dotnet + branch = wrappers diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index e06d3a622..55b472263 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit e06d3a622ee7cfbff138e1e2529e70379aa659c2 +Subproject commit 55b4722632136069920a85b58ce96b973fa2b95c diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 8604368e5..a36ff65ae 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -59,9 +59,8 @@ def auto_id(self): def name(self): if self._element == 0: return '--root--' - # TODO rewrite as action to avoid: "System.Windows.Controls.Label: ListBox and Grid" - val = self.get_property('Title') or self.get_property('Header') or self.get_property('Content') - return val or '' + reply = ConnectionManager().call_action('GetName', self._pid, element_id=self._element) + return reply['value'] @property def rich_text(self): From ab377039fe17fe4bc72eca3fa0c9868d82a7ab54 Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 19 Apr 2022 23:23:22 +0300 Subject: [PATCH 20/59] add WindowWrapper --- pywinauto/controls/wpf_controls.py | 78 ++++++++++++++++ pywinauto/controls/wpfwrapper.py | 122 ++++++++++++++++++++++++- pywinauto/unittests/test_wpfwrapper.py | 80 ++++++++++++++++ pywinauto/windows/injected | 2 +- pywinauto/windows/wpf_element_info.py | 8 +- 5 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 pywinauto/controls/wpf_controls.py diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py new file mode 100644 index 000000000..900fb031e --- /dev/null +++ b/pywinauto/controls/wpf_controls.py @@ -0,0 +1,78 @@ +"""Wrap various WPF windows controls. To be used with 'wpf' backend.""" +import locale +import time +import comtypes +import six + +from . import wpfwrapper +from . import win32_controls +from . import common_controls +from .. import findbestmatch +from .. import timings +from ..windows.wpf_element_info import WPFElementInfo +from ..windows.injected.api import * + + +# ==================================================================== +class WindowWrapper(wpfwrapper.WPFWrapper): + + """Wrap a WPF Window control""" + + _control_types = ['Window'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(WindowWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def move_window(self, x=None, y=None, width=None, height=None): + """Move the window to the new coordinates + + * **x** Specifies the new left position of the window. + Defaults to the current left position of the window. + * **y** Specifies the new top position of the window. + Defaults to the current top position of the window. + * **width** Specifies the new width of the window. + Defaults to the current width of the window. + * **height** Specifies the new height of the window. + Defaults to the current height of the window. + """ + cur_rect = self.rectangle() + + # if no X is specified - so use current coordinate + if x is None: + x = cur_rect.left + else: + try: + y = x.top + width = x.width() + height = x.height() + x = x.left + except AttributeError: + pass + + # if no Y is specified - so use current coordinate + if y is None: + y = cur_rect.top + + # if no width is specified - so use current width + if width is None: + width = cur_rect.width() + + # if no height is specified - so use current height + if height is None: + height = cur_rect.height() + + # ask for the window to be moved + self.set_property('Left', x) + self.set_property('Top', y) + self.set_property('Width', width) + self.set_property('Height', height) + + time.sleep(timings.Timings.after_movewindow_wait) + + # ----------------------------------------------------------- + def is_dialog(self): + """Window is always a dialog so return True""" + return True diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index cc55b1690..56ebb05bb 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -65,6 +65,13 @@ def __init__(self, element_info): def get_property(self, name, error_if_not_exists=False): return self.element_info.get_property(name, error_if_not_exists) + def set_property(self, name, value, is_enum=False): + ConnectionManager().call_action('SetProperty', self.element_info.pid, + element_id=self.element_info.runtime_id, + name=name, + value=value, is_enum=is_enum) + return self + def automation_id(self): """Return the Automation ID of the control""" return self.element_info.auto_id @@ -78,14 +85,125 @@ def has_keyboard_focus(self): return self.get_property('IsKeyboardFocused') or False def set_focus(self): - reply = ConnectionManager().call_action('SetFocus', self.element_info.pid, - element_id=self.element_info.runtime_id) + ConnectionManager().call_action('SetFocus', self.element_info.pid, + element_id=self.element_info.runtime_id) return self def get_active(self): """Return wrapper object for current active element""" element_info = self.backend.element_info_class.get_active(self.element_info._pid) + if element_info is None: + return None return self.backend.generic_wrapper_class(element_info) + def is_active(self): + """Whether the window is active or not""" + focused_wrap = self.get_active() + if focused_wrap is None: + return False + return (focused_wrap.top_level_parent() == self.top_level_parent()) + + + # System.Windows.WindowState enum + NORMAL=0 + MAXIMIZED=1 + MINIMIZED=2 + + # ----------------------------------------------------------- + def close(self): + """ + Close the window + """ + ConnectionManager().call_action('InvokeMethod', self.element_info.pid, + element_id=self.element_info.runtime_id, + name='Close') + + # ----------------------------------------------------------- + def minimize(self): + """ + Minimize the window + """ + self.set_property('WindowState', 'Minimized', is_enum=True) + return self + + # ----------------------------------------------------------- + def maximize(self): + """ + Maximize the window + + Only controls supporting Window pattern should answer + """ + self.set_property('WindowState', 'Maximized', is_enum=True) + return self + + # ----------------------------------------------------------- + def restore(self): + """ + Restore the window to normal size + + Only controls supporting Window pattern should answer + """ + # it's very strange, but set WindowState to Normal is not enough... + self.set_property('WindowState', 'Normal', is_enum=True) + restore_rect = self.get_property('RestoreBounds') + self.move_window(restore_rect['left'], + restore_rect['top'], + self.get_property('Width'), + self.get_property('Height')) + self.set_property('WindowState', 'Normal', is_enum=True) + return self + + # ----------------------------------------------------------- + def get_show_state(self): + """Get the show state and Maximized/minimzed/restored state + + Returns values as following + + Normal = 0 + Maximized = 1 + Minimized = 2 + """ + val = self.element_info.get_property('WindowState') + if val == 'Normal': + return self.NORMAL + elif val == 'Maximized': + return self.MAXIMIZED + elif val == 'Minimized': + return self.MINIMIZED + else: + raise ValueError('Unexpected WindowState property value: ' + str(val)) + + # ----------------------------------------------------------- + def is_minimized(self): + """Indicate whether the window is minimized or not""" + return self.get_show_state() == self.MINIMIZED + + # ----------------------------------------------------------- + def is_maximized(self): + """Indicate whether the window is maximized or not""" + return self.get_show_state() == self.MAXIMIZED + + # ----------------------------------------------------------- + def is_normal(self): + """Indicate whether the window is normal (i.e. not minimized and not maximized)""" + return self.get_show_state() == self.NORMAL + + def move_window(self, x=None, y=None, width=None, height=None): + """Move the window to the new coordinates + The method should be implemented explicitly by controls that + support this action. The most obvious is the Window control. + Otherwise the method throws AttributeError + + * **x** Specifies the new left position of the window. + Defaults to the current left position of the window. + * **y** Specifies the new top position of the window. + Defaults to the current top position of the window. + * **width** Specifies the new width of the window. Defaults to the + current width of the window. + * **height** Specifies the new height of the window. Default to the + current height of the window. + """ + raise AttributeError("This method is not supported for {0}".format(self)) + backend.register('wpf', WPFElementInfo, WPFWrapper) \ No newline at end of file diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 7bb4c6ab8..6998acae5 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -23,6 +23,8 @@ import pywinauto.controls.wpf_controls as wpf_ctls from pywinauto.controls.wpfwrapper import WPFWrapper from pywinauto.windows.wpf_element_info import WPFElementInfo +from pywinauto.windows.injected.api import InjectedTargetError +from pywinauto.windows.injected.channel import BrokenPipeError wpf_samples_folder = os.path.join( os.path.dirname(__file__), r"..\..\apps\WPF_samples") @@ -278,5 +280,83 @@ def test_right_click_input(self): self.assertEqual(self.label.window_text(), "RightClick") +class WindowWrapperTests(unittest.TestCase): + + """Unit tests for the WPFWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + mouse.move((-500, 500)) # remove the mouse from the screen to avoid side effects + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_close(self): + """Test close method of a control""" + wrp = self.dlg.find() + wrp.close() + + try: + # process can be in the termination state at this moment + self.assertEqual(self.dlg.exists(), False) + except (InjectedTargetError, BrokenPipeError): + pass + + def test_move_window(self): + """Test move_window without any parameters""" + + # move_window with default parameters + prevRect = self.dlg.rectangle() + self.dlg.move_window() + self.assertEqual(prevRect, self.dlg.rectangle()) + + # move_window call for a not supported control + button = self.dlg.by(class_name="Button", name="OK") + self.assertRaises(AttributeError, button.move_window) + + # Make RECT stub to avoid import win32structures + Rect = collections.namedtuple('Rect', 'left top right bottom') + prev_rect = self.dlg.rectangle() + new_rect = Rect._make([i + 5 for i in prev_rect]) + + self.dlg.move_window( + new_rect.left, + new_rect.top, + new_rect.right - new_rect.left, + new_rect.bottom - new_rect.top + ) + time.sleep(0.1) + logger = ActionLogger() + logger.log("prev_rect = %s", prev_rect) + logger.log("new_rect = %s", new_rect) + logger.log("self.dlg.rectangle() = %s", self.dlg.rectangle()) + self.assertEqual(self.dlg.rectangle(), new_rect) + + self.dlg.move_window(prev_rect) + self.assertEqual(self.dlg.rectangle(), prev_rect) + + def test_minimize_maximize(self): + """Test window minimize/maximize operations""" + wrp = self.dlg.minimize() + self.dlg.wait_not('active') + self.assertEqual(wrp.is_minimized(), True) + wrp.maximize() + self.dlg.wait('active') + self.assertEqual(wrp.is_maximized(), True) + wrp.minimize() + self.dlg.wait_not('active') + wrp.restore() + self.dlg.wait('active') + self.assertEqual(wrp.is_normal(), True) + + if __name__ == "__main__": unittest.main() diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 55b472263..d526b3da7 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 55b4722632136069920a85b58ce96b973fa2b95c +Subproject commit d526b3da75241b475bf379e4e35abcd81f8b9d1e diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index a36ff65ae..dd0ac0777 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -153,10 +153,10 @@ def rectangle(self): rect = RECT() if self._element != 0: reply = ConnectionManager().call_action('GetRectangle', self._pid, element_id=self._element) - rect.left = reply['left'] - rect.right = reply['right'] - rect.top = reply['top'] - rect.bottom = reply['bottom'] + rect.left = reply['value']['left'] + rect.right = reply['value']['right'] + rect.top = reply['value']['top'] + rect.bottom = reply['value']['bottom'] return rect def dump_window(self): From beb034e6bf4dd91e87a8b7f1abf387131e244e28 Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 21 Apr 2022 15:07:14 +0300 Subject: [PATCH 21/59] add buttonwrapper --- pywinauto/controls/wpf_controls.py | 112 +++++++++++++++++++++++++ pywinauto/controls/wpfwrapper.py | 15 +++- pywinauto/unittests/test_wpfwrapper.py | 72 ++++++++++++++++ 3 files changed, 196 insertions(+), 3 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 900fb031e..cf3c6fa6c 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -76,3 +76,115 @@ def move_window(self, x=None, y=None, width=None, height=None): def is_dialog(self): """Window is always a dialog so return True""" return True + + +class ButtonWrapper(wpfwrapper.WPFWrapper): + + """Wrap a UIA-compatible Button, CheckBox or RadioButton control""" + + _control_types = ['Button', + 'CheckBox', + 'RadioButton', + ] + + UNCHECKED = 0 + CHECKED = 1 + INDETERMINATE = 2 + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(ButtonWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def toggle(self): + """ + An interface to Toggle method of the Toggle control pattern. + + Control supporting the Toggle pattern cycles through its + toggle states in the following order: + ToggleState_On, ToggleState_Off and, + if supported, ToggleState_Indeterminate + + Usually applied for the check box control. + + The radio button control does not implement IToggleProvider, + because it is not capable of cycling through its valid states. + Toggle a state of a check box control. (Use 'select' method instead) + Notice, a radio button control isn't supported by UIA. + https://msdn.microsoft.com/en-us/library/windows/desktop/ee671290(v=vs.85).aspx + """ + + current_state = self.get_property('IsChecked') + if self.get_property('IsThreeState'): + states = (True, False, None) + else: + states = (True, False) + + if current_state is None and not self.get_property('IsThreeState'): + next_state = False + else: + next_state = states[(states.index(current_state)+1) % len(states)] + + self.set_property('IsChecked', next_state) + + name = self.element_info.name + control_type = self.element_info.control_type + + if name and control_type: + self.actions.log('Toggled ' + control_type.lower() + ' "' + name + '"') + # Return itself so that action can be chained + return self + + # ----------------------------------------------------------- + def get_toggle_state(self): + """ + Get a toggle state of a check box control. + + The toggle state is represented by an integer + 0 - unchecked + 1 - checked + 2 - indeterminate + """ + val = self.get_property('IsChecked') + if val is None: + return self.INDETERMINATE + return self.CHECKED if val else self.UNCHECKED + + + # ----------------------------------------------------------- + def is_dialog(self): + """Buttons are never dialogs so return False""" + return False + + # ----------------------------------------------------------- + def click(self): + """Click the Button control by raising the ButtonBase.Click event""" + self.raise_event('Click') + # Return itself so that action can be chained + return self + + def select(self): + """Select the item + + Usually applied for controls like: a radio button, a tree view item + or a list item. + """ + self.set_property('IsChecked', True) + + 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. + + Usually applied for controls like: a radio button, a tree view item, + a list item. + """ + return self.get_property('IsChecked') diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index 56ebb05bb..95eabf4b1 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -72,6 +72,17 @@ def set_property(self, name, value, is_enum=False): value=value, is_enum=is_enum) return self + def invoke_method(self, name): + ConnectionManager().call_action('InvokeMethod', self.element_info.pid, + element_id=self.element_info.runtime_id, + name=name) + return self + + def raise_event(self, name): + ConnectionManager().call_action('RaiseEvent', self.element_info.pid, + element_id=self.element_info.runtime_id, + name=name) + def automation_id(self): """Return the Automation ID of the control""" return self.element_info.auto_id @@ -114,9 +125,7 @@ def close(self): """ Close the window """ - ConnectionManager().call_action('InvokeMethod', self.element_info.pid, - element_id=self.element_info.runtime_id, - name='Close') + self.invoke_method('Close') # ----------------------------------------------------------- def minimize(self): diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 6998acae5..2fbb63aa1 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -358,5 +358,77 @@ def test_minimize_maximize(self): self.assertEqual(wrp.is_normal(), True) +class ButtonWrapperTests(unittest.TestCase): + + """Unit tests for the WPF controls inherited from ButtonBase""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_check_box(self): + """Test 'toggle' and 'toggle_state' for the check box control""" + # Get a current state of the check box control + check_box = self.dlg.CheckBox.find() + cur_state = check_box.get_toggle_state() + self.assertEqual(cur_state, wpf_ctls.ButtonWrapper.INDETERMINATE) + + # Toggle the next state + cur_state = check_box.toggle().get_toggle_state() + + # Get a new state of the check box control + self.assertEqual(cur_state, wpf_ctls.ButtonWrapper.UNCHECKED) + + cur_state = check_box.select().get_toggle_state() + self.assertEqual(cur_state, wpf_ctls.ButtonWrapper.CHECKED) + + def test_toggle_button(self): + """Test 'toggle' and 'toggle_state' for the toggle button control""" + # Get a current state of the check box control + button = self.dlg.ToggleMe.find() + cur_state = button.get_toggle_state() + self.assertEqual(cur_state, button.CHECKED) + + # Toggle the next state + cur_state = button.toggle().get_toggle_state() + + # Get a new state of the check box control + self.assertEqual(cur_state, button.UNCHECKED) + + # Toggle the next state + cur_state = button.toggle().get_toggle_state() + self.assertEqual(cur_state, button.CHECKED) + + def test_button_click(self): + """Test the click method for the Button control""" + label = self.dlg.by(control_type="Text", + name="TestLabel").find() + self.dlg.Apply.click() + self.assertEqual(label.window_text(), "ApplyClick") + + def test_radio_button(self): + """Test 'select' and 'is_selected' for the radio button control""" + yes = self.dlg.Yes.find() + cur_state = yes.is_selected() + self.assertEqual(cur_state, False) + + cur_state = yes.select().is_selected() + self.assertEqual(cur_state, True) + + no = self.dlg.No.find() + cur_state = no.select().is_selected() + self.assertEqual(cur_state, True) + + if __name__ == "__main__": unittest.main() From effd80f64ffe18caecc13fe3c296ac6af9213d2d Mon Sep 17 00:00:00 2001 From: eltio Date: Fri, 22 Apr 2022 00:58:16 +0300 Subject: [PATCH 22/59] add ComboBox wrapper --- pywinauto/controls/wpf_controls.py | 91 ++++++++++++++++++++++++++ pywinauto/unittests/test_wpfwrapper.py | 78 ++++++++++++++++++++++ 2 files changed, 169 insertions(+) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index cf3c6fa6c..df05e306e 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -188,3 +188,94 @@ def is_selected(self): a list item. """ return self.get_property('IsChecked') + +class ComboBoxWrapper(wpfwrapper.WPFWrapper): + + """Wrap a UIA CoboBox control""" + + _control_types = ['ComboBox'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(ComboBoxWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def expand(self): + self.set_property('IsDropDownOpen', True) + return self + + # ----------------------------------------------------------- + def collapse(self): + self.set_property('IsDropDownOpen', False) + return self + + # ----------------------------------------------------------- + def is_editable(self): + return self.get_property('IsEditable') + + # ----------------------------------------------------------- + def is_expanded(self): + """Test if the control is expanded""" + return self.get_property('IsDropDownOpen') + + # ----------------------------------------------------------- + def is_collapsed(self): + """Test if the control is collapsed""" + return not self.get_property('IsDropDownOpen') + + # ----------------------------------------------------------- + def texts(self): + """Return the text of the items in the combobox""" + return [child.element_info.rich_text for child in self.iter_children()] + + # ----------------------------------------------------------- + def select(self, item): + """ + Select the ComboBox item + + The item can be either a 0 based index of the item to select + or it can be the string that you want to select + """ + if isinstance(item, six.integer_types): + self.set_property('SelectedIndex', item) + else: + index = None + for i, child in enumerate(self.iter_children()): + if child.element_info.rich_text == item: + index = 1 + if index is None: + raise ValueError('no such item: {}'.format(item)) + self.set_property('SelectedIndex', index) + return self + + # ----------------------------------------------------------- + # TODO: add selected_texts for a combobox with a multi-select support + def selected_text(self): + """ + Return the selected text or None + + Notice, that in case of multi-select it will be only the text from + a first selected item + """ + selected_index = self.get_property('SelectedIndex') + if selected_index == -1: + return '' + return self.children()[selected_index].element_info.rich_text + + # ----------------------------------------------------------- + # TODO: add selected_indices for a combobox with multi-select support + def selected_index(self): + """Return the selected index""" + return self.get_property('SelectedIndex') + + + # ----------------------------------------------------------- + def item_count(self): + """ + Return the number of items in the combobox + + The interface is kept mostly for a backward compatibility with + the native ComboBox interface + """ + return len(self.children()) \ No newline at end of file diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 2fbb63aa1..f33107292 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -429,6 +429,84 @@ def test_radio_button(self): cur_state = no.select().is_selected() self.assertEqual(cur_state, True) +class ComboBoxTests(unittest.TestCase): + + """Unit tests for the ComboBoxWrapper class with WPF app""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + + self.dlg = self.app.WPFSampleApplication + self.combo = self.dlg.by(control_type="ComboBox").find() + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_expand_collapse(self): + """Test methods .expand() and .collapse() for WPF combo box""" + self.dlg.set_focus() + + combo = self.combo + combo_name = self.combo.element_info.name + + self.assertFalse(combo.is_expanded(), + msg='{} combo box must be collapsed initially'.format(combo_name)) + + # test that method allows chaining + self.assertEqual(combo.expand(), combo, + msg='Method .expand() for {} combo box must return self'.format(combo_name)) + self.assertTrue(combo.is_expanded(), + msg='{} combo box has not been expanded!'.format(combo_name)) + + # .expand() keeps already expanded state (and still allows chaining) + self.assertEqual(combo.expand(), combo, + msg='Method .expand() for {} combo box must return self, always!'.format(combo_name)) + self.assertTrue(combo.is_expanded(), + msg='{} combo box does NOT keep expanded state!'.format(combo_name)) + + # collapse + self.assertEqual(combo.collapse(), combo, + msg='Method .collapse() for {} combo box must return self'.format(combo_name)) + self.assertFalse(combo.is_expanded(), + msg='{} combo box has not been collapsed!'.format(combo_name)) + + # collapse already collapsed should keep collapsed state + self.assertEqual(combo.collapse(), combo, + msg='Method .collapse() for {} combo box must return self, always!'.format(combo_name)) + self.assertFalse(combo.is_expanded(), + msg='{} combo box does NOT keep collapsed state!'.format(combo_name)) + + def test_texts(self): + """Test method .texts() for WPF combo box""" + self.dlg.set_focus() + texts = [u'Combo Item 1', u'Combo Item 2'] + + self.assertEqual(self.combo.texts(), texts) + self.assertEqual(self.combo.expand().texts(), texts) + self.assertTrue(self.combo.is_expanded()) + + def test_select(self): + """Test method .select() for WPF combo box""" + self.dlg.set_focus() + + self.assertEqual(self.combo.selected_index(), -1) # nothing selected + self.combo.select(u'Combo Item 2') + self.assertEqual(self.combo.selected_text(), u'Combo Item 2') + self.assertEqual(self.combo.selected_index(), 1) + self.combo.select(0) + self.assertEqual(self.combo.selected_text(), u'Combo Item 1') + self.assertEqual(self.combo.selected_index(), 0) + + def test_item_count(self): + """Test method .item_count() for WPF combo box""" + self.dlg.set_focus() + self.assertEqual(self.combo.item_count(), 2) if __name__ == "__main__": unittest.main() From 28bee81af7aa533bfe48968c861bc17c82eb8215 Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 23 Apr 2022 10:17:13 +0300 Subject: [PATCH 23/59] fix ImportError on Python 2.7 --- pywinauto/windows/wpf_element_info.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index dd0ac0777..73c6155b0 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -7,7 +7,6 @@ from pywinauto.handleprops import dumpwindow, controlid from pywinauto.element_info import ElementInfo -from .application import Application from .win32structures import RECT from .injected.api import * @@ -188,6 +187,8 @@ def __ne__(self, other): @classmethod def get_active(cls, app_or_pid): """Return current active element""" + from .application import Application + if isinstance(app_or_pid, integer_types): pid = app_or_pid elif isinstance(handle_or_elem, Application): From 9ee962941bfa3de33c7809d14e9f262a4480b9e2 Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 23 Apr 2022 10:22:41 +0300 Subject: [PATCH 24/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index d526b3da7..9efeb1a20 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit d526b3da75241b475bf379e4e35abcd81f8b9d1e +Subproject commit 9efeb1a208e5cb605ac67ebf6e153240861e4281 From 7d95358165517e10f9c2225517ae529d413f1930 Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 26 Apr 2022 00:48:01 +0300 Subject: [PATCH 25/59] add EditWrapper --- pywinauto/controls/wpf_controls.py | 182 ++++++++++++++++++++++++- pywinauto/unittests/test_wpfwrapper.py | 92 +++++++++++++ 2 files changed, 271 insertions(+), 3 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index df05e306e..fc610f4c8 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -1,7 +1,6 @@ """Wrap various WPF windows controls. To be used with 'wpf' backend.""" import locale import time -import comtypes import six from . import wpfwrapper @@ -269,7 +268,6 @@ def selected_index(self): """Return the selected index""" return self.get_property('SelectedIndex') - # ----------------------------------------------------------- def item_count(self): """ @@ -278,4 +276,182 @@ def item_count(self): The interface is kept mostly for a backward compatibility with the native ComboBox interface """ - return len(self.children()) \ No newline at end of file + return len(self.children()) + + +class EditWrapper(wpfwrapper.WPFWrapper): + + """Wrap an Edit control""" + + _control_types = ['Edit'] + has_title = False + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(EditWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + @property + def writable_props(self): + """Extend default properties list.""" + props = super(EditWrapper, self).writable_props + props.extend(['selection_indices']) + return props + + # ----------------------------------------------------------- + def line_count(self): + """Return how many lines there are in the Edit""" + return self.window_text().count("\n") + 1 + + # ----------------------------------------------------------- + def line_length(self, line_index): + """Return how many characters there are in the line""" + # need to first get a character index of that line + lines = self.window_text().splitlines() + if line_index < len(lines): + return len(lines[line_index]) + elif line_index == self.line_count() - 1: + return 0 + else: + raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index)) + + # ----------------------------------------------------------- + def get_line(self, line_index): + """Return the line specified""" + lines = self.window_text().splitlines() + if line_index < len(lines): + return lines[line_index] + elif line_index == self.line_count() - 1: + return "" + else: + raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index)) + + # ----------------------------------------------------------- + def get_value(self): + """Return the current value of the element""" + return self.get_property('Text') or '' + + # ----------------------------------------------------------- + def is_editable(self): + """Return the edit possibility of the element""" + return not self.get_property('IsReadOnly') + + # ----------------------------------------------------------- + def texts(self): + """Get the text of the edit control""" + texts = [ self.get_line(i) for i in range(self.line_count()) ] + + return texts + + # ----------------------------------------------------------- + def text_block(self): + """Get the text of the edit control""" + return self.window_text() + + # ----------------------------------------------------------- + def selection_indices(self): + """The start and end indices of the current selection""" + start = self.get_property('SelectionStart') + end = start + self.get_property('SelectionLength') + + return start, end + + # ----------------------------------------------------------- + def set_window_text(self, text, append=False): + """Override set_window_text for edit controls because it should not be + used for Edit controls. + + Edit Controls should either use set_edit_text() or type_keys() to modify + the contents of the edit control. + """ + self.verify_actionable() + + if append: + text = self.window_text() + text + + self.set_property('Text', text) + + # ----------------------------------------------------------- + def set_edit_text(self, text, pos_start=None, pos_end=None): + """Set the text of the edit control""" + self.verify_actionable() + + # allow one or both of pos_start and pos_end to be None + if pos_start is not None or pos_end is not None: + # if only one has been specified - then set the other + # to the current selection start or end + start, end = self.selection_indices() + if pos_start is None: + pos_start = start + if pos_end is None and not isinstance(start, six.string_types): + pos_end = end + else: + pos_start = 0 + pos_end = len(self.window_text()) + + if isinstance(text, six.text_type): + if six.PY3: + aligned_text = text + else: + aligned_text = text.encode(locale.getpreferredencoding()) + elif isinstance(text, six.binary_type): + if six.PY3: + aligned_text = text.decode(locale.getpreferredencoding()) + else: + aligned_text = text + else: + # convert a non-string input + if six.PY3: + aligned_text = six.text_type(text) + else: + aligned_text = six.binary_type(text) + + # Calculate new text value + current_text = self.window_text() + new_text = current_text[:pos_start] + aligned_text + current_text[pos_end:] + + self.set_property('Text', new_text) + + # time.sleep(Timings.after_editsetedittext_wait) + + if isinstance(aligned_text, six.text_type): + self.actions.log('Set text to the edit box: ' + aligned_text) + else: + self.actions.log(b'Set text to the edit box: ' + aligned_text) + + # return this control so that actions can be chained. + return self + # set set_text as an alias to set_edit_text + set_text = set_edit_text + + # ----------------------------------------------------------- + def select(self, start=0, end=None): + """Set the edit selection of the edit control""" + self.verify_actionable() + self.set_focus() + + if isinstance(start, six.integer_types): + if isinstance(end, six.integer_types) and start > end: + start, end = end, start + elif end is None: + end = len(self.window_text()) + else: + # if we have been asked to select a string + if isinstance(start, six.text_type): + string_to_select = start + elif isinstance(start, six.binary_type): + string_to_select = start.decode(locale.getpreferredencoding()) + else: + raise ValueError('start and end should be integer or string') + + start = self.window_text().find(string_to_select) + if start < 0: + raise RuntimeError("Text '{0}' hasn't been found".format(string_to_select)) + end = start + len(string_to_select) + + self.set_property('SelectionStart', start) + self.set_property('SelectionLength', end-start) + + # return this control so that actions can be chained. + return self diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index f33107292..ccc154731 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -508,5 +508,97 @@ def test_item_count(self): self.dlg.set_focus() self.assertEqual(self.combo.item_count(), 2) + +class EditWrapperTests(unittest.TestCase): + + """Unit tests for the EditWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + + self.app = app + self.dlg = app.WPFSampleApplication + + self.edit = self.dlg.by(class_name="TextBox").find() + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_set_window_text(self): + """Test setting text value of control (the text in textbox itself)""" + text_to_set = "This test" + + self.edit.set_window_text(text_to_set) + self.assertEqual(self.edit.text_block(), text_to_set) + + self.edit.set_window_text(" is done", True) + self.assertEqual(self.edit.text_block(), text_to_set + " is done") + + def test_set_text(self): + """Test setting the text of the edit control""" + self.edit.set_edit_text("Some text") + self.assertEqual(self.edit.text_block(), "Some text") + + self.edit.set_edit_text(579) + self.assertEqual(self.edit.text_block(), "579") + + self.edit.set_edit_text(333, pos_start=1, pos_end=2) + self.assertEqual(self.edit.text_block(), "53339") + + def test_line_count(self): + """Test getting the line count of the edit control""" + self.edit.set_edit_text("Here is some text") + + self.assertEqual(self.edit.line_count(), 1) + + def test_get_line(self): + """Test getting each line of the edit control""" + test_data = "Here is some text" + self.edit.set_edit_text(test_data) + + self.assertEqual(self.edit.get_line(0), test_data) + + def test_get_value(self): + """Test getting value of the edit control""" + test_data = "Some value" + self.edit.set_edit_text(test_data) + + self.assertEqual(self.edit.get_value(), test_data) + + def test_text_block(self): + """Test getting the text block of the edit control""" + test_data = "Here is some text" + self.edit.set_edit_text(test_data) + + self.assertEqual(self.edit.text_block(), test_data) + + def test_select(self): + """Test selecting text in the edit control in various ways""" + self.edit.set_edit_text("Some text") + + self.edit.select(0, 0) + self.assertEqual((0, 0), self.edit.selection_indices()) + + self.edit.select() + self.assertEqual((0, 9), self.edit.selection_indices()) + + self.edit.select(1, 7) + self.assertEqual((1, 7), self.edit.selection_indices()) + + self.edit.select(5, 2) + self.assertEqual((2, 5), self.edit.selection_indices()) + + self.edit.select("me t") + self.assertEqual((2, 6), self.edit.selection_indices()) + + self.assertRaises(RuntimeError, self.edit.select, "123") + + if __name__ == "__main__": unittest.main() From 4f6cfd47cb0638e8ab5ef1d5ee89de9713c2df92 Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 26 Apr 2022 18:55:14 +0300 Subject: [PATCH 26/59] add tab control wrapper --- pywinauto/controls/wpf_controls.py | 43 ++++++++++++++++++++++++++ pywinauto/controls/wpfwrapper.py | 5 ++- pywinauto/unittests/test_wpfwrapper.py | 39 +++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index fc610f4c8..28dce7d39 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -188,6 +188,7 @@ def is_selected(self): """ return self.get_property('IsChecked') + class ComboBoxWrapper(wpfwrapper.WPFWrapper): """Wrap a UIA CoboBox control""" @@ -455,3 +456,45 @@ def select(self, start=0, end=None): # return this control so that actions can be chained. return self + + +class TabControlWrapper(wpfwrapper.WPFWrapper): + + """Wrap an UIA-compatible Tab control""" + + _control_types = ['Tab'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(TabControlWrapper, self).__init__(elem) + + # ---------------------------------------------------------------- + def get_selected_tab(self): + """Return an index of a selected tab""" + return self.get_property('SelectedIndex') + + # ---------------------------------------------------------------- + def tab_count(self): + """Return a number of tabs""" + return len(self.children()) + + # ---------------------------------------------------------------- + def select(self, item): + """Select a tab by index or by name""" + if isinstance(item, six.integer_types): + self.set_property('SelectedIndex', item) + else: + index = None + for i, child in enumerate(self.iter_children()): + if child.element_info.rich_text == item: + index = 1 + if index is None: + raise ValueError('no such item: {}'.format(item)) + self.set_property('SelectedIndex', index) + return self + + # ---------------------------------------------------------------- + def texts(self): + """Tabs texts""" + return self.children_texts() diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index 95eabf4b1..d574f80bb 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -102,7 +102,7 @@ def set_focus(self): def get_active(self): """Return wrapper object for current active element""" - element_info = self.backend.element_info_class.get_active(self.element_info._pid) + element_info = self.backend.element_info_class.get_active(self.element_info.pid) if element_info is None: return None return self.backend.generic_wrapper_class(element_info) @@ -114,6 +114,9 @@ def is_active(self): return False return (focused_wrap.top_level_parent() == self.top_level_parent()) + def children_texts(self): + """Get texts of the control's children""" + return [c.window_text() for c in self.children()] # System.Windows.WindowState enum NORMAL=0 diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index ccc154731..cc2954cc6 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -600,5 +600,44 @@ def test_select(self): self.assertRaises(RuntimeError, self.edit.select, "123") +class TabControlWrapperTests(unittest.TestCase): + + """Unit tests for the TabControlWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + dlg = app.WPFSampleApplication + + self.app = app + self.ctrl = dlg.by(class_name="TabControl").find() + self.texts = [u"General", u"Tree and List Views", u"ListBox and Grid"] + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_tab_count(self): + """Test the tab count in the Tab control""" + self.assertEqual(self.ctrl.tab_count(), len(self.texts)) + + def test_get_selected_tab(self): + """Test selecting a tab by index or by name and getting an index of the selected tab""" + # Select a tab by name, use chaining to get the index of the selected tab + idx = self.ctrl.select(u"Tree and List Views").get_selected_tab() + self.assertEqual(idx, 1) + # Select a tab by index + self.ctrl.select(0) + self.assertEqual(self.ctrl.get_selected_tab(), 0) + + def test_texts(self): + """Make sure the tabs captions are read correctly""" + self.assertEqual(self.ctrl.texts(), self.texts) + + if __name__ == "__main__": unittest.main() From f7500a5dc783e2bd77cff570e35d7484fc032b55 Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 26 Apr 2022 18:59:39 +0300 Subject: [PATCH 27/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 9efeb1a20..6e1279299 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 9efeb1a208e5cb605ac67ebf6e153240861e4281 +Subproject commit 6e12792997423c99771e11ff7bb297ecfd896939 From 6a34682b0619a11f9a832b71fea9630f14a9d79e Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 26 Apr 2022 22:29:42 +0300 Subject: [PATCH 28/59] add slider and toolbar wrappers --- pywinauto/controls/wpf_controls.py | 190 ++++++++++++++++++++++++- pywinauto/unittests/test_wpfwrapper.py | 109 ++++++++++++++ 2 files changed, 298 insertions(+), 1 deletion(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 28dce7d39..266658433 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -460,7 +460,7 @@ def select(self, start=0, end=None): class TabControlWrapper(wpfwrapper.WPFWrapper): - """Wrap an UIA-compatible Tab control""" + """Wrap an WPF Tab control""" _control_types = ['Tab'] @@ -498,3 +498,191 @@ def select(self, item): def texts(self): """Tabs texts""" return self.children_texts() + + +class SliderWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF Slider control""" + + _control_types = ['Slider'] + has_title = False + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(SliderWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def min_value(self): + """Get the minimum value of the Slider""" + return self.get_property('Minimum') + + # ----------------------------------------------------------- + def max_value(self): + """Get the maximum value of the Slider""" + return self.get_property('Maximum') + + # ----------------------------------------------------------- + def small_change(self): + """ + Get a small change of slider's thumb + + This change is achieved by pressing left and right arrows + when slider's thumb has keyboard focus. + """ + return self.get_property('SmallChange') + + # ----------------------------------------------------------- + def large_change(self): + """ + Get a large change of slider's thumb + + This change is achieved by pressing PgUp and PgDown keys + when slider's thumb has keyboard focus. + """ + return self.get_property('LargeChange') + + # ----------------------------------------------------------- + def value(self): + """Get a current position of slider's thumb""" + return self.get_property('Value') + + # ----------------------------------------------------------- + def set_value(self, value): + """Set position of slider's thumb""" + if isinstance(value, float): + value_to_set = value + elif isinstance(value, six.integer_types): + value_to_set = value + elif isinstance(value, six.text_type): + value_to_set = float(value) + else: + raise ValueError("value should be either string or number") + + min_value = self.min_value() + max_value = self.max_value() + if not (min_value <= value_to_set <= max_value): + raise ValueError("value should be bigger than {0} and smaller than {1}".format(min_value, max_value)) + + self.set_property('Value', value_to_set) + + +class ToolbarWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF ToolBar control + + The control's children usually are: Buttons, SplitButton, + MenuItems, ThumbControls, TextControls, Separators, CheckBoxes. + Notice that ToolTip controls are children of the top window and + not of the toolbar. + """ + + _control_types = ['ToolBar'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(ToolbarWrapper, self).__init__(elem) + self.win32_wrapper = None + if len(self.children()) <= 1 and self.element_info.handle is not None: + self.win32_wrapper = common_controls.ToolbarWrapper(self.element_info.handle) + + @property + def writable_props(self): + """Extend default properties list.""" + props = super(ToolbarWrapper, self).writable_props + props.extend(['button_count']) + return props + + # ---------------------------------------------------------------- + def texts(self): + """Return texts of the Toolbar""" + return [c.window_text() for c in self.buttons()] + + #---------------------------------------------------------------- + def button_count(self): + """Return a number of buttons on the ToolBar""" + return len(self.children()) + + # ---------------------------------------------------------------- + def buttons(self): + """Return all available buttons""" + return self.children() + + # ---------------------------------------------------------------- + def button(self, button_identifier, exact=True): + """Return a button by the specified identifier + + * **button_identifier** can be either an index of a button or + a string with the text of the button. + * **exact** flag specifies if the exact match for the text look up + has to be applied. + """ + cc = self.buttons() + texts = [c.window_text() for c in cc] + + if isinstance(button_identifier, six.string_types): + self.actions.log('Toolbar buttons: ' + str(texts)) + + if exact: + try: + button_index = texts.index(button_identifier) + except ValueError: + raise findbestmatch.MatchError(items=texts, tofind=button_identifier) + else: + # one of these will be returned for the matching text + indices = [i for i in range(0, len(texts))] + + # find which index best matches that text + button_index = findbestmatch.find_best_match(button_identifier, texts, indices) + else: + button_index = button_identifier + + return cc[button_index] + + # ---------------------------------------------------------------- + def check_button(self, button_identifier, make_checked, exact=True): + """Find where the button is and toggle it + + * **button_identifier** can be either an index of the button or + a string with the text on the button. + * **make_checked** specifies the required toggled state of the button. + If the button is already in the specified state the state isn't changed. + * **exact** flag specifies if the exact match for the text look up + has to be applied + """ + + self.actions.logSectionStart('Checking "' + self.window_text() + + '" toolbar button "' + str(button_identifier) + '"') + button = self.button(button_identifier, exact=exact) + if make_checked: + self.actions.log('Pressing down toolbar button "' + str(button_identifier) + '"') + else: + self.actions.log('Pressing up toolbar button "' + str(button_identifier) + '"') + + if not button.is_enabled(): + self.actions.log('Toolbar button is not enabled!') + raise RuntimeError("Toolbar button is not enabled!") + + res = (button.get_toggle_state() == ButtonWrapper.CHECKED) + if res != make_checked: + button.toggle() + + self.actions.logSectionEnd() + return button + + def collapse(self): + """Collapse overflow area of the ToolBar (IsOverflowOpen property)""" + self.set_property('IsOverflowOpen', False) + + def expand(self): + """Expand overflow area of the ToolBar (IsOverflowOpen property)""" + self.set_property('IsOverflowOpen', True) + + def is_expanded(self): + """Check if the ToolBar overflow area is currently visible""" + return not self.get_property('HasOverflowItems') or self.get_property('IsOverflowOpen') + + def is_collapsed(self): + """Check if the ToolBar overflow area is not visible""" + return not self.is_expanded() diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index cc2954cc6..9dbb4a3bd 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -639,5 +639,114 @@ def test_texts(self): self.assertEqual(self.ctrl.texts(), self.texts) +class SliderWrapperTests(unittest.TestCase): + + """Unit tests for the SliderWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + + self.app = app + self.dlg = app.WPFSampleApplication + + self.slider = self.dlg.by(class_name="Slider").find() + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_min_value(self): + """Test getting minimum value of the Slider""" + self.assertEqual(self.slider.min_value(), 0.0) + + def test_max_value(self): + """Test getting maximum value of the Slider""" + self.assertEqual(self.slider.max_value(), 100.0) + + def test_small_change(self): + """Test Getting small change of slider's thumb""" + self.assertEqual(self.slider.small_change(), 0.1) + + def test_large_change(self): + """Test Getting large change of slider's thumb""" + self.assertEqual(self.slider.large_change(), 1.0) + + def test_value(self): + """Test getting current position of slider's thumb""" + self.assertEqual(self.slider.value(), 70.0) + + def test_set_value(self): + """Test setting position of slider's thumb""" + self.slider.set_value(24) + self.assertEqual(self.slider.value(), 24.0) + + self.slider.set_value(33.3) + self.assertEqual(self.slider.value(), 33.3) + + self.slider.set_value("75.4") + self.assertEqual(self.slider.value(), 75.4) + + self.assertRaises(ValueError, self.slider.set_value, -1) + self.assertRaises(ValueError, self.slider.set_value, 102) + + self.assertRaises(ValueError, self.slider.set_value, [50, ]) + + +class ToolbarWpfTests(unittest.TestCase): + + """Unit tests for ToolbarWrapper class on WPF demo""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + self.dlg = self.app.WPFSampleApplication + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_button_access_wpf(self): + """Test getting access to buttons on Toolbar of WPF demo""" + # Read a second toolbar with buttons: "button1, button2" + tb = self.dlg.Toolbar2.find() + self.assertEqual(tb.button_count(), 2) + self.assertEqual(len(tb.texts()), 2) + + # Test if it's in writable properties + props = set(tb.get_properties().keys()) + self.assertEqual('button_count' in props, True) + + expect_txt = "button 1" + self.assertEqual(tb.button(0).window_text(), expect_txt) + + found_txt = tb.button(expect_txt, exact=True).window_text() + self.assertEqual(found_txt, expect_txt) + + found_txt = tb.button("b 1", exact=False).window_text() + self.assertEqual(found_txt, expect_txt) + + expect_txt = "button 2" + found_txt = tb.button(expect_txt, exact=True).window_text() + self.assertEqual(found_txt, expect_txt) + + # Notice that findbestmatch.MatchError is subclassed from IndexError + self.assertRaises(IndexError, tb.button, "BaD n_$E ", exact=False) + + def test_overflow_area_status(self): + """Check if overflow area visible (note: OverflowButton is inactive in the sample""" + tb = self.dlg.Toolbar2.find() + self.assertTrue(tb.is_expanded()) + self.assertFalse(tb.is_collapsed()) + + if __name__ == "__main__": unittest.main() From 9a68da3fc42a02c6c38ef1f222b525f755da3fbe Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 28 Apr 2022 14:02:33 +0300 Subject: [PATCH 29/59] add support of filter criteria in WpfElementInfo --- pywinauto/windows/wpf_element_info.py | 41 +++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 73c6155b0..e95c887fa 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -10,6 +10,26 @@ from .win32structures import RECT from .injected.api import * + +def is_element_satisfying_criteria(element, process=None, class_name=None, name=None, control_type=None, + **kwargs): + """Check if element satisfies filter criteria""" + is_appropriate_control_type = True + if control_type: + if isinstance(control_type, string_types): + is_appropriate_control_type = element.control_type == control_type + else: + raise TypeError('control_type must be string') + + def is_none_or_equals(criteria, prop): + return criteria is None or prop == criteria + + return is_none_or_equals(process, element.process_id) \ + and is_none_or_equals(class_name, element.class_name) \ + and is_none_or_equals(name, element.name) \ + and is_appropriate_control_type + + class WPFElementInfo(ElementInfo): re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "value"] exact_only_props = ["handle", "pid", "control_id", "enabled", "visible", "rectangle", "framework_id", "runtime_id"] @@ -123,11 +143,13 @@ def control_type(self): return reply['value'] def iter_children(self, **kwargs): - if 'process' in kwargs: + if 'process' in kwargs and self._pid is None: self._pid = kwargs['process'] reply = ConnectionManager().call_action('GetChildren', self._pid, element_id=self._element) - for elem in reply['elements']: - yield WPFElementInfo(elem, pid=self._pid) + for elem_id in reply['elements']: + element = WPFElementInfo(elem_id, pid=self._pid) + if is_element_satisfying_criteria(element, **kwargs): + yield element def descendants(self, **kwargs): return list(self.iter_descendants(**kwargs)) @@ -135,17 +157,20 @@ def descendants(self, **kwargs): def iter_descendants(self, **kwargs): cache_enable = kwargs.pop('cache_enable', False) depth = kwargs.pop("depth", None) + process = kwargs.pop("process", None) if not isinstance(depth, (integer_types, type(None))) or isinstance(depth, integer_types) and depth < 0: raise Exception("Depth must be an integer") if depth == 0: return - for child in self.iter_children(**kwargs): - yield child + for child in self.iter_children(process=process): + if is_element_satisfying_criteria(child, **kwargs): + yield child if depth is not None: kwargs["depth"] = depth - 1 for c in child.iter_descendants(**kwargs): - yield c + if is_element_satisfying_criteria(c, **kwargs): + yield c @property def rectangle(self): @@ -191,8 +216,8 @@ def get_active(cls, app_or_pid): if isinstance(app_or_pid, integer_types): pid = app_or_pid - elif isinstance(handle_or_elem, Application): - pid = app.process + elif isinstance(app_or_pid, Application): + pid = app_or_pid.process else: raise TypeError("UIAElementInfo object can be initialized " + \ "with integer or IUIAutomationElement instance only!") From ec49d241fae0954b281a73a9a5db08e49e4d6867 Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 28 Apr 2022 14:04:37 +0300 Subject: [PATCH 30/59] add menu wrapper --- pywinauto/controls/wpf_controls.py | 137 +++++++++++++++++++++++++ pywinauto/controls/wpfwrapper.py | 27 +++++ pywinauto/unittests/test_wpfwrapper.py | 64 ++++++++++++ 3 files changed, 228 insertions(+) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 266658433..d0e7cac7b 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -686,3 +686,140 @@ def is_expanded(self): def is_collapsed(self): """Check if the ToolBar overflow area is not visible""" return not self.is_expanded() + + +class MenuItemWrapper(wpfwrapper.WPFWrapper): + + """Wrap an UIA-compatible MenuItem control""" + + _control_types = ['MenuItem'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(MenuItemWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def items(self): + """Find all items of the menu item""" + return self.children(control_type="MenuItem") + + # ----------------------------------------------------------- + def select(self): + """Select Menu item by raising Click event""" + self.raise_event('Click') + + +class MenuWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF MenuBar or Menu control""" + + _control_types = ['Menu'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(MenuWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def items(self): + """Find all menu items""" + return self.children(control_type="MenuItem") + + # ----------------------------------------------------------- + def item_by_index(self, idx): + """Find a menu item specified by the index""" + item = self.items()[idx] + return item + + # ----------------------------------------------------------- + def _activate(self, item): + """Activate the specified item""" + if not item.is_active(): + item.set_focus() + item.set_property('IsSubmenuOpen', True) + + # ----------------------------------------------------------- + def _sub_item_by_text(self, menu, name, exact, is_last): + """Find a menu sub-item by the specified text""" + sub_item = None + items = menu.items() + if items: + if exact: + for i in items: + if name == i.window_text(): + sub_item = i + break + else: + texts = [] + for i in items: + texts.append(i.window_text()) + sub_item = findbestmatch.find_best_match(name, texts, items) + + if sub_item is None: + raise IndexError('Item `{}` not found'.format(name)) + + self._activate(sub_item) + return sub_item + + # ----------------------------------------------------------- + def _sub_item_by_idx(self, menu, idx, is_last): + """Find a menu sub-item by the specified index""" + sub_item = None + items = menu.items() + if items: + sub_item = items[idx] + + if sub_item is None: + raise IndexError('Item with index {} not found'.format(idx)) + + self._activate(sub_item) + return sub_item + + # ----------------------------------------------------------- + def item_by_path(self, path, exact=False): + """Find a menu item specified by the path + + The full path syntax is specified in: + :py:meth:`.controls.menuwrapper.Menu.get_menu_path` + + Note: $ - specifier is not supported + """ + # Get the path parts + menu_items = [p.strip() for p in path.split("->")] + items_cnt = len(menu_items) + if items_cnt == 0: + raise IndexError() + for item in menu_items: + if not item: + raise IndexError("Empty item name between '->' separators") + + def next_level_menu(parent_menu, item_name, is_last): + if item_name.startswith("#"): + return self._sub_item_by_idx(parent_menu, int(item_name[1:]), is_last) + else: + return self._sub_item_by_text(parent_menu, item_name, exact, is_last) + + # Find a top level menu item and select it. After selecting this item + # a new Menu control is created and placed on the dialog. It can be + # a direct child or a descendant. + # Sometimes we need to re-discover Menu again + try: + menu = next_level_menu(self, menu_items[0], items_cnt == 1) + if items_cnt == 1: + return menu + + if not menu.items(): + self._activate(menu) + timings.wait_until( + timings.Timings.window_find_timeout, + timings.Timings.window_find_retry, + lambda: len(self.top_level_parent().descendants(control_type="Menu")) > 0) + menu = self.top_level_parent().descendants(control_type="Menu")[0] + + for i in range(1, items_cnt): + menu = next_level_menu(menu, menu_items[i], items_cnt == i + 1) + except AttributeError: + raise IndexError() + + return menu diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index d574f80bb..38bc66f47 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -217,5 +217,32 @@ def move_window(self, x=None, y=None, width=None, height=None): """ raise AttributeError("This method is not supported for {0}".format(self)) + 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. + It is not supported by backends based on DLL injection. + 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.descendants(control_type="Menu") + if not cc: + raise AttributeError + menu = cc[0] + menu.item_by_path(path, exact).select() + backend.register('wpf', WPFElementInfo, WPFWrapper) \ No newline at end of file diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 9dbb4a3bd..ef7665407 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -748,5 +748,69 @@ def test_overflow_area_status(self): self.assertFalse(tb.is_collapsed()) +class MenuWrapperWpfTests(unittest.TestCase): + + """Unit tests for the MenuWrapper class on WPF demo""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + self.dlg = self.app.WPFSampleApplication + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_menu_by_index(self): + """Test selecting a WPF menu item by index""" + path = "#0->#1->#1" # "File->Close->Later" + self.dlg.menu_select(path) + label = self.dlg.MenuLaterClickStatic.find() + self.assertEqual(label.window_text(), u"MenuLaterClick") + + # Non-existing paths + path = "#5->#1" + self.assertRaises(IndexError, self.dlg.menu_select, path) + path = "#0->#1->#1->#2->#3" + self.assertRaises(IndexError, self.dlg.menu_select, path) + + def test_menu_by_exact_text(self): + """Test selecting a WPF menu item by exact text match""" + path = "_File->_Close->_Later" + self.dlg.menu_select(path, True) + label = self.dlg.MenuLaterClickStatic.find() + self.assertEqual(label.window_text(), u"MenuLaterClick") + + # A non-exact menu name + path = "File->About" + self.assertRaises(IndexError, self.dlg.menu_select, path, True) + + def test_menu_by_best_match_text(self): + """Test selecting a WPF menu item by best match text""" + path = "file-> close -> later" + self.dlg.menu_select(path, False) + label = self.dlg.MenuLaterClickStatic.find() + self.assertEqual(label.window_text(), u"MenuLaterClick") + + def test_menu_by_mixed_match(self): + """Test selecting a WPF menu item by a path with mixed specifiers""" + path = "file-> #1 -> later" + self.dlg.menu_select(path, False) + label = self.dlg.MenuLaterClickStatic.find() + self.assertEqual(label.window_text(), u"MenuLaterClick") + + # Bad specifiers + path = "file-> 1 -> later" + self.assertRaises(IndexError, self.dlg.menu_select, path) + path = "#0->#1->1" + self.assertRaises(IndexError, self.dlg.menu_select, path) + path = "0->#1->1" + self.assertRaises(IndexError, self.dlg.menu_select, path) + + if __name__ == "__main__": unittest.main() From 8fec5b4e6a5bb61a31a662c73679d69fa192ef0d Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 30 Apr 2022 15:57:16 +0300 Subject: [PATCH 31/59] add treeview support --- pywinauto/controls/wpf_controls.py | 213 +++++++++++++++++++++++++ pywinauto/unittests/test_wpfwrapper.py | 170 ++++++++++++++++++++ 2 files changed, 383 insertions(+) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index d0e7cac7b..3bee0e220 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -823,3 +823,216 @@ def next_level_menu(parent_menu, item_name, is_last): raise IndexError() return menu + + +class TreeItemWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF TreeItem control + + In addition to the provided methods of the wrapper + additional inherited methods can be especially helpful: + select(), extend(), collapse(), is_extended(), is_collapsed(), + click_input(), rectangle() and many others + """ + + _control_types = ['TreeItem'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(TreeItemWrapper, self).__init__(elem) + + # ----------------------------------------------------------- + def ensure_visible(self): + """Make sure that the TreeView item is visible""" + self.invoke_method('BringIntoView') + + # ----------------------------------------------------------- + def get_child(self, child_spec, exact=False): + """Return the child item of this item + + Accepts either a string or an index. + If a string is passed then it returns the child item + with the best match for the string. + """ + cc = self.children(control_type='TreeItem') + if isinstance(child_spec, six.string_types): + texts = [c.window_text() for c in cc] + if exact: + if child_spec in texts: + index = texts.index(child_spec) + else: + raise IndexError('There is no child equal to "' + str(child_spec) + '" in ' + str(texts)) + else: + indices = range(0, len(texts)) + index = findbestmatch.find_best_match( + child_spec, texts, indices, limit_ratio=.6) + + else: + index = child_spec + + return cc[index] + + # ----------------------------------------------------------- + def _calc_click_coords(self): + """Override the BaseWrapper helper method + + Set coordinates close to a left part of the item rectangle + + The returned coordinates are always absolute + """ + + # TODO get rectangle of text area + rect = self.rectangle() + coords = (rect.left + int(float(rect.width()) / 4.), + rect.top + int(float(rect.height()) / 2.)) + return coords + + # ----------------------------------------------------------- + def sub_elements(self, depth=None): + """Return a list of all visible sub-items of this control""" + return self.descendants(control_type="TreeItem", depth=depth) + + def expand(self): + self.set_property('IsExpanded', True) + return self + + # ----------------------------------------------------------- + def collapse(self): + self.set_property('IsExpanded', False) + return self + + # ----------------------------------------------------------- + def is_expanded(self): + """Test if the control is expanded""" + return self.get_property('IsExpanded') + + # ----------------------------------------------------------- + def is_collapsed(self): + """Test if the control is collapsed""" + return not self.get_property('IsExpanded') + + # ----------------------------------------------------------- + def select(self): + self.set_property('IsSelected', True) + return self + + # ----------------------------------------------------------- + def is_selected(self): + """Test if the control is expanded""" + return self.get_property('IsSelected') + + +# ==================================================================== +class TreeViewWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF Tree control""" + + _control_types = ['Tree'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(TreeViewWrapper, self).__init__(elem) + + @property + def writable_props(self): + """Extend default properties list.""" + props = super(TreeViewWrapper, self).writable_props + props.extend(['item_count']) + return props + + # ----------------------------------------------------------- + def item_count(self, depth=None): + """Return a number of items in TreeView""" + return len(self.descendants(control_type="TreeItem", depth=depth)) + + # ----------------------------------------------------------- + def roots(self): + """Return root elements of TreeView""" + return self.children(control_type="TreeItem") + + # ----------------------------------------------------------- + def get_item(self, path, exact=False): + r"""Read a TreeView item + + * **path** a path to the item to return. This can be one of + the following: + + * A string separated by \\ characters. The first character must + be \\. This string is split on the \\ characters and each of + these is used to find the specific child at each level. The + \\ represents the root item - so you don't need to specify the + root itself. + * A list/tuple of strings - The first item should be the root + element. + * A list/tuple of integers - The first item the index which root + to select. Indexing always starts from zero: get_item((0, 2, 3)) + + * **exact** a flag to request exact match of strings in the path + or apply a fuzzy logic of best_match thus allowing non-exact + path specifiers + """ + if not self.item_count(): + return None + + # Ensure the path is absolute + if isinstance(path, six.string_types): + if not path.startswith("\\"): + raise RuntimeError( + "Only absolute paths allowed - " + "please start the path with \\") + path = path.split("\\")[1:] + + # find the correct root elem + if isinstance(path[0], int): + current_elem = self.roots()[path[0]] + else: + roots = self.roots() + texts = [r.window_text() for r in roots] + if exact: + if path[0] in texts: + current_elem = roots[texts.index(path[0])] + else: + raise IndexError("There is no root element equal to '{0}'".format(path[0])) + else: + try: + current_elem = findbestmatch.find_best_match( + path[0], texts, roots, limit_ratio=.6) + except IndexError: + raise IndexError("There is no root element similar to '{0}'".format(path[0])) + + # now for each of the lower levels + # just index into it's children + for child_spec in path[1:]: + try: + # ensure that the item is expanded as this is sometimes + # required for loading tree view branches + current_elem.expand() + current_elem = current_elem.get_child(child_spec, exact) + except IndexError: + if isinstance(child_spec, six.string_types): + raise IndexError("Item '{0}' does not have a child '{1}'".format( + current_elem.window_text(), child_spec)) + else: + raise IndexError("Item '{0}' does not have {1} children".format( + current_elem.window_text(), child_spec + 1)) + + return current_elem + + # ----------------------------------------------------------- + def print_items(self, max_depth=None): + """Print all items with line indents""" + self.text = "" + + def _print_one_level(item, ident): + """Get texts for the item and its children""" + self.text += " " * ident + item.window_text() + "\n" + if max_depth is None or ident <= max_depth: + for child in item.children(control_type="TreeItem"): + _print_one_level(child, ident + 1) + + for root in self.roots(): + _print_one_level(root, 0) + + return self.text \ No newline at end of file diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index ef7665407..974bf44fe 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -812,5 +812,175 @@ def test_menu_by_mixed_match(self): self.assertRaises(IndexError, self.dlg.menu_select, path) +class TreeViewWpfTests(unittest.TestCase): + + """Unit tests for TreeViewWrapper class on WPF demo""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + self.app = Application(backend='wpf') + self.app = self.app.start(wpf_app_1) + self.dlg = self.app.WPFSampleApplication + tab_itm = self.dlg.TreeAndListViews.set_focus() + self.ctrl = tab_itm.descendants(control_type="Tree")[0] + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_tv_item_count_and_roots(self): + """Test getting roots and a total number of items in TreeView""" + self.assertEqual(self.ctrl.item_count(), 27) + + # Test if it's in writable properties + props = set(self.ctrl.get_properties().keys()) + self.assertEqual('item_count' in props, True) + + roots = self.ctrl.roots() + self.assertEqual(len(roots), 1) + self.assertEqual(roots[0].texts()[0], u'Date Elements') + + sub_items = roots[0].sub_elements(depth=1) + self.assertEqual(len(sub_items), 4) + self.assertEqual(sub_items[0].window_text(), u'Empty Date') + self.assertEqual(sub_items[-1].window_text(), u'Years') + + expected_str = "Date Elements\n Empty Date\n Week\n Monday\n Tuesday\n Wednsday\n" + expected_str += " Thursday\n Friday\n Saturday\n Sunday\n " + expected_str += "Months\n January\n February\n March\n April\n June\n July\n August\n Semptember\n " + expected_str += "October\n November\n December\n Years\n 2015\n 2016\n 2017\n 2018\n" + self.assertEqual(self.ctrl.print_items(), expected_str) + + def test_tv_item_select(self): + """Test selecting an item from TreeView""" + # Find by a path with indexes + itm = self.ctrl.get_item((0, 2, 3)) + self.assertEqual(itm.is_selected(), False) + + # Select + itm.select() + self.assertEqual(itm.is_selected(), True) + + # A second call to Select doesn't remove selection + itm.select() + self.assertEqual(itm.is_selected(), True) + + itm = self.ctrl.get_item((0, 3, 2)) + itm.ensure_visible() + self.assertEqual(itm.is_selected(), False) + # coords = itm.children(control_type='Text')[0].rectangle().mid_point() + # itm.click_input(coords=coords, absolute=True) + # self.assertEqual(itm.is_selected(), True) + + def test_tv_get_item(self): + """Test getting an item from TreeView""" + # Find by a path with indexes + itm = self.ctrl.get_item((0, 2, 3)) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'April') + + # Find by a path with strings + itm = self.ctrl.get_item('\\Date Elements\\Months\\April', exact=True) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'April') + + itm = self.ctrl.get_item('\\ Date Elements \\ months \\ april', exact=False) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'April') + + itm = self.ctrl.get_item('\\Date Elements', exact=False) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'Date Elements') + + # Try to find the last item in the tree hierarchy + itm = self.ctrl.get_item('\\Date Elements\\Years\\2018', exact=False) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'2018') + + itm = self.ctrl.get_item((0, 3, 3)) + self.assertEqual(isinstance(itm, wpf_ctls.TreeItemWrapper), True) + self.assertEqual(itm.window_text(), u'2018') + + self.assertRaises(RuntimeError, + self.ctrl.get_item, + 'Date Elements\\months', + exact=False) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\_X_- \\months', + exact=False) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\_X_- \\ months', + exact=True) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\Date Elements\\ months \\ aprel', + exact=False) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\Date Elements\\ months \\ april\\', + exact=False) + + self.assertRaises(IndexError, + self.ctrl.get_item, + '\\Date Elements\\ months \\ aprel', + exact=True) + + self.assertRaises(IndexError, self.ctrl.get_item, (0, 200, 1)) + + self.assertRaises(IndexError, self.ctrl.get_item, (130, 2, 1)) + + def test_tv_drag_n_drop(self): + """Test moving an item with mouse over TreeView""" + # Make sure the both nodes are visible + self.ctrl.get_item('\\Date Elements\\weeks').collapse() + itm_from = self.ctrl.get_item('\\Date Elements\\Years') + itm_to = self.ctrl.get_item('\\Date Elements\\Empty Date') + + itm_from.drag_mouse_input(itm_to) + + # Verify that the item and its sub-items are attached to the new node + itm = self.ctrl.get_item('\\Date Elements\\Empty Date\\Years') + self.assertEqual(itm.window_text(), 'Years') + itm = self.ctrl.get_item((0, 0, 0, 0)) + self.assertEqual(itm.window_text(), '2015') + itm = self.ctrl.get_item('\\Date Elements\\Empty Date\\Years') + itm.collapse() + + itm_from = self.ctrl.get_item('\\Date Elements\\Empty Date\\Years') + itm_to = self.ctrl.get_item(r'\Date Elements\Months') + self.ctrl.drag_mouse_input(itm_to, itm_from) + itm = self.ctrl.get_item(r'\Date Elements\Months\Years') + self.assertEqual(itm.window_text(), 'Years') + + # Error handling: drop on itself + self.assertRaises(AttributeError, + self.ctrl.drag_mouse_input, + itm_from, itm_from) + + # Drag-n-drop by manually calculated absolute coordinates + itm_from = self.ctrl.get_item(r'\Date Elements\Months') + itm_from.collapse() + r = itm_from.rectangle() + coords_from = (int(r.left + (r.width() / 4.0)), + int(r.top + (r.height() / 2.0))) + + r = self.ctrl.get_item(r'\Date Elements\Weeks').rectangle() + coords_to = (int(r.left + (r.width() / 4.0)), + int(r.top + (r.height() / 2.0))) + + self.ctrl.drag_mouse_input(coords_to, coords_from) + itm = self.ctrl.get_item(r'\Date Elements\Weeks\Months') + self.assertEqual(itm.window_text(), 'Months') + + if __name__ == "__main__": unittest.main() From 05435b205aaba384af291026027e41b4e691ceeb Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 30 Apr 2022 16:20:33 +0300 Subject: [PATCH 32/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 6e1279299..83eebda66 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 6e12792997423c99771e11ff7bb297ecfd896939 +Subproject commit 83eebda6699ea7ebe23e83e1ae3403af55acc087 From 330bf3b5f8faa3f6fef401ef1a244f8c538f8872 Mon Sep 17 00:00:00 2001 From: eltio Date: Mon, 2 May 2022 00:37:34 +0300 Subject: [PATCH 33/59] add listbox support --- pywinauto/controls/wpf_controls.py | 157 ++++++++++++++++++++++++- pywinauto/unittests/test_wpfwrapper.py | 110 +++++++++++++++++ 2 files changed, 266 insertions(+), 1 deletion(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 3bee0e220..042aa255a 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -1035,4 +1035,159 @@ def _print_one_level(item, ident): for root in self.roots(): _print_one_level(root, 0) - return self.text \ No newline at end of file + return self.text + + +class ListItemWrapper(wpfwrapper.WPFWrapper): + + """Wrap an UIA-compatible ListViewItem control""" + + _control_types = ['ListItem', ] + + # ----------------------------------------------------------- + def __init__(self, elem, container=None): + """Initialize the control""" + super(ListItemWrapper, self).__init__(elem) + + # Init a pointer to the item's container wrapper. + # It must be set by a container wrapper producing the item. + # Notice that the self.parent property isn't the same + # because it results in a different instance of a wrapper. + self.container = container + + def texts(self): + """Return a list of item texts""" + return [self.window_text()] + + def select(self): + """Select the item + + Usually applied for controls like: a radio button, a tree view item + or a list item. + """ + self.set_property('IsSelected', True) + + 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. + + Usually applied for controls like: a radio button, a tree view item, + a list item. + """ + return self.get_property('IsSelected') + + +class ListViewWrapper(wpfwrapper.WPFWrapper): + + """Wrap an UIA-compatible ListView control""" + + _control_types = ['List'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(ListViewWrapper, self).__init__(elem) + + def __getitem__(self, key): + return self.get_item(key) + + # ----------------------------------------------------------- + def item_count(self): + """A number of items in the List""" + return len(self.children()) + + # ----------------------------------------------------------- + def cells(self): + """Return list of list of cells for any type of control""" + return self.children(content_only=True) + + # ----------------------------------------------------------- + def get_item(self, row): + """Return an item of the ListView control + + * **row** can be either an index of the row or a string + with the text of a cell in the row you want returned. + """ + # Verify arguments + if isinstance(row, six.string_types): + # Get DataGrid row + try: + itm = self.descendants(name=row)[0] + # Applications like explorer.exe usually return ListItem + # directly while other apps can return only a cell. + # In this case we need to take its parent - the whole row. + if not isinstance(itm, ListItemWrapper): + itm = itm.parent() + except IndexError: + raise ValueError("Element '{0}' not found".format(row)) + elif isinstance(row, six.integer_types): + # Get the item by a row index + list_items = self.children(content_only=True) + itm = list_items[row] + else: + raise TypeError("String type or integer is expected") + + # Give to the item a pointer on its container + itm.container = self + return itm + + item = get_item # this is an alias to be consistent with other content elements + + # ----------------------------------------------------------- + def get_items(self): + """Return all items of the ListView control""" + return self.children(content_only=True) + + items = get_items # this is an alias to be consistent with other content elements + + # ----------------------------------------------------------- + def get_item_rect(self, item_index): + """Return the bounding rectangle of the list view item + + The method is kept mostly for a backward compatibility + with the native ListViewWrapper interface + """ + itm = self.get_item(item_index) + return itm.rectangle() + + def get_selection(self): + # TODO get selected items directly from SelectedItems property + return [child for child in self.iter_children() if child.is_selected()] + + # ----------------------------------------------------------- + def get_selected_count(self): + """Return a number of selected items + + The call can be quite expensive as we retrieve all + the selected items in order to count them + """ + selection = self.get_selection() + if selection: + return len(selection) + else: + return 0 + + # ----------------------------------------------------------- + def texts(self): + """Return a list of item texts""" + return [elem.texts() for elem in self.children(content_only=True)] + + # ----------------------------------------------------------- + @property + def writable_props(self): + """Extend default properties list.""" + props = super(ListViewWrapper, self).writable_props + props.extend(['column_count', + 'item_count', + 'columns', + # 'items', + ]) + return props diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 974bf44fe..a04732a6a 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -982,5 +982,115 @@ def test_tv_drag_n_drop(self): self.assertEqual(itm.window_text(), 'Months') +class ListViewWrapperTests(unittest.TestCase): + + """Unit tests for the ListViewWrapper class""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + dlg = app.WPFSampleApplication + + self.app = app + + self.listbox_datagrid_tab = dlg.ListBox_and_Grid + + self.listbox_texts = [ + [u"TextItem 1", ], + [u"TextItem 2", ], + [u"ButtonItem", ], + [u"CheckItem", ], + [u"TextItem 3", ], + [u"TextItem 4", ], + [u"TextItem 5", ], + [u"TextItem 6", ], + [u"TextItem 7", ], + [u"TextItem 8", ], + ] + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_item_count(self): + """Test the items count in the ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + self.assertEqual(listbox.item_count(), len(self.listbox_texts)) + + def test_cells(self): + """Test getting a cells of the ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + cells = listbox.cells() + self.assertEqual(cells[listbox.item_count() - 1].window_text(), "TextItem 8") + self.assertEqual(cells[3].window_text(), "CheckItem") + + def test_get_item(self): + """Test getting an item of ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + item = listbox.get_item(u"TextItem 2") + self.assertEqual(item.texts(), self.listbox_texts[1]) + + item = listbox.get_item(3) + self.assertEqual(item.texts(), self.listbox_texts[3]) + + item = listbox.get_item(u"TextItem 8") + self.assertEqual(item.texts(), self.listbox_texts[9]) + + def test_get_items(self): + """Test getting all items of ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + content = [item.texts() for item in listbox.get_items()] + self.assertEqual(content, self.listbox_texts) + + def test_texts(self): + """Test getting all items of ListView controls""" + # ListBox + self.listbox_datagrid_tab.set_focus() + listbox = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + self.assertEqual(listbox.texts(), self.listbox_texts) + + def test_select_and_get_item(self): + """Test selecting an item of the ListView control""" + self.listbox_datagrid_tab.set_focus() + self.ctrl = self.listbox_datagrid_tab.descendants(class_name=u"ListBox")[0] + self.assertEqual(self.ctrl.texts(), self.listbox_texts) + # Verify get_selected_count + self.assertEqual(self.ctrl.get_selected_count(), 0) + + # Select by an index + row = 1 + i = self.ctrl.get_item(row) + self.assertEqual(i.is_selected(), False) + i.select() + self.assertEqual(i.is_selected(), True) + cnt = self.ctrl.get_selected_count() + self.assertEqual(cnt, 1) + rect = self.ctrl.get_item_rect(row) + self.assertEqual(rect, i.rectangle()) + + # Select by text + row = 'TextItem 6' + i = self.ctrl.get_item(row) + i.select() + self.assertEqual(i.is_selected(), True) + i = self.ctrl.get_item(7) # re-get the item by a row index + self.assertEqual(i.is_selected(), True) + + row = None + self.assertRaises(TypeError, self.ctrl.get_item, row) + + if __name__ == "__main__": unittest.main() From 455adf2ac26bc10da4abe298b335e81e4e85b658 Mon Sep 17 00:00:00 2001 From: eltio Date: Wed, 4 May 2022 13:59:13 +0300 Subject: [PATCH 34/59] remove extra code for adding pid to criteria --- pywinauto/base_application.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pywinauto/base_application.py b/pywinauto/base_application.py index d842963ad..a8f97c2b8 100644 --- a/pywinauto/base_application.py +++ b/pywinauto/base_application.py @@ -480,10 +480,7 @@ def by(self, **criteria): new_item = WindowSpecification(self.criteria[0], allow_magic_lookup=self.allow_magic_lookup) new_item.criteria.extend(self.criteria[1:]) - if self.app is not None: - criteria['pid'] = self.app.process new_item.criteria.append(criteria) - return new_item def __getitem__(self, key): From ef4703397713fdd5ff10bc4f00dd7b54b4542d5b Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 7 May 2022 17:15:53 +0300 Subject: [PATCH 35/59] fix code style --- pywinauto/controls/wpf_controls.py | 2 +- pywinauto/controls/wpfwrapper.py | 21 ++++++++--------- pywinauto/unittests/test_wpf_element_info.py | 4 +--- pywinauto/unittests/test_wpfwrapper.py | 5 +--- pywinauto/windows/wpf_element_info.py | 24 ++++++++------------ 5 files changed, 22 insertions(+), 34 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 042aa255a..c9e03a7a4 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -9,7 +9,7 @@ from .. import findbestmatch from .. import timings from ..windows.wpf_element_info import WPFElementInfo -from ..windows.injected.api import * +from ..windows.injected.api import ConnectionManager # ==================================================================== diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index 38bc66f47..e5de4983a 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -4,21 +4,17 @@ from __future__ import print_function import six -import time -import warnings -import threading from .. import backend from .. import WindowNotFoundError # noqa #E402 -from ..timings import Timings from .win_base_wrapper import WinBaseWrapper from ..base_wrapper import BaseMeta -from ..windows.injected.api import * +from ..windows.injected.api import ConnectionManager from ..windows.wpf_element_info import WPFElementInfo -class WpfMeta(BaseMeta): - """Metaclass for UiaWrapper objects""" +class WpfMeta(BaseMeta): + """Metaclass for WpfWrapper objects""" control_type_to_cls = {} def __init__(cls, name, bases, attrs): @@ -31,7 +27,7 @@ def __init__(cls, name, bases, attrs): @staticmethod def find_wrapper(element): - """Find the correct wrapper for this UIA element""" + """Find the correct wrapper for this WPF element""" # Check for a more specific wrapper in the registry try: @@ -42,6 +38,7 @@ def find_wrapper(element): return wrapper_match + @six.add_metaclass(WpfMeta) class WPFWrapper(WinBaseWrapper): _control_types = [] @@ -119,9 +116,9 @@ def children_texts(self): return [c.window_text() for c in self.children()] # System.Windows.WindowState enum - NORMAL=0 - MAXIMIZED=1 - MINIMIZED=2 + NORMAL = 0 + MAXIMIZED = 1 + MINIMIZED = 2 # ----------------------------------------------------------- def close(self): @@ -245,4 +242,4 @@ def menu_select(self, path, exact=False, ): menu.item_by_path(path, exact).select() -backend.register('wpf', WPFElementInfo, WPFWrapper) \ No newline at end of file +backend.register('wpf', WPFElementInfo, WPFWrapper) diff --git a/pywinauto/unittests/test_wpf_element_info.py b/pywinauto/unittests/test_wpf_element_info.py index 7aca2f47e..e86630a57 100644 --- a/pywinauto/unittests/test_wpf_element_info.py +++ b/pywinauto/unittests/test_wpf_element_info.py @@ -1,7 +1,6 @@ import unittest import os import sys -import mock sys.path.append(".") from pywinauto.windows.application import Application # noqa: E402 @@ -9,14 +8,13 @@ from pywinauto.sysinfo import is_x64_Python # noqa: E402 from pywinauto.timings import Timings # noqa: E402 -from pywinauto.windows.wpf_element_info import WPFElementInfo - wpf_samples_folder = os.path.join( os.path.dirname(__file__), r"..\..\apps\WPF_samples") if is_x64_Python(): wpf_samples_folder = os.path.join(wpf_samples_folder, 'x64') wpf_app_1 = os.path.join(wpf_samples_folder, u"WpfApplication1.exe") + class WPFElementInfoTests(unittest.TestCase): """Unit tests for the WPFlementInfo class""" diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index a04732a6a..61f7a3202 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -8,7 +8,6 @@ import collections import unittest import mock -import six sys.path.append(".") from pywinauto.windows.application import Application # noqa: E402 @@ -16,13 +15,10 @@ from pywinauto.sysinfo import is_x64_Python # noqa: E402 from pywinauto.timings import Timings, wait_until # noqa: E402 from pywinauto.actionlogger import ActionLogger # noqa: E402 -from pywinauto import Desktop from pywinauto import mouse # noqa: E402 -from pywinauto import WindowNotFoundError # noqa: E402 import pywinauto.controls.wpf_controls as wpf_ctls from pywinauto.controls.wpfwrapper import WPFWrapper -from pywinauto.windows.wpf_element_info import WPFElementInfo from pywinauto.windows.injected.api import InjectedTargetError from pywinauto.windows.injected.channel import BrokenPipeError @@ -32,6 +28,7 @@ wpf_samples_folder = os.path.join(wpf_samples_folder, 'x64') wpf_app_1 = os.path.join(wpf_samples_folder, u"WpfApplication1.exe") + def _set_timings(): """Setup timings for WPF related tests""" Timings.defaults() diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index e95c887fa..9dafeffd3 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -1,14 +1,10 @@ -"""Implementation of the class to deal with an UI element of WPF via injected DLL""" -import json +"""Implementation of the class to deal with an UI element of WPF via injected managed DLL/assembly""" +from six import integer_types, string_types -from six import integer_types, text_type, string_types -from ctypes.wintypes import tagPOINT -import warnings - -from pywinauto.handleprops import dumpwindow, controlid +from pywinauto.handleprops import dumpwindow from pywinauto.element_info import ElementInfo from .win32structures import RECT -from .injected.api import * +from .injected.api import ConnectionManager, NotFoundError, UnsupportedActionError def is_element_satisfying_criteria(element, process=None, class_name=None, name=None, control_type=None, @@ -44,13 +40,13 @@ def __init__(self, elem_id=None, cache_enable=False, pid=None): If elem_id is None create an instance for UI root element. """ - self._pid=pid + self._pid = pid if elem_id is not None: if isinstance(elem_id, integer_types): # Create instance of WPFElementInfo from a handle self._element = elem_id else: - raise TypeError("WPFElementInfo object can be initialized " + \ + raise TypeError("WPFElementInfo object can be initialized " "with integer instance only!") else: self._element = 0 @@ -83,7 +79,7 @@ def name(self): @property def rich_text(self): - if self.control_type=='Edit': + if self.control_type == 'Edit': return self.get_property('Text') or '' return self.name @@ -219,8 +215,8 @@ def get_active(cls, app_or_pid): elif isinstance(app_or_pid, Application): pid = app_or_pid.process else: - raise TypeError("UIAElementInfo object can be initialized " + \ - "with integer or IUIAutomationElement instance only!") + raise TypeError("WPFElementInfo object can be initialized " + "with integer or Application instance only!") try: reply = ConnectionManager().call_action('GetFocusedElement', pid) @@ -229,4 +225,4 @@ def get_active(cls, app_or_pid): else: return None except UnsupportedActionError: - return None \ No newline at end of file + return None From 206e80ad94828a39941dfcc040f8f4e181837b95 Mon Sep 17 00:00:00 2001 From: eltio Date: Mon, 9 May 2022 14:43:38 +0300 Subject: [PATCH 36/59] Add support of ListView with a GridView view --- pywinauto/controls/wpf_controls.py | 166 ++++++++++++++++++++++++- pywinauto/unittests/test_wpfwrapper.py | 139 +++++++++++++++++++++ 2 files changed, 301 insertions(+), 4 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index c9e03a7a4..2b84ee6b4 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -4,12 +4,9 @@ import six from . import wpfwrapper -from . import win32_controls from . import common_controls from .. import findbestmatch from .. import timings -from ..windows.wpf_element_info import WPFElementInfo -from ..windows.injected.api import ConnectionManager # ==================================================================== @@ -1057,7 +1054,9 @@ def __init__(self, elem, container=None): def texts(self): """Return a list of item texts""" - return [self.window_text()] + if len(self.children()) == 0: + return [self.window_text()] + return [elem.window_text() for elem in self.descendants() if len(elem.window_text()) > 0] def select(self): """Select the item @@ -1180,6 +1179,165 @@ def texts(self): """Return a list of item texts""" return [elem.texts() for elem in self.children(content_only=True)] + # ----------------------------------------------------------- + @property + def writable_props(self): + """Extend default properties list.""" + props = super(ListViewWrapper, self).writable_props + props.extend(['item_count', + # 'items', + ]) + return props + + +class HeaderItemWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF Header Item control""" + + _control_types = ['HeaderItem'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(HeaderItemWrapper, self).__init__(elem) + + +class DataGridWrapper(wpfwrapper.WPFWrapper): + + """Wrap an WPF ListView control with a GridView view""" + + _control_types = ['DataGrid'] + + # ----------------------------------------------------------- + def __init__(self, elem): + """Initialize the control""" + super(DataGridWrapper, self).__init__(elem) + + def __getitem__(self, key): + return self.get_item(key) + + # ----------------------------------------------------------- + def item_count(self): + """A number of items in the ListView""" + return len(self.children(control_type='ListItem')) + + # ----------------------------------------------------------- + def column_count(self): + """Return the number of columns""" + return len(self.children(control_type='HeaderItem')) + + # ----------------------------------------------------------- + def get_header_controls(self): + """Return Header controls associated with the Table""" + return self.children(control_type='HeaderItem') + + columns = get_header_controls + + # ----------------------------------------------------------- + def get_column(self, col_index): + """Get the information for a column of the ListView""" + col = self.columns()[col_index] + return col + + # ----------------------------------------------------------- + def cells(self): + """Return list of list of cells for any type of contol""" + rows = self.children(control_type='ListItem') + return [row.children()[0].children(content_only=True) for row in rows] + + # ----------------------------------------------------------- + def cell(self, row, column): + """Return a cell in the with a GridView view + + Only for controls with Grid pattern support + + * **row** is an index of a row in the list. + * **column** is an index of a column in the specified row. + + The returned cell can be of different control types. + Mostly: TextBlock, ImageControl, EditControl, DataItem + or even another layer of data items (Group, DataGrid) + """ + if not isinstance(row, six.integer_types) or not isinstance(column, six.integer_types): + raise TypeError("row and column must be numbers") + + _row = self.get_item(row).children()[0] + cell_elem = _row.children()[column] + + return cell_elem + + # ----------------------------------------------------------- + def get_item(self, row): + """Return an item of the ListView control + + * **row** can be either an index of the row or a string + with the text of a cell in the row you want returned. + """ + # Verify arguments + if isinstance(row, six.string_types): + # Get DataGrid row + try: + itm = self.descendants(name=row)[0] + # Applications like explorer.exe usually return ListItem + # directly while other apps can return only a cell. + # In this case we need to take its parent - the whole row. + while itm is not None and not isinstance(itm, ListItemWrapper): + itm = itm.parent() + except IndexError: + raise ValueError("Element '{0}' not found".format(row)) + elif isinstance(row, six.integer_types): + # Get the item by a row index + list_items = self.children(control_type='ListItem') + itm = list_items[row] + else: + raise TypeError("String type or integer is expected") + + # Give to the item a pointer on its container + if itm is not None: + itm.container = self + return itm + + item = get_item # this is an alias to be consistent with other content elements + + # ----------------------------------------------------------- + def get_items(self): + """Return all items of the ListView control""" + return self.children(control_type='ListItem') + + items = get_items # this is an alias to be consistent with other content elements + + # ----------------------------------------------------------- + def get_item_rect(self, item_index): + """Return the bounding rectangle of the list view item + + The method is kept mostly for a backward compatibility + with the native ListViewWrapper interface + """ + itm = self.get_item(item_index) + return itm.rectangle() + + def get_selection(self): + # TODO get selected items directly from SelectedItems property + return [child for child in self.iter_children(control_type='ListItem') if child.is_selected()] + + # ----------------------------------------------------------- + def get_selected_count(self): + """Return a number of selected items + + The call can be quite expensieve as we retrieve all + the selected items in order to count them + """ + selection = self.get_selection() + if selection: + return len(selection) + else: + return 0 + + # ----------------------------------------------------------- + def texts(self): + """Return a list of item texts""" + return [elem.texts() for elem in self.descendants(control_type='ListItem')] + # ----------------------------------------------------------- @property def writable_props(self): diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 61f7a3202..b55f3d27e 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -1089,5 +1089,144 @@ def test_select_and_get_item(self): self.assertRaises(TypeError, self.ctrl.get_item, row) +class GridListViewWrapperTests(unittest.TestCase): + + """Unit tests for the DataGridWrapper class with a GridView view""" + + def setUp(self): + """Set some data and ensure the application is in the state we want""" + _set_timings() + + # start the application + app = Application(backend='wpf') + app = app.start(wpf_app_1) + dlg = app.WPFSampleApplication + + self.app = app + + self.listview_tab = dlg.Tree_and_List_Views + + self.listview_texts = [ + [u"1", u"Tomatoe", u"Red"], + [u"2", u"Cucumber", u"Green", ], + [u"3", u"Reddish", u"Purple", ], + [u"4", u"Cauliflower", u"White", ], + [u"5", u"Cupsicum", u"Yellow", ], + [u"6", u"Cupsicum", u"Red", ], + [u"7", u"Cupsicum", u"Green", ], + ] + + def tearDown(self): + """Close the application after tests""" + self.app.kill() + + def test_item_count(self): + """Test the items count in the ListView controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + self.assertEqual(listview.item_count(), len(self.listview_texts)) + + def test_column_count(self): + """Test the columns count in the ListView controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + self.assertEqual(listview.column_count(), len(self.listview_texts[0])) + + def test_get_header_control(self): + """Test getting a Header control and Header Item control of ListView controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + hdr_itm = listview.get_header_controls()[1] + # HeaderItem of ListView + self.assertTrue(isinstance(hdr_itm, wpf_ctls.HeaderItemWrapper)) + self.assertEquals('Name', hdr_itm.element_info.name) + + def test_get_column(self): + """Test get_column() method for the ListView controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + listview_col = listview.get_column(1) + self.assertEqual(listview_col.texts()[0], u"Name") + + def test_cell(self): + """Test getting a cell of the ListView controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + cell = listview.cell(3, 2) + self.assertEqual(cell.window_text(), self.listview_texts[3][2]) + + def test_cells(self): + """Test getting a cells of the ListView controls""" + def compare_cells(cells, control): + for i in range(0, control.item_count()): + for j in range(0, control.column_count()): + self.assertEqual(cells[i][j], control.cell(i, j)) + + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + compare_cells(listview.cells(), listview) + + def test_get_item(self): + """Test getting an item of ListView controls""" + # ListView + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + item = listview.get_item(u"Reddish") + self.assertEqual(item.texts(), self.listview_texts[2]) + + self.assertRaises(ValueError, listview.get_item, u"Apple") + + def test_get_items(self): + """Test getting all items of ListView controls""" + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + content = [item.texts() for item in listview.get_items()] + self.assertEqual(content, self.listview_texts) + + def test_texts(self): + """Test getting all items of ListView controls""" + self.listview_tab.set_focus() + listview = self.listview_tab.descendants(class_name=u"ListView")[0] + self.assertEqual(listview.texts(), self.listview_texts) + + def test_select_and_get_item(self): + """Test selecting an item of the ListView control""" + self.listview_tab.set_focus() + self.ctrl = self.listview_tab.descendants(class_name=u"ListView")[0] + # Verify get_selected_count + self.assertEqual(self.ctrl.get_selected_count(), 0) + + # Select by an index + row = 1 + i = self.ctrl.get_item(row) + self.assertEqual(i.is_selected(), False) + i.select() + self.assertEqual(i.is_selected(), True) + cnt = self.ctrl.get_selected_count() + self.assertEqual(cnt, 1) + rect = self.ctrl.get_item_rect(row) + self.assertEqual(rect, i.rectangle()) + + # Select by text + row = '3' + i = self.ctrl.get_item(row) + i.select() + self.assertEqual(i.is_selected(), True) + row = 'White' + i = self.ctrl.get_item(row) + i.select() + i = self.ctrl.get_item(3) # re-get the item by a row index + self.assertEqual(i.is_selected(), True) + + row = None + self.assertRaises(TypeError, self.ctrl.get_item, row) + + if __name__ == "__main__": unittest.main() From 68ae33bf5008ceb60e14ba510874ba82eea42c2d Mon Sep 17 00:00:00 2001 From: eltio Date: Mon, 9 May 2022 14:49:54 +0300 Subject: [PATCH 37/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 83eebda66..02aef9d23 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 83eebda6699ea7ebe23e83e1ae3403af55acc087 +Subproject commit 02aef9d233fed842581405f3de3292c251506f17 From 019dce7938bed5dffeaaa6ce02cfd9483bfee328 Mon Sep 17 00:00:00 2001 From: eltio Date: Mon, 9 May 2022 15:34:33 +0300 Subject: [PATCH 38/59] add value property --- pywinauto/windows/wpf_element_info.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 9dafeffd3..6d859c53b 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -83,6 +83,12 @@ def rich_text(self): return self.get_property('Text') or '' return self.name + @property + def value(self): + if self.control_type == 'Edit': + return self.get_property('Text') or self.get_property('Password') or '' + return '' + @property def control_id(self): return self._element From 1ac3f90c78f197291b6d38050bbfb0c815f7ed17 Mon Sep 17 00:00:00 2001 From: eltio Date: Mon, 9 May 2022 16:48:00 +0300 Subject: [PATCH 39/59] fix test and typo in DataGridWrapper --- pywinauto/controls/wpf_controls.py | 2 +- pywinauto/unittests/test_wpfwrapper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 2b84ee6b4..b03e40f5b 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -1342,7 +1342,7 @@ def texts(self): @property def writable_props(self): """Extend default properties list.""" - props = super(ListViewWrapper, self).writable_props + props = super(DataGridWrapper, self).writable_props props.extend(['column_count', 'item_count', 'columns', diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index b55f3d27e..600abd5ef 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -408,7 +408,7 @@ def test_toggle_button(self): def test_button_click(self): """Test the click method for the Button control""" - label = self.dlg.by(control_type="Text", + label = self.dlg.by(class_name="Label", name="TestLabel").find() self.dlg.Apply.click() self.assertEqual(label.window_text(), "ApplyClick") From 7c7118dc66bdef68eaf085259eb79027163d502b Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 10 May 2022 01:23:10 +0300 Subject: [PATCH 40/59] add DataGrid support --- pywinauto/controls/wpf_controls.py | 63 +++++++------ pywinauto/controls/wpfwrapper.py | 5 +- pywinauto/unittests/test_wpfwrapper.py | 124 ++++++++++++++++++++++--- 3 files changed, 150 insertions(+), 42 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index b03e40f5b..eb0b0018a 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -76,7 +76,7 @@ def is_dialog(self): class ButtonWrapper(wpfwrapper.WPFWrapper): - """Wrap a UIA-compatible Button, CheckBox or RadioButton control""" + """Wrap a WPF Button, CheckBox or RadioButton control""" _control_types = ['Button', 'CheckBox', @@ -95,20 +95,10 @@ def __init__(self, elem): # ----------------------------------------------------------- def toggle(self): """ - An interface to Toggle method of the Toggle control pattern. - - Control supporting the Toggle pattern cycles through its - toggle states in the following order: - ToggleState_On, ToggleState_Off and, - if supported, ToggleState_Indeterminate + Switch state of checkable controls in cycle between CHECKED/UNCHECKED or + CHECKED/UNCHECKED/INDETERMINATE (if a control is three-state) Usually applied for the check box control. - - The radio button control does not implement IToggleProvider, - because it is not capable of cycling through its valid states. - Toggle a state of a check box control. (Use 'select' method instead) - Notice, a radio button control isn't supported by UIA. - https://msdn.microsoft.com/en-us/library/windows/desktop/ee671290(v=vs.85).aspx """ current_state = self.get_property('IsChecked') @@ -188,7 +178,7 @@ def is_selected(self): class ComboBoxWrapper(wpfwrapper.WPFWrapper): - """Wrap a UIA CoboBox control""" + """Wrap a WPF CoboBox control""" _control_types = ['ComboBox'] @@ -687,7 +677,7 @@ def is_collapsed(self): class MenuItemWrapper(wpfwrapper.WPFWrapper): - """Wrap an UIA-compatible MenuItem control""" + """Wrap an WPF MenuItem control""" _control_types = ['MenuItem'] @@ -1037,7 +1027,7 @@ def _print_one_level(item, ident): class ListItemWrapper(wpfwrapper.WPFWrapper): - """Wrap an UIA-compatible ListViewItem control""" + """Wrap an WPF ListViewItem and DataGrid row controls""" _control_types = ['ListItem', ] @@ -1054,9 +1044,16 @@ def __init__(self, elem, container=None): def texts(self): """Return a list of item texts""" - if len(self.children()) == 0: - return [self.window_text()] - return [elem.window_text() for elem in self.descendants() if len(elem.window_text()) > 0] + children = self.children() + if len(children) == 1 and children[0].element_info.control_type == 'Pane': + items_holder = children[0] # grid ListViewItem + + descendants = items_holder.children() + if len(descendants) == 1 and descendants[0].element_info.control_type == 'Pane': + return [self.window_text()] # ListBoxItem or non-grid ListViewItem + else: + items_holder = self # DataGridRow + return [elem.window_text() for elem in items_holder.children()] def select(self): """Select the item @@ -1086,7 +1083,7 @@ def is_selected(self): class ListViewWrapper(wpfwrapper.WPFWrapper): - """Wrap an UIA-compatible ListView control""" + """Wrap an WPF ListView control""" _control_types = ['List'] @@ -1204,7 +1201,7 @@ def __init__(self, elem): class DataGridWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF ListView control with a GridView view""" + """Wrap WPF ListView (with a GridView view) or DataGrid control""" _control_types = ['DataGrid'] @@ -1218,7 +1215,7 @@ def __getitem__(self, key): # ----------------------------------------------------------- def item_count(self): - """A number of items in the ListView""" + """A number of items in the Grid""" return len(self.children(control_type='ListItem')) # ----------------------------------------------------------- @@ -1241,9 +1238,17 @@ def get_column(self, col_index): # ----------------------------------------------------------- def cells(self): - """Return list of list of cells for any type of contol""" + """Return list of list of cells for any type of control""" rows = self.children(control_type='ListItem') - return [row.children()[0].children(content_only=True) for row in rows] + + result = [] + for row in rows: + children = row.children() + if len(children) == 1 and children[0].element_info.control_type == 'Pane': + result.append(children[0].children()) + else: + result.append(children) + return result # ----------------------------------------------------------- def cell(self, row, column): @@ -1261,9 +1266,11 @@ def cell(self, row, column): if not isinstance(row, six.integer_types) or not isinstance(column, six.integer_types): raise TypeError("row and column must be numbers") - _row = self.get_item(row).children()[0] - cell_elem = _row.children()[column] - + _row = self.get_item(row).children() + if len(_row) == 1 and _row[0].element_info.control_type == 'Pane': + cell_elem = _row[0].children()[column] + else: + cell_elem = _row[column] return cell_elem # ----------------------------------------------------------- @@ -1324,7 +1331,7 @@ def get_selection(self): def get_selected_count(self): """Return a number of selected items - The call can be quite expensieve as we retrieve all + The call can be quite expensive as we retrieve all the selected items in order to count them """ selection = self.get_selection() diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index e5de4983a..3d781aef0 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -6,7 +6,6 @@ import six from .. import backend -from .. import WindowNotFoundError # noqa #E402 from .win_base_wrapper import WinBaseWrapper from ..base_wrapper import BaseMeta from ..windows.injected.api import ConnectionManager @@ -109,7 +108,7 @@ def is_active(self): focused_wrap = self.get_active() if focused_wrap is None: return False - return (focused_wrap.top_level_parent() == self.top_level_parent()) + return focused_wrap.top_level_parent() == self.top_level_parent() def children_texts(self): """Get texts of the control's children""" @@ -164,7 +163,7 @@ def restore(self): # ----------------------------------------------------------- def get_show_state(self): - """Get the show state and Maximized/minimzed/restored state + """Get the show state and Maximized/minimized/restored state Returns values as following diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 600abd5ef..d007727b4 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -1091,7 +1091,7 @@ def test_select_and_get_item(self): class GridListViewWrapperTests(unittest.TestCase): - """Unit tests for the DataGridWrapper class with a GridView view""" + """Unit tests for the DataGridWrapper class""" def setUp(self): """Set some data and ensure the application is in the state we want""" @@ -1105,6 +1105,7 @@ def setUp(self): self.app = app self.listview_tab = dlg.Tree_and_List_Views + self.listbox_datagrid_tab = dlg.ListBox_and_Grid self.listview_texts = [ [u"1", u"Tomatoe", u"Red"], @@ -1116,26 +1117,43 @@ def setUp(self): [u"7", u"Cupsicum", u"Green", ], ] + self.datagrid_texts = [ + [u"0", u"A0", u"B0", u"C0", u"D0", u"E0", u"", ], + [u"1", u"A1", u"B1", u"C1", u"D1", u"E1", u"", ], + [u"2", u"A2", u"B2", u"C2", u"D2", u"E2", u"", ], + [u"3", u"A3", u"B3", u"C3", u"D3", u"E3", u"", ], + ] + def tearDown(self): """Close the application after tests""" self.app.kill() def test_item_count(self): - """Test the items count in the ListView controls""" + """Test the items count in grid controls""" # ListView self.listview_tab.set_focus() listview = self.listview_tab.descendants(class_name=u"ListView")[0] self.assertEqual(listview.item_count(), len(self.listview_texts)) + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + self.assertEqual(datagrid.item_count(), len(self.datagrid_texts)) + def test_column_count(self): - """Test the columns count in the ListView controls""" + """Test the columns count in grid controls""" # ListView self.listview_tab.set_focus() listview = self.listview_tab.descendants(class_name=u"ListView")[0] self.assertEqual(listview.column_count(), len(self.listview_texts[0])) + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + self.assertEqual(datagrid.column_count(), len(self.datagrid_texts[0]) - 1) + def test_get_header_control(self): - """Test getting a Header control and Header Item control of ListView controls""" + """Test getting a Header control and Header Item control of grid controls""" # ListView self.listview_tab.set_focus() listview = self.listview_tab.descendants(class_name=u"ListView")[0] @@ -1144,24 +1162,49 @@ def test_get_header_control(self): self.assertTrue(isinstance(hdr_itm, wpf_ctls.HeaderItemWrapper)) self.assertEquals('Name', hdr_itm.element_info.name) + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + grid_hdr_item = datagrid.get_header_controls()[2] + self.assertEquals('B', grid_hdr_item.element_info.name) + self.assertTrue(isinstance(grid_hdr_item, wpf_ctls.HeaderItemWrapper)) + def test_get_column(self): - """Test get_column() method for the ListView controls""" + """Test get_column() method for grid controls""" # ListView self.listview_tab.set_focus() listview = self.listview_tab.descendants(class_name=u"ListView")[0] listview_col = listview.get_column(1) self.assertEqual(listview_col.texts()[0], u"Name") + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + datagrid_col = datagrid.get_column(2) + self.assertEqual(datagrid_col.texts()[0], u"B") + + self.assertRaises(IndexError, datagrid.get_column, 10) + def test_cell(self): - """Test getting a cell of the ListView controls""" + """Test getting a cell of grid controls""" # ListView self.listview_tab.set_focus() listview = self.listview_tab.descendants(class_name=u"ListView")[0] cell = listview.cell(3, 2) self.assertEqual(cell.window_text(), self.listview_texts[3][2]) + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + cell = datagrid.cell(2, 0) + self.assertEqual(cell.window_text(), self.datagrid_texts[2][0]) + + self.assertRaises(TypeError, datagrid.cell, 1.5, 1) + + self.assertRaises(IndexError, datagrid.cell, 10, 10) + def test_cells(self): - """Test getting a cells of the ListView controls""" + """Test getting a cells of grid controls""" def compare_cells(cells, control): for i in range(0, control.item_count()): for j in range(0, control.column_count()): @@ -1172,8 +1215,13 @@ def compare_cells(cells, control): listview = self.listview_tab.descendants(class_name=u"ListView")[0] compare_cells(listview.cells(), listview) + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + compare_cells(datagrid.cells(), datagrid) + def test_get_item(self): - """Test getting an item of ListView controls""" + """Test getting an item of grid controls""" # ListView self.listview_tab.set_focus() listview = self.listview_tab.descendants(class_name=u"ListView")[0] @@ -1182,20 +1230,42 @@ def test_get_item(self): self.assertRaises(ValueError, listview.get_item, u"Apple") + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + item = datagrid.get_item(u"B2") + self.assertEqual(item.texts(), self.datagrid_texts[2]) + + item = datagrid.get_item(3) + self.assertEqual(item.texts(), self.datagrid_texts[3]) + + self.assertRaises(TypeError, datagrid.get_item, 12.3) + def test_get_items(self): - """Test getting all items of ListView controls""" + """Test getting all items of grid controls""" self.listview_tab.set_focus() listview = self.listview_tab.descendants(class_name=u"ListView")[0] content = [item.texts() for item in listview.get_items()] self.assertEqual(content, self.listview_texts) + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + content = [item.texts() for item in datagrid.get_items()] + self.assertEqual(content, self.datagrid_texts) + def test_texts(self): - """Test getting all items of ListView controls""" + """Test getting all items of grid controls""" self.listview_tab.set_focus() listview = self.listview_tab.descendants(class_name=u"ListView")[0] self.assertEqual(listview.texts(), self.listview_texts) - def test_select_and_get_item(self): + # DataGrid + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + self.assertEqual(datagrid.texts(), self.datagrid_texts) + + def test_select_and_get_item_listview(self): """Test selecting an item of the ListView control""" self.listview_tab.set_focus() self.ctrl = self.listview_tab.descendants(class_name=u"ListView")[0] @@ -1227,6 +1297,38 @@ def test_select_and_get_item(self): row = None self.assertRaises(TypeError, self.ctrl.get_item, row) + def test_select_and_get_item_datagrid(self): + """Test selecting an item of the DataGrid control""" + self.listbox_datagrid_tab.set_focus() + datagrid = self.listbox_datagrid_tab.descendants(class_name=u"DataGrid")[0] + # Verify get_selected_count + self.assertEqual(datagrid.get_selected_count(), 0) + + # Select by an index + row = 1 + i = datagrid.get_item(row) + self.assertEqual(i.is_selected(), False) + i.select() + self.assertEqual(i.is_selected(), True) + cnt = datagrid.get_selected_count() + self.assertEqual(cnt, 1) + rect = datagrid.get_item_rect(row) + self.assertEqual(rect, i.rectangle()) + + # Select by text + row = 'A3' + i = datagrid.get_item(row) + i.select() + self.assertEqual(i.is_selected(), True) + row = 'B0' + i = datagrid.get_item(row) + i.select() + i = datagrid.get_item(3) # re-get the item by a row index + self.assertEqual(i.is_selected(), True) + + row = None + self.assertRaises(TypeError, datagrid.get_item, row) + if __name__ == "__main__": unittest.main() From db836c216b700e519bb69068fdc695de2945597f Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 10 May 2022 01:37:50 +0300 Subject: [PATCH 41/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 02aef9d23..c4af37ec4 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 02aef9d233fed842581405f3de3292c251506f17 +Subproject commit c4af37ec4e181693255b7d93f3ed602a8e0a600a From 22966cec571dfe18a70d882ec2be2cf05aa8731a Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 10 May 2022 11:43:00 +0300 Subject: [PATCH 42/59] fix getting text of ListView items --- pywinauto/controls/wpf_controls.py | 31 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index eb0b0018a..7fcbd2b01 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -1029,7 +1029,7 @@ class ListItemWrapper(wpfwrapper.WPFWrapper): """Wrap an WPF ListViewItem and DataGrid row controls""" - _control_types = ['ListItem', ] + _control_types = ['ListItem', 'DataItem'] # ----------------------------------------------------------- def __init__(self, elem, container=None): @@ -1044,16 +1044,15 @@ def __init__(self, elem, container=None): def texts(self): """Return a list of item texts""" - children = self.children() - if len(children) == 1 and children[0].element_info.control_type == 'Pane': - items_holder = children[0] # grid ListViewItem - - descendants = items_holder.children() - if len(descendants) == 1 and descendants[0].element_info.control_type == 'Pane': - return [self.window_text()] # ListBoxItem or non-grid ListViewItem + if self.element_info.control_type == 'ListItem': + return [self.window_text()] # ListBoxItem else: - items_holder = self # DataGridRow - return [elem.window_text() for elem in items_holder.children()] + children = self.children() + if len(children) == 1 and children[0].element_info.control_type == 'Pane': + items_holder = children[0] # ListViewItem + else: + items_holder = self # DataGridRow + return [elem.window_text() for elem in items_holder.children()] def select(self): """Select the item @@ -1216,7 +1215,7 @@ def __getitem__(self, key): # ----------------------------------------------------------- def item_count(self): """A number of items in the Grid""" - return len(self.children(control_type='ListItem')) + return len(self.children(control_type='DataItem')) # ----------------------------------------------------------- def column_count(self): @@ -1239,7 +1238,7 @@ def get_column(self, col_index): # ----------------------------------------------------------- def cells(self): """Return list of list of cells for any type of control""" - rows = self.children(control_type='ListItem') + rows = self.children(control_type='DataItem') result = [] for row in rows: @@ -1294,7 +1293,7 @@ def get_item(self, row): raise ValueError("Element '{0}' not found".format(row)) elif isinstance(row, six.integer_types): # Get the item by a row index - list_items = self.children(control_type='ListItem') + list_items = self.children(control_type='DataItem') itm = list_items[row] else: raise TypeError("String type or integer is expected") @@ -1309,7 +1308,7 @@ def get_item(self, row): # ----------------------------------------------------------- def get_items(self): """Return all items of the ListView control""" - return self.children(control_type='ListItem') + return self.children(control_type='DataItem') items = get_items # this is an alias to be consistent with other content elements @@ -1325,7 +1324,7 @@ def get_item_rect(self, item_index): def get_selection(self): # TODO get selected items directly from SelectedItems property - return [child for child in self.iter_children(control_type='ListItem') if child.is_selected()] + return [child for child in self.iter_children(control_type='DataItem') if child.is_selected()] # ----------------------------------------------------------- def get_selected_count(self): @@ -1343,7 +1342,7 @@ def get_selected_count(self): # ----------------------------------------------------------- def texts(self): """Return a list of item texts""" - return [elem.texts() for elem in self.descendants(control_type='ListItem')] + return [elem.texts() for elem in self.descendants(control_type='DataItem')] # ----------------------------------------------------------- @property From e9b7856dac6d9403b5e4df322fc4c113fb7c6dcf Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 10 May 2022 11:48:25 +0300 Subject: [PATCH 43/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index c4af37ec4..935868f7f 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit c4af37ec4e181693255b7d93f3ed602a8e0a600a +Subproject commit 935868f7f128a1573791314177d77687089c3bc1 From e0a56b1f1d9eee469f25f03f6f64f46383352f1b Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 10 May 2022 14:28:20 +0300 Subject: [PATCH 44/59] update submodule, minor change --- pywinauto/windows/injected | 2 +- pywinauto/windows/wpf_element_info.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 935868f7f..b3fa5db3f 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 935868f7f128a1573791314177d77687089c3bc1 +Subproject commit b3fa5db3fb30ec58dbd3e5b2428898240c3b959e diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 6d859c53b..ae2c35ea0 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -157,7 +157,8 @@ def descendants(self, **kwargs): return list(self.iter_descendants(**kwargs)) def iter_descendants(self, **kwargs): - cache_enable = kwargs.pop('cache_enable', False) + # TODO implement cache support + # cache_enable = kwargs.pop('cache_enable', False) depth = kwargs.pop("depth", None) process = kwargs.pop("process", None) if not isinstance(depth, (integer_types, type(None))) or isinstance(depth, integer_types) and depth < 0: From 4274d644a3a00deff9f40cf9eb73c2effd5841f3 Mon Sep 17 00:00:00 2001 From: eltio Date: Wed, 11 May 2022 11:47:41 +0300 Subject: [PATCH 45/59] fix creation of WPFWrapper subclasses --- pywinauto/controls/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pywinauto/controls/__init__.py b/pywinauto/controls/__init__.py index 3aa6fdacf..d0f92ce56 100644 --- a/pywinauto/controls/__init__.py +++ b/pywinauto/controls/__init__.py @@ -49,6 +49,7 @@ from . import win32_controls from . import wpfwrapper + from . import wpf_controls from ..base_wrapper import InvalidElement From 6e48904d8d684fdaa797019f2482981860b294a9 Mon Sep 17 00:00:00 2001 From: eltio Date: Wed, 11 May 2022 11:57:21 +0300 Subject: [PATCH 46/59] rename exceptions from injected submodule --- pywinauto/unittests/test_wpfwrapper.py | 6 +++--- pywinauto/windows/wpf_element_info.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index d007727b4..9b6717bd3 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -19,8 +19,8 @@ import pywinauto.controls.wpf_controls as wpf_ctls from pywinauto.controls.wpfwrapper import WPFWrapper -from pywinauto.windows.injected.api import InjectedTargetError -from pywinauto.windows.injected.channel import BrokenPipeError +from pywinauto.windows.injected.api import InjectedBaseError +from pywinauto.windows.injected.channel import InjectedBrokenPipeError wpf_samples_folder = os.path.join( os.path.dirname(__file__), r"..\..\apps\WPF_samples") @@ -304,7 +304,7 @@ def test_close(self): try: # process can be in the termination state at this moment self.assertEqual(self.dlg.exists(), False) - except (InjectedTargetError, BrokenPipeError): + except (InjectedBaseError, InjectedBrokenPipeError): pass def test_move_window(self): diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index ae2c35ea0..1979d6316 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -4,7 +4,7 @@ from pywinauto.handleprops import dumpwindow from pywinauto.element_info import ElementInfo from .win32structures import RECT -from .injected.api import ConnectionManager, NotFoundError, UnsupportedActionError +from .injected.api import ConnectionManager, InjectedNotFoundError, InjectedUnsupportedActionError def is_element_satisfying_criteria(element, process=None, class_name=None, name=None, control_type=None, @@ -193,7 +193,7 @@ def get_property(self, name, error_if_not_exists=False): try: reply = ConnectionManager().call_action('GetProperty', self._pid, element_id=self._element, name=name) return reply['value'] - except NotFoundError as e: + except InjectedNotFoundError as e: if error_if_not_exists: raise e return None @@ -231,5 +231,5 @@ def get_active(cls, app_or_pid): return cls(reply['value'], pid=pid) else: return None - except UnsupportedActionError: + except InjectedUnsupportedActionError: return None From e7a5ae20302a7bdaf25d1ccbe6af49e4c531601d Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 12 May 2022 15:22:45 +0300 Subject: [PATCH 47/59] fix docstrings for grid wrapper --- pywinauto/controls/wpf_controls.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 7fcbd2b01..6cd3107a0 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -1200,7 +1200,9 @@ def __init__(self, elem): class DataGridWrapper(wpfwrapper.WPFWrapper): - """Wrap WPF ListView (with a GridView view) or DataGrid control""" + """Wrap WPF controls that displays data in a customizable grid. + + It can be DataGrid or ListView control in a GridView mode""" _control_types = ['DataGrid'] @@ -1231,7 +1233,7 @@ def get_header_controls(self): # ----------------------------------------------------------- def get_column(self, col_index): - """Get the information for a column of the ListView""" + """Get the information for a column of the grid""" col = self.columns()[col_index] return col @@ -1251,16 +1253,10 @@ def cells(self): # ----------------------------------------------------------- def cell(self, row, column): - """Return a cell in the with a GridView view - - Only for controls with Grid pattern support + """Return a cell in the grid * **row** is an index of a row in the list. * **column** is an index of a column in the specified row. - - The returned cell can be of different control types. - Mostly: TextBlock, ImageControl, EditControl, DataItem - or even another layer of data items (Group, DataGrid) """ if not isinstance(row, six.integer_types) or not isinstance(column, six.integer_types): raise TypeError("row and column must be numbers") @@ -1274,7 +1270,7 @@ def cell(self, row, column): # ----------------------------------------------------------- def get_item(self, row): - """Return an item of the ListView control + """Return an item (elements in the specified row) of the grid control * **row** can be either an index of the row or a string with the text of a cell in the row you want returned. @@ -1307,7 +1303,7 @@ def get_item(self, row): # ----------------------------------------------------------- def get_items(self): - """Return all items of the ListView control""" + """Return all items of the grid control""" return self.children(control_type='DataItem') items = get_items # this is an alias to be consistent with other content elements From 50b7ec655f12dece947cd9ffa162ee4cf938b855 Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 12 May 2022 15:23:22 +0300 Subject: [PATCH 48/59] update submodule --- .gitmodules | 2 +- pywinauto/windows/injected | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 8275e83de..1b1f390dd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "pywinauto/windows/injected"] path = pywinauto/windows/injected url = https://github.com/eltimen/injected.git - branch = wrappers + branch = improvements diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index b3fa5db3f..78cc4f64c 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit b3fa5db3fb30ec58dbd3e5b2428898240c3b959e +Subproject commit 78cc4f64c00a4569de04aa013015d60b1a946bd4 From 0526c7ae7e0e9fd083470e42981f300d4b155145 Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 12 May 2022 18:23:28 +0300 Subject: [PATCH 49/59] fix children() test --- pywinauto/unittests/test_wpf_element_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/unittests/test_wpf_element_info.py b/pywinauto/unittests/test_wpf_element_info.py index e86630a57..92527f997 100644 --- a/pywinauto/unittests/test_wpf_element_info.py +++ b/pywinauto/unittests/test_wpf_element_info.py @@ -61,7 +61,7 @@ def testVisible(self): def testChildren(self): """Test whether a list of only immediate children of the element is equal""" self.assertEqual(len(self.ctrl.children()), 1) - self.assertEqual(len(self.ctrl.children()[0].children()), 2) + self.assertEqual(len(self.ctrl.children()[0].children()[0].children()), 2) def test_children_generator(self): """Test whether children generator iterates over correct elements""" From 89541c9074e8ce213eef0ff8a4c6f5eb46c93947 Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 24 May 2022 19:54:29 +0300 Subject: [PATCH 50/59] add method to get list of native properties --- pywinauto/controls/wpfwrapper.py | 4 ++++ pywinauto/unittests/test_wpf_element_info.py | 3 ++- pywinauto/unittests/test_wpfwrapper.py | 8 ++++++++ pywinauto/windows/wpf_element_info.py | 5 +++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index 3d781aef0..d13fc8647 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -61,6 +61,10 @@ def __init__(self, element_info): def get_property(self, name, error_if_not_exists=False): return self.element_info.get_property(name, error_if_not_exists) + def get_properties(self): + """Return a dict with names and types of available properties of the element""" + return self.element_info.get_properties() + def set_property(self, name, value, is_enum=False): ConnectionManager().call_action('SetProperty', self.element_info.pid, element_id=self.element_info.runtime_id, diff --git a/pywinauto/unittests/test_wpf_element_info.py b/pywinauto/unittests/test_wpf_element_info.py index 92527f997..38357c437 100644 --- a/pywinauto/unittests/test_wpf_element_info.py +++ b/pywinauto/unittests/test_wpf_element_info.py @@ -16,7 +16,7 @@ class WPFElementInfoTests(unittest.TestCase): - """Unit tests for the WPFlementInfo class""" + """Unit tests for the WPFElementInfo class""" def setUp(self): """Set some data and ensure the application is in the state we want""" @@ -101,5 +101,6 @@ def test_descendants_generator(self): descendants = [desc for desc in self.ctrl.iter_descendants(depth=3)] self.assertSequenceEqual(self.ctrl.descendants(depth=3), descendants) + if __name__ == "__main__": unittest.main() diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 9b6717bd3..49a5d059c 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -161,6 +161,14 @@ def test_texts(self): """Test getting texts of a control""" self.assertEqual(self.dlg.texts(), ['WPF Sample Application']) + def test_get_properties(self): + """Test getting list of properties of a control""" + props_dict = self.dlg.get_properties() + self.assertTrue(type(props_dict) is dict) + self.assertIn('Name', props_dict) + self.assertEquals(props_dict['Name'], 'String') + self.assertGreater(len(props_dict), 25) + def test_children(self): """Test getting children of a control""" tab_ctrl = self.dlg.by(class_name="TabControl").find() diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 1979d6316..47047a197 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -198,6 +198,11 @@ def get_property(self, name, error_if_not_exists=False): raise e return None + def get_properties(self): + """Return a dict with names and types of available properties of the element""" + reply = ConnectionManager().call_action('GetProperties', self._pid, element_id=self._element) + return reply['value'] + def __hash__(self): """Return a unique hash value based on the element's ID""" return hash(self._element) From 2622f90822f48fd981674bdbbd8283ba58766f5a Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 24 May 2022 21:04:46 +0300 Subject: [PATCH 51/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 78cc4f64c..ae8368f20 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 78cc4f64c00a4569de04aa013015d60b1a946bd4 +Subproject commit ae8368f20b4ff0cae6e41aae29eaaee8c99e6782 From b9609a72314b783664284b7164b47b0f00dfb267 Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 26 May 2022 15:44:43 +0300 Subject: [PATCH 52/59] rename property-related methods in WPFWrapper due to conflict with get_properties in BaseWrapper --- pywinauto/controls/wpf_controls.py | 98 +++++++++++++------------- pywinauto/controls/wpfwrapper.py | 30 ++++---- pywinauto/unittests/test_wpfwrapper.py | 2 +- pywinauto/windows/wpf_element_info.py | 14 ++-- 4 files changed, 72 insertions(+), 72 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 6cd3107a0..36b3f67e7 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -61,10 +61,10 @@ def move_window(self, x=None, y=None, width=None, height=None): height = cur_rect.height() # ask for the window to be moved - self.set_property('Left', x) - self.set_property('Top', y) - self.set_property('Width', width) - self.set_property('Height', height) + self.set_native_property('Left', x) + self.set_native_property('Top', y) + self.set_native_property('Width', width) + self.set_native_property('Height', height) time.sleep(timings.Timings.after_movewindow_wait) @@ -101,18 +101,18 @@ def toggle(self): Usually applied for the check box control. """ - current_state = self.get_property('IsChecked') - if self.get_property('IsThreeState'): + current_state = self.get_native_property('IsChecked') + if self.get_native_property('IsThreeState'): states = (True, False, None) else: states = (True, False) - if current_state is None and not self.get_property('IsThreeState'): + if current_state is None and not self.get_native_property('IsThreeState'): next_state = False else: next_state = states[(states.index(current_state)+1) % len(states)] - self.set_property('IsChecked', next_state) + self.set_native_property('IsChecked', next_state) name = self.element_info.name control_type = self.element_info.control_type @@ -132,7 +132,7 @@ def get_toggle_state(self): 1 - checked 2 - indeterminate """ - val = self.get_property('IsChecked') + val = self.get_native_property('IsChecked') if val is None: return self.INDETERMINATE return self.CHECKED if val else self.UNCHECKED @@ -156,7 +156,7 @@ def select(self): Usually applied for controls like: a radio button, a tree view item or a list item. """ - self.set_property('IsChecked', True) + self.set_native_property('IsChecked', True) name = self.element_info.name control_type = self.element_info.control_type @@ -173,7 +173,7 @@ def is_selected(self): Usually applied for controls like: a radio button, a tree view item, a list item. """ - return self.get_property('IsChecked') + return self.get_native_property('IsChecked') class ComboBoxWrapper(wpfwrapper.WPFWrapper): @@ -189,27 +189,27 @@ def __init__(self, elem): # ----------------------------------------------------------- def expand(self): - self.set_property('IsDropDownOpen', True) + self.set_native_property('IsDropDownOpen', True) return self # ----------------------------------------------------------- def collapse(self): - self.set_property('IsDropDownOpen', False) + self.set_native_property('IsDropDownOpen', False) return self # ----------------------------------------------------------- def is_editable(self): - return self.get_property('IsEditable') + return self.get_native_property('IsEditable') # ----------------------------------------------------------- def is_expanded(self): """Test if the control is expanded""" - return self.get_property('IsDropDownOpen') + return self.get_native_property('IsDropDownOpen') # ----------------------------------------------------------- def is_collapsed(self): """Test if the control is collapsed""" - return not self.get_property('IsDropDownOpen') + return not self.get_native_property('IsDropDownOpen') # ----------------------------------------------------------- def texts(self): @@ -225,7 +225,7 @@ def select(self, item): or it can be the string that you want to select """ if isinstance(item, six.integer_types): - self.set_property('SelectedIndex', item) + self.set_native_property('SelectedIndex', item) else: index = None for i, child in enumerate(self.iter_children()): @@ -233,7 +233,7 @@ def select(self, item): index = 1 if index is None: raise ValueError('no such item: {}'.format(item)) - self.set_property('SelectedIndex', index) + self.set_native_property('SelectedIndex', index) return self # ----------------------------------------------------------- @@ -245,7 +245,7 @@ def selected_text(self): Notice, that in case of multi-select it will be only the text from a first selected item """ - selected_index = self.get_property('SelectedIndex') + selected_index = self.get_native_property('SelectedIndex') if selected_index == -1: return '' return self.children()[selected_index].element_info.rich_text @@ -254,7 +254,7 @@ def selected_text(self): # TODO: add selected_indices for a combobox with multi-select support def selected_index(self): """Return the selected index""" - return self.get_property('SelectedIndex') + return self.get_native_property('SelectedIndex') # ----------------------------------------------------------- def item_count(self): @@ -318,12 +318,12 @@ def get_line(self, line_index): # ----------------------------------------------------------- def get_value(self): """Return the current value of the element""" - return self.get_property('Text') or '' + return self.get_native_property('Text') or '' # ----------------------------------------------------------- def is_editable(self): """Return the edit possibility of the element""" - return not self.get_property('IsReadOnly') + return not self.get_native_property('IsReadOnly') # ----------------------------------------------------------- def texts(self): @@ -340,8 +340,8 @@ def text_block(self): # ----------------------------------------------------------- def selection_indices(self): """The start and end indices of the current selection""" - start = self.get_property('SelectionStart') - end = start + self.get_property('SelectionLength') + start = self.get_native_property('SelectionStart') + end = start + self.get_native_property('SelectionLength') return start, end @@ -358,7 +358,7 @@ def set_window_text(self, text, append=False): if append: text = self.window_text() + text - self.set_property('Text', text) + self.set_native_property('Text', text) # ----------------------------------------------------------- def set_edit_text(self, text, pos_start=None, pos_end=None): @@ -399,7 +399,7 @@ def set_edit_text(self, text, pos_start=None, pos_end=None): current_text = self.window_text() new_text = current_text[:pos_start] + aligned_text + current_text[pos_end:] - self.set_property('Text', new_text) + self.set_native_property('Text', new_text) # time.sleep(Timings.after_editsetedittext_wait) @@ -438,8 +438,8 @@ def select(self, start=0, end=None): raise RuntimeError("Text '{0}' hasn't been found".format(string_to_select)) end = start + len(string_to_select) - self.set_property('SelectionStart', start) - self.set_property('SelectionLength', end-start) + self.set_native_property('SelectionStart', start) + self.set_native_property('SelectionLength', end - start) # return this control so that actions can be chained. return self @@ -459,7 +459,7 @@ def __init__(self, elem): # ---------------------------------------------------------------- def get_selected_tab(self): """Return an index of a selected tab""" - return self.get_property('SelectedIndex') + return self.get_native_property('SelectedIndex') # ---------------------------------------------------------------- def tab_count(self): @@ -470,7 +470,7 @@ def tab_count(self): def select(self, item): """Select a tab by index or by name""" if isinstance(item, six.integer_types): - self.set_property('SelectedIndex', item) + self.set_native_property('SelectedIndex', item) else: index = None for i, child in enumerate(self.iter_children()): @@ -478,7 +478,7 @@ def select(self, item): index = 1 if index is None: raise ValueError('no such item: {}'.format(item)) - self.set_property('SelectedIndex', index) + self.set_native_property('SelectedIndex', index) return self # ---------------------------------------------------------------- @@ -502,12 +502,12 @@ def __init__(self, elem): # ----------------------------------------------------------- def min_value(self): """Get the minimum value of the Slider""" - return self.get_property('Minimum') + return self.get_native_property('Minimum') # ----------------------------------------------------------- def max_value(self): """Get the maximum value of the Slider""" - return self.get_property('Maximum') + return self.get_native_property('Maximum') # ----------------------------------------------------------- def small_change(self): @@ -517,7 +517,7 @@ def small_change(self): This change is achieved by pressing left and right arrows when slider's thumb has keyboard focus. """ - return self.get_property('SmallChange') + return self.get_native_property('SmallChange') # ----------------------------------------------------------- def large_change(self): @@ -527,12 +527,12 @@ def large_change(self): This change is achieved by pressing PgUp and PgDown keys when slider's thumb has keyboard focus. """ - return self.get_property('LargeChange') + return self.get_native_property('LargeChange') # ----------------------------------------------------------- def value(self): """Get a current position of slider's thumb""" - return self.get_property('Value') + return self.get_native_property('Value') # ----------------------------------------------------------- def set_value(self, value): @@ -551,7 +551,7 @@ def set_value(self, value): if not (min_value <= value_to_set <= max_value): raise ValueError("value should be bigger than {0} and smaller than {1}".format(min_value, max_value)) - self.set_property('Value', value_to_set) + self.set_native_property('Value', value_to_set) class ToolbarWrapper(wpfwrapper.WPFWrapper): @@ -660,15 +660,15 @@ def check_button(self, button_identifier, make_checked, exact=True): def collapse(self): """Collapse overflow area of the ToolBar (IsOverflowOpen property)""" - self.set_property('IsOverflowOpen', False) + self.set_native_property('IsOverflowOpen', False) def expand(self): """Expand overflow area of the ToolBar (IsOverflowOpen property)""" - self.set_property('IsOverflowOpen', True) + self.set_native_property('IsOverflowOpen', True) def is_expanded(self): """Check if the ToolBar overflow area is currently visible""" - return not self.get_property('HasOverflowItems') or self.get_property('IsOverflowOpen') + return not self.get_native_property('HasOverflowItems') or self.get_native_property('IsOverflowOpen') def is_collapsed(self): """Check if the ToolBar overflow area is not visible""" @@ -724,7 +724,7 @@ def _activate(self, item): """Activate the specified item""" if not item.is_active(): item.set_focus() - item.set_property('IsSubmenuOpen', True) + item.set_native_property('IsSubmenuOpen', True) # ----------------------------------------------------------- def _sub_item_by_text(self, menu, name, exact, is_last): @@ -881,33 +881,33 @@ def sub_elements(self, depth=None): return self.descendants(control_type="TreeItem", depth=depth) def expand(self): - self.set_property('IsExpanded', True) + self.set_native_property('IsExpanded', True) return self # ----------------------------------------------------------- def collapse(self): - self.set_property('IsExpanded', False) + self.set_native_property('IsExpanded', False) return self # ----------------------------------------------------------- def is_expanded(self): """Test if the control is expanded""" - return self.get_property('IsExpanded') + return self.get_native_property('IsExpanded') # ----------------------------------------------------------- def is_collapsed(self): """Test if the control is collapsed""" - return not self.get_property('IsExpanded') + return not self.get_native_property('IsExpanded') # ----------------------------------------------------------- def select(self): - self.set_property('IsSelected', True) + self.set_native_property('IsSelected', True) return self # ----------------------------------------------------------- def is_selected(self): """Test if the control is expanded""" - return self.get_property('IsSelected') + return self.get_native_property('IsSelected') # ==================================================================== @@ -1060,7 +1060,7 @@ def select(self): Usually applied for controls like: a radio button, a tree view item or a list item. """ - self.set_property('IsSelected', True) + self.set_native_property('IsSelected', True) name = self.element_info.name control_type = self.element_info.control_type @@ -1077,7 +1077,7 @@ def is_selected(self): Usually applied for controls like: a radio button, a tree view item, a list item. """ - return self.get_property('IsSelected') + return self.get_native_property('IsSelected') class ListViewWrapper(wpfwrapper.WPFWrapper): diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index d13fc8647..9bc5d2844 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -58,14 +58,14 @@ def __init__(self, element_info): """ WinBaseWrapper.__init__(self, element_info, backend.registry.backends['wpf']) - def get_property(self, name, error_if_not_exists=False): - return self.element_info.get_property(name, error_if_not_exists) + def get_native_property(self, name, error_if_not_exists=False): + return self.element_info.get_native_property(name, error_if_not_exists) - def get_properties(self): + def get_native_properties(self): """Return a dict with names and types of available properties of the element""" - return self.element_info.get_properties() + return self.element_info.get_native_properties() - def set_property(self, name, value, is_enum=False): + def set_native_property(self, name, value, is_enum=False): ConnectionManager().call_action('SetProperty', self.element_info.pid, element_id=self.element_info.runtime_id, name=name, @@ -89,11 +89,11 @@ def automation_id(self): def is_keyboard_focusable(self): """Return True if the element can be focused with keyboard""" - return self.get_property('Focusable') or False + return self.get_native_property('Focusable') or False def has_keyboard_focus(self): """Return True if the element is focused with keyboard""" - return self.get_property('IsKeyboardFocused') or False + return self.get_native_property('IsKeyboardFocused') or False def set_focus(self): ConnectionManager().call_action('SetFocus', self.element_info.pid, @@ -135,7 +135,7 @@ def minimize(self): """ Minimize the window """ - self.set_property('WindowState', 'Minimized', is_enum=True) + self.set_native_property('WindowState', 'Minimized', is_enum=True) return self # ----------------------------------------------------------- @@ -145,7 +145,7 @@ def maximize(self): Only controls supporting Window pattern should answer """ - self.set_property('WindowState', 'Maximized', is_enum=True) + self.set_native_property('WindowState', 'Maximized', is_enum=True) return self # ----------------------------------------------------------- @@ -156,13 +156,13 @@ def restore(self): Only controls supporting Window pattern should answer """ # it's very strange, but set WindowState to Normal is not enough... - self.set_property('WindowState', 'Normal', is_enum=True) - restore_rect = self.get_property('RestoreBounds') + self.set_native_property('WindowState', 'Normal', is_enum=True) + restore_rect = self.get_native_property('RestoreBounds') self.move_window(restore_rect['left'], restore_rect['top'], - self.get_property('Width'), - self.get_property('Height')) - self.set_property('WindowState', 'Normal', is_enum=True) + self.get_native_property('Width'), + self.get_native_property('Height')) + self.set_native_property('WindowState', 'Normal', is_enum=True) return self # ----------------------------------------------------------- @@ -175,7 +175,7 @@ def get_show_state(self): Maximized = 1 Minimized = 2 """ - val = self.element_info.get_property('WindowState') + val = self.element_info.get_native_property('WindowState') if val == 'Normal': return self.NORMAL elif val == 'Maximized': diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 49a5d059c..70c1d61de 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -163,7 +163,7 @@ def test_texts(self): def test_get_properties(self): """Test getting list of properties of a control""" - props_dict = self.dlg.get_properties() + props_dict = self.dlg.get_native_properties() self.assertTrue(type(props_dict) is dict) self.assertIn('Name', props_dict) self.assertEquals(props_dict['Name'], 'String') diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 47047a197..32f168143 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -68,7 +68,7 @@ def auto_id(self): """Return AutomationId of the element""" if self._element == 0: return '' - return self.get_property('Name') or '' + return self.get_native_property('Name') or '' @property def name(self): @@ -80,13 +80,13 @@ def name(self): @property def rich_text(self): if self.control_type == 'Edit': - return self.get_property('Text') or '' + return self.get_native_property('Text') or '' return self.name @property def value(self): if self.control_type == 'Edit': - return self.get_property('Text') or self.get_property('Password') or '' + return self.get_native_property('Text') or self.get_native_property('Password') or '' return '' @property @@ -118,13 +118,13 @@ def class_name(self): def enabled(self): if self._element == 0: return True - return self.get_property('IsEnabled') or False + return self.get_native_property('IsEnabled') or False @property def visible(self): if self._element == 0: return True - return self.get_property('IsVisible') or False + return self.get_native_property('IsVisible') or False @property def parent(self): @@ -189,7 +189,7 @@ def rectangle(self): def dump_window(self): return dumpwindow(self.handle) - def get_property(self, name, error_if_not_exists=False): + def get_native_property(self, name, error_if_not_exists=False): try: reply = ConnectionManager().call_action('GetProperty', self._pid, element_id=self._element, name=name) return reply['value'] @@ -198,7 +198,7 @@ def get_property(self, name, error_if_not_exists=False): raise e return None - def get_properties(self): + def get_native_properties(self): """Return a dict with names and types of available properties of the element""" reply = ConnectionManager().call_action('GetProperties', self._pid, element_id=self._element) return reply['value'] From 7ff48f6eb787e08b6237d111a16499927c8666f7 Mon Sep 17 00:00:00 2001 From: eltio Date: Tue, 7 Jun 2022 22:01:16 +0300 Subject: [PATCH 53/59] fix issues from code analysis (mostly docstrings) --- pywinauto/controls/wpf_controls.py | 303 ++++++++++--------- pywinauto/controls/wpfwrapper.py | 102 +++++-- pywinauto/unittests/test_wpf_element_info.py | 32 ++ pywinauto/unittests/test_wpfwrapper.py | 34 ++- pywinauto/windows/wpf_element_info.py | 90 +++++- 5 files changed, 381 insertions(+), 180 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 36b3f67e7..b3bcbdce8 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -1,3 +1,35 @@ +# -*- coding: utf-8 -*- +# GUI Application automation and testing library +# Copyright (C) 2006-2017 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. + """Wrap various WPF windows controls. To be used with 'wpf' backend.""" import locale import time @@ -12,18 +44,18 @@ # ==================================================================== class WindowWrapper(wpfwrapper.WPFWrapper): - """Wrap a WPF Window control""" + """Wrap a WPF Window control.""" _control_types = ['Window'] # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(WindowWrapper, self).__init__(elem) # ----------------------------------------------------------- def move_window(self, x=None, y=None, width=None, height=None): - """Move the window to the new coordinates + """Move the window to the new coordinates. * **x** Specifies the new left position of the window. Defaults to the current left position of the window. @@ -70,13 +102,13 @@ def move_window(self, x=None, y=None, width=None, height=None): # ----------------------------------------------------------- def is_dialog(self): - """Window is always a dialog so return True""" + """Window is always a dialog so return True.""" return True class ButtonWrapper(wpfwrapper.WPFWrapper): - """Wrap a WPF Button, CheckBox or RadioButton control""" + """Wrap a WPF Button, CheckBox or RadioButton control.""" _control_types = ['Button', 'CheckBox', @@ -89,14 +121,13 @@ class ButtonWrapper(wpfwrapper.WPFWrapper): # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(ButtonWrapper, self).__init__(elem) # ----------------------------------------------------------- def toggle(self): - """ - Switch state of checkable controls in cycle between CHECKED/UNCHECKED or - CHECKED/UNCHECKED/INDETERMINATE (if a control is three-state) + """Switch state of checkable controls in cycle between CHECKED/UNCHECKED or + CHECKED/UNCHECKED/INDETERMINATE (if a control is three-state). Usually applied for the check box control. """ @@ -124,8 +155,7 @@ def toggle(self): # ----------------------------------------------------------- def get_toggle_state(self): - """ - Get a toggle state of a check box control. + """Get a toggle state of a check box control. The toggle state is represented by an integer 0 - unchecked @@ -140,18 +170,18 @@ def get_toggle_state(self): # ----------------------------------------------------------- def is_dialog(self): - """Buttons are never dialogs so return False""" + """Buttons are never dialogs so return False.""" return False # ----------------------------------------------------------- def click(self): - """Click the Button control by raising the ButtonBase.Click event""" + """Click the Button control by raising the ButtonBase.Click event.""" self.raise_event('Click') # Return itself so that action can be chained return self def select(self): - """Select the item + """Select the item. Usually applied for controls like: a radio button, a tree view item or a list item. @@ -178,48 +208,50 @@ def is_selected(self): class ComboBoxWrapper(wpfwrapper.WPFWrapper): - """Wrap a WPF CoboBox control""" + """Wrap a WPF ComboBox control.""" _control_types = ['ComboBox'] # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(ComboBoxWrapper, self).__init__(elem) # ----------------------------------------------------------- def expand(self): + """Display items of the combobox.""" self.set_native_property('IsDropDownOpen', True) return self # ----------------------------------------------------------- def collapse(self): + """Hide items of the combobox.""" self.set_native_property('IsDropDownOpen', False) return self # ----------------------------------------------------------- def is_editable(self): + """Return the edit possibility of the element.""" return self.get_native_property('IsEditable') # ----------------------------------------------------------- def is_expanded(self): - """Test if the control is expanded""" + """Test if the control is expanded.""" return self.get_native_property('IsDropDownOpen') # ----------------------------------------------------------- def is_collapsed(self): - """Test if the control is collapsed""" + """Test if the control is collapsed.""" return not self.get_native_property('IsDropDownOpen') # ----------------------------------------------------------- def texts(self): - """Return the text of the items in the combobox""" + """Return the text of the items in the combobox.""" return [child.element_info.rich_text for child in self.iter_children()] # ----------------------------------------------------------- def select(self, item): - """ - Select the ComboBox item + """Select the combobox item. The item can be either a 0 based index of the item to select or it can be the string that you want to select @@ -228,19 +260,18 @@ def select(self, item): self.set_native_property('SelectedIndex', item) else: index = None - for i, child in enumerate(self.iter_children()): + for child in self.iter_children(): if child.element_info.rich_text == item: index = 1 if index is None: - raise ValueError('no such item: {}'.format(item)) + raise IndexError('no such item: {}'.format(item)) self.set_native_property('SelectedIndex', index) return self # ----------------------------------------------------------- # TODO: add selected_texts for a combobox with a multi-select support def selected_text(self): - """ - Return the selected text or None + """Return the selected text or None. Notice, that in case of multi-select it will be only the text from a first selected item @@ -253,13 +284,12 @@ def selected_text(self): # ----------------------------------------------------------- # TODO: add selected_indices for a combobox with multi-select support def selected_index(self): - """Return the selected index""" + """Return the selected index.""" return self.get_native_property('SelectedIndex') # ----------------------------------------------------------- def item_count(self): - """ - Return the number of items in the combobox + """Return the number of items in the ComboBox. The interface is kept mostly for a backward compatibility with the native ComboBox interface @@ -269,14 +299,14 @@ def item_count(self): class EditWrapper(wpfwrapper.WPFWrapper): - """Wrap an Edit control""" + """Wrap an Edit control.""" _control_types = ['Edit'] has_title = False # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(EditWrapper, self).__init__(elem) # ----------------------------------------------------------- @@ -289,12 +319,12 @@ def writable_props(self): # ----------------------------------------------------------- def line_count(self): - """Return how many lines there are in the Edit""" + """Return how many lines there are in the Edit.""" return self.window_text().count("\n") + 1 # ----------------------------------------------------------- def line_length(self, line_index): - """Return how many characters there are in the line""" + """Return how many characters there are in the line.""" # need to first get a character index of that line lines = self.window_text().splitlines() if line_index < len(lines): @@ -306,7 +336,7 @@ def line_length(self, line_index): # ----------------------------------------------------------- def get_line(self, line_index): - """Return the line specified""" + """Return the line specified.""" lines = self.window_text().splitlines() if line_index < len(lines): return lines[line_index] @@ -317,29 +347,29 @@ def get_line(self, line_index): # ----------------------------------------------------------- def get_value(self): - """Return the current value of the element""" + """Return the current value of the element.""" return self.get_native_property('Text') or '' # ----------------------------------------------------------- def is_editable(self): - """Return the edit possibility of the element""" + """Return the edit possibility of the element.""" return not self.get_native_property('IsReadOnly') # ----------------------------------------------------------- def texts(self): - """Get the text of the edit control""" + """Get the text of the edit control.""" texts = [ self.get_line(i) for i in range(self.line_count()) ] return texts # ----------------------------------------------------------- def text_block(self): - """Get the text of the edit control""" + """Get the text of the edit control.""" return self.window_text() # ----------------------------------------------------------- def selection_indices(self): - """The start and end indices of the current selection""" + """The start and end indices of the current selection.""" start = self.get_native_property('SelectionStart') end = start + self.get_native_property('SelectionLength') @@ -347,8 +377,7 @@ def selection_indices(self): # ----------------------------------------------------------- def set_window_text(self, text, append=False): - """Override set_window_text for edit controls because it should not be - used for Edit controls. + """Override set_window_text for edit controls because it should not be used for Edit controls. Edit Controls should either use set_edit_text() or type_keys() to modify the contents of the edit control. @@ -362,7 +391,7 @@ def set_window_text(self, text, append=False): # ----------------------------------------------------------- def set_edit_text(self, text, pos_start=None, pos_end=None): - """Set the text of the edit control""" + """Set the text of the edit control.""" self.verify_actionable() # allow one or both of pos_start and pos_end to be None @@ -415,7 +444,7 @@ def set_edit_text(self, text, pos_start=None, pos_end=None): # ----------------------------------------------------------- def select(self, start=0, end=None): - """Set the edit selection of the edit control""" + """Set the edit selection of the edit control.""" self.verify_actionable() self.set_focus() @@ -447,72 +476,71 @@ def select(self, start=0, end=None): class TabControlWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF Tab control""" + """Wrap an WPF Tab control.""" _control_types = ['Tab'] # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(TabControlWrapper, self).__init__(elem) # ---------------------------------------------------------------- def get_selected_tab(self): - """Return an index of a selected tab""" + """Return an index of a selected tab.""" return self.get_native_property('SelectedIndex') # ---------------------------------------------------------------- def tab_count(self): - """Return a number of tabs""" + """Return a number of tabs.""" return len(self.children()) # ---------------------------------------------------------------- def select(self, item): - """Select a tab by index or by name""" + """Select a tab by index or by name.""" if isinstance(item, six.integer_types): self.set_native_property('SelectedIndex', item) else: index = None - for i, child in enumerate(self.iter_children()): + for child in self.iter_children(): if child.element_info.rich_text == item: index = 1 if index is None: - raise ValueError('no such item: {}'.format(item)) + raise IndexError('no such item: {}'.format(item)) self.set_native_property('SelectedIndex', index) return self # ---------------------------------------------------------------- def texts(self): - """Tabs texts""" + """Tabs texts.""" return self.children_texts() class SliderWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF Slider control""" + """Wrap an WPF Slider control.""" _control_types = ['Slider'] has_title = False # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(SliderWrapper, self).__init__(elem) # ----------------------------------------------------------- def min_value(self): - """Get the minimum value of the Slider""" + """Get the minimum value of the Slider.""" return self.get_native_property('Minimum') # ----------------------------------------------------------- def max_value(self): - """Get the maximum value of the Slider""" + """Get the maximum value of the Slider.""" return self.get_native_property('Maximum') # ----------------------------------------------------------- def small_change(self): - """ - Get a small change of slider's thumb + """Get a small change of slider's thumb. This change is achieved by pressing left and right arrows when slider's thumb has keyboard focus. @@ -521,8 +549,7 @@ def small_change(self): # ----------------------------------------------------------- def large_change(self): - """ - Get a large change of slider's thumb + """Get a large change of slider's thumb. This change is achieved by pressing PgUp and PgDown keys when slider's thumb has keyboard focus. @@ -531,12 +558,12 @@ def large_change(self): # ----------------------------------------------------------- def value(self): - """Get a current position of slider's thumb""" + """Get a current position of slider's thumb.""" return self.get_native_property('Value') # ----------------------------------------------------------- def set_value(self, value): - """Set position of slider's thumb""" + """Set position of slider's thumb.""" if isinstance(value, float): value_to_set = value elif isinstance(value, six.integer_types): @@ -556,7 +583,7 @@ def set_value(self, value): class ToolbarWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF ToolBar control + """Wrap an WPF ToolBar control. The control's children usually are: Buttons, SplitButton, MenuItems, ThumbControls, TextControls, Separators, CheckBoxes. @@ -568,7 +595,7 @@ class ToolbarWrapper(wpfwrapper.WPFWrapper): # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(ToolbarWrapper, self).__init__(elem) self.win32_wrapper = None if len(self.children()) <= 1 and self.element_info.handle is not None: @@ -583,22 +610,22 @@ def writable_props(self): # ---------------------------------------------------------------- def texts(self): - """Return texts of the Toolbar""" + """Return texts of the Toolbar.""" return [c.window_text() for c in self.buttons()] #---------------------------------------------------------------- def button_count(self): - """Return a number of buttons on the ToolBar""" + """Return a number of buttons on the ToolBar.""" return len(self.children()) # ---------------------------------------------------------------- def buttons(self): - """Return all available buttons""" + """Return all available buttons.""" return self.children() # ---------------------------------------------------------------- def button(self, button_identifier, exact=True): - """Return a button by the specified identifier + """Return a button by the specified identifier. * **button_identifier** can be either an index of a button or a string with the text of the button. @@ -629,7 +656,7 @@ def button(self, button_identifier, exact=True): # ---------------------------------------------------------------- def check_button(self, button_identifier, make_checked, exact=True): - """Find where the button is and toggle it + """Find where the button is and toggle it. * **button_identifier** can be either an index of the button or a string with the text on the button. @@ -659,76 +686,76 @@ def check_button(self, button_identifier, make_checked, exact=True): return button def collapse(self): - """Collapse overflow area of the ToolBar (IsOverflowOpen property)""" + """Collapse overflow area of the ToolBar (IsOverflowOpen property).""" self.set_native_property('IsOverflowOpen', False) def expand(self): - """Expand overflow area of the ToolBar (IsOverflowOpen property)""" + """Expand overflow area of the ToolBar (IsOverflowOpen property).""" self.set_native_property('IsOverflowOpen', True) def is_expanded(self): - """Check if the ToolBar overflow area is currently visible""" + """Check if the ToolBar overflow area is currently visible.""" return not self.get_native_property('HasOverflowItems') or self.get_native_property('IsOverflowOpen') def is_collapsed(self): - """Check if the ToolBar overflow area is not visible""" + """Check if the ToolBar overflow area is not visible.""" return not self.is_expanded() class MenuItemWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF MenuItem control""" + """Wrap an WPF MenuItem control.""" _control_types = ['MenuItem'] # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(MenuItemWrapper, self).__init__(elem) # ----------------------------------------------------------- def items(self): - """Find all items of the menu item""" + """Find all items of the menu item.""" return self.children(control_type="MenuItem") # ----------------------------------------------------------- def select(self): - """Select Menu item by raising Click event""" + """Select Menu item by raising Click event.""" self.raise_event('Click') class MenuWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF MenuBar or Menu control""" + """Wrap an WPF MenuBar or Menu control.""" _control_types = ['Menu'] # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(MenuWrapper, self).__init__(elem) # ----------------------------------------------------------- def items(self): - """Find all menu items""" + """Find all menu items.""" return self.children(control_type="MenuItem") # ----------------------------------------------------------- def item_by_index(self, idx): - """Find a menu item specified by the index""" + """Find a menu item specified by the index.""" item = self.items()[idx] return item # ----------------------------------------------------------- def _activate(self, item): - """Activate the specified item""" + """Activate the specified item.""" if not item.is_active(): item.set_focus() item.set_native_property('IsSubmenuOpen', True) # ----------------------------------------------------------- - def _sub_item_by_text(self, menu, name, exact, is_last): - """Find a menu sub-item by the specified text""" + def _sub_item_by_text(self, menu, name, exact): + """Find a menu sub-item by the specified text.""" sub_item = None items = menu.items() if items: @@ -750,8 +777,8 @@ def _sub_item_by_text(self, menu, name, exact, is_last): return sub_item # ----------------------------------------------------------- - def _sub_item_by_idx(self, menu, idx, is_last): - """Find a menu sub-item by the specified index""" + def _sub_item_by_idx(self, menu, idx): + """Find a menu sub-item by the specified index.""" sub_item = None items = menu.items() if items: @@ -765,7 +792,7 @@ def _sub_item_by_idx(self, menu, idx, is_last): # ----------------------------------------------------------- def item_by_path(self, path, exact=False): - """Find a menu item specified by the path + """Find a menu item specified by the path. The full path syntax is specified in: :py:meth:`.controls.menuwrapper.Menu.get_menu_path` @@ -783,9 +810,8 @@ def item_by_path(self, path, exact=False): def next_level_menu(parent_menu, item_name, is_last): if item_name.startswith("#"): - return self._sub_item_by_idx(parent_menu, int(item_name[1:]), is_last) - else: - return self._sub_item_by_text(parent_menu, item_name, exact, is_last) + return self._sub_item_by_idx(parent_menu, int(item_name[1:])) + return self._sub_item_by_text(parent_menu, item_name, exact) # Find a top level menu item and select it. After selecting this item # a new Menu control is created and placed on the dialog. It can be @@ -814,7 +840,7 @@ def next_level_menu(parent_menu, item_name, is_last): class TreeItemWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF TreeItem control + """Wrap an WPF TreeItem control. In addition to the provided methods of the wrapper additional inherited methods can be especially helpful: @@ -826,17 +852,17 @@ class TreeItemWrapper(wpfwrapper.WPFWrapper): # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(TreeItemWrapper, self).__init__(elem) # ----------------------------------------------------------- def ensure_visible(self): - """Make sure that the TreeView item is visible""" + """Make sure that the TreeView item is visible.""" self.invoke_method('BringIntoView') # ----------------------------------------------------------- def get_child(self, child_spec, exact=False): - """Return the child item of this item + """Return the child item of this item. Accepts either a string or an index. If a string is passed then it returns the child item @@ -862,7 +888,7 @@ def get_child(self, child_spec, exact=False): # ----------------------------------------------------------- def _calc_click_coords(self): - """Override the BaseWrapper helper method + """Override the BaseWrapper helper method. Set coordinates close to a left part of the item rectangle @@ -877,7 +903,7 @@ def _calc_click_coords(self): # ----------------------------------------------------------- def sub_elements(self, depth=None): - """Return a list of all visible sub-items of this control""" + """Return a list of all visible sub-items of this control.""" return self.descendants(control_type="TreeItem", depth=depth) def expand(self): @@ -891,12 +917,12 @@ def collapse(self): # ----------------------------------------------------------- def is_expanded(self): - """Test if the control is expanded""" + """Test if the control is expanded.""" return self.get_native_property('IsExpanded') # ----------------------------------------------------------- def is_collapsed(self): - """Test if the control is collapsed""" + """Test if the control is collapsed.""" return not self.get_native_property('IsExpanded') # ----------------------------------------------------------- @@ -906,20 +932,20 @@ def select(self): # ----------------------------------------------------------- def is_selected(self): - """Test if the control is expanded""" + """Test if the control is expanded.""" return self.get_native_property('IsSelected') # ==================================================================== class TreeViewWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF Tree control""" + """Wrap an WPF Tree control.""" _control_types = ['Tree'] # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(TreeViewWrapper, self).__init__(elem) @property @@ -931,17 +957,17 @@ def writable_props(self): # ----------------------------------------------------------- def item_count(self, depth=None): - """Return a number of items in TreeView""" + """Return a number of items in TreeView.""" return len(self.descendants(control_type="TreeItem", depth=depth)) # ----------------------------------------------------------- def roots(self): - """Return root elements of TreeView""" + """Return root elements of TreeView.""" return self.children(control_type="TreeItem") # ----------------------------------------------------------- def get_item(self, path, exact=False): - r"""Read a TreeView item + r"""Read a TreeView item. * **path** a path to the item to return. This can be one of the following: @@ -1009,11 +1035,11 @@ def get_item(self, path, exact=False): # ----------------------------------------------------------- def print_items(self, max_depth=None): - """Print all items with line indents""" + """Print all items with line indents.""" self.text = "" def _print_one_level(item, ident): - """Get texts for the item and its children""" + """Get texts for the item and its children.""" self.text += " " * ident + item.window_text() + "\n" if max_depth is None or ident <= max_depth: for child in item.children(control_type="TreeItem"): @@ -1027,13 +1053,13 @@ def _print_one_level(item, ident): class ListItemWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF ListViewItem and DataGrid row controls""" + """Wrap an WPF ListViewItem and DataGrid row controls.""" _control_types = ['ListItem', 'DataItem'] # ----------------------------------------------------------- def __init__(self, elem, container=None): - """Initialize the control""" + """Initialize the control.""" super(ListItemWrapper, self).__init__(elem) # Init a pointer to the item's container wrapper. @@ -1043,19 +1069,18 @@ def __init__(self, elem, container=None): self.container = container def texts(self): - """Return a list of item texts""" + """Return a list of item texts.""" if self.element_info.control_type == 'ListItem': - return [self.window_text()] # ListBoxItem + return [self.window_text()] # ListBoxItem + children = self.children() + if len(children) == 1 and children[0].element_info.control_type == 'Pane': + items_holder = children[0] # ListViewItem else: - children = self.children() - if len(children) == 1 and children[0].element_info.control_type == 'Pane': - items_holder = children[0] # ListViewItem - else: - items_holder = self # DataGridRow - return [elem.window_text() for elem in items_holder.children()] + items_holder = self # DataGridRow + return [elem.window_text() for elem in items_holder.children()] def select(self): - """Select the item + """Select the item. Usually applied for controls like: a radio button, a tree view item or a list item. @@ -1082,13 +1107,13 @@ def is_selected(self): class ListViewWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF ListView control""" + """Wrap an WPF ListView control.""" _control_types = ['List'] # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(ListViewWrapper, self).__init__(elem) def __getitem__(self, key): @@ -1096,17 +1121,17 @@ def __getitem__(self, key): # ----------------------------------------------------------- def item_count(self): - """A number of items in the List""" + """A number of items in the List.""" return len(self.children()) # ----------------------------------------------------------- def cells(self): - """Return list of list of cells for any type of control""" + """Return list of list of cells for any type of control.""" return self.children(content_only=True) # ----------------------------------------------------------- def get_item(self, row): - """Return an item of the ListView control + """Return an item of the ListView control. * **row** can be either an index of the row or a string with the text of a cell in the row you want returned. @@ -1138,14 +1163,14 @@ def get_item(self, row): # ----------------------------------------------------------- def get_items(self): - """Return all items of the ListView control""" + """Return all items of the ListView control.""" return self.children(content_only=True) items = get_items # this is an alias to be consistent with other content elements # ----------------------------------------------------------- def get_item_rect(self, item_index): - """Return the bounding rectangle of the list view item + """Return the bounding rectangle of the list view item. The method is kept mostly for a backward compatibility with the native ListViewWrapper interface @@ -1154,12 +1179,13 @@ def get_item_rect(self, item_index): return itm.rectangle() def get_selection(self): + """Get selected items.""" # TODO get selected items directly from SelectedItems property return [child for child in self.iter_children() if child.is_selected()] # ----------------------------------------------------------- def get_selected_count(self): - """Return a number of selected items + """Return a number of selected items. The call can be quite expensive as we retrieve all the selected items in order to count them @@ -1172,7 +1198,7 @@ def get_selected_count(self): # ----------------------------------------------------------- def texts(self): - """Return a list of item texts""" + """Return a list of item texts.""" return [elem.texts() for elem in self.children(content_only=True)] # ----------------------------------------------------------- @@ -1188,7 +1214,7 @@ def writable_props(self): class HeaderItemWrapper(wpfwrapper.WPFWrapper): - """Wrap an WPF Header Item control""" + """Wrap an WPF Header Item control.""" _control_types = ['HeaderItem'] @@ -1202,13 +1228,14 @@ class DataGridWrapper(wpfwrapper.WPFWrapper): """Wrap WPF controls that displays data in a customizable grid. - It can be DataGrid or ListView control in a GridView mode""" + It can be DataGrid or ListView control in a GridView mode. + """ _control_types = ['DataGrid'] # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(DataGridWrapper, self).__init__(elem) def __getitem__(self, key): @@ -1216,30 +1243,30 @@ def __getitem__(self, key): # ----------------------------------------------------------- def item_count(self): - """A number of items in the Grid""" + """A number of items in the Grid.""" return len(self.children(control_type='DataItem')) # ----------------------------------------------------------- def column_count(self): - """Return the number of columns""" + """Return the number of columns.""" return len(self.children(control_type='HeaderItem')) # ----------------------------------------------------------- def get_header_controls(self): - """Return Header controls associated with the Table""" + """Return Header controls associated with the Table.""" return self.children(control_type='HeaderItem') columns = get_header_controls # ----------------------------------------------------------- def get_column(self, col_index): - """Get the information for a column of the grid""" + """Get the information for a column of the grid.""" col = self.columns()[col_index] return col # ----------------------------------------------------------- def cells(self): - """Return list of list of cells for any type of control""" + """Return list of list of cells for any type of control.""" rows = self.children(control_type='DataItem') result = [] @@ -1253,7 +1280,7 @@ def cells(self): # ----------------------------------------------------------- def cell(self, row, column): - """Return a cell in the grid + """Return a cell in the grid. * **row** is an index of a row in the list. * **column** is an index of a column in the specified row. @@ -1270,7 +1297,7 @@ def cell(self, row, column): # ----------------------------------------------------------- def get_item(self, row): - """Return an item (elements in the specified row) of the grid control + """Return an item (elements in the specified row) of the grid control. * **row** can be either an index of the row or a string with the text of a cell in the row you want returned. @@ -1303,14 +1330,14 @@ def get_item(self, row): # ----------------------------------------------------------- def get_items(self): - """Return all items of the grid control""" + """Return all items of the grid control.""" return self.children(control_type='DataItem') items = get_items # this is an alias to be consistent with other content elements # ----------------------------------------------------------- def get_item_rect(self, item_index): - """Return the bounding rectangle of the list view item + """Return the bounding rectangle of the list view item. The method is kept mostly for a backward compatibility with the native ListViewWrapper interface @@ -1324,7 +1351,7 @@ def get_selection(self): # ----------------------------------------------------------- def get_selected_count(self): - """Return a number of selected items + """Return a number of selected items. The call can be quite expensive as we retrieve all the selected items in order to count them @@ -1337,7 +1364,7 @@ def get_selected_count(self): # ----------------------------------------------------------- def texts(self): - """Return a list of item texts""" + """Return a list of item texts.""" return [elem.texts() for elem in self.descendants(control_type='DataItem')] # ----------------------------------------------------------- diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index 9bc5d2844..e1d59f8ee 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -1,4 +1,36 @@ -"""Basic wrapping of WPF elements""" +# -*- coding: utf-8 -*- +# GUI Application automation and testing library +# Copyright (C) 2006-2017 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 WPF elements.""" from __future__ import unicode_literals from __future__ import print_function @@ -13,11 +45,13 @@ class WpfMeta(BaseMeta): - """Metaclass for WpfWrapper objects""" + + """Metaclass for WpfWrapper objects.""" + control_type_to_cls = {} def __init__(cls, name, bases, attrs): - """Register the control types""" + """Register the control types.""" BaseMeta.__init__(cls, name, bases, attrs) @@ -26,7 +60,7 @@ def __init__(cls, name, bases, attrs): @staticmethod def find_wrapper(element): - """Find the correct wrapper for this WPF element""" + """Find the correct wrapper for this WPF element.""" # Check for a more specific wrapper in the registry try: @@ -40,16 +74,18 @@ def find_wrapper(element): @six.add_metaclass(WpfMeta) class WPFWrapper(WinBaseWrapper): + + """Default wrapper for WPF control in the specified process""" + _control_types = [] def __new__(cls, element_info): - """Construct the control wrapper""" + """Construct the control wrapper.""" return super(WPFWrapper, cls)._create_wrapper(cls, element_info, WPFWrapper) # ----------------------------------------------------------- def __init__(self, element_info): - """ - Initialize the control + """Initialize the control. * **element_info** is either a valid UIAElementInfo or it can be an instance or subclass of UIAWrapper. @@ -59,13 +95,19 @@ def __init__(self, element_info): WinBaseWrapper.__init__(self, element_info, backend.registry.backends['wpf']) def get_native_property(self, name, error_if_not_exists=False): + """Return value of the specified property of the .NET object corresponding to the element.""" return self.element_info.get_native_property(name, error_if_not_exists) def get_native_properties(self): - """Return a dict with names and types of available properties of the element""" + """Return a dict with names and types of available properties of + the .NET object corresponding to the element.""" return self.element_info.get_native_properties() def set_native_property(self, name, value, is_enum=False): + """Set value of the specified native property via .NET reflection. + + * **is_enum** set it to True if ``value`` is the name of enum member + """ ConnectionManager().call_action('SetProperty', self.element_info.pid, element_id=self.element_info.runtime_id, name=name, @@ -73,49 +115,55 @@ def set_native_property(self, name, value, is_enum=False): return self def invoke_method(self, name): + """Invoke the specified method of the .NET object corresponding to the element. + + Method arguments are not supported yet. + """ ConnectionManager().call_action('InvokeMethod', self.element_info.pid, element_id=self.element_info.runtime_id, name=name) return self def raise_event(self, name): + """Raise the specified event of the .NET object corresponding to the element.""" ConnectionManager().call_action('RaiseEvent', self.element_info.pid, element_id=self.element_info.runtime_id, name=name) def automation_id(self): - """Return the Automation ID of the control""" + """Return the Automation ID of the control.""" return self.element_info.auto_id def is_keyboard_focusable(self): - """Return True if the element can be focused with keyboard""" + """Return True if the element can be focused with keyboard.""" return self.get_native_property('Focusable') or False def has_keyboard_focus(self): - """Return True if the element is focused with keyboard""" + """Return True if the element is focused with keyboard.""" return self.get_native_property('IsKeyboardFocused') or False def set_focus(self): + """Set the focus to this element.""" ConnectionManager().call_action('SetFocus', self.element_info.pid, element_id=self.element_info.runtime_id) return self def get_active(self): - """Return wrapper object for current active element""" + """Return wrapper object for current active element.""" element_info = self.backend.element_info_class.get_active(self.element_info.pid) if element_info is None: return None return self.backend.generic_wrapper_class(element_info) def is_active(self): - """Whether the window is active or not""" + """Whether the window is active or not.""" focused_wrap = self.get_active() if focused_wrap is None: return False return focused_wrap.top_level_parent() == self.top_level_parent() def children_texts(self): - """Get texts of the control's children""" + """Get texts of the control's children.""" return [c.window_text() for c in self.children()] # System.Windows.WindowState enum @@ -125,23 +173,18 @@ def children_texts(self): # ----------------------------------------------------------- def close(self): - """ - Close the window - """ + """Close the window.""" self.invoke_method('Close') # ----------------------------------------------------------- def minimize(self): - """ - Minimize the window - """ + """Minimize the window.""" self.set_native_property('WindowState', 'Minimized', is_enum=True) return self # ----------------------------------------------------------- def maximize(self): - """ - Maximize the window + """Maximize the window. Only controls supporting Window pattern should answer """ @@ -150,8 +193,7 @@ def maximize(self): # ----------------------------------------------------------- def restore(self): - """ - Restore the window to normal size + """Restore the window to normal size. Only controls supporting Window pattern should answer """ @@ -167,7 +209,7 @@ def restore(self): # ----------------------------------------------------------- def get_show_state(self): - """Get the show state and Maximized/minimized/restored state + """Get the show state and Maximized/minimized/restored state. Returns values as following @@ -187,24 +229,24 @@ def get_show_state(self): # ----------------------------------------------------------- def is_minimized(self): - """Indicate whether the window is minimized or not""" + """Indicate whether the window is minimized or not.""" return self.get_show_state() == self.MINIMIZED # ----------------------------------------------------------- def is_maximized(self): - """Indicate whether the window is maximized or not""" + """Indicate whether the window is maximized or not.""" return self.get_show_state() == self.MAXIMIZED # ----------------------------------------------------------- def is_normal(self): - """Indicate whether the window is normal (i.e. not minimized and not maximized)""" + """Indicate whether the window is normal (i.e. not minimized and not maximized).""" return self.get_show_state() == self.NORMAL def move_window(self, x=None, y=None, width=None, height=None): """Move the window to the new coordinates The method should be implemented explicitly by controls that support this action. The most obvious is the Window control. - Otherwise the method throws AttributeError + Otherwise the method throws AttributeError. * **x** Specifies the new left position of the window. Defaults to the current left position of the window. @@ -218,7 +260,7 @@ def move_window(self, x=None, y=None, width=None, height=None): raise AttributeError("This method is not supported for {0}".format(self)) def menu_select(self, path, exact=False, ): - """Select a menu item specified in the path + """Select a menu item specified in the path. The full path syntax is specified in: :py:meth:`pywinauto.menuwrapper.Menu.get_menu_path` diff --git a/pywinauto/unittests/test_wpf_element_info.py b/pywinauto/unittests/test_wpf_element_info.py index 38357c437..723eda370 100644 --- a/pywinauto/unittests/test_wpf_element_info.py +++ b/pywinauto/unittests/test_wpf_element_info.py @@ -1,3 +1,35 @@ +# -*- coding: utf-8 -*- +# 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. + import unittest import os import sys diff --git a/pywinauto/unittests/test_wpfwrapper.py b/pywinauto/unittests/test_wpfwrapper.py index 70c1d61de..0dd32c2d0 100644 --- a/pywinauto/unittests/test_wpfwrapper.py +++ b/pywinauto/unittests/test_wpfwrapper.py @@ -1,3 +1,35 @@ +# -*- coding: utf-8 -*- +# 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. + """Tests for WPFWrapper""" from __future__ import print_function from __future__ import unicode_literals @@ -13,7 +45,7 @@ from pywinauto.windows.application import Application # noqa: E402 from pywinauto.base_application import WindowSpecification # noqa: E402 from pywinauto.sysinfo import is_x64_Python # noqa: E402 -from pywinauto.timings import Timings, wait_until # noqa: E402 +from pywinauto.timings import Timings # noqa: E402 from pywinauto.actionlogger import ActionLogger # noqa: E402 from pywinauto import mouse # noqa: E402 diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 32f168143..2eb60a1cf 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -1,4 +1,35 @@ -"""Implementation of the class to deal with an UI element of WPF via injected managed DLL/assembly""" +# 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. + +"""Implementation of the class to deal with an UI element of WPF via injected managed DLL/assembly.""" from six import integer_types, string_types from pywinauto.handleprops import dumpwindow @@ -9,7 +40,7 @@ def is_element_satisfying_criteria(element, process=None, class_name=None, name=None, control_type=None, **kwargs): - """Check if element satisfies filter criteria""" + """Check if element satisfies filter criteria.""" is_appropriate_control_type = True if control_type: if isinstance(control_type, string_types): @@ -27,6 +58,9 @@ def is_none_or_equals(criteria, prop): class WPFElementInfo(ElementInfo): + + """Class to get information about WPF UI element in the specified process.""" + re_props = ["class_name", "name", "auto_id", "control_type", "full_control_type", "value"] exact_only_props = ["handle", "pid", "control_id", "enabled", "visible", "rectangle", "framework_id", "runtime_id"] search_order = ["handle", "control_type", "class_name", "pid", "control_id", "visible", "enabled", "name", @@ -35,8 +69,7 @@ class WPFElementInfo(ElementInfo): assert set(re_props + exact_only_props) == set(search_order) def __init__(self, elem_id=None, cache_enable=False, pid=None): - """ - Create an instance of WPFElementInfo from an ID of the element (int or long). + """Create an instance of WPFElementInfo from an ID of the element (int or long). If elem_id is None create an instance for UI root element. """ @@ -58,6 +91,7 @@ def set_cache_strategy(self, cached): @property def handle(self): + """Return handle of the element.""" if self._element == 0: return None reply = ConnectionManager().call_action('GetHandle', self._pid, element_id=self._element) @@ -65,13 +99,14 @@ def handle(self): @property def auto_id(self): - """Return AutomationId of the element""" + """Return AutomationId of the element.""" if self._element == 0: return '' return self.get_native_property('Name') or '' @property def name(self): + """Return name of the element.""" if self._element == 0: return '--root--' reply = ConnectionManager().call_action('GetName', self._pid, element_id=self._element) @@ -79,36 +114,46 @@ def name(self): @property def rich_text(self): + """Return rich_text of the element.""" if self.control_type == 'Edit': return self.get_native_property('Text') or '' return self.name @property def value(self): + """Return value of the element (in order to search elements by this property). + + Only specified element types (like Edit) are supported. + Use :py:meth:`get_native_property` to get value of the property with the specified name.""" if self.control_type == 'Edit': return self.get_native_property('Text') or self.get_native_property('Password') or '' return '' @property def control_id(self): + """Return element ID (may be different from run to run).""" return self._element @property def process_id(self): + """Return process ID of the element.""" return self._pid pid = process_id @property def framework_id(self): + """Return framework ID of the element (always is 'WPF', for compatibility with UIA).""" return "WPF" @property def runtime_id(self): + """Return element ID (may be different from run to run).""" return self._element @property def class_name(self): + """Return class name of the element.""" if self._element == 0: return '' reply = ConnectionManager().call_action('GetTypeName', self._pid, element_id=self._element) @@ -116,35 +161,48 @@ def class_name(self): @property def enabled(self): + """Check if the element is enabled.""" if self._element == 0: return True return self.get_native_property('IsEnabled') or False @property def visible(self): + """Check if the element is visible.""" if self._element == 0: return True return self.get_native_property('IsVisible') or False @property def parent(self): + """Return parent of the element.""" if self._element == 0: return None reply = ConnectionManager().call_action('GetParent', self._pid, element_id=self._element) return WPFElementInfo(reply['value'], pid=self._pid) def children(self, **kwargs): + """Return a list of only immediate children of the element. + + * **kwargs** is a criteria to reduce a list by process, + class_name, control_type, content_only and/or title. + """ return list(self.iter_children(**kwargs)) @property def control_type(self): - """Return control type of element""" + """Return control type of element.""" if self._element == 0: return None reply = ConnectionManager().call_action('GetControlType', self._pid, element_id=self._element) return reply['value'] def iter_children(self, **kwargs): + """Return a generator of only immediate children of the element. + + * **kwargs** is a criteria to reduce a list by process, + class_name, control_type, content_only and/or title. + """ if 'process' in kwargs and self._pid is None: self._pid = kwargs['process'] reply = ConnectionManager().call_action('GetChildren', self._pid, element_id=self._element) @@ -154,9 +212,15 @@ def iter_children(self, **kwargs): yield element def descendants(self, **kwargs): + """Return a list of all descendant children of the element. + + * **kwargs** is a criteria to reduce a list by process, + class_name, control_type, content_only and/or title. + """ return list(self.iter_descendants(**kwargs)) def iter_descendants(self, **kwargs): + """Iterate over descendants of the element.""" # TODO implement cache support # cache_enable = kwargs.pop('cache_enable', False) depth = kwargs.pop("depth", None) @@ -177,6 +241,7 @@ def iter_descendants(self, **kwargs): @property def rectangle(self): + """Return rectangle of the element.""" rect = RECT() if self._element != 0: reply = ConnectionManager().call_action('GetRectangle', self._pid, element_id=self._element) @@ -187,9 +252,11 @@ def rectangle(self): return rect def dump_window(self): + """Dump window to a set of properties.""" return dumpwindow(self.handle) def get_native_property(self, name, error_if_not_exists=False): + """Return value of the specified property of the .NET object corresponding to the element.""" try: reply = ConnectionManager().call_action('GetProperty', self._pid, element_id=self._element, name=name) return reply['value'] @@ -199,27 +266,28 @@ def get_native_property(self, name, error_if_not_exists=False): return None def get_native_properties(self): - """Return a dict with names and types of available properties of the element""" + """Return a dict with names and types of available properties of + the .NET object corresponding to the element.""" reply = ConnectionManager().call_action('GetProperties', self._pid, element_id=self._element) return reply['value'] def __hash__(self): - """Return a unique hash value based on the element's ID""" + """Return a unique hash value based on the element's ID.""" return hash(self._element) def __eq__(self, other): - """Check if 2 WPFElementInfo objects describe 1 actual element""" + """Check if 2 WPFElementInfo objects describe 1 actual element.""" if not isinstance(other, WPFElementInfo): return False return self._element == other._element def __ne__(self, other): - """Check if 2 WPFElementInfo objects describe 2 different elements""" + """Check if 2 WPFElementInfo objects describe 2 different elements.""" return not (self == other) @classmethod def get_active(cls, app_or_pid): - """Return current active element""" + """Return current active element.""" from .application import Application if isinstance(app_or_pid, integer_types): From b1012a4910d804faf334517dbc0ecc3d81b93694 Mon Sep 17 00:00:00 2001 From: eltio Date: Mon, 20 Jun 2022 00:39:57 +0300 Subject: [PATCH 54/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index ae8368f20..1290568db 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit ae8368f20b4ff0cae6e41aae29eaaee8c99e6782 +Subproject commit 1290568db3a7a9bd6f5f8601b9c1e8b2b990c310 From 9600950c9a28fcd793d8915583281315f1d01d07 Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 23 Jun 2022 20:27:40 +0300 Subject: [PATCH 55/59] fix some codacy warnings --- pywinauto/controls/wpf_controls.py | 34 ++++++++++++--------------- pywinauto/controls/wpfwrapper.py | 11 ++++----- pywinauto/windows/wpf_element_info.py | 9 ++++--- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index b3bcbdce8..61c9c22fc 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -126,12 +126,11 @@ def __init__(self, elem): # ----------------------------------------------------------- def toggle(self): - """Switch state of checkable controls in cycle between CHECKED/UNCHECKED or - CHECKED/UNCHECKED/INDETERMINATE (if a control is three-state). + """Switch state of checkable controls (yclically). + Supported states are CHECKED/UNCHECKED or CHECKED/UNCHECKED/INDETERMINATE (if a control is three-state). Usually applied for the check box control. """ - current_state = self.get_native_property('IsChecked') if self.get_native_property('IsThreeState'): states = (True, False, None) @@ -329,10 +328,9 @@ def line_length(self, line_index): lines = self.window_text().splitlines() if line_index < len(lines): return len(lines[line_index]) - elif line_index == self.line_count() - 1: + if line_index == self.line_count() - 1: return 0 - else: - raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index)) + raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index)) # ----------------------------------------------------------- def get_line(self, line_index): @@ -340,7 +338,7 @@ def get_line(self, line_index): lines = self.window_text().splitlines() if line_index < len(lines): return lines[line_index] - elif line_index == self.line_count() - 1: + if line_index == self.line_count() - 1: return "" else: raise IndexError("There are only {0} lines but given index is {1}".format(self.line_count(), line_index)) @@ -645,7 +643,7 @@ def button(self, button_identifier, exact=True): raise findbestmatch.MatchError(items=texts, tofind=button_identifier) else: # one of these will be returned for the matching text - indices = [i for i in range(0, len(texts))] + indices = list(range(0, len(texts))) # find which index best matches that text button_index = findbestmatch.find_best_match(button_identifier, texts, indices) @@ -665,7 +663,6 @@ def check_button(self, button_identifier, make_checked, exact=True): * **exact** flag specifies if the exact match for the text look up has to be applied """ - self.actions.logSectionStart('Checking "' + self.window_text() + '" toolbar button "' + str(button_identifier) + '"') button = self.button(button_identifier, exact=exact) @@ -808,7 +805,7 @@ def item_by_path(self, path, exact=False): if not item: raise IndexError("Empty item name between '->' separators") - def next_level_menu(parent_menu, item_name, is_last): + def next_level_menu(parent_menu, item_name): if item_name.startswith("#"): return self._sub_item_by_idx(parent_menu, int(item_name[1:])) return self._sub_item_by_text(parent_menu, item_name, exact) @@ -818,7 +815,7 @@ def next_level_menu(parent_menu, item_name, is_last): # a direct child or a descendant. # Sometimes we need to re-discover Menu again try: - menu = next_level_menu(self, menu_items[0], items_cnt == 1) + menu = next_level_menu(self, menu_items[0]) if items_cnt == 1: return menu @@ -831,7 +828,7 @@ def next_level_menu(parent_menu, item_name, is_last): menu = self.top_level_parent().descendants(control_type="Menu")[0] for i in range(1, items_cnt): - menu = next_level_menu(menu, menu_items[i], items_cnt == i + 1) + menu = next_level_menu(menu, menu_items[i]) except AttributeError: raise IndexError() @@ -894,7 +891,6 @@ def _calc_click_coords(self): The returned coordinates are always absolute """ - # TODO get rectangle of text area rect = self.rectangle() coords = (rect.left + int(float(rect.width()) / 4.), @@ -907,11 +903,13 @@ def sub_elements(self, depth=None): return self.descendants(control_type="TreeItem", depth=depth) def expand(self): + """Display child items of the tree item.""" self.set_native_property('IsExpanded', True) return self # ----------------------------------------------------------- def collapse(self): + """Hide child items of the tree item.""" self.set_native_property('IsExpanded', False) return self @@ -1027,9 +1025,8 @@ def get_item(self, path, exact=False): if isinstance(child_spec, six.string_types): raise IndexError("Item '{0}' does not have a child '{1}'".format( current_elem.window_text(), child_spec)) - else: - raise IndexError("Item '{0}' does not have {1} children".format( - current_elem.window_text(), child_spec + 1)) + raise IndexError("Item '{0}' does not have {1} children".format( + current_elem.window_text(), child_spec + 1)) return current_elem @@ -1193,8 +1190,7 @@ def get_selected_count(self): selection = self.get_selection() if selection: return len(selection) - else: - return 0 + return 0 # ----------------------------------------------------------- def texts(self): @@ -1220,7 +1216,7 @@ class HeaderItemWrapper(wpfwrapper.WPFWrapper): # ----------------------------------------------------------- def __init__(self, elem): - """Initialize the control""" + """Initialize the control.""" super(HeaderItemWrapper, self).__init__(elem) diff --git a/pywinauto/controls/wpfwrapper.py b/pywinauto/controls/wpfwrapper.py index e1d59f8ee..053657bc1 100644 --- a/pywinauto/controls/wpfwrapper.py +++ b/pywinauto/controls/wpfwrapper.py @@ -52,7 +52,6 @@ class WpfMeta(BaseMeta): def __init__(cls, name, bases, attrs): """Register the control types.""" - BaseMeta.__init__(cls, name, bases, attrs) for t in cls._control_types: @@ -61,7 +60,6 @@ def __init__(cls, name, bases, attrs): @staticmethod def find_wrapper(element): """Find the correct wrapper for this WPF element.""" - # Check for a more specific wrapper in the registry try: wrapper_match = WpfMeta.control_type_to_cls[element.control_type] @@ -75,7 +73,7 @@ def find_wrapper(element): @six.add_metaclass(WpfMeta) class WPFWrapper(WinBaseWrapper): - """Default wrapper for WPF control in the specified process""" + """Default wrapper for WPF control in the specified process.""" _control_types = [] @@ -220,12 +218,11 @@ def get_show_state(self): val = self.element_info.get_native_property('WindowState') if val == 'Normal': return self.NORMAL - elif val == 'Maximized': + if val == 'Maximized': return self.MAXIMIZED - elif val == 'Minimized': + if val == 'Minimized': return self.MINIMIZED - else: - raise ValueError('Unexpected WindowState property value: ' + str(val)) + raise ValueError('Unexpected WindowState property value: ' + str(val)) # ----------------------------------------------------------- def is_minimized(self): diff --git a/pywinauto/windows/wpf_element_info.py b/pywinauto/windows/wpf_element_info.py index 2eb60a1cf..2373f18c0 100644 --- a/pywinauto/windows/wpf_element_info.py +++ b/pywinauto/windows/wpf_element_info.py @@ -124,7 +124,8 @@ def value(self): """Return value of the element (in order to search elements by this property). Only specified element types (like Edit) are supported. - Use :py:meth:`get_native_property` to get value of the property with the specified name.""" + Use :py:meth:`get_native_property` to get value of the property with the specified name. + """ if self.control_type == 'Edit': return self.get_native_property('Text') or self.get_native_property('Password') or '' return '' @@ -266,8 +267,7 @@ def get_native_property(self, name, error_if_not_exists=False): return None def get_native_properties(self): - """Return a dict with names and types of available properties of - the .NET object corresponding to the element.""" + """Return a dict with names and types of available properties of the .NET object related to the element.""" reply = ConnectionManager().call_action('GetProperties', self._pid, element_id=self._element) return reply['value'] @@ -302,7 +302,6 @@ def get_active(cls, app_or_pid): reply = ConnectionManager().call_action('GetFocusedElement', pid) if reply['value'] > 0: return cls(reply['value'], pid=pid) - else: - return None + return None except InjectedUnsupportedActionError: return None From 42665a6061dedb7dc2d9004e7b0c6d2fd02d076b Mon Sep 17 00:00:00 2001 From: eltio Date: Thu, 23 Jun 2022 23:43:51 +0300 Subject: [PATCH 56/59] improve error messages in .item_by_path() method (UIA and WPF) --- pywinauto/controls/uia_controls.py | 9 +++++---- pywinauto/controls/wpf_controls.py | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pywinauto/controls/uia_controls.py b/pywinauto/controls/uia_controls.py index 9b511ec2e..16a834754 100644 --- a/pywinauto/controls/uia_controls.py +++ b/pywinauto/controls/uia_controls.py @@ -1126,7 +1126,7 @@ def item_by_path(self, path, exact=False): menu_items = [p.strip() for p in path.split("->")] items_cnt = len(menu_items) if items_cnt == 0: - raise IndexError() + raise IndexError("Menu path has incorrect format, no item identifiers found") for item in menu_items: if not item: raise IndexError("Empty item name between '->' separators") @@ -1141,8 +1141,9 @@ def next_level_menu(parent_menu, item_name, is_last): # a new Menu control is created and placed on the dialog. It can be # a direct child or a descendant. # Sometimes we need to re-discover Menu again + i = 0 try: - menu = next_level_menu(self, menu_items[0], items_cnt == 1) + menu = next_level_menu(self, menu_items[i], items_cnt == 1) if items_cnt == 1: return menu @@ -1156,8 +1157,8 @@ def next_level_menu(parent_menu, item_name, is_last): for i in range(1, items_cnt): menu = next_level_menu(menu, menu_items[i], items_cnt == i + 1) - except(AttributeError): - raise IndexError() + except (AttributeError, IndexError): + raise IndexError('Incorrect menu path, item "{}" (index {}) not found'.format(menu_items[i], i)) return menu diff --git a/pywinauto/controls/wpf_controls.py b/pywinauto/controls/wpf_controls.py index 61c9c22fc..ec1a39cac 100644 --- a/pywinauto/controls/wpf_controls.py +++ b/pywinauto/controls/wpf_controls.py @@ -800,7 +800,7 @@ def item_by_path(self, path, exact=False): menu_items = [p.strip() for p in path.split("->")] items_cnt = len(menu_items) if items_cnt == 0: - raise IndexError() + raise IndexError("Menu path has incorrect format, no item identifiers found") for item in menu_items: if not item: raise IndexError("Empty item name between '->' separators") @@ -814,8 +814,9 @@ def next_level_menu(parent_menu, item_name): # a new Menu control is created and placed on the dialog. It can be # a direct child or a descendant. # Sometimes we need to re-discover Menu again + i = 0 try: - menu = next_level_menu(self, menu_items[0]) + menu = next_level_menu(self, menu_items[i]) if items_cnt == 1: return menu @@ -829,8 +830,8 @@ def next_level_menu(parent_menu, item_name): for i in range(1, items_cnt): menu = next_level_menu(menu, menu_items[i]) - except AttributeError: - raise IndexError() + except (AttributeError, IndexError): + raise IndexError('Incorrect menu path, item "{}" (index {}) not found'.format(menu_items[i], i)) return menu From 97e666741f625eebb7a443c6c7cff1662432e9bc Mon Sep 17 00:00:00 2001 From: eltio Date: Fri, 24 Jun 2022 22:10:31 +0300 Subject: [PATCH 57/59] update submodule --- pywinauto/windows/injected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 1290568db..8e871680b 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 1290568db3a7a9bd6f5f8601b9c1e8b2b990c310 +Subproject commit 8e871680bae8407aaeca0a1c69b6479f87168e11 From 30a2b29592feb1863ad8a77dd4212efd756c81df Mon Sep 17 00:00:00 2001 From: eltio Date: Sat, 25 Jun 2022 01:05:20 +0300 Subject: [PATCH 58/59] fix missing pid in __getattribute__ of WindowsSpecification from wrapper --- pywinauto/base_application.py | 6 +++++- pywinauto/base_wrapper.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pywinauto/base_application.py b/pywinauto/base_application.py index a8f97c2b8..336f50ce0 100644 --- a/pywinauto/base_application.py +++ b/pywinauto/base_application.py @@ -576,8 +576,12 @@ def __getattribute__(self, attr_name): # if we have been asked for an attribute of the dialog # then resolve the window and return the attribute if self.backend.name in ('wpf'): + if self.app is None and len(self.criteria) > 0 and 'pid' in self.criteria[0]: + pid = self.criteria[0]['pid'] + else: + pid = self.app.process desktop_wrapper = self.backend.generic_wrapper_class( - self.backend.element_info_class(pid=self.app.process) + self.backend.element_info_class(pid=pid) ) else: desktop_wrapper = self.backend.generic_wrapper_class(self.backend.element_info_class()) diff --git a/pywinauto/base_wrapper.py b/pywinauto/base_wrapper.py index 8bb20e8f1..478b34506 100644 --- a/pywinauto/base_wrapper.py +++ b/pywinauto/base_wrapper.py @@ -170,6 +170,8 @@ def by(self, **criteria): criteria['backend'] = self.backend.name criteria['parent'] = self.element_info + if self.backend.name in ('wpf'): + criteria['pid'] = self.element_info.pid child_specification = WindowSpecification(criteria) return child_specification From 946316efd5c35b7df9c00c7abccf968513a760c9 Mon Sep 17 00:00:00 2001 From: eltio Date: Mon, 27 Jun 2022 14:14:00 +0300 Subject: [PATCH 59/59] switch injected submodule to upstream repo --- .gitmodules | 4 ++-- pywinauto/windows/injected | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 1b1f390dd..08b58f454 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "pywinauto/windows/injected"] path = pywinauto/windows/injected - url = https://github.com/eltimen/injected.git - branch = improvements + url = https://github.com/pywinauto/injected.git + branch = master diff --git a/pywinauto/windows/injected b/pywinauto/windows/injected index 8e871680b..4957c5f2d 160000 --- a/pywinauto/windows/injected +++ b/pywinauto/windows/injected @@ -1 +1 @@ -Subproject commit 8e871680bae8407aaeca0a1c69b6479f87168e11 +Subproject commit 4957c5f2d551615649c02e93b36dacc1dff5a164