Skip to content

Commit

Permalink
Merge pull request pywinauto#922 from airelil/atspi
Browse files Browse the repository at this point in the history
Issue pywinauto#668: add WindowWrapper for uia backend
  • Loading branch information
airelil authored Apr 26, 2020
2 parents ecaf8dd + e0a47f5 commit 1b474df
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 18 deletions.
8 changes: 8 additions & 0 deletions pywinauto/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ def __repr__(self):
return "<RECT L%d, T%d, R%d, B%d>" % (
self.left, self.top, self.right, self.bottom)

# ----------------------------------------------------------------
def __iter__(self):
"""Allow iteration through coordinates"""
yield self.left
yield self.top
yield self.right
yield self.bottom

# ----------------------------------------------------------------
def __sub__(self, other):
"""Return a new rectangle which is offset from the one passed in"""
Expand Down
13 changes: 2 additions & 11 deletions pywinauto/controls/hwndwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,13 +1080,7 @@ def menu_select(self, path, exact=False, ):
MenuSelect = deprecated(menu_select)

# -----------------------------------------------------------
def move_window(
self,
x = None,
y = None,
width = None,
height = None,
repaint = True):
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.
Expand All @@ -1097,9 +1091,6 @@ def move_window(
current width of the window.
* **height** Specifies the new height of the window. Default to the
current height of the window.
* **repaint** Whether the window should be repainted or not.
Defaults to True
"""
cur_rect = self.rectangle()

Expand Down Expand Up @@ -1128,7 +1119,7 @@ def move_window(
height = cur_rect.height()

# ask for the window to be moved
ret = win32functions.MoveWindow(self, x, y, width, height, repaint)
ret = win32functions.MoveWindow(self, x, y, width, height, True)

# check that it worked correctly
if not ret:
Expand Down
64 changes: 64 additions & 0 deletions pywinauto/controls/uia_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

"""Wrap various UIA windows controls. To be used with 'uia' backend."""
import locale
import time
import comtypes
import six

Expand All @@ -48,6 +49,69 @@
from ..windows.uia_element_info import elements_from_uia_array


# ====================================================================
class WindowWrapper(uiawrapper.UIAWrapper):

"""Wrap a UIA-compatible 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.iface_transform.Move(x, y)
self.iface_transform.Resize(width, height)

time.sleep(timings.Timings.after_movewindow_wait)

# -----------------------------------------------------------
def is_dialog(self):
"""Window is always a dialog so return True"""
return True


# ====================================================================
class ButtonWrapper(uiawrapper.UIAWrapper):

Expand Down
18 changes: 18 additions & 0 deletions pywinauto/controls/uiawrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,5 +800,23 @@ def _texts_from_item_container(self):
pass
return texts

# -----------------------------------------------------------
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('uia', UIAElementInfo, UIAWrapper)
10 changes: 9 additions & 1 deletion pywinauto/unittests/test_atspi_element_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import unittest
import re
import mock
import ctypes

if sys.platform.startswith("linux"):
sys.path.append(".")
Expand Down Expand Up @@ -119,6 +118,15 @@ def test_RECT_repr(self):
r0 = RECT(0)
self.assertEqual(r0.__repr__(), "<RECT L0, T0, R0, B0>")

def test_RECT_iter(self):
"""Test RECT is iterable"""
r = RECT(1, 2, 3, 4)
(left, top, right, bottom) = r
self.assertEqual(left, r.left)
self.assertEqual(right, r.right)
self.assertEqual(top, r.top)
self.assertEqual(bottom, r.bottom)


class AtspiElementInfoTests(unittest.TestCase):

Expand Down
2 changes: 1 addition & 1 deletion pywinauto/unittests/test_hwndwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ def testEquals(self):
# def testVerifyActionable(self):

def testMoveWindow_same(self):
"""Test calling movewindow without any parameters"""
"""Test calling move_window without any parameters"""
prevRect = self.dlg.rectangle()
self.dlg.move_window()
self.assertEqual(prevRect, self.dlg.rectangle())
Expand Down
45 changes: 40 additions & 5 deletions pywinauto/unittests/test_uiawrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import time
import os
import sys
import collections
import unittest
import mock
import six
Expand Down Expand Up @@ -179,6 +180,40 @@ def test_is_dialog(self):
button = self.dlg.child_window(class_name="Button",
name="OK").wrapper_object()
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.child_window(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 = ", prev_rect)
logger.log("new_rect = ", new_rect)
logger.log("self.dlg.rectangle() = ", 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"""
Expand Down Expand Up @@ -529,8 +564,8 @@ def test_pretty_print(self):
"^<uia_controls.StaticWrapper - 'TestLabel', Static, [0-9-]+>$")

wrp = self.dlg.wrapper_object()
assert_regex(wrp.__str__(), "^uiawrapper\.UIAWrapper - 'WPF Sample Application', Dialog$")
assert_regex(wrp.__repr__(), "^<uiawrapper\.UIAWrapper - 'WPF Sample Application', Dialog, [0-9-]+>$")
assert_regex(wrp.__str__(), "^uia_controls\.WindowWrapper - 'WPF Sample Application', Dialog$")
assert_regex(wrp.__repr__(), "^<uia_controls\.WindowWrapper - 'WPF Sample Application', Dialog, [0-9-]+>$")

# ElementInfo.__str__
assert_regex(wrp.element_info.__str__(),
Expand All @@ -541,10 +576,10 @@ def test_pretty_print(self):
# mock a failure in window_text() method
orig = wrp.window_text
wrp.window_text = mock.Mock(return_value="") # empty text
assert_regex(wrp.__str__(), "^uiawrapper\.UIAWrapper - '', Dialog$")
assert_regex(wrp.__repr__(), "^<uiawrapper\.UIAWrapper - '', Dialog, [0-9-]+>$")
assert_regex(wrp.__str__(), "^uia_controls\.WindowWrapper - '', Dialog$")
assert_regex(wrp.__repr__(), "^<uia_controls\.WindowWrapper - '', Dialog, [0-9-]+>$")
wrp.window_text.return_value = u'\xd1\xc1\\\xa1\xb1\ua000' # unicode string
assert_regex(wrp.__str__(), "^uiawrapper\.UIAWrapper - '.+', Dialog$")
assert_regex(wrp.__str__(), "^uia_controls\.WindowWrapper - '.+', Dialog$")
wrp.window_text = orig # restore the original method

# mock a failure in element_info.name property (it's based on _get_name())
Expand Down
9 changes: 9 additions & 0 deletions pywinauto/unittests/test_win32functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ def test_RECT_repr(self):
r0 = RECT(0)
self.assertEqual(r0.__repr__(), "<RECT L0, T0, R0, B0>")

def test_RECT_iter(self):
"""Test RECT is iterable"""
r = RECT(1, 2, 3, 4)
(left, top, right, bottom) = r
self.assertEqual(left, r.left)
self.assertEqual(right, r.right)
self.assertEqual(top, r.top)
self.assertEqual(bottom, r.bottom)

def test_Structure(self):
class Structure0(Structure):
_fields_ = [("f0", ctypes.c_int)]
Expand Down

0 comments on commit 1b474df

Please sign in to comment.