MSAA也就是Microsoft© Active Accessibility,是微软90年代推出的一套技术。事实上,这套东西的初衷是为了供残疾人使用windows系统,比如放大镜,鼠标事件等等。这套东西后来被广泛使用在自动化测试中,也就是我们常说的IAccessible 接口,由于它为自动化测试提供了方便,IBM也扩展了这套接口,IAccessible2(如果没记错的话);web版本的,也就是ActiveX。说到这些,就必须提到ole这门技术,我的理解是微软内部一开始只是为了自己自动化方便,做了一套最原始的ole automation的东西(主要技术是边界值检测和引用计数),后来发现这东西很好用,甚至被盖茨钦点,最终发展成后来的com,从某种角度来说,这东西可以说是90年代技术的巅峰。当然,IAccessible由oleacc.dll在com组件中提供。它的鸡肋就是接口太少,只能访问UI元素的名字,位置,描述等,一些其他的操作并不支持——这也是后来微软发展UI Automation的理由之一(扩展了两个接口,一个用来实现基本的处理,另一个实现外部调用和msaa的兼容性,在.net平台有效)。自己当初在office组的时候正巧负责mac os版中的oleauto模块迁移,深感这套技术的厉害之处(我们叫它mac上的伪com),可以看到从8位到64位的整个迁移过程。由于这里主要是讨论MSAA在python中的实现和架构设计,至于本源问题个人觉得没必要深究。当然UIS和MSAA相比,各有各的特点,相互之间通过代理和桥打交道。http://blogs.msdn.com/b/lixiong/archive/2009/03/28/msaa-uia-brief-explanation.aspx,这个链接里清楚的描述了他们的关系。我上面的简单描述并不一定正确,可以自行深入了解。下来开始讨论正格朗的东西(UIA的使用和实现有时间再讨论)。
IAccessible接口的中文资料应该不是很难找,但是python使用和封装的具体方法,好像没有什么中文资料(英文的我也没找到,只好自己封装了)。
通过oldacc.dll得到的IAccessible返回一个UI的引用,如果这个元素支持IAccessible接口,意味着这个元素是由程序来完成接口的实现,一般的标准win32控件都属于这类,也就是标准win32控件原生支持msaa的访问。就如同标准的win32控件和wpf控件支持UIA一个道理,如果不支持微软就是打自己的嘴了。
如果一个元素并不支持IAccessible接口,一些类名如user和COMCTL32的元素,oldacc.dll会自动创建代理为其实现IAccessible接口,大多数情况下可以得到它的引用。一些其他的UI元素比如directUI和其他自定义UI元素,如要得到IAccessible的支持,则需要完成相关代理的开发。鱼与熊掌不可兼得,寻求速度和美感的同时,必须要有一些牺牲,UIA中同样需要自己去完成provider来支持对UIAutomation的访问。Chrome开源代码里有一套在chrome浏览器中实现IAccessible的现成示例,以后有时间会专门讨论chrome浏览器源代码中的自动化测试的相关代码。一般实现需要3个步骤:继承IAccessible_base,注册WM_GETOBJECT的OnGetObject事件,完成OnGetObject事件使外部代理可以获取IAccessible实例;完成成员各自的IAccessible接口和子节点的派发。
整体来说,思路是这样的:
- 封装一个UIElement或者UIObject的元素类,通过初始化获取其IAccessible接口;
- 获取元素相关信息,如name,role,location,description等等;
- 获取其子元素个数和子元素的IAccessible接口;
- 提供遍历子元素方法;
- 提供子元素查找方法;
现在我们分别完成这些步骤。
获取IAccessible接口的获取:
MSAA提供了三个得到IAccessible接口的方法:AccessibleObjectFromWindow,AccessibleObjectFromPoint和AccessibleObjectFromEvent,这3个函数中,基本上常用的以第一个为主,我们使用这个对UIElement进行初始化。剩下的两个参照文档和示例可以自己去完成,下面这个链接包含大部分用户方法:http://msdn.microsoft.com/en-us/library/windows/desktop/dd742692(v=vs.85).aspx。
确定了我们要使用AccessibleObjectFromWindow,现在来研究一下它。我们要研究的东西其实很简单,就是from window的这个window是哪个window。事实上,IAccessible支持的元素,比窗口要多——也就是说,搜索同样的窗口和搜索这个窗口上的IAccessible,前者的效率会高一些。所以我们会先使用win32函数找到窗口句柄,然后从句柄中获取IAccessible的接口。
一些必要的win32和com模块需要导入:
import comtypes, comtypes.automation, comtypes.client import ctypes, ctypes.wintypes
然后是类的初始化函数:
class Element(object): def __init__(self,objHandle,iObjectId=0): def _get_hwnd(objHandle): if objHandle in (0,None):#无窗口的情况,使用根窗口 hwnd=ctypes.windll.user32.GetDesktopWindow() return hwnd elif isinstance(objHandle,basestring):#传入窗口名字或类名 objHandle=unicode(objHandle) hwnd = ctypes.windll.user32.FindWindowW(objHandle, None) or ctypes.windll.user32.FindWindowW(None, objHandle) if hwnd>0: return hwnd elif isinstance(objHandle,(int,long)):#直接传入hwnd if objHandle>0: return objHandle else: pass hwnd=_get_hwnd(objHandle) if hwnd and hwnd>0: IAccessible = ctypes.POINTER(comtypes.gen.Accessibility.IAccessible)() ctypes.oledll.oleacc.AccessibleObjectFromWindow(hwnd, 0, ctypes.byref(comtypes.gen.Accessibility.IAccessible._iid_), ctypes.byref(IAccessible)) self.IAccessible = IAccessible self.iObjectId = iObjectId#默认objectid,后面有用处子节点
截图显示,我们已经获取到了IAccessible的接口,接下来,我们获取元素的各部分信息,我们并没必要实现所有信息的获取(如果有必要方法相似),必要的信息包括,元素名称,元素类型(msaa中的acc_role),元素位置,元素值,元素描述。
这些元素当中,最重要的应该是类型——acc_role。因为在后面的查询中,用到最多的就是这个类型了。Msaa中支持的类型有63个,内部由1~63代替,可以做成一个字典以便后面映射方便。这些类型的值包括:
roles=[ u'TitleBar', u'MenuBar', u'ScrollBar', u'Grip', u'Sound', u'Cursor', u'Caret', u'Alert', u'Window', u'Client', u'PopupMenu', u'MenuItem', u'Tooltip', u'Application', u'Document', u'Pane', u'Chart', u'Dialog', u'Border', u'Grouping', u'Separator', u'ToolBar', u'StatusBar', u'Table', u'ColumnHeader', u'RowHeader', u'Column', u'Row', u'Cell', u'Link', u'HelpBalloon', u'Character', u'List', u'ListItem', u'Outline', u'OutlineItem', u'PageTab', u'PropertyPage', u'Indicator', u'Graphic', u'Text', u'EditableText', u'PushButton', u'CheckBox', u'RadioButton', u'ComboBox', u'DropDown', u'ProgressBar', u'Dial', u'HotKeyField', u'Slider', u'SpinBox', u'Diagram', u'Animation', u'Equation', u'DropDownButton', u'MenuButton', u'GridDropDownButton', u'WhiteSpace', u'PageTabList', u'Clock', u'SplitButton', u'IPAddress', ]
下面是一个映射:
>>> RoleMapps=dict([(i+1,roles[i]) for i in range(63)]) >>> RoleMapps {1: u'TitleBar', 2: u'MenuBar', 3: u'ScrollBar', 4: u'Grip', 5: u'Sound', 6: u'Cursor', 7: u'Caret', 8: u'Alert', 9: u'Window', 10: u'Client', 11: u'PopupMenu', 12: u'MenuItem', 13: u'Tooltip', 14: u'Application', 15: u'Document', 16: u'Pane', 17: u'Chart', 18: u'Dialog', 19: u'Border', 20: u'Grouping', 21: u'Separator', 22: u'ToolBar', 23: u'StatusBar', 24: u'Table', 25: u'ColumnHeader', 26: u'RowHeader', 27: u'Column', 28: u'Row', 29: u'Cell', 30: u'Link', 31: u'HelpBalloon', 32: u'Character', 33: u'List', 34: u'ListItem', 35: u'Outline', 36: u'OutlineItem', 37: u'PageTab', 38: u'PropertyPage', 39: u'Indicator', 40: u'Graphic', 41: u'Text', 42: u'EditableText', 43: u'PushButton', 44: u'CheckBox', 45: u'RadioButton', 46: u'ComboBox', 47: u'DropDown', 48: u'ProgressBar', 49: u'Dial', 50: u'HotKeyField', 51: u'Slider', 52: u'SpinBox', 53: u'Diagram', 54: u'Animation', 55: u'Equation', 56: u'DropDownButton', 57: u'MenuButton', 58: u'GridDropDownButton', 59: u'WhiteSpace', 60: u'PageTabList', 61: u'Clock', 62: u'SplitButton', 63: u'IPAddress'}
获取和设置类型值,可以直接调用com方法:
def accValue(self, objValue=None): objChildId = comtypes.automation.VARIANT() objChildId.vt = comtypes.automation.VT_I4 objChildId.value = self.iObjectId objBSTRValue = comtypes.automation.BSTR() if objValue is None: self.IAccessible._IAccessible__com__get_accValue(objChildId, ctypes.byref(objBSTRValue)) return objBSTRValue.value else: objBSTRValue.value = objValue self.IAccessible._IAccessible__com__set_accValue(objChildId, objValue) return objBSTRValue.value
当然如果为了使用方便,可以再提供只读的属性返回映射中的值:
def accRoleName(self): try: iRole = self.accRole() return AccRoleNameMap.get(iRole) except: return None
同样的几个只读元素属性,位置,子元素个数,描述,父元素,状态,帮助等,以及几个读写元素名字,值,选中操作等,使用同样的方法完成(代码未测试):
def accLocation(self): objChildId = comtypes.automation.VARIANT() objChildId.vt = comtypes.automation.VT_I4 objChildId.value = self.iObjectId objL, objT, objW, objH = ctypes.c_long(), ctypes.c_long(), ctypes.c_long(), ctypes.c_long() self.IAccessible._IAccessible__com_accLocation(ctypes.byref(objL), ctypes.byref(objT), ctypes.byref(objW), ctypes.byref(objH), objChildId) return (objL.value, objT.value, objW.value, objH.value) def accChildCount(self): #之前提到的objectid,0代表本元素的IAccessible的接口 if self.iObjectId == 0: return self.IAccessible.accChildCount else: return 0 def accDescription(self): objChildId = comtypes.automation.VARIANT() objChildId.vt = comtypes.automation.VT_I4 objChildId.value = self.iObjectId objDescription = comtypes.automation.BSTR() self.IAccessible._IAccessible__com__get_accDescription(objChildId, ctypes.byref(objDescription)) return objDescription.value def accParent(self): return self.IAccessible.accParent() def accState(self): objChildId = comtypes.automation.VARIANT() objChildId.vt = comtypes.automation.VT_I4 objChildId.value = self.iObjectId objState = comtypes.automation.VARIANT() self.IAccessible._IAccessible__com__get_accState(objChildId, ctypes.byref(objState)) return objState.value def accHelpTopic(self): return self.IAccessible.accHelpTopic()
读写元素:
def accName(self, objValue=None): objChildId = comtypes.automation.VARIANT() objChildId.vt = comtypes.automation.VT_I4 objChildId.value = self.iObjectId if objValue is None: objName = comtypes.automation.BSTR() self.IAccessible._IAccessible__com__get_accName(objChildId, ctypes.byref(objName)) return objName.value else: self.IAccessible._IAccessible__com__set_accName(objChildId, objValue) def accValue(self, objValue=None): objChildId = comtypes.automation.VARIANT() objChildId.vt = comtypes.automation.VT_I4 objChildId.value = self.iObjectId objBSTRValue = comtypes.automation.BSTR() if objValue is None: self.IAccessible._IAccessible__com__get_accValue(objChildId, ctypes.byref(objBSTRValue)) return objBSTRValue.value else: objBSTRValue.value = objValue self.IAccessible._IAccessible__com__set_accValue(objChildId, objValue) return objBSTRValue.value def accSelect(self, iSelection): #iSelection代表select 的flag,1,2,4,8,16,常用的是1和2,代表聚焦和选中 if self.iObjectId: return self.IAccessible.accSelect(iSelection, self.iObjectId) else: return self.IAccessible.accSelect(iSelection)
大部分元素的读写就完成了,如果还需要其他的元素可以用相应的接口实现。下面具体讨论一下对子元素的操作,包括读取,遍历和查询。
一个元素的子元素的特点是:
- 和父元素是聚合关系;
- 和父元素类型一致;
所以我们需要解决的第一是如何组织这种聚合结构关系,第二是如何实例化子元素的Element。
对于第一个问题,一般来说面向对象的设计最容易想到就是直接将一个所以子元素一次性放到一个集合中,使用的时候在进行处理;另外就是可以采用发生和迭代的方法,将子元素动态的依附到父元素实例上,直接通过实例进行枚举。Python中推荐后一种,因为python的开发中架构上不是那么精确,一般都是一个大概样子,各种架构实现大部分也都是技巧性的(当然包括类的实现亦如此),但是相对前一种编程难一些,不过时间效率上会高。当然这个需要看情况,有些时候需要将子元素中资源存放,以便再次使用的时候方便——这种对聚合元素的调用决定了结构的设计,说开了就是一个缓存,但是缓存的度决定了哪种设计。0缓存就是第二种方法,最大缓存(子元素完全缓存)就是第一种方法,当然如果涉及到所有子元素的递归,那么很明显不适合这样的大缓存,递归的查找和匹配只需要将关键位缓存就可以了。类中尽可能少的去使用空间,至于需要多少就交给外部调用的人来决定。
对于第二个问题,就需要先讨论一下之前代码中提到的iObjectId。我们一开始的设计Element类的初始化方法中,使用了这个这个带默认值的参数,这个参数有它特定的含义,还要从IAccessible接口的获取谈起——这个内容是不可避免的,我了解到的很多国内想使用这个东西的人,都找不到相关的中文材料,英文的材料也是解释很多示例很少,虽然解释很清晰,但是大部分比较晦涩——跟Python的情况差不多,没有特别好的文档和示例,都是大家摸索着来。之前初始化的时候我们最终使用的数据其实是两个,一个是IAccessible,一个是iObjectId,这个也是接口要求的——事实上很多资料上写的比较混乱,但是一个元素的表示确实由这两个值决定的,决定的方法是这样的:
任何元素都可以使用接口+ID的方式表示,这种表示方法有两种途径:如果接口属于本元素,则ID为0;如果接口是父元素,则ID为元素在其父元素中的次序,即 父接口+序号或本接口+0。怎么去选择方法呢?当一个子元素不支持IAccessible的接口时,我们只有用父接口+序号的方法去表示,当子元素支持IAccessible的接口时,我们显然要选取一种,我个人的建议是这样的:如果只是单独的想获取这个子元素的属性信息,大可不必获取它的IAccessible接口,直接使用父接口+序号的方法,效率会很高;如果想递归去的获取所有支持IAccessible接口元素的属性,则需要设置好这种结构,使用本IAccessible+0的方式结构上会比较统一,当然在这里会判断一下,不支持IAccessible接口的子元素我们直接用父接口+序号的形式进行标示。
def __iter__(self): #为自身迭代设置临界值 if self.iObjectId > 0: raise StopIteration() #定义子元素数量和容器,像之前讨论过的一样,这部分也可以专门指定一个结合完成聚合结构,如果有必要的话 objAccChildArray = (comtypes.automation.VARIANT * self.IAccessible.accChildCount)() objAccChildCount = ctypes.c_long() #通过接口获取数量和元素集合 ctypes.oledll.oleacc.AccessibleChildren(self.IAccessible, 0, self.IAccessible.accChildCount, objAccChildArray, ctypes.byref(objAccChildCount)) #循环生成对应的Element实例并直接返回 for i in xrange(objAccChildCount.value): objAccChild = objAccChildArray[i] if objAccChild.vt == comtypes.automation.VT_DISPATCH: yield Element(objAccChild.value.QueryInterface(comtypes.gen.Accessibility.IAccessible), 0) #即comtypes.automation.VT_I4的情况,不支持IAccessible的接口 else: yield Element(self.IAccessible, i+1)
基本上,我们对于一个Element的封装,主要部分就差不多了,当然这里主要是给出一种思路和方法,没有太多的时间进行验证,一般自动化的底层设计验证起来比较麻烦,因为上层可能遇到各种各样的问题,这个只能按自己的项目需求进行判断了。一般来说,这样的基础类,还需要提供一些查询匹配子元素的方法,查询匹配的方式,就是提供父元素和子元素的属性条件,返回父元素下满足子元素过滤条件的所有匹配子元素。当然,由于我们上面的示例代码中,只是列出了各种属性值的获取方式,元素的方法等其他属性并没有实现,下面的匹配方法中,也只给出来对属性的过滤,当然并不完善,只是一个思路,部分异常没有处理。
首先定义一个匹配判断的方法:
def match(self, **kwargs): flag= True try: for strProperty in kwargs: try: attr = getattr(self, strProperty) except AttributeError: continue try: value = attr() except: value = None else: if type(value) != str: value = value.encode('gbk') if value.lower() != kwargs[strProperty].lower(): flag = False break except Exception, ex: flag = False return flag
有了判断的标准,就可以对子元素进行过滤了,如下:
def finditer(self, **kwargs): temp = list(self) while temp: objElement = temp.pop(0) if objElement.match(**kwargs): yield objElement if objElement.IAccessible.accChildCount > 0: temp.insert(0, list(objElement))
外部查询接口:
def findall(self, **kwargs): try: return list(self.finditer(**kwargs)) except: pass
好了,基本上一个automation element元素类就构造好了,也可以投入使用,但是里面还包含着一些问题,主要有:
- 效率问题:如果递归过滤一个元素中的所有自元素,效率相对较低,可以采取一些集合作为缓冲;当然如果外围使用按层次调用,也可以每次只过滤直接子元素;
- 异常问题:基本上只做了一半的异常处理,一些其他的异常处理没有去做;
- 方法成员没有完成,只完成了属性成员;
- 没有调试。。。
有兴趣的朋友们可以自己去完善和改善,python的世界就是这么一点一点的完美的。
IAccessible的接口常用的基本上也就这么多了,有时间可以再讨论一下UIAutomation的使用,让然UIA的部分相对容易一些,毕竟资料还是很容易搜到的。