You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1822 lines
67 KiB
1822 lines
67 KiB
# GUI Application automation and testing library
|
|
# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors
|
|
# https://github.com/pywinauto/pywinauto/graphs/contributors
|
|
# http://pywinauto.readthedocs.io/en/latest/credits.html
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright notice, this
|
|
# list of conditions and the following disclaimer.
|
|
#
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
#
|
|
# * Neither the name of pywinauto nor the names of its
|
|
# contributors may be used to endorse or promote products derived from
|
|
# this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
"""Basic wrapping of Windows controls"""
|
|
from __future__ import unicode_literals
|
|
from __future__ import print_function
|
|
|
|
# pylint: disable-msg=W0611
|
|
|
|
# import sys
|
|
import copy
|
|
import time
|
|
import re
|
|
import ctypes
|
|
import win32api
|
|
import win32gui
|
|
import win32con
|
|
import win32process
|
|
import win32event
|
|
import six
|
|
import pywintypes
|
|
import warnings
|
|
|
|
# the wrappers may be used in an environment that does not need
|
|
# the actions - as such I don't want to require sendkeys - so
|
|
# the following makes the import optional.
|
|
|
|
from .. import win32functions
|
|
from .. import win32defines
|
|
from .. import win32structures
|
|
from .. import controlproperties
|
|
from ..actionlogger import ActionLogger
|
|
from .. import keyboard
|
|
from .. import mouse
|
|
from ..timings import Timings
|
|
from .. import timings
|
|
from .. import handleprops
|
|
from ..win32_element_info import HwndElementInfo
|
|
from .. import backend
|
|
|
|
# I leave this optional because PIL is a large dependency
|
|
try:
|
|
from PIL import ImageGrab
|
|
except ImportError:
|
|
ImageGrab = None
|
|
|
|
# also import MenuItemNotEnabled so that it is
|
|
# accessible from HwndWrapper module
|
|
from .menuwrapper import Menu #, MenuItemNotEnabled
|
|
|
|
from ..base_wrapper import BaseWrapper
|
|
from ..base_wrapper import BaseMeta
|
|
from .. import deprecated
|
|
|
|
|
|
#====================================================================
|
|
class ControlNotEnabled(RuntimeError):
|
|
|
|
"""Raised when a control is not enabled"""
|
|
pass
|
|
|
|
#====================================================================
|
|
class ControlNotVisible(RuntimeError):
|
|
|
|
"""Raised when a control is not visible"""
|
|
pass
|
|
|
|
#====================================================================
|
|
class InvalidWindowHandle(RuntimeError):
|
|
|
|
"""Raised when an invalid handle is passed to HwndWrapper"""
|
|
|
|
def __init__(self, hwnd):
|
|
"""Initialise the RuntimError parent with the mesage"""
|
|
RuntimeError.__init__(self,
|
|
"Handle {0} is not a vaild window handle".format(hwnd))
|
|
|
|
#=========================================================================
|
|
class HwndMeta(BaseMeta):
|
|
|
|
"""Metaclass for HwndWrapper objects"""
|
|
re_wrappers = {}
|
|
str_wrappers = {}
|
|
|
|
def __init__(cls, name, bases, attrs):
|
|
"""
|
|
Register the class names
|
|
|
|
Both the regular expression or the classes directly are registered.
|
|
"""
|
|
BaseMeta.__init__(cls, name, bases, attrs)
|
|
|
|
for win_class in cls.windowclasses:
|
|
HwndMeta.re_wrappers[re.compile(win_class)] = cls
|
|
HwndMeta.str_wrappers[win_class] = cls
|
|
|
|
@staticmethod
|
|
def find_wrapper(element):
|
|
"""Find the correct wrapper for this native element"""
|
|
if isinstance(element, six.integer_types):
|
|
element = HwndElementInfo(element)
|
|
class_name = element.class_name
|
|
|
|
try:
|
|
return HwndMeta.str_wrappers[class_name]
|
|
except KeyError:
|
|
wrapper_match = None
|
|
|
|
for regex, wrapper in HwndMeta.re_wrappers.items():
|
|
if regex.match(class_name):
|
|
wrapper_match = wrapper
|
|
HwndMeta.str_wrappers[class_name] = wrapper
|
|
|
|
return wrapper
|
|
|
|
# if it is a dialog then override the wrapper we found
|
|
# and make it a DialogWrapper
|
|
if handleprops.is_toplevel_window(element.handle):
|
|
wrapper_match = DialogWrapper
|
|
|
|
if wrapper_match is None:
|
|
wrapper_match = HwndWrapper
|
|
return wrapper_match
|
|
|
|
#====================================================================
|
|
@six.add_metaclass(HwndMeta)
|
|
class HwndWrapper(BaseWrapper):
|
|
|
|
"""
|
|
Default wrapper for controls.
|
|
|
|
All other wrappers are derived from this.
|
|
|
|
This class wraps a lot of functionality of underlying windows API
|
|
features for working with windows.
|
|
|
|
Most of the methods apply to every single window type. For example
|
|
you can click() on any window.
|
|
|
|
Most of the methods of this class are simple wrappers around
|
|
API calls and as such they try do the simplest thing possible.
|
|
|
|
An HwndWrapper object can be passed directly to a ctypes wrapped
|
|
C function - and it will get converted to a Long with the value of
|
|
it's handle (see ctypes, _as_parameter_).
|
|
"""
|
|
handle = None
|
|
|
|
# -----------------------------------------------------------
|
|
def __new__(cls, element):
|
|
"""Construct the control wrapper"""
|
|
return super(HwndWrapper, cls)._create_wrapper(cls, element, HwndWrapper)
|
|
|
|
# -----------------------------------------------------------
|
|
def __init__(self, element_info):
|
|
"""
|
|
Initialize the control
|
|
|
|
* **element_info** is either a valid HwndElementInfo or it can be an
|
|
instance or subclass of HwndWrapper.
|
|
If the handle is not valid then an InvalidWindowHandle error
|
|
is raised.
|
|
"""
|
|
if isinstance(element_info, six.integer_types):
|
|
element_info = HwndElementInfo(element_info)
|
|
if hasattr(element_info, "element_info"):
|
|
element_info = element_info.element_info
|
|
|
|
BaseWrapper.__init__(self, element_info, backend.registry.backends['win32'])
|
|
|
|
# verify that we have been passed in a valid windows handle
|
|
if not handleprops.iswindow(self.handle):
|
|
raise InvalidWindowHandle(self.handle)
|
|
|
|
# make it so that ctypes conversion happens correctly
|
|
self._as_parameter_ = self.handle
|
|
|
|
@property
|
|
def writable_props(self):
|
|
"""Extend default properties list."""
|
|
props = super(HwndWrapper, self).writable_props
|
|
props.extend(['style',
|
|
'exstyle',
|
|
'user_data',
|
|
'context_help_id',
|
|
'fonts',
|
|
'client_rects',
|
|
'is_unicode',
|
|
'menu_items',
|
|
'automation_id',
|
|
])
|
|
return props
|
|
|
|
# -----------------------------------------------------------
|
|
def style(self):
|
|
"""
|
|
Returns the style of window
|
|
|
|
Return value is a long.
|
|
|
|
Combination of WS_* and specific control specific styles.
|
|
See HwndWrapper.has_style() to easily check if the window has a
|
|
particular style.
|
|
"""
|
|
return handleprops.style(self)
|
|
# Non PEP-8 alias
|
|
Style = deprecated(style)
|
|
|
|
# -----------------------------------------------------------
|
|
def exstyle(self):
|
|
"""
|
|
Returns the Extended style of window
|
|
|
|
Return value is a long.
|
|
|
|
Combination of WS_* and specific control specific styles.
|
|
See HwndWrapper.has_style() to easily check if the window has a
|
|
particular style.
|
|
"""
|
|
return handleprops.exstyle(self)
|
|
# Non PEP-8 alias
|
|
ExStyle = deprecated(exstyle, deprecated_name='ExStyle')
|
|
|
|
#------------------------------------------------------------
|
|
def automation_id(self):
|
|
"""Return the .NET name of the control"""
|
|
return self.element_info.automation_id
|
|
|
|
#------------------------------------------------------------
|
|
def control_type(self):
|
|
"""Return the .NET type of the control"""
|
|
return self.element_info.control_type
|
|
|
|
#------------------------------------------------------------
|
|
def full_control_type(self):
|
|
"""Return the .NET type of the control (full, uncut)"""
|
|
return self.element_info.full_control_type
|
|
|
|
# -----------------------------------------------------------
|
|
def user_data(self):
|
|
"""
|
|
Extra data associted with the window
|
|
|
|
This value is a long value that has been associated with the window
|
|
and rarely has useful data (or at least data that you know the use
|
|
of).
|
|
"""
|
|
return handleprops.userdata(self)
|
|
# Non PEP-8 alias
|
|
UserData = deprecated(user_data)
|
|
|
|
# -----------------------------------------------------------
|
|
def context_help_id(self):
|
|
"""Return the Context Help ID of the window"""
|
|
return handleprops.contexthelpid(self)
|
|
# Non PEP-8 alias
|
|
ContextHelpID = deprecated(context_help_id, deprecated_name='ContextHelpID')
|
|
|
|
# -----------------------------------------------------------
|
|
def is_active(self):
|
|
"""Whether the window is active or not"""
|
|
return self.top_level_parent() == self.get_active()
|
|
# Non PEP-8 alias
|
|
IsActive = deprecated(is_active)
|
|
|
|
# -----------------------------------------------------------
|
|
def is_unicode(self):
|
|
"""
|
|
Whether the window is unicode or not
|
|
|
|
A window is Unicode if it was registered by the Wide char version
|
|
of RegisterClass(Ex).
|
|
"""
|
|
return handleprops.isunicode(self)
|
|
# Non PEP-8 alias
|
|
IsUnicode = deprecated(is_unicode)
|
|
|
|
# -----------------------------------------------------------
|
|
def client_rect(self):
|
|
"""
|
|
Returns the client rectangle of window
|
|
|
|
The client rectangle is the window rectangle minus any borders that
|
|
are not available to the control for drawing.
|
|
|
|
Both top and left are always 0 for this method.
|
|
|
|
This method returns a RECT structure, Which has attributes - top,
|
|
left, right, bottom. and has methods width() and height().
|
|
See win32structures.RECT for more information.
|
|
"""
|
|
return handleprops.clientrect(self)
|
|
# Non PEP-8 alias
|
|
ClientRect = deprecated(client_rect)
|
|
|
|
# -----------------------------------------------------------
|
|
#def client_to_screen(self, client_point):
|
|
# """Maps point from client to screen coordinates"""
|
|
# point = win32structures.POINT()
|
|
# if isinstance(client_point, win32structures.POINT):
|
|
# point.x = client_point.x
|
|
# point.y = client_point.y
|
|
# else:
|
|
# point.x = client_point[0]
|
|
# point.y = client_point[1]
|
|
# win32functions.client_to_screen(self, ctypes.byref(point))
|
|
#
|
|
# # return tuple in any case because
|
|
# # coords param is always expected to be tuple
|
|
# return point.x, point.y
|
|
|
|
# -----------------------------------------------------------
|
|
def font(self):
|
|
"""
|
|
Return the font of the window
|
|
|
|
The font of the window is used to draw the text of that window.
|
|
It is a structure which has attributes for font name, height, width
|
|
etc.
|
|
|
|
See win32structures.LOGFONTW for more information.
|
|
"""
|
|
return handleprops.font(self)
|
|
# Non PEP-8 alias
|
|
Font = deprecated(font)
|
|
|
|
# -----------------------------------------------------------
|
|
def has_style(self, style):
|
|
"""Return True if the control has the specified style"""
|
|
return handleprops.has_style(self, style)
|
|
# Non PEP-8 alias
|
|
HasStyle = deprecated(has_style)
|
|
|
|
# -----------------------------------------------------------
|
|
def has_exstyle(self, exstyle):
|
|
"""Return True if the control has the specified extended style"""
|
|
return handleprops.has_exstyle(self, exstyle)
|
|
# Non PEP-8 alias
|
|
HasExStyle = deprecated(has_exstyle, deprecated_name='HasExStyle')
|
|
|
|
# -----------------------------------------------------------
|
|
def is_dialog(self):
|
|
"""Return true if the control is a top level window"""
|
|
if not ("isdialog" in self._cache.keys()):
|
|
self._cache['isdialog'] = handleprops.is_toplevel_window(self)
|
|
|
|
return self._cache['isdialog']
|
|
|
|
# -----------------------------------------------------------
|
|
def client_rects(self):
|
|
"""
|
|
Return the client rect for each item in this control
|
|
|
|
It is a list of rectangles for the control. It is frequently over-ridden
|
|
to extract all rectangles from a control with multiple items.
|
|
|
|
It is always a list with one or more rectangles:
|
|
|
|
* First elemtent is the client rectangle of the control
|
|
* Subsequent elements contain the client rectangle of any items of
|
|
the control (e.g. items in a listbox/combobox, tabs in a
|
|
tabcontrol)
|
|
"""
|
|
return [self.client_rect(), ]
|
|
# Non PEP-8 alias
|
|
ClientRects = deprecated(client_rects)
|
|
|
|
# -----------------------------------------------------------
|
|
def fonts(self):
|
|
"""
|
|
Return the font for each item in this control
|
|
|
|
It is a list of fonts for the control. It is frequently over-ridden
|
|
to extract all fonts from a control with multiple items.
|
|
|
|
It is always a list with one or more fonts:
|
|
|
|
* First elemtent is the control font
|
|
* Subsequent elements contain the font of any items of
|
|
the control (e.g. items in a listbox/combobox, tabs in a
|
|
tabcontrol)
|
|
"""
|
|
return [self.font(), ]
|
|
# Non PEP-8 alias
|
|
Fonts = deprecated(fonts)
|
|
|
|
# -----------------------------------------------------------
|
|
def send_command(self, commandID):
|
|
return self.send_message(win32defines.WM_COMMAND, commandID)
|
|
# Non PEP-8 alias
|
|
SendCommand = deprecated(send_command)
|
|
|
|
# -----------------------------------------------------------
|
|
def post_command(self, commandID):
|
|
return self.post_message(win32defines.WM_COMMAND, commandID)
|
|
# Non PEP-8 alias
|
|
PostCommand = deprecated(post_command)
|
|
|
|
# -----------------------------------------------------------
|
|
#def notify(self, code):
|
|
# "Send a notification to the parent (not tested yet)"
|
|
|
|
# # now we need to notify the parent that the state has changed
|
|
# nmhdr = win32structures.NMHDR()
|
|
# nmhdr.hwndFrom = self.handle
|
|
# nmhdr.idFrom = self.control_id()
|
|
# nmhdr.code = code
|
|
|
|
# from ..remote_memory_block import RemoteMemoryBlock
|
|
# remote_mem = RemoteMemoryBlock(self, size=ctypes.sizeof(nmhdr))
|
|
# remote_mem.Write(nmhdr, size=ctypes.sizeof(nmhdr))
|
|
|
|
# retval = self.parent().send_message(
|
|
# win32defines.WM_NOTIFY,
|
|
# self.handle,
|
|
# remote_mem)
|
|
# #if retval != win32defines.TRUE:
|
|
# # print('retval = ' + str(retval))
|
|
# # raise ctypes.WinError()
|
|
# del remote_mem
|
|
|
|
# return retval
|
|
# Non PEP-8 alias
|
|
#Notify = deprecated(notify)
|
|
|
|
# -----------------------------------------------------------
|
|
def _ensure_enough_privileges(self, message_name):
|
|
"""Ensure the Python process has enough rights to send some window messages"""
|
|
pid = handleprops.processid(self.handle)
|
|
if not handleprops.has_enough_privileges(pid):
|
|
raise RuntimeError('Not enough rights to use {} message/function for target process ' \
|
|
'(to resolve it run the script as Administrator)'.format(message_name))
|
|
|
|
# -----------------------------------------------------------
|
|
def send_message(self, message, wparam = 0, lparam = 0):
|
|
"""Send a message to the control and wait for it to return"""
|
|
wParamAddress = wparam
|
|
if hasattr(wparam, 'mem_address'):
|
|
wParamAddress = wparam.mem_address
|
|
lParamAddress = lparam
|
|
if hasattr(lparam, 'mem_address'):
|
|
lParamAddress = lparam.mem_address
|
|
|
|
CArgObject = type(ctypes.byref(ctypes.c_int(0)))
|
|
if isinstance(wparam, CArgObject):
|
|
wParamAddress = ctypes.addressof(wparam._obj)
|
|
if isinstance(lparam, CArgObject):
|
|
lParamAddress = ctypes.addressof(lparam._obj)
|
|
|
|
return win32gui.SendMessage(self.handle, message, wParamAddress, lParamAddress)
|
|
|
|
# Non PEP-8 alias
|
|
SendMessage = deprecated(send_message)
|
|
|
|
# -----------------------------------------------------------
|
|
def send_chars(self,
|
|
chars,
|
|
with_spaces=True,
|
|
with_tabs=True,
|
|
with_newlines=True):
|
|
"""
|
|
Silently send a character string to the control in an inactive window
|
|
|
|
If a virtual key with no corresponding character is encountered
|
|
(e.g. VK_LEFT, VK_DELETE), a KeySequenceError is raised. Consider using
|
|
the method send_keystrokes for such input.
|
|
"""
|
|
input_locale_id = ctypes.windll.User32.GetKeyboardLayout(0)
|
|
keys = keyboard.parse_keys(chars, with_spaces, with_tabs, with_newlines)
|
|
|
|
for key in keys:
|
|
key_info = key.get_key_info()
|
|
flags = key_info[2]
|
|
|
|
unicode_char = (
|
|
flags & keyboard.KEYEVENTF_UNICODE ==
|
|
keyboard.KEYEVENTF_UNICODE)
|
|
|
|
if unicode_char:
|
|
_, char = key_info[:2]
|
|
vk = ctypes.windll.User32.VkKeyScanExW(char, input_locale_id) & 0xFF
|
|
scan = keyboard.MapVirtualKey(vk, 0)
|
|
else:
|
|
vk, scan = key_info[:2]
|
|
char = keyboard.MapVirtualKey(vk, 2)
|
|
|
|
if char > 0:
|
|
lparam = 1 << 0 | scan << 16 | (flags & 1) << 24
|
|
win32api.SendMessage(self.handle, win32con.WM_CHAR, char, lparam)
|
|
else:
|
|
raise keyboard.KeySequenceError(
|
|
'no WM_CHAR code for {key}, use method send_keystrokes instead'.
|
|
format(key=key))
|
|
|
|
# -----------------------------------------------------------
|
|
def send_keystrokes(self,
|
|
keystrokes,
|
|
with_spaces=True,
|
|
with_tabs=True,
|
|
with_newlines=True):
|
|
"""
|
|
Silently send keystrokes to the control in an inactive window
|
|
|
|
It parses modifiers Shift(+), Control(^), Menu(%) and Sequences like "{TAB}", "{ENTER}"
|
|
For more information about Sequences and Modifiers navigate to module `keyboard`_
|
|
|
|
.. _`keyboard`: pywinauto.keyboard.html
|
|
|
|
Due to the fact that each application handles input differently and this method
|
|
is meant to be used on inactive windows, it may work only partially depending
|
|
on the target app. If the window being inactive is not essential, use the robust
|
|
`type_keys`_ method.
|
|
|
|
.. _`type_keys`: pywinauto.base_wrapper.html#pywinauto.base_wrapper.BaseWrapper.type_keys
|
|
"""
|
|
user32 = ctypes.windll.User32
|
|
PBYTE256 = ctypes.c_ubyte * 256
|
|
|
|
win32gui.SendMessage(self.handle, win32con.WM_ACTIVATE,
|
|
win32con.WA_ACTIVE, 0)
|
|
target_thread_id = user32.GetWindowThreadProcessId(self.handle, None)
|
|
current_thread_id = win32api.GetCurrentThreadId()
|
|
attach_success = user32.AttachThreadInput(target_thread_id, current_thread_id, True) != 0
|
|
if not attach_success:
|
|
warnings.warn('Failed to attach app\'s thread to the current thread\'s message queue',
|
|
UserWarning, stacklevel=2)
|
|
|
|
keyboard_state_stack = [PBYTE256()]
|
|
user32.GetKeyboardState(ctypes.byref(keyboard_state_stack[-1]))
|
|
|
|
input_locale_id = ctypes.windll.User32.GetKeyboardLayout(0)
|
|
context_code = 0
|
|
|
|
keys = keyboard.parse_keys(keystrokes, with_spaces, with_tabs, with_newlines)
|
|
key_combos_present = any([isinstance(k, keyboard.EscapedKeyAction) for k in keys])
|
|
if key_combos_present:
|
|
warnings.warn('Key combinations may or may not work depending on the target app',
|
|
UserWarning, stacklevel=2)
|
|
|
|
try:
|
|
for key in keys:
|
|
vk, scan, flags = key.get_key_info()
|
|
|
|
if vk == keyboard.VK_MENU or context_code == 1:
|
|
down_msg, up_msg = win32con.WM_SYSKEYDOWN, win32con.WM_SYSKEYUP
|
|
else:
|
|
down_msg, up_msg = win32con.WM_KEYDOWN, win32con.WM_KEYUP
|
|
|
|
repeat = 1
|
|
shift_state = 0
|
|
unicode_codepoint = flags & keyboard.KEYEVENTF_UNICODE != 0
|
|
if unicode_codepoint:
|
|
char = scan
|
|
vk_with_flags = user32.VkKeyScanExW(char, input_locale_id)
|
|
vk = vk_with_flags & 0xFF
|
|
shift_state = (vk_with_flags & 0xFF00) >> 8
|
|
scan = keyboard.MapVirtualKey(vk, 0)
|
|
|
|
if key.down and vk > 0:
|
|
new_keyboard_state = copy.deepcopy(keyboard_state_stack[-1])
|
|
|
|
new_keyboard_state[vk] |= 128
|
|
if shift_state & 1 == 1:
|
|
new_keyboard_state[keyboard.VK_SHIFT] |= 128
|
|
# NOTE: if there are characters with CTRL or ALT in the shift
|
|
# state, make sure to add these keys to new_keyboard_state
|
|
|
|
keyboard_state_stack.append(new_keyboard_state)
|
|
|
|
lparam = (
|
|
repeat << 0 |
|
|
scan << 16 |
|
|
(flags & 1) << 24 |
|
|
context_code << 29 |
|
|
0 << 31)
|
|
|
|
user32.SetKeyboardState(ctypes.byref(keyboard_state_stack[-1]))
|
|
win32api.PostMessage(self.handle, down_msg, vk, lparam)
|
|
if vk == keyboard.VK_MENU:
|
|
context_code = 1
|
|
|
|
# a delay for keyboard state to take effect
|
|
time.sleep(0.01)
|
|
|
|
if key.up and vk > 0:
|
|
keyboard_state_stack.pop()
|
|
|
|
lparam = (
|
|
repeat << 0 |
|
|
scan << 16 |
|
|
(flags & 1) << 24 |
|
|
context_code << 29 |
|
|
1 << 30 |
|
|
1 << 31)
|
|
|
|
win32api.PostMessage(self.handle, up_msg, vk, lparam)
|
|
user32.SetKeyboardState(ctypes.byref(keyboard_state_stack[-1]))
|
|
|
|
if vk == keyboard.VK_MENU:
|
|
context_code = 0
|
|
|
|
# a delay for keyboard state to take effect
|
|
time.sleep(0.01)
|
|
|
|
except pywintypes.error as e:
|
|
if e.winerror == 1400:
|
|
warnings.warn('Application exited before the end of keystrokes',
|
|
UserWarning, stacklevel=2)
|
|
else:
|
|
warnings.warn(e.strerror, UserWarning, stacklevel=2)
|
|
user32.SetKeyboardState(ctypes.byref(keyboard_state_stack[0]))
|
|
|
|
if attach_success:
|
|
user32.AttachThreadInput(target_thread_id, current_thread_id, False)
|
|
|
|
# -----------------------------------------------------------
|
|
def send_message_timeout(
|
|
self,
|
|
message,
|
|
wparam = 0,
|
|
lparam = 0,
|
|
timeout = None,
|
|
timeoutflags = win32defines.SMTO_NORMAL):
|
|
"""
|
|
Send a message to the control and wait for it to return or to timeout
|
|
|
|
If no timeout is given then a default timeout of .01 of a second will
|
|
be used.
|
|
"""
|
|
if timeout is None:
|
|
timeout = Timings.sendmessagetimeout_timeout
|
|
|
|
result = -1
|
|
try:
|
|
(_, result) = win32gui.SendMessageTimeout(
|
|
int(self.handle),
|
|
message,
|
|
wparam,
|
|
lparam,
|
|
timeoutflags,
|
|
int(timeout * 1000)
|
|
)
|
|
except Exception as exc:
|
|
#import traceback
|
|
#print('____________________________________________________________')
|
|
#print('self.handle =', int(self.handle), ', message =', message,
|
|
# ', wparam =', wparam, ', lparam =', lparam, ', timeout =', timeout)
|
|
#print('Exception: ', exc)
|
|
#print(traceback.format_exc())
|
|
result = str(exc)
|
|
|
|
return result #result.value
|
|
# Non PEP-8 alias
|
|
SendMessageTimeout = deprecated(send_message_timeout)
|
|
|
|
# -----------------------------------------------------------
|
|
def post_message(self, message, wparam = 0, lparam = 0):
|
|
"""Post a message to the control message queue and return"""
|
|
return win32functions.PostMessage(self, message, wparam, lparam)
|
|
|
|
# Non PEP-8 alias
|
|
PostMessage = deprecated(post_message)
|
|
|
|
# # -----------------------------------------------------------
|
|
# def notify_menu_select(self, menu_id):
|
|
# """Notify the dialog that one of it's menu items was selected
|
|
#
|
|
# **This method is Deprecated**
|
|
# """
|
|
#
|
|
# import warnings
|
|
# warning_msg = "HwndWrapper.NotifyMenuSelect() is deprecated - " \
|
|
# "equivalent functionality is being moved to the MenuWrapper class."
|
|
# warnings.warn(warning_msg, DeprecationWarning)
|
|
#
|
|
# self.set_focus()
|
|
#
|
|
# msg = win32defines.WM_COMMAND
|
|
# return self.send_message_timeout(
|
|
# msg,
|
|
# win32functions.MakeLong(0, menu_id), #wparam
|
|
# )
|
|
# Non PEP-8 alias
|
|
# NotifyMenuSelect = deprecated(notify_menu_select)
|
|
|
|
# -----------------------------------------------------------
|
|
def notify_parent(self, message, controlID = None):
|
|
"""Send the notification message to parent of this control"""
|
|
if controlID is None:
|
|
controlID = self.control_id()
|
|
|
|
return self.parent().post_message(
|
|
win32defines.WM_COMMAND,
|
|
win32functions.MakeLong(message, controlID),
|
|
self)
|
|
# Non PEP-8 alias
|
|
NotifyParent = deprecated(notify_parent)
|
|
|
|
# -----------------------------------------------------------
|
|
def __hash__(self):
|
|
"""Returns the hash value of the handle"""
|
|
return hash(self.handle)
|
|
|
|
#-----------------------------------------------------------
|
|
def wait_for_idle(self):
|
|
"""Backend specific function to wait for idle state of a thread or a window"""
|
|
win32functions.WaitGuiThreadIdle(self.handle)
|
|
|
|
# -----------------------------------------------------------
|
|
def click(self, button="left", pressed="", coords=(0, 0),
|
|
double=False, absolute=False):
|
|
"""
|
|
Simulates a mouse click on the control
|
|
|
|
This method sends WM_* messages to the control, to do a more
|
|
'realistic' mouse click use click_input() which uses mouse_event() API
|
|
to perform the click.
|
|
|
|
This method does not require that the control be visible on the screen
|
|
(i.e. it can be hidden beneath another window and it will still work).
|
|
"""
|
|
self.verify_actionable()
|
|
self._ensure_enough_privileges('WM_*BUTTONDOWN/UP')
|
|
|
|
_perform_click(self, button, pressed, coords, double, absolute=absolute)
|
|
return self
|
|
# Non PEP-8 alias
|
|
Click = deprecated(click)
|
|
|
|
# -----------------------------------------------------------
|
|
def close_click(
|
|
self, button = "left", pressed = "", coords = (0, 0), double = False):
|
|
"""
|
|
Perform a click action that should make the window go away
|
|
|
|
The only difference from click is that there are extra delays
|
|
before and after the click action.
|
|
"""
|
|
time.sleep(Timings.before_closeclick_wait)
|
|
|
|
_perform_click(self, button, pressed, coords, double)
|
|
|
|
def has_closed():
|
|
closed = not (
|
|
handleprops.iswindow(self) or
|
|
handleprops.iswindow(self.parent()))
|
|
if not closed:
|
|
# try closing again
|
|
try:
|
|
_perform_click(self, button, pressed, coords, double)
|
|
except Exception:
|
|
return True # already closed
|
|
return closed
|
|
|
|
# Keep waiting until both this control and it's parent
|
|
# are no longer valid controls
|
|
timings.wait_until(
|
|
Timings.closeclick_dialog_close_wait,
|
|
Timings.closeclick_retry,
|
|
has_closed
|
|
)
|
|
|
|
time.sleep(Timings.after_closeclick_wait)
|
|
|
|
return self
|
|
# Non PEP-8 alias
|
|
CloseClick = deprecated(close_click)
|
|
|
|
# -----------------------------------------------------------
|
|
def close_alt_f4(self):
|
|
"""Close the window by pressing Alt+F4 keys."""
|
|
time.sleep(Timings.before_closeclick_wait)
|
|
self.type_keys('%{F4}')
|
|
time.sleep(Timings.after_closeclick_wait)
|
|
|
|
return self
|
|
# Non PEP-8 alias
|
|
CloseAltF4 = deprecated(close_alt_f4)
|
|
|
|
# -----------------------------------------------------------
|
|
def double_click(
|
|
self, button = "left", pressed = "", coords = (0, 0)):
|
|
"""Perform a double click action"""
|
|
_perform_click(self, button, pressed, coords, double = True)
|
|
return self
|
|
# Non PEP-8 alias
|
|
DoubleClick = deprecated(double_click)
|
|
|
|
# -----------------------------------------------------------
|
|
def right_click(
|
|
self, pressed = "", coords = (0, 0)):
|
|
"""Perform a right click action"""
|
|
_perform_click(
|
|
self, "right", "right " + pressed, coords, button_up = False)
|
|
_perform_click(self, "right", pressed, coords, button_down = False)
|
|
return self
|
|
# Non PEP-8 alias
|
|
RightClick = deprecated(right_click)
|
|
|
|
# -----------------------------------------------------------
|
|
def press_mouse(self, button ="left", coords = (0, 0), pressed =""):
|
|
"""Press the mouse button"""
|
|
#flags, click_point = _calc_flags_and_coords(pressed, coords)
|
|
|
|
_perform_click(self, button, pressed, coords, button_down=True, button_up=False)
|
|
return self
|
|
# Non PEP-8 alias
|
|
PressMouse = deprecated(press_mouse)
|
|
|
|
# -----------------------------------------------------------
|
|
def release_mouse(self, button ="left", coords = (0, 0), pressed =""):
|
|
"""Release the mouse button"""
|
|
#flags, click_point = _calc_flags_and_coords(pressed, coords)
|
|
_perform_click(self, button, pressed, coords, button_down=False, button_up=True)
|
|
return self
|
|
# Non PEP-8 alias
|
|
ReleaseMouse = deprecated(release_mouse)
|
|
|
|
# -----------------------------------------------------------
|
|
def move_mouse(self, coords = (0, 0), pressed ="", absolute = False):
|
|
"""Move the mouse by WM_MOUSEMOVE"""
|
|
if not absolute:
|
|
self.actions.log('Moving mouse to relative (client) coordinates ' + str(coords).replace('\n', ', '))
|
|
|
|
_perform_click(self, button='move', coords=coords, absolute=absolute, pressed=pressed)
|
|
|
|
win32functions.WaitGuiThreadIdle(self.handle)
|
|
return self
|
|
# Non PEP-8 alias
|
|
MoveMouse = deprecated(move_mouse)
|
|
|
|
# -----------------------------------------------------------
|
|
def drag_mouse(self, button ="left",
|
|
press_coords = (0, 0),
|
|
release_coords = (0, 0),
|
|
pressed = ""):
|
|
"""Drag the mouse"""
|
|
if isinstance(press_coords, win32structures.POINT):
|
|
press_coords = (press_coords.x, press_coords.y)
|
|
|
|
if isinstance(release_coords, win32structures.POINT):
|
|
release_coords = (release_coords.x, release_coords.y)
|
|
|
|
_pressed = pressed
|
|
if not _pressed:
|
|
_pressed = "left"
|
|
|
|
self.press_mouse(button, press_coords, pressed=pressed)
|
|
for i in range(5):
|
|
self.move_mouse((press_coords[0] + i, press_coords[1]), pressed=_pressed)
|
|
time.sleep(Timings.drag_n_drop_move_mouse_wait)
|
|
self.move_mouse(release_coords, pressed=_pressed)
|
|
time.sleep(Timings.before_drop_wait)
|
|
self.release_mouse(button, release_coords, pressed=pressed)
|
|
time.sleep(Timings.after_drag_n_drop_wait)
|
|
return self
|
|
# Non PEP-8 alias
|
|
DragMouse = deprecated(drag_mouse)
|
|
|
|
# -----------------------------------------------------------
|
|
def set_window_text(self, text, append = False):
|
|
"""Set the text of the window"""
|
|
self.verify_actionable()
|
|
|
|
if append:
|
|
text = self.window_text() + text
|
|
|
|
text = ctypes.c_wchar_p(six.text_type(text))
|
|
self.post_message(win32defines.WM_SETTEXT, 0, text)
|
|
win32functions.WaitGuiThreadIdle(self.handle)
|
|
|
|
self.actions.log('Set text to the ' + self.friendly_class_name() + ': ' + str(text))
|
|
return self
|
|
# Non PEP-8 alias
|
|
SetWindowText = deprecated(set_window_text)
|
|
|
|
# -----------------------------------------------------------
|
|
def debug_message(self, text):
|
|
"""Write some debug text over the window"""
|
|
# don't draw if dialog is not visible
|
|
|
|
dc = win32functions.CreateDC("DISPLAY", None, None, None )
|
|
|
|
if not dc:
|
|
raise ctypes.WinError()
|
|
|
|
rect = self.rectangle()
|
|
|
|
#ret = win32functions.TextOut(
|
|
# dc, rect.left, rect.top, six.text_type(text), len(text))
|
|
ret = win32functions.DrawText(
|
|
dc,
|
|
six.text_type(text),
|
|
len(text),
|
|
ctypes.byref(rect),
|
|
win32defines.DT_SINGLELINE)
|
|
|
|
# delete the Display context that we created
|
|
win32functions.DeleteDC(dc)
|
|
|
|
if not ret:
|
|
raise ctypes.WinError()
|
|
|
|
return self
|
|
# Non PEP-8 alias
|
|
DebugMessage = deprecated(debug_message)
|
|
|
|
# -----------------------------------------------------------
|
|
def set_transparency(self, alpha = 120):
|
|
"""Set the window transparency from 0 to 255 by alpha attribute"""
|
|
if not (0 <= alpha <= 255):
|
|
raise ValueError('alpha should be in [0, 255] interval!')
|
|
# TODO: implement SetExStyle method
|
|
win32gui.SetWindowLong(self.handle, win32defines.GWL_EXSTYLE, self.exstyle() | win32con.WS_EX_LAYERED)
|
|
win32gui.SetLayeredWindowAttributes(self.handle, win32api.RGB(0,0,0), alpha, win32con.LWA_ALPHA)
|
|
# Non PEP-8 alias
|
|
SetTransparency = deprecated(set_transparency)
|
|
|
|
# -----------------------------------------------------------
|
|
def popup_window(self):
|
|
"""Return owned enabled Popup window wrapper if shown.
|
|
|
|
If there is no enabled popups at that time, it returns **self**.
|
|
See MSDN reference:
|
|
https://msdn.microsoft.com/en-us/library/windows/desktop/ms633515.aspx
|
|
|
|
Please do not use in production code yet - not tested fully
|
|
"""
|
|
popup = win32functions.GetWindow(self, win32defines.GW_ENABLEDPOPUP)
|
|
|
|
return popup
|
|
# Non PEP-8 alias
|
|
PopupWindow = deprecated(popup_window)
|
|
|
|
# -----------------------------------------------------------
|
|
def owner(self):
|
|
"""Return the owner window for the window if it exists
|
|
|
|
Returns None if there is no owner.
|
|
"""
|
|
owner = win32functions.GetWindow(self, win32defines.GW_OWNER)
|
|
if owner:
|
|
return HwndWrapper(owner)
|
|
else:
|
|
return None
|
|
# Non PEP-8 alias
|
|
Owner = deprecated(owner)
|
|
|
|
# -----------------------------------------------------------
|
|
# def context_menu_select(self, path, x = None, y = None):
|
|
# "TODO context_menu_select Not Implemented"
|
|
# pass
|
|
# #raise NotImplementedError(
|
|
# # "HwndWrapper.ContextMenuSelect not implemented yet")
|
|
# # Non PEP-8 alias
|
|
# ContextMenuSelect = deprecated(context_menu_select)
|
|
|
|
# -----------------------------------------------------------
|
|
def _menu_handle(self):
|
|
"""Simple overridable method to get the menu handle"""
|
|
hMenu = win32gui.GetMenu(self.handle)
|
|
is_main_menu = True
|
|
if not hMenu:
|
|
self._ensure_enough_privileges('MN_GETHMENU')
|
|
hMenu = self.send_message(self.handle, win32defines.MN_GETHMENU);
|
|
is_main_menu = False
|
|
return (hMenu, is_main_menu)
|
|
|
|
# -----------------------------------------------------------
|
|
def menu(self):
|
|
"""Return the menu of the control"""
|
|
hMenu, is_main_menu = self._menu_handle()
|
|
if hMenu: # and win32functions.IsMenu(menu_hwnd):
|
|
return Menu(self, hMenu, is_main_menu=is_main_menu)
|
|
return None
|
|
# Non PEP-8 alias
|
|
Menu = deprecated(menu)
|
|
|
|
# -----------------------------------------------------------
|
|
def menu_item(self, path, exact = False):
|
|
"""Return the menu item specified by path
|
|
|
|
Path can be a string in the form "MenuItem->MenuItem->MenuItem..."
|
|
where each MenuItem is the text of an item at that level of the menu.
|
|
E.g. ::
|
|
|
|
File->Export->ExportAsPNG
|
|
|
|
spaces are not important so you could also have written... ::
|
|
|
|
File -> Export -> Export As PNG
|
|
|
|
"""
|
|
if self.appdata is not None:
|
|
menu_appdata = self.appdata['menu_items']
|
|
else:
|
|
menu_appdata = None
|
|
|
|
menu = self.menu()
|
|
if menu:
|
|
return self.menu().get_menu_path(path, appdata = menu_appdata, exact=exact)[-1]
|
|
|
|
raise RuntimeError("There is no menu.")
|
|
# Non PEP-8 alias
|
|
MenuItem = deprecated(menu_item)
|
|
|
|
# -----------------------------------------------------------
|
|
def menu_items(self):
|
|
"""Return the menu items for the dialog
|
|
|
|
If there are no menu items then return an empty list
|
|
"""
|
|
if self.is_dialog() and self.menu():
|
|
#menu_handle = win32functions.GetMenu(self)
|
|
#self.send_message(win32defines.WM_INITMENU, menu_handle)
|
|
return self.menu().get_properties()['menu_items']
|
|
|
|
#self.send_message(win32defines.WM_INITMENU, menu_handle)
|
|
#return _GetMenuItems(menu_handle, self)
|
|
else:
|
|
return []
|
|
# Non PEP-8 alias
|
|
MenuItems = deprecated(menu_items)
|
|
|
|
# # -----------------------------------------------------------
|
|
# def menu_click(self, path):
|
|
# "Select the MenuItem specifed in path"
|
|
#
|
|
# self.verify_actionable()
|
|
#
|
|
# self.set_focus()
|
|
#
|
|
# menu = Menu(self, self._menu_handle())
|
|
#
|
|
# path_items = menu.get_menu_path(path)
|
|
#
|
|
# for menu_item in path_items:
|
|
# if not menu_item.is_enabled():
|
|
# raise MenuItemNotEnabled(
|
|
# "MenuItem '%s' is disabled"% menu_item.text())
|
|
#
|
|
# menu_item.click()
|
|
#
|
|
# return self
|
|
# # Non PEP-8 alias
|
|
# MenuClick = deprecated(menu_click)
|
|
|
|
# -----------------------------------------------------------
|
|
def menu_select(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`
|
|
"""
|
|
self.verify_actionable()
|
|
|
|
self.menu_item(path, exact=exact).select()
|
|
# Non PEP-8 alias
|
|
MenuSelect = deprecated(menu_select)
|
|
|
|
# -----------------------------------------------------------
|
|
def move_window(
|
|
self,
|
|
x = None,
|
|
y = None,
|
|
width = None,
|
|
height = None,
|
|
repaint = True):
|
|
"""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. Default to the
|
|
current height of the window.
|
|
* **repaint** Whether the window should be repainted or not.
|
|
Defaults to True
|
|
|
|
"""
|
|
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
|
|
ret = win32functions.MoveWindow(self, x, y, width, height, repaint)
|
|
|
|
# check that it worked correctly
|
|
if not ret:
|
|
raise ctypes.WinError()
|
|
|
|
win32functions.WaitGuiThreadIdle(self.handle)
|
|
time.sleep(Timings.after_movewindow_wait)
|
|
# Non PEP-8 alias
|
|
MoveWindow = deprecated(move_window)
|
|
|
|
# -----------------------------------------------------------
|
|
def close(self, wait_time = 0):
|
|
"""Close the window
|
|
|
|
Code modified from http://msdn.microsoft.com/msdnmag/issues/02/08/CQA/
|
|
|
|
"""
|
|
window_text = self.window_text()
|
|
|
|
# tell the window it must close
|
|
self.post_message(win32defines.WM_CLOSE)
|
|
|
|
#unused var: start = time.time()
|
|
# Keeps trying while
|
|
# we have not timed out and
|
|
# window is still a valid handle and
|
|
# window is still visible
|
|
# any one of these conditions evaluates to false means the window is
|
|
# closed or we have timed out
|
|
def has_closed():
|
|
return not (handleprops.iswindow(self) and self.is_visible())
|
|
|
|
if not wait_time:
|
|
wait_time = Timings.closeclick_dialog_close_wait
|
|
|
|
# Keep waiting until both this control and it's parent
|
|
# are no longer valid controls
|
|
timings.wait_until(
|
|
wait_time,
|
|
Timings.closeclick_retry,
|
|
has_closed
|
|
)
|
|
|
|
self.actions.log('Closed window "{0}"'.format(window_text))
|
|
# Non PEP-8 alias
|
|
Close = deprecated(close)
|
|
|
|
# -----------------------------------------------------------
|
|
def maximize(self):
|
|
"""Maximize the window"""
|
|
win32functions.ShowWindow(self, win32defines.SW_MAXIMIZE)
|
|
self.actions.log('Maximized window "{0}"'.format(self.window_text()))
|
|
return self
|
|
# Non PEP-8 alias
|
|
Maximize = deprecated(maximize)
|
|
|
|
# -----------------------------------------------------------
|
|
def minimize(self):
|
|
"""Minimize the window"""
|
|
win32functions.ShowWindow(self, win32defines.SW_MINIMIZE)
|
|
# TODO: wait while window is minimized
|
|
self.actions.log('Minimized window "{0}"'.format(self.window_text()))
|
|
return self
|
|
# Non PEP-8 alias
|
|
Minimize = deprecated(minimize)
|
|
|
|
# -----------------------------------------------------------
|
|
def restore(self):
|
|
"""Restore the window to its previous state (normal or maximized)"""
|
|
win32functions.ShowWindow(self, win32defines.SW_RESTORE)
|
|
self.actions.log('Restored window "{0}"'.format(self.window_text()))
|
|
return self
|
|
# Non PEP-8 alias
|
|
Restore = deprecated(restore)
|
|
|
|
# -----------------------------------------------------------
|
|
def get_show_state(self):
|
|
"""Get the show state and Maximized/minimzed/restored state
|
|
|
|
Returns a value that is a union of the following
|
|
|
|
* SW_HIDE the window is hidden.
|
|
* SW_MAXIMIZE the window is maximized
|
|
* SW_MINIMIZE the window is minimized
|
|
* SW_RESTORE the window is in the 'restored'
|
|
state (neither minimized or maximized)
|
|
* SW_SHOW The window is not hidden
|
|
"""
|
|
wp = win32structures.WINDOWPLACEMENT()
|
|
wp.lenght = ctypes.sizeof(wp)
|
|
|
|
ret = win32functions.GetWindowPlacement(self, ctypes.byref(wp))
|
|
|
|
if not ret:
|
|
raise ctypes.WinError()
|
|
|
|
return wp.showCmd
|
|
# Non PEP-8 alias
|
|
GetShowState = deprecated(get_show_state)
|
|
|
|
# -----------------------------------------------------------
|
|
def is_minimized(self):
|
|
"""Indicate whether the window is minimized or not"""
|
|
return self.get_show_state() == win32defines.SW_SHOWMINIMIZED
|
|
|
|
# -----------------------------------------------------------
|
|
def is_maximized(self):
|
|
"""Indicate whether the window is maximized or not"""
|
|
return self.get_show_state() == win32defines.SW_SHOWMAXIMIZED
|
|
|
|
# -----------------------------------------------------------
|
|
def is_normal(self):
|
|
"""Indicate whether the window is normal (i.e. not minimized and not maximized)"""
|
|
return self.get_show_state() == win32defines.SW_SHOWNORMAL
|
|
|
|
# -----------------------------------------------------------
|
|
def get_active(self):
|
|
"""Return a handle to the active window within the process"""
|
|
gui_info = win32structures.GUITHREADINFO()
|
|
gui_info.cbSize = ctypes.sizeof(gui_info)
|
|
window_thread_id, _ = win32process.GetWindowThreadProcessId(int(self.handle))
|
|
ret = win32functions.GetGUIThreadInfo(
|
|
window_thread_id,
|
|
ctypes.byref(gui_info))
|
|
|
|
if not ret:
|
|
raise ctypes.WinError()
|
|
|
|
hwndActive = gui_info.hwndActive
|
|
if hwndActive:
|
|
return HwndWrapper(hwndActive)
|
|
else:
|
|
return None
|
|
# Non PEP-8 alias
|
|
GetActive = deprecated(get_active)
|
|
|
|
# -----------------------------------------------------------
|
|
def get_focus(self):
|
|
"""Return the control in the process of this window that has the Focus
|
|
"""
|
|
gui_info = win32structures.GUITHREADINFO()
|
|
gui_info.cbSize = ctypes.sizeof(gui_info)
|
|
window_thread_id, _ = win32process.GetWindowThreadProcessId(self.handle)
|
|
ret = win32functions.GetGUIThreadInfo(
|
|
window_thread_id,
|
|
ctypes.byref(gui_info))
|
|
|
|
if not ret:
|
|
return None
|
|
|
|
return HwndWrapper(gui_info.hwndFocus)
|
|
# Non PEP-8 alias
|
|
GetFocus = deprecated(get_focus)
|
|
|
|
# -----------------------------------------------------------
|
|
def set_focus(self):
|
|
"""
|
|
Set the focus to this control.
|
|
|
|
Bring the window to the foreground first.
|
|
The system restricts which processes can set the foreground window
|
|
(https://msdn.microsoft.com/en-us/library/windows/desktop/ms633539(v=vs.85).aspx)
|
|
so the mouse cursor is removed from the screen to prevent any side effects.
|
|
"""
|
|
# "steal the focus" if there is another active window
|
|
# otherwise it is already into the foreground and no action required
|
|
if not self.has_focus():
|
|
# Notice that we need to move the mouse out of the screen
|
|
# but we don't use the built-in methods of the class:
|
|
# self.mouse_move doesn't do the job well even with absolute=True
|
|
# self.move_mouse_input can't be used as it calls click_input->set_focus
|
|
mouse.move(coords=(-10000, 500)) # move the mouse out of screen to the left
|
|
|
|
# change active window
|
|
if self.is_minimized():
|
|
if self.was_maximized():
|
|
self.maximize()
|
|
else:
|
|
self.restore()
|
|
else:
|
|
win32gui.ShowWindow(self.handle, win32con.SW_SHOW)
|
|
win32gui.SetForegroundWindow(self.handle)
|
|
|
|
# make sure that we are idle before returning
|
|
win32functions.WaitGuiThreadIdle(self.handle)
|
|
|
|
# only sleep if we had to change something!
|
|
time.sleep(Timings.after_setfocus_wait)
|
|
|
|
return self
|
|
# Non PEP-8 alias
|
|
SetFocus = deprecated(set_focus)
|
|
|
|
def has_focus(self):
|
|
"""Check the window is in focus (foreground)"""
|
|
return self.handle == win32gui.GetForegroundWindow()
|
|
|
|
def has_keyboard_focus(self):
|
|
"""Check the keyboard focus on this control."""
|
|
control_thread = win32process.GetWindowThreadProcessId(self.handle)[0]
|
|
win32process.AttachThreadInput(control_thread, win32api.GetCurrentThreadId(), 1)
|
|
focused = win32gui.GetFocus()
|
|
win32process.AttachThreadInput(control_thread, win32api.GetCurrentThreadId(), 0)
|
|
|
|
win32functions.WaitGuiThreadIdle(self.handle)
|
|
|
|
return self.handle == focused
|
|
|
|
def set_keyboard_focus(self):
|
|
"""Set the keyboard focus to this control."""
|
|
control_thread = win32process.GetWindowThreadProcessId(self.handle)[0]
|
|
win32process.AttachThreadInput(control_thread, win32api.GetCurrentThreadId(), 1)
|
|
win32gui.SetFocus(self.handle)
|
|
win32process.AttachThreadInput(control_thread, win32api.GetCurrentThreadId(), 0)
|
|
|
|
win32functions.WaitGuiThreadIdle(self.handle)
|
|
|
|
time.sleep(Timings.after_setfocus_wait)
|
|
return self
|
|
|
|
# -----------------------------------------------------------
|
|
def set_application_data(self, appdata):
|
|
"""Application data is data from a previous run of the software
|
|
|
|
It is essential for running scripts written for one spoke language
|
|
on a different spoken language
|
|
"""
|
|
self.appdata = appdata
|
|
|
|
|
|
_scroll_types = {"left": {
|
|
"line" : win32defines.SB_LINELEFT,
|
|
"page" : win32defines.SB_PAGELEFT,
|
|
"end" : win32defines.SB_LEFT,
|
|
},
|
|
"right": {
|
|
"line" : win32defines.SB_LINERIGHT,
|
|
"page" : win32defines.SB_PAGERIGHT,
|
|
"end" : win32defines.SB_RIGHT,
|
|
},
|
|
"up": {
|
|
"line" : win32defines.SB_LINEUP,
|
|
"page" : win32defines.SB_PAGEUP,
|
|
"end" : win32defines.SB_TOP,
|
|
},
|
|
"down": {
|
|
"line" : win32defines.SB_LINEDOWN,
|
|
"page" : win32defines.SB_PAGEDOWN,
|
|
"end" : win32defines.SB_BOTTOM,
|
|
},
|
|
}
|
|
# Non PEP-8 alias
|
|
SetApplicationData = deprecated(set_application_data)
|
|
|
|
# -----------------------------------------------------------
|
|
def scroll(self, direction, amount, count = 1, retry_interval = None):
|
|
"""Ask the control to scroll itself
|
|
|
|
**direction** can be any of "up", "down", "left", "right"
|
|
**amount** can be one of "line", "page", "end"
|
|
**count** (optional) the number of times to scroll
|
|
"""
|
|
self._ensure_enough_privileges('WM_HSCROLL/WM_VSCROLL')
|
|
|
|
# check which message we want to send
|
|
if direction.lower() in ("left", "right"):
|
|
message = win32defines.WM_HSCROLL
|
|
elif direction.lower() in ("up", "down"):
|
|
message = win32defines.WM_VSCROLL
|
|
|
|
# the constant that matches direction, and how much
|
|
try:
|
|
scroll_type = \
|
|
self._scroll_types[direction.lower()][amount.lower()]
|
|
except KeyError:
|
|
raise ValueError("""Wrong arguments:
|
|
direction can be any of "up", "down", "left", "right"
|
|
amount can be any of "line", "page", "end"
|
|
""")
|
|
|
|
# Scroll as often as we have been asked to
|
|
if retry_interval is None:
|
|
retry_interval = Timings.scroll_step_wait
|
|
while count > 0:
|
|
self.send_message(message, scroll_type)
|
|
time.sleep(retry_interval)
|
|
count -= 1
|
|
|
|
return self
|
|
# Non PEP-8 alias
|
|
Scroll = deprecated(scroll)
|
|
|
|
# -----------------------------------------------------------
|
|
def get_toolbar(self):
|
|
"""Get the first child toolbar if it exists"""
|
|
for child in self.children():
|
|
if child.__class__.__name__ == 'ToolbarWrapper':
|
|
return child
|
|
|
|
return None
|
|
# Non PEP-8 alias
|
|
GetToolbar = deprecated(get_toolbar)
|
|
|
|
# Non PEP-8 aliases for BaseWrapper methods
|
|
# We keep them for the backward compatibility in legacy scripts
|
|
ClickInput = deprecated(BaseWrapper.click_input)
|
|
DoubleClickInput = deprecated(BaseWrapper.double_click_input)
|
|
RightClickInput = deprecated(BaseWrapper.right_click_input)
|
|
VerifyVisible = deprecated(BaseWrapper.verify_visible)
|
|
_NeedsImageProp = deprecated(BaseWrapper._needs_image_prop, deprecated_name='_NeedsImageProp')
|
|
FriendlyClassName = deprecated(BaseWrapper.friendly_class_name)
|
|
Class = deprecated(BaseWrapper.class_name, deprecated_name='Class')
|
|
WindowText = deprecated(BaseWrapper.window_text)
|
|
ControlID = deprecated(BaseWrapper.control_id, deprecated_name='ControlID')
|
|
IsVisible = deprecated(BaseWrapper.is_visible)
|
|
IsEnabled = deprecated(BaseWrapper.is_enabled)
|
|
Rectangle = deprecated(BaseWrapper.rectangle)
|
|
ClientToScreen = deprecated(BaseWrapper.client_to_screen)
|
|
ProcessID = deprecated(BaseWrapper.process_id, deprecated_name='ProcessID')
|
|
IsDialog = deprecated(BaseWrapper.is_dialog)
|
|
Parent = deprecated(BaseWrapper.parent)
|
|
TopLevelParent = deprecated(BaseWrapper.top_level_parent)
|
|
Texts = deprecated(BaseWrapper.texts)
|
|
Children = deprecated(BaseWrapper.children)
|
|
CaptureAsImage = deprecated(BaseWrapper.capture_as_image)
|
|
GetProperties = deprecated(BaseWrapper.get_properties)
|
|
DrawOutline = deprecated(BaseWrapper.draw_outline)
|
|
IsChild = deprecated(BaseWrapper.is_child)
|
|
VerifyActionable = deprecated(BaseWrapper.verify_actionable)
|
|
VerifyEnabled = deprecated(BaseWrapper.verify_enabled)
|
|
PressMouseInput = deprecated(BaseWrapper.press_mouse_input)
|
|
ReleaseMouseInput = deprecated(BaseWrapper.release_mouse_input)
|
|
MoveMouseInput = deprecated(BaseWrapper.move_mouse_input)
|
|
DragMouseInput = deprecated(BaseWrapper.drag_mouse_input)
|
|
WheelMouseInput = deprecated(BaseWrapper.wheel_mouse_input)
|
|
TypeKeys = deprecated(BaseWrapper.type_keys)
|
|
|
|
|
|
#====================================================================
|
|
# the main reason for this is just to make sure that
|
|
# a Dialog is a known class - and we don't need to take
|
|
# an image of it (as an unknown control class)
|
|
class DialogWrapper(HwndWrapper):
|
|
|
|
"""Wrap a dialog"""
|
|
|
|
friendlyclassname = "Dialog"
|
|
#windowclasses = ["#32770", ]
|
|
can_be_label = True
|
|
|
|
#-----------------------------------------------------------
|
|
def __init__(self, hwnd):
|
|
"""Initialize the DialogWrapper
|
|
|
|
The only extra functionality here is to modify self.friendlyclassname
|
|
to make it "Dialog" if the class is "#32770" otherwise to leave it
|
|
the same as the window class.
|
|
"""
|
|
HwndWrapper.__init__(self, hwnd)
|
|
|
|
if self.class_name() == "#32770":
|
|
self.friendlyclassname = "Dialog"
|
|
else:
|
|
self.friendlyclassname = self.class_name()
|
|
|
|
#-----------------------------------------------------------
|
|
def run_tests(self, tests_to_run = None, ref_controls = None):
|
|
"""Run the tests on dialog"""
|
|
# the tests package is imported only when running unittests
|
|
from .. import tests
|
|
|
|
# get all the controls
|
|
controls = [self] + self.children()
|
|
|
|
# add the reference controls
|
|
if ref_controls is not None:
|
|
matched_flags = controlproperties.SetReferenceControls(
|
|
controls, ref_controls)
|
|
|
|
# todo: allow some checking of how well the controls matched
|
|
# matched_flags says how well they matched
|
|
# 1 = same number of controls
|
|
# 2 = ID's matched
|
|
# 4 = control classes matched
|
|
# i.e. 1 + 2 + 4 = perfect match
|
|
|
|
return tests.run_tests(controls, tests_to_run)
|
|
# Non PEP-8 alias
|
|
RunTests = deprecated(run_tests)
|
|
|
|
#-----------------------------------------------------------
|
|
def write_to_xml(self, filename):
|
|
"""Write the dialog an XML file (requires elementtree)"""
|
|
controls = [self] + self.children()
|
|
props = [ctrl.get_properties() for ctrl in controls]
|
|
|
|
from .. import xml_helpers
|
|
xml_helpers.WriteDialogToFile(filename, props)
|
|
# Non PEP-8 alias
|
|
WriteToXML = deprecated(write_to_xml)
|
|
|
|
#-----------------------------------------------------------
|
|
def client_area_rect(self):
|
|
"""Return the client area rectangle
|
|
|
|
From MSDN:
|
|
The client area of a control is the bounds of the control, minus the
|
|
nonclient elements such as scroll bars, borders, title bars, and
|
|
menus.
|
|
"""
|
|
rect = win32structures.RECT(self.rectangle())
|
|
self.send_message(win32defines.WM_NCCALCSIZE, 0, ctypes.byref(rect))
|
|
return rect
|
|
# Non PEP-8 alias
|
|
ClientAreaRect = deprecated(client_area_rect)
|
|
|
|
#-----------------------------------------------------------
|
|
def hide_from_taskbar(self):
|
|
"""Hide the dialog from the Windows taskbar"""
|
|
win32functions.ShowWindow(self, win32defines.SW_HIDE)
|
|
win32functions.SetWindowLongPtr(self, win32defines.GWL_EXSTYLE, self.exstyle() | win32defines.WS_EX_TOOLWINDOW)
|
|
win32functions.ShowWindow(self, win32defines.SW_SHOW)
|
|
# Non PEP-8 alias
|
|
HideFromTaskbar = deprecated(hide_from_taskbar)
|
|
|
|
#-----------------------------------------------------------
|
|
def show_in_taskbar(self):
|
|
"""Show the dialog in the Windows taskbar"""
|
|
win32functions.ShowWindow(self, win32defines.SW_HIDE)
|
|
win32functions.SetWindowLongPtr(self, win32defines.GWL_EXSTYLE,
|
|
self.exstyle() | win32defines.WS_EX_APPWINDOW)
|
|
win32functions.ShowWindow(self, win32defines.SW_SHOW)
|
|
# Non PEP-8 alias
|
|
ShowInTaskbar = deprecated(show_in_taskbar)
|
|
|
|
#-----------------------------------------------------------
|
|
def is_in_taskbar(self):
|
|
"""Check whether the dialog is shown in the Windows taskbar
|
|
|
|
Thanks to David Heffernan for the idea:
|
|
http://stackoverflow.com/questions/30933219/hide-window-from-taskbar-without-using-ws-ex-toolwindow
|
|
A window is represented in the taskbar if:
|
|
It has no owner and it does not have the WS_EX_TOOLWINDOW extended style,
|
|
or it has the WS_EX_APPWINDOW extended style.
|
|
"""
|
|
return self.has_exstyle(win32defines.WS_EX_APPWINDOW) or \
|
|
(self.owner() is None and not self.has_exstyle(win32defines.WS_EX_TOOLWINDOW))
|
|
# Non PEP-8 alias
|
|
IsInTaskbar = deprecated(is_in_taskbar)
|
|
|
|
#-----------------------------------------------------------
|
|
def force_close(self):
|
|
"""Close the dialog forcefully using WM_QUERYENDSESSION and return the result
|
|
|
|
Window has let us know that it doesn't want to die - so we abort
|
|
this means that the app is not hung - but knows it doesn't want
|
|
to close yet - e.g. it is asking the user if they want to save.
|
|
"""
|
|
self.send_message_timeout(
|
|
win32defines.WM_QUERYENDSESSION,
|
|
timeout = .5,
|
|
timeoutflags = (win32defines.SMTO_ABORTIFHUNG)) # |
|
|
#win32defines.SMTO_NOTIMEOUTIFNOTHUNG)) # |
|
|
#win32defines.SMTO_BLOCK)
|
|
|
|
# get a handle we can wait on
|
|
_, pid = win32process.GetWindowThreadProcessId(int(self.handle))
|
|
try:
|
|
process_wait_handle = win32api.OpenProcess(
|
|
win32con.SYNCHRONIZE | win32con.PROCESS_TERMINATE,
|
|
0,
|
|
pid)
|
|
except win32gui.error:
|
|
return True # already closed
|
|
|
|
result = win32event.WaitForSingleObject(
|
|
process_wait_handle,
|
|
int(Timings.after_windowclose_timeout * 1000))
|
|
|
|
return result != win32con.WAIT_TIMEOUT
|
|
|
|
# #-----------------------------------------------------------
|
|
# def read_controls_from_xml(self, filename):
|
|
# from pywinauto import xml_helpers
|
|
# [controlproperties.ControlProps(ctrl) for
|
|
# ctrl in xml_helpers.ReadPropertiesFromFile(handle)]
|
|
# # Non PEP-8 alias
|
|
# ReadControlsFromXML = deprecated(read_controls_from_xml)
|
|
|
|
# #-----------------------------------------------------------
|
|
# def add_reference(self, reference):
|
|
#
|
|
# if len(self.children() != len(reference)):
|
|
# raise "different number of reference controls"
|
|
#
|
|
# for i, ctrl in enumerate(reference):
|
|
# # loop over each of the controls
|
|
# # and set the reference
|
|
# if isinstance(ctrl, dict):
|
|
# ctrl = CtrlProps(ctrl)
|
|
#
|
|
# self.
|
|
# if ctrl.class_name() != self.children()[i+1].class_name():
|
|
# print "different classes"
|
|
# # Non PEP-8 alias
|
|
# AddReference = deprecated(add_reference)
|
|
|
|
|
|
#====================================================================
|
|
def _perform_click(
|
|
ctrl,
|
|
button = "left",
|
|
pressed = "",
|
|
coords = (0, 0),
|
|
double = False,
|
|
button_down = True,
|
|
button_up = True,
|
|
absolute = False,
|
|
):
|
|
"""Low level method for performing click operations"""
|
|
if ctrl is None:
|
|
ctrl = HwndWrapper(win32functions.GetDesktopWindow())
|
|
ctrl.verify_actionable()
|
|
ctrl_text = ctrl.window_text()
|
|
if ctrl_text is None:
|
|
ctrl_text = six.text_type(ctrl_text)
|
|
|
|
ctrl_friendly_class_name = ctrl.friendly_class_name()
|
|
|
|
if isinstance(coords, win32structures.RECT):
|
|
coords = coords.mid_point()
|
|
# allow points objects to be passed as the coords
|
|
elif isinstance(coords, win32structures.POINT):
|
|
coords = [coords.x, coords.y]
|
|
else:
|
|
coords = list(coords)
|
|
|
|
if absolute:
|
|
coords = ctrl.client_to_screen(coords)
|
|
|
|
# figure out the messages for click/press
|
|
msgs = []
|
|
if not double:
|
|
if button.lower() == 'left':
|
|
if button_down:
|
|
msgs.append(win32defines.WM_LBUTTONDOWN)
|
|
if button_up:
|
|
msgs.append(win32defines.WM_LBUTTONUP)
|
|
elif button.lower() == 'middle':
|
|
if button_down:
|
|
msgs.append(win32defines.WM_MBUTTONDOWN)
|
|
if button_up:
|
|
msgs.append(win32defines.WM_MBUTTONUP)
|
|
elif button.lower() == 'right':
|
|
if button_down:
|
|
msgs.append(win32defines.WM_RBUTTONDOWN)
|
|
if button_up:
|
|
msgs.append(win32defines.WM_RBUTTONUP)
|
|
elif button.lower() == 'move':
|
|
msgs.append(win32defines.WM_MOUSEMOVE)
|
|
|
|
# figure out the messages for double clicking
|
|
else:
|
|
if button.lower() == 'left':
|
|
msgs = (
|
|
win32defines.WM_LBUTTONDOWN,
|
|
win32defines.WM_LBUTTONUP,
|
|
win32defines.WM_LBUTTONDBLCLK,
|
|
win32defines.WM_LBUTTONUP)
|
|
elif button.lower() == 'middle':
|
|
msgs = (
|
|
win32defines.WM_MBUTTONDOWN,
|
|
win32defines.WM_MBUTTONUP,
|
|
win32defines.WM_MBUTTONDBLCLK,
|
|
win32defines.WM_MBUTTONUP)
|
|
elif button.lower() == 'right':
|
|
msgs = (
|
|
win32defines.WM_RBUTTONDOWN,
|
|
win32defines.WM_RBUTTONUP,
|
|
win32defines.WM_RBUTTONDBLCLK,
|
|
win32defines.WM_RBUTTONUP)
|
|
elif button.lower() == 'move':
|
|
msgs.append(win32defines.WM_MOUSEMOVE)
|
|
|
|
# figure out the flags and pack coordinates
|
|
flags, click_point = _calc_flags_and_coords(pressed, coords)
|
|
|
|
#control_thread = win32functions.GetWindowThreadProcessId(ctrl, 0)
|
|
#win32functions.AttachThreadInput(win32functions.GetCurrentThreadId(), control_thread, win32defines.TRUE)
|
|
# TODO: check return value of AttachThreadInput properly
|
|
|
|
# send each message
|
|
for msg in msgs:
|
|
win32functions.PostMessage(ctrl, msg, win32structures.WPARAM(flags), win32structures.LPARAM(click_point))
|
|
#ctrl.post_message(msg, flags, click_point)
|
|
#flags = 0
|
|
|
|
time.sleep(Timings.sendmessagetimeout_timeout)
|
|
|
|
# wait until the thread can accept another message
|
|
win32functions.WaitGuiThreadIdle(ctrl.handle)
|
|
|
|
# detach the Python process with the process that self is in
|
|
#win32functions.AttachThreadInput(win32functions.GetCurrentThreadId(), control_thread, win32defines.FALSE)
|
|
# TODO: check return value of AttachThreadInput properly
|
|
|
|
# wait a certain(short) time after the click
|
|
time.sleep(Timings.after_click_wait)
|
|
|
|
if button.lower() == 'move':
|
|
message = 'Moved mouse over ' + ctrl_friendly_class_name + ' "' + ctrl_text + \
|
|
'" to screen point ' + str(tuple(coords)) + ' by WM_MOUSEMOVE'
|
|
else:
|
|
message = 'Clicked ' + ctrl_friendly_class_name + ' "' + ctrl_text + \
|
|
'" by ' + str(button) + ' button event ' + str(tuple(coords))
|
|
if double:
|
|
message = 'Double-c' + message[1:]
|
|
ActionLogger().log(message)
|
|
|
|
_mouse_flags = {
|
|
"left": win32defines.MK_LBUTTON,
|
|
"right": win32defines.MK_RBUTTON,
|
|
"middle": win32defines.MK_MBUTTON,
|
|
"shift": win32defines.MK_SHIFT,
|
|
"control": win32defines.MK_CONTROL,
|
|
}
|
|
|
|
#====================================================================
|
|
def _calc_flags_and_coords(pressed, coords):
|
|
"""Calculate the flags to use and the coordinates for mouse actions"""
|
|
flags = 0
|
|
|
|
for key in pressed.split():
|
|
flags |= _mouse_flags[key.lower()]
|
|
|
|
click_point = win32functions.MakeLong(coords[1], coords[0])
|
|
|
|
return flags, click_point
|
|
|
|
#====================================================================
|
|
class _DummyControl(dict):
|
|
|
|
"""A subclass of dict so that we can assign attributes"""
|
|
pass
|
|
|
|
#====================================================================
|
|
def get_dialog_props_from_handle(hwnd):
|
|
"""Get the properties of all the controls as a list of dictionaries"""
|
|
# wrap the dialog handle and start a new list for the
|
|
# controls on the dialog
|
|
try:
|
|
controls = [hwnd, ]
|
|
controls.extend(hwnd.children())
|
|
except AttributeError:
|
|
controls = [HwndWrapper(hwnd), ]
|
|
|
|
# add all the children of the dialog
|
|
controls.extend(controls[0].children())
|
|
|
|
props = []
|
|
|
|
# Add each control to the properties for this dialog
|
|
for ctrl in controls:
|
|
# Get properties for each control and wrap them in
|
|
# _DummyControl so that we can assign handle
|
|
ctrl_props = _DummyControl(ctrl.get_properties())
|
|
|
|
# assign the handle
|
|
ctrl_props.handle = ctrl.handle
|
|
|
|
# offset the rectangle from the dialog rectangle
|
|
ctrl_props['rectangle'] -= controls[0].rectangle()
|
|
|
|
props.append(ctrl_props)
|
|
|
|
return props
|
|
# Non PEP-8 alias
|
|
GetDialogPropsFromHandle = deprecated(get_dialog_props_from_handle)
|
|
|
|
|
|
backend.register('win32', HwndElementInfo, HwndWrapper)
|
|
backend.registry.backends['win32'].dialog_class = DialogWrapper
|
|
backend.activate('win32') # default
|