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.
498 lines
16 KiB
498 lines
16 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.
|
|
|
|
"""Module containing operations for reading and writing dialogs as XML"""
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
from xml.etree.cElementTree import Element
|
|
from xml.etree.cElementTree import SubElement
|
|
from xml.etree.cElementTree import ElementTree
|
|
|
|
import six
|
|
import ctypes
|
|
import re
|
|
import bz2, base64
|
|
try:
|
|
import PIL.Image
|
|
PIL_imported = True
|
|
except ImportError:
|
|
PIL_imported = False
|
|
from . import controls
|
|
|
|
# reported that they are not used - but in fact they are
|
|
# through a search of globals()
|
|
|
|
class XMLParsingError(RuntimeError):
|
|
"""Wrap parsing Exceptions"""
|
|
pass
|
|
|
|
|
|
#DONE: Make the dialog reading function not actually know about the
|
|
# types of each element (so that we can read the control properties
|
|
# without having to know each and every element type)
|
|
# probably need to store info on what type things are.
|
|
#
|
|
# - if it is a ctypes struct then there is a __type__ field
|
|
# which says what kind of stuct it is
|
|
# - If it is an image then a "_IMG" is appeded to the the element tag
|
|
# - if it is a long then _LONG is appended to attribute name
|
|
# everything else is considered a string!
|
|
|
|
#-----------------------------------------------------------------------------
|
|
def _set_node_props(element, name, value):
|
|
"""Set the properties of the node based on the type of object"""
|
|
# if it is a ctypes structure
|
|
if isinstance(value, ctypes.Structure):
|
|
|
|
# create an element for the structure
|
|
struct_elem = SubElement(element, name)
|
|
#clsModule = value.__class__.__module__
|
|
cls_name = value.__class__.__name__
|
|
struct_elem.set("__type__", "{0}".format(cls_name))
|
|
|
|
# iterate over the fields in the structure
|
|
for prop_name in value._fields_:
|
|
prop_name = prop_name[0]
|
|
item_val = getattr(value, prop_name)
|
|
|
|
if isinstance(item_val, six.integer_types):
|
|
prop_name += "_LONG"
|
|
item_val = six.text_type(item_val)
|
|
|
|
struct_elem.set(prop_name, _escape_specials(item_val))
|
|
|
|
elif hasattr(value, 'tobytes') and hasattr(value, 'size'):
|
|
try:
|
|
# if the image is too big then don't try to
|
|
# write it out - it would probably product a MemoryError
|
|
# anyway
|
|
if value.size[0] * value.size[1] > (5000*5000):
|
|
raise MemoryError
|
|
|
|
#print('type(value) = ' + str(type(value)))
|
|
image_data = base64.encodestring(bz2.compress(value.tobytes())).decode('utf-8')
|
|
_set_node_props(
|
|
element,
|
|
name + "_IMG",
|
|
{
|
|
"mode": value.mode,
|
|
"size_x":value.size[0],
|
|
"size_y":value.size[1],
|
|
"data":image_data
|
|
})
|
|
|
|
# a system error is raised from time to time when we try to grab
|
|
# the image of a control that has 0 height or width
|
|
except (SystemError, MemoryError):
|
|
pass
|
|
|
|
|
|
elif isinstance(value, (list, tuple)):
|
|
# add the element to hold the values
|
|
# we do this to be able to support empty lists
|
|
listelem = SubElement(element, name + "_LIST")
|
|
|
|
for i, attrval in enumerate(value):
|
|
_set_node_props(listelem, "%s_%05d"%(name, i), attrval)
|
|
|
|
elif isinstance(value, dict):
|
|
dict_elem = SubElement(element, name)
|
|
|
|
for item_name, val in value.items():
|
|
_set_node_props(dict_elem, item_name, val)
|
|
|
|
else:
|
|
if isinstance(value, bool):
|
|
value = six.integer_types[-1](value)
|
|
|
|
if isinstance(value, six.integer_types):
|
|
name += "_LONG"
|
|
|
|
element.set(name, _escape_specials(value))
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
def WriteDialogToFile(filename, props):
|
|
"""
|
|
Write the props to the file
|
|
|
|
props can be either a dialog or a dictionary
|
|
"""
|
|
# if we are passed in a wrapped handle then
|
|
# get the properties
|
|
try:
|
|
props[0].keys()
|
|
except (TypeError, AttributeError):
|
|
props = controls.get_dialog_props_from_handle(props)
|
|
|
|
# build a tree structure
|
|
root = Element("DIALOG")
|
|
root.set("_version_", "2.0")
|
|
for ctrl in props:
|
|
ctrlelem = SubElement(root, "CONTROL")
|
|
for name, value in sorted(ctrl.items()):
|
|
_set_node_props(ctrlelem, name, value)
|
|
|
|
# wrap it in an ElementTree instance, and save as XML
|
|
tree = ElementTree(root)
|
|
tree.write(filename, encoding="utf-8")
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
def _escape_specials(string):
|
|
"""Ensure that some characters are escaped before writing to XML"""
|
|
# ensure it is unicode
|
|
string = six.text_type(string)
|
|
|
|
# escape backslashs
|
|
string = string.replace('\\', r'\\')
|
|
|
|
# escape non printable characters (chars below 30)
|
|
for i in range(0, 32):
|
|
string = string.replace(six.unichr(i), "\\%02d"%i)
|
|
|
|
return string
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
def _un_escape_specials(string):
|
|
"""Replace escaped characters with real character"""
|
|
# Unescape all the escape characters
|
|
for i in range(0, 32):
|
|
string = string.replace("\\%02d"%i, six.unichr(i))
|
|
|
|
# convert doubled backslashes to a single backslash
|
|
string = string.replace(r'\\', '\\')
|
|
|
|
return six.text_type(string)
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
def _xml_to_struct(element, struct_type = None):
|
|
"""
|
|
Convert an ElementTree to a ctypes Struct
|
|
|
|
If struct_type is not specified then element['__type__']
|
|
will be used for the ctypes struct type
|
|
"""
|
|
# handle if we are passed in an element or a dictionary
|
|
try:
|
|
attribs = element.attrib
|
|
except AttributeError:
|
|
attribs = element
|
|
|
|
# if the type has not been passed in
|
|
if not struct_type:
|
|
# get the type and create an instance of the type
|
|
struct = globals()[attribs["__type__"]]()
|
|
else:
|
|
# create an instance of the type
|
|
struct = globals()[struct_type]()
|
|
|
|
# get the attribute and set them upper case
|
|
struct_attribs = dict((at.upper(), at) for at in dir(struct))
|
|
|
|
# for each of the attributes in the element
|
|
for prop_name in attribs:
|
|
|
|
# get the value
|
|
val = attribs[prop_name]
|
|
|
|
# if the value ends with "_long"
|
|
if prop_name.endswith("_LONG"):
|
|
# get an long attribute out of the value
|
|
val = six.integer_types[-1](val)
|
|
prop_name = prop_name[:-5]
|
|
|
|
# if the value is a string
|
|
elif isinstance(val, six.string_types):
|
|
# make sure it if Unicode
|
|
val = six.text_type(val)
|
|
|
|
# now we can have all upper case attribute name
|
|
# but structure name will not be upper case
|
|
if prop_name.upper() in struct_attribs:
|
|
prop_name = struct_attribs[prop_name.upper()]
|
|
|
|
# set the appropriate attribute of the Struct
|
|
setattr(struct, prop_name, val)
|
|
|
|
# reutrn the struct
|
|
return struct
|
|
|
|
|
|
#====================================================================
|
|
def _old_xml_to_titles(element):
|
|
"""For OLD XML files convert the titles as a list"""
|
|
# get all the attribute names
|
|
title_names = element.keys()
|
|
|
|
# sort them to make sure we get them in the right order
|
|
title_names.sort()
|
|
|
|
# build up the array
|
|
titles = []
|
|
for name in title_names:
|
|
val = element[name]
|
|
val = val.replace('\\n', '\n')
|
|
val = val.replace('\\x12', '\x12')
|
|
val = val.replace('\\\\', '\\')
|
|
|
|
titles.append(six.text_type(val))
|
|
|
|
return titles
|
|
|
|
|
|
#====================================================================
|
|
# TODO: this function should be broken up into smaller functions
|
|
# for each type of processing e.g.
|
|
# ElementTo
|
|
def _extract_properties(properties, prop_name, prop_value):
|
|
"""
|
|
Hmmm - confusing - can't remember exactly how
|
|
all these similar functions call each other
|
|
"""
|
|
# get the base property name and number if it in the form
|
|
# "PROPNAME_00001" = ('PROPNAME', 1)
|
|
prop_name, reqd_index = _split_number(prop_name)
|
|
|
|
# if there is no required index, and the property
|
|
# was not already set - then just set it
|
|
|
|
# if this is an indexed member of a list
|
|
if reqd_index is None:
|
|
# Have we hit a property with this name already
|
|
if prop_name in properties:
|
|
# try to append current value to the property
|
|
try:
|
|
properties[prop_name].append(prop_value)
|
|
|
|
# if that fails then we need to make sure that
|
|
# the curruen property is a list and then
|
|
# append it
|
|
except AttributeError:
|
|
new_val = [properties[prop_name], prop_value]
|
|
properties[prop_name] = new_val
|
|
# No index, no previous property with that name
|
|
# - just set the property
|
|
else:
|
|
properties[prop_name] = prop_value
|
|
|
|
# OK - so it HAS an index
|
|
else:
|
|
|
|
# make sure that the property is a list
|
|
properties.setdefault(prop_name, [])
|
|
|
|
# make sure that the list has enough elements
|
|
while 1:
|
|
if len(properties[prop_name]) <= reqd_index:
|
|
properties[prop_name].append('')
|
|
else:
|
|
break
|
|
|
|
# put our value in at the right index
|
|
properties[prop_name][reqd_index] = prop_value
|
|
|
|
|
|
#====================================================================
|
|
def _get_attributes(element):
|
|
"""Get the attributes from an element"""
|
|
properties = {}
|
|
|
|
# get all the attributes
|
|
for attrib_name, val in element.attrib.items():
|
|
|
|
# if it is 'Long' element convert it to an long
|
|
if attrib_name.endswith("_LONG"):
|
|
val = six.integer_types[-1](val)
|
|
attrib_name = attrib_name[:-5]
|
|
|
|
else:
|
|
# otherwise it is a string - make sure we get it as a unicode string
|
|
val = _un_escape_specials(val)
|
|
|
|
_extract_properties(properties, attrib_name, val)
|
|
|
|
return properties
|
|
|
|
|
|
#====================================================================
|
|
number = re.compile(r"^(.*)_(\d{5})$")
|
|
def _split_number(prop_name):
|
|
"""
|
|
Return (string, number) for a prop_name in the format string_number
|
|
|
|
The number part has to be 5 digits long
|
|
None is returned if there is no _number part
|
|
|
|
e.g.
|
|
>>> _split_number("NoNumber")
|
|
('NoNumber', None)
|
|
>>> _split_number("Anumber_00003")
|
|
('Anumber', 3)
|
|
>>> _split_number("notEnoughDigits_0003")
|
|
('notEnoughDigits_0003', None)
|
|
"""
|
|
found = number.search(prop_name)
|
|
|
|
if not found:
|
|
return prop_name, None
|
|
|
|
return found.group(1), int(found.group(2))
|
|
|
|
|
|
#====================================================================
|
|
def _read_xml_structure(control_element):
|
|
"""
|
|
Convert an element into nested Python objects
|
|
|
|
The values will be returned in a dictionary as following:
|
|
|
|
- the attributes will be items of the dictionary
|
|
for each subelement
|
|
|
|
+ if it has a __type__ attribute then it is converted to a
|
|
ctypes structure
|
|
+ if the element tag ends with _IMG then it is converted to
|
|
a PIL image
|
|
|
|
- If there are elements with the same name or attributes with
|
|
ordering e.g. texts_00001, texts_00002 they will be put into a
|
|
list (in the correct order)
|
|
"""
|
|
|
|
# get the attributes for the current element
|
|
properties = _get_attributes(control_element)
|
|
|
|
for elem in control_element:
|
|
# if it is a ctypes structure
|
|
if "__type__" in elem.attrib:
|
|
# create a new instance of the correct type
|
|
|
|
# grab the data
|
|
propval = _xml_to_struct(elem)
|
|
|
|
elif elem.tag.endswith("_IMG"):
|
|
elem.tag = elem.tag[:-4]
|
|
|
|
# get image Attribs
|
|
img = _get_attributes(elem)
|
|
data = bz2.decompress(base64.decodestring(img['data'].encode('utf-8')))
|
|
|
|
if PIL_imported is False:
|
|
raise RuntimeError('PIL is not installed!')
|
|
propval = PIL.Image.frombytes(
|
|
img['mode'],
|
|
(img['size_x'], img['size_y']),
|
|
data)
|
|
|
|
elif elem.tag.endswith("_LIST"):
|
|
# All this is just to handle the edge case of
|
|
# an empty list
|
|
elem.tag = elem.tag[:-5]
|
|
|
|
# read the structure
|
|
propval = _read_xml_structure(elem)
|
|
|
|
# if it was empty then convert the returned dict
|
|
# to a list
|
|
if propval == {}:
|
|
propval = list()
|
|
|
|
# otherwise extract the list out of the returned dict
|
|
else:
|
|
propval = propval[elem.tag]
|
|
|
|
else:
|
|
propval = _read_xml_structure(elem)
|
|
|
|
_extract_properties(properties, elem.tag, propval)
|
|
|
|
return properties
|
|
|
|
|
|
#====================================================================
|
|
def ReadPropertiesFromFile(filename):
|
|
"""Return a list of controls from XML file filename"""
|
|
parsed = ElementTree().parse(filename)
|
|
|
|
# Return the list that has been stored under 'CONTROL'
|
|
props = _read_xml_structure(parsed)['CONTROL']
|
|
if not isinstance(props, list):
|
|
props = [props]
|
|
|
|
# it is an old XML so let's fix it up a little
|
|
if not ("_version_" in parsed.attrib.keys()):
|
|
|
|
# find each of the control elements
|
|
for ctrl_prop in props:
|
|
|
|
ctrl_prop['fonts'] = [_xml_to_struct(ctrl_prop['FONT'], "LOGFONTW"), ]
|
|
|
|
ctrl_prop['rectangle'] = \
|
|
_xml_to_struct(ctrl_prop["RECTANGLE"], "RECT")
|
|
|
|
ctrl_prop['client_rects'] = [
|
|
_xml_to_struct(ctrl_prop["CLIENTRECT"], "RECT"),]
|
|
|
|
ctrl_prop['texts'] = _old_xml_to_titles(ctrl_prop["TITLES"])
|
|
|
|
ctrl_prop['class_name'] = ctrl_prop['CLASS']
|
|
ctrl_prop['context_help_id'] = ctrl_prop['HELPID']
|
|
ctrl_prop['control_id'] = ctrl_prop['CTRLID']
|
|
ctrl_prop['exstyle'] = ctrl_prop['EXSTYLE']
|
|
ctrl_prop['friendly_class_name'] = ctrl_prop['FRIENDLYCLASS']
|
|
ctrl_prop['is_unicode'] = ctrl_prop['ISUNICODE']
|
|
ctrl_prop['is_visible'] = ctrl_prop['ISVISIBLE']
|
|
ctrl_prop['style'] = ctrl_prop['STYLE']
|
|
ctrl_prop['user_data'] = ctrl_prop['USERDATA']
|
|
|
|
for prop_name in [
|
|
'CLASS',
|
|
'CLIENTRECT',
|
|
'CTRLID',
|
|
'EXSTYLE',
|
|
'FONT',
|
|
'FRIENDLYCLASS',
|
|
'HELPID',
|
|
'ISUNICODE',
|
|
'ISVISIBLE',
|
|
'RECTANGLE',
|
|
'STYLE',
|
|
'TITLES',
|
|
'USERDATA',
|
|
]:
|
|
del(ctrl_prop[prop_name])
|
|
|
|
return props
|