You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ORPA-pyOpenRPA/3rdParty/pywinauto/controls/menuwrapper.py

702 lines
22 KiB

6 years ago
# GUI Application automation and testing library
# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors
# https://github.com/pywinauto/pywinauto/graphs/contributors
# http://pywinauto.readthedocs.io/en/latest/credits.html
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of pywinauto nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Wrapper around Menu's and Menu items
These wrappers allow you to work easily with menu items.
You can select or click on items and check if they are
checked or unchecked.
"""
from __future__ import unicode_literals
import ctypes
import ctypes.wintypes
import time
import win32gui
import win32gui_struct
import locale
import six
from functools import wraps
from .. import win32structures
from .. import win32functions
from .. import win32defines
from .. import findbestmatch
from .. import mouse
from ..remote_memory_block import RemoteMemoryBlock
from ..timings import Timings
from .. import deprecated
class MenuItemInfo(object):
"""A holder for Menu Item Info"""
def __init__(self):
self.fType = 0
self.fState = 0
self.wID = 0
self.hSubMenu = 0
self.hbmpChecked = 0
self.hbmpUnchecked = 0
self.dwItemData = 0
self.text = ""
self.hbmpItem = 0
class MenuInfo(object):
"""A holder for Menu Info"""
def __init__(self):
self.dwStyle = 0
self.cyMax = 0
self.hbrBack = 0
self.dwContextHelpID = 0
self.dwMenuData = 0
class MenuItemNotEnabled(RuntimeError):
"""Raised when a menu item is not enabled"""
pass
class MenuInaccessible(RuntimeError):
"""Raised when a menu has handle but inaccessible."""
pass
def ensure_accessible(method):
"""Decorator for Menu instance methods"""
@wraps(method)
def check(instance, *args, **kwargs):
"""Check if the instance is accessible"""
if not instance.accessible:
raise MenuInaccessible
else:
return method(instance, *args, **kwargs)
return check
class MenuItem(object):
"""Wrap a menu item"""
def __init__(self, ctrl, menu, index, on_main_menu=False):
"""
Initialize the menu item
* **ctrl** The dialog or control that owns this menu
* **menu** The menu that this item is on
* **index** The Index of this menu item on the menu
* **on_main_menu** True if the item is on the main menu
"""
self._index = index
self.menu = menu
self.ctrl = ctrl
self.on_main_menu = on_main_menu
def _read_item(self):
"""Read the menu item info
See https://msdn.microsoft.com/en-us/library/windows/desktop/ms647980.aspx
for more information.
"""
item_info = MenuItemInfo()
buf, extras = win32gui_struct.EmptyMENUITEMINFO()
win32gui.GetMenuItemInfo(self.menu.handle, self._index, True, buf)
item_info.fType, item_info.fState, item_info.wID, item_info.hSubMenu, \
item_info.hbmpChecked, item_info.hbmpUnchecked, item_info.dwItemData, \
item_info.text, item_info.hbmpItem = win32gui_struct.UnpackMENUITEMINFO(buf)
return item_info
def friendly_class_name(self):
"""Return friendly class name"""
return "MenuItem"
# Non PEP-8 alias
FriendlyClassName = deprecated(friendly_class_name)
#def __print__(self, ctrl, menu, index):
# print('Menu ' + six.text_type(ctrl) + '; ' + six.text_type(menu) + '; ' + six.text_type(index))
def rectangle(self):
"""Get the rectangle of the menu item"""
rect = win32structures.RECT()
if self.on_main_menu:
ctrl = self.ctrl
else:
ctrl = 0
# make it as HMENU type
hMenu = ctypes.wintypes.HMENU(self.menu.handle)
#(rect.left.value, rect.top.value, rect.right.value, rect.bottom.value) = \
# win32gui.GetMenuItemRect(ctrl.handle, self.menu.handle, self.index)
#self.__print__(ctrl, hMenu, self.index)
win32functions.GetMenuItemRect(
ctrl,
hMenu,
self._index,
ctypes.byref(rect))
return rect
# Non PEP-8 alias
Rectangle = deprecated(rectangle)
def index(self):
"""Return the index of this menu item"""
return self._index
# Non PEP-8 alias
Index = deprecated(index)
def state(self):
"""Return the state of this menu item"""
return self._read_item().fState
# Non PEP-8 alias
State = deprecated(state)
def item_id(self):
"""Return the ID of this menu item"""
return self._read_item().wID
# Non PEP-8 alias
ID = deprecated(item_id, deprecated_name='ID')
def item_type(self):
"""
Return the Type of this menu item
Main types are MF_STRING, MF_BITMAP, MF_SEPARATOR.
See https://msdn.microsoft.com/en-us/library/windows/desktop/ms647980.aspx
for further information.
"""
return self._read_item().fType
# Non PEP-8 alias
Type = deprecated(item_type, deprecated_name='Type')
def text(self):
"""Return the text of this menu item"""
item_info = self._read_item()
if six.PY2:
item_info.text = item_info.text.decode(locale.getpreferredencoding())
# OWNERDRAW case try to get string from BCMenu
if item_info.fType & 256 and not item_info.text:
mem = RemoteMemoryBlock(self.ctrl)
address = item_info.dwItemData
s = win32structures.LPWSTR()
mem.Read(s, address)
address = s
s = ctypes.create_unicode_buffer(100)
try:
mem.Read(s, address)
item_info.text = s.value
except Exception:
item_info.text = '!! non-supported owner drawn item !!' # TODO: look into Tkinter case
del mem
return item_info.text
# Non PEP-8 alias
Text = deprecated(text)
def sub_menu(self):
"""Return the SubMenu or None if no submenu"""
submenu_handle = self._read_item().hSubMenu
if submenu_handle:
win32gui.SendMessageTimeout(self.ctrl.handle, win32defines.WM_INITMENUPOPUP,
submenu_handle,
self._index,
win32defines.SMTO_NORMAL,
0)
return Menu(self.ctrl, submenu_handle, False, self)
return None
# Non PEP-8 alias
SubMenu = deprecated(sub_menu)
def is_enabled(self):
"""Return True if the item is enabled."""
return not (
self.state() & win32defines.MF_DISABLED or
self.state() & win32defines.MF_GRAYED)
# Non PEP-8 alias
IsEnabled = deprecated(is_enabled)
def is_checked(self):
"Return True if the item is checked."
return bool(self.state() & win32defines.MF_CHECKED)
# Non PEP-8 alias
IsChecked = deprecated(is_checked)
def click_input(self):
"""
Click on the menu item in a more realistic way
If the menu is open it will click with the mouse event on the item.
If the menu is not open each of it's parent's will be opened
until the item is visible.
"""
self.ctrl.verify_actionable()
rect = self.rectangle()
if not self.is_enabled():
raise MenuItemNotEnabled(
"MenuItem {0} is disabled".format(self.text()))
# if the item is not visible - work up along it's parents
# until we find an item we CAN click on
if rect == (0, 0, 0, 0) and self.menu.owner_item:
self.menu.owner_item.click_input()
rect = self.rectangle()
x_pt = int(float(rect.left + rect.right) / 2.)
y_pt = int(float(rect.top + rect.bottom) / 2.)
mouse.click(coords=(x_pt, y_pt))
win32functions.WaitGuiThreadIdle(self.ctrl)
time.sleep(Timings.after_menu_wait)
# Non PEP-8 alias
ClickInput = deprecated(click_input)
def select(self):
"""
Select the menu item
This will send a message to the parent window that the
item was picked.
"""
if not self.is_enabled():
raise MenuItemNotEnabled(
"MenuItem {0} is disabled".format(self.text()))
#if self.state() & win32defines.MF_BYPOSITION:
# print self.text(), "BYPOSITION"
# self.ctrl.NotifyMenuSelect(self.index(), True)
#else:
# seems like set_focus might be messing with getting the
# id for Popup menu items - so I calling it before set_focus
command_id = self.item_id()
# notify the control that a menu item was selected
self.ctrl.set_focus()
self.ctrl.send_message_timeout(
self.menu.COMMAND, command_id, timeout=1.0)
win32functions.WaitGuiThreadIdle(self.ctrl)
time.sleep(Timings.after_menu_wait)
# _perform_click() doesn't work for MenuItem, so let's call select() method
click = select
# Non PEP-8 alias
Click = deprecated(click)
# Non PEP-8 alias
Select = deprecated(select)
def get_properties(self):
"""
Return the properties for the item as a dict
If this item opens a sub menu then call Menu.get_properties()
to return the list of items in the sub menu. This is avialable
under the 'menu_items' key.
"""
props = {}
props['index'] = self.index()
props['state'] = self.state()
props['item_type'] = self.item_type()
props['item_id'] = self.item_id()
props['text'] = self.text()
submenu = self.sub_menu()
if submenu:
if submenu.accessible:
props['menu_items'] = submenu.get_properties()
else:
# Submenu is attached to the item but not accessible,
# so just mark that it exists without any additional information.
props['menu_items'] = []
return props
# Non PEP-8 alias
GetProperties = deprecated(get_properties)
def __repr__(self):
"""Return a representation of the object as a string"""
if six.PY3:
return "<MenuItem " + self.text() + ">"
else:
return "<MenuItem " + self.text().encode(locale.getpreferredencoding()) + ">"
# def check(self):
# item = self._read_item()
# item.fMask = win32defines.MIIM_STATE
# item.fState &= win32defines.MF_CHECKED
#
# #ret = win32functions.SetMenuItemInfo(
# # self.menuhandle,
# # self.item_id(),
# # 0, # by position
# # ctypes.byref(item))
# #
# #if not ret:
# # raise ctypes.WinError()
#
# print win32functions.CheckMenuItem(
# self.menuhandle,
# self.index,
# win32defines.MF_BYPOSITION | win32defines.MF_CHECKED)
#
# win32functions.DrawMenuBar(self.ctrl)
#
# def uncheck(self):
# item = self._read_item()
# item.fMask = win32defines.MIIM_STATE
# item.fState &= win32defines.MF_UNCHECKED
#
# ret = win32functions.SetMenuItemInfo(
# self.menuhandle,
# self.item_id(),
# 0, # by position
# ctypes.byref(item))
#
# if not ret:
# raise ctypes.WinError()
#
# win32functions.DrawMenuBar(self.ctrl)
#
class Menu(object):
"""A simple wrapper around a menu handle
A menu supports methods for querying the menu
and getting it's menu items.
"""
def __init__(self,
owner_ctrl,
menuhandle,
is_main_menu=True,
owner_item=None):
"""Initialize the class
* **owner_ctrl** is the Control that owns this menu
* **menuhandle** is the menu handle of the menu
* **is_main_menu** we have to track whether it is the main menu
or a popup menu
* **owner_item** The item that contains this menu - this will be
None for the main menu, it will be a MenuItem instance for a
submenu.
"""
self.ctrl = owner_ctrl
self.handle = menuhandle
self.is_main_menu = is_main_menu
self.owner_item = owner_item
self._as_parameter_ = self.handle
self.accessible = True
if self.is_main_menu:
self.ctrl.send_message_timeout(win32defines.WM_INITMENU, self.handle)
menu_info = MenuInfo()
buf = win32gui_struct.EmptyMENUINFO()
try:
win32gui.GetMenuInfo(self.handle, buf)
except win32gui.error:
self.accessible = False
else:
menu_info.dwStyle, menu_info.cyMax, menu_info.hbrBack, menu_info.dwContextHelpID,\
menu_info.dwMenuData = win32gui_struct.UnpackMENUINFO(buf)
if menu_info.dwStyle & win32defines.MNS_NOTIFYBYPOS:
self.COMMAND = win32defines.WM_MENUCOMMAND
else:
self.COMMAND = win32defines.WM_COMMAND
@ensure_accessible
def item_count(self):
"""Return the count of items in this menu"""
return win32gui.GetMenuItemCount(self.handle)
# Non PEP-8 alias
ItemCount = deprecated(item_count)
@ensure_accessible
def item(self, index, exact=False):
"""
Return a specific menu item
* **index** is the 0 based index or text of the menu item you want.
* **exact** is True means exact matching for item text,
False means best matching.
"""
if isinstance(index, six.string_types):
if self.ctrl.appdata is not None:
menu_appdata = self.ctrl.appdata['menu_items']
else:
menu_appdata = None
return self.get_menu_path(index, appdata=menu_appdata, exact=exact)[-1]
return MenuItem(self.ctrl, self, index, self.is_main_menu)
# Non PEP-8 alias
Item = deprecated(item)
@ensure_accessible
def items(self):
"Return a list of all the items in this menu"
items = []
for i in range(0, self.item_count()):
items.append(self.item(i))
return items
# Non PEP-8 alias
Items = deprecated(items)
@ensure_accessible
def get_properties(self):
"""
Return the properties for the menu as a list of dictionaries
This method is actually recursive. It calls get_properties() for each
of the items. If the item has a sub menu it will call this
get_properties to get the sub menu items.
"""
item_props = []
for item in self.items():
item_props.append(item.get_properties())
return {'menu_items': item_props}
# Non PEP-8 alias
GetProperties = deprecated(get_properties)
@ensure_accessible
def get_menu_path(self, path, path_items=None, appdata=None, exact=False):
"""
Walk the items in this menu to find the item specified by a path
The path is specified by a list of items separated by '->'. Each item
can be either a string (can include spaces) e.g. "Save As" or a zero
based index of the item to return prefaced by # e.g. #1 or an ID of
the item prefaced by $ specifier.
These can be mixed as necessary. For example:
- "#0 -> Save As",
- "$23453 -> Save As",
- "Tools -> #0 -> Configure"
Text matching is done using a 'best match' fuzzy algorithm, so you don't
have to add all punctuation, ellipses, etc.
ID matching is performed against wID field of MENUITEMINFO structure
(https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx)
"""
if path_items is None:
path_items = []
# get the first part (and remainder)
parts = [p.strip() for p in path.split("->", 1)]
current_part = parts[0]
if current_part.startswith("#"):
index = int(current_part[1:])
best_item = self.item(index)
elif current_part.startswith("$"):
# get the IDs from the menu items
if appdata is None:
item_IDs = [item.item_id() for item in self.items()]
else:
item_IDs = [item['item_id'] for item in appdata]
item_id = int(current_part[1:])
# find the item that best matches the current part
best_item = self.item(item_IDs.index(item_id))
else:
# get the text names from the menu items
if appdata is None:
item_texts = [item.text() for item in self.items()]
else:
item_texts = [item['text'] for item in appdata]
if exact:
if current_part not in item_texts:
raise IndexError('There are no menu item "' + str(current_part) + '" in ' + str(item_texts))
best_item = self.items()[item_texts.index(current_part)]
else:
# find the item that best matches the current part
best_item = findbestmatch.find_best_match(
current_part,
item_texts,
self.items())
path_items.append(best_item)
# if there are more parts - then get the next level
if parts[1:]:
if appdata:
appdata = appdata[best_item.index()]['menu_items']
if best_item.sub_menu() is not None:
best_item.sub_menu().get_menu_path(
"->".join(parts[1:]),
path_items,
appdata,
exact=exact)
return path_items
# Non PEP-8 alias
GetMenuPath = deprecated(get_menu_path)
def __repr__(self):
"""Return a simple representation of the menu"""
return "<Menu {0}>".format(self.handle)
# def get_properties(self):
#
# for i in range(0, self.item_count()):
# menu_info = self.item(self, i)[0]
#
# item_prop['index'] = i
# item_prop['state'] = menu_info.fState
# item_prop['item_type'] = menu_info.fType
# item_prop['item_id'] = menu_info.wID
#
# else:
# item_prop['text'] = ""
#
# if self.IsSubMenu(i):
# item_prop['menu_items'] = self.sub_menu(i).get_properties()
#
# return item_prop
##====================================================================
#def _get_menu_items(menu_handle, ctrl):
# """Get the menu items as a list of dictionaries"""
# # If it doesn't have a real menu just return
# if not win32functions.IsMenu(menu_handle):
# return []
#
#
# menu = Menu(ctrl, menu_handle)
#
# props = []
# for item in menu.items():
# item_props = {}
#
# item_prop['index'] = item.index()
# item_prop['state'] = item.state()
# item_prop['item_type'] = item.item_type()
# item_prop['item_id'] = item.item_id()
# item_prop['text'] = item.text()
#
# props.append(item_props)
#
#
#
# items = []
#
#
# # for each menu item
# for i in range(0, item_count):
#
# item_prop = {}
#
# # get the information on the menu Item
# menu_info = win32structures.MENUITEMINFOW()
# menu_info.cbSize = ctypes.sizeof (menu_info)
# menu_info.fMask = \
# win32defines.MIIM_CHECKMARKS | \
# win32defines.MIIM_ID | \
# win32defines.MIIM_STATE | \
# win32defines.MIIM_SUBMENU | \
# win32defines.MIIM_TYPE #| \
# #MIIM_FTYPE #| \
# #MIIM_STRING
# #MIIM_DATA | \
#
#
# ret = win32functions.GetMenuItemInfo (
# menu_handle, i, True, ctypes.byref(menu_info))
# if not ret:
# raise ctypes.WinError()
#
# # if there is text
# if menu_info.cch:
# # allocate a buffer
# buffer_size = menu_info.cch+1
# text = ctypes.create_unicode_buffer(buffer_size)
#
# # update the structure and get the text info
# menu_info.dwTypeData = ctypes.addressof(text)
# menu_info.cch = buffer_size
# win32functions.GetMenuItemInfo (
# menu_handle, i, True, ctypes.byref(menu_info))
# item_prop['text'] = text.value
# else:
# item_prop['text'] = ""
#
# # if it's a sub menu then get it's items
# if menu_info.hSubMenu:
# # make sure that the app updates the menu if it need to
# ctrl.send_message(
# win32defines.WM_INITMENUPOPUP, menu_info.hSubMenu, i)
#
# #ctrl.send_message(
# # win32defines.WM_INITMENU, menu_info.hSubMenu, )
#
# # get the sub menu items
# sub_menu_items = _GetMenuItems(menu_info.hSubMenu, ctrl)
#
# # append them
# item_prop['menu_items'] = sub_menu_items
#
# items.append(item_prop)
#
# return items
# # Non PEP-8 alias
# _GetMenuItems = deprecated(_get_menu_items)
#