MSAA --> UIA
English version:
说穿了,MSAA就是IAccessible Interface
这个Interface本来是准备给盲人用的, 结果却在大多数情况下用作UI Automation的一种实现方法. 但是呢, 这个接口包含的方法又太少了, 非常的鸡肋, 只能够读名字,类型,位置. 对于一些特别的属性, 比如ClassName, Orientation什么的, 就没有暴露出来. 也不能支持UI操作, 比如选择, 移动窗口, 更没有提供Event Support
所以呢,微软想想,觉得是应该专门给UIAutomation提供一套独立的Interface了, 于是就给了两个:
IAccessibleEx
IRawElementProviderSimple
这两个Interface归类是归入Windows Automation API的, 而不是MSAA.
第一个Interface IAccessibleEx, 名字看起来跟MSAA的IAccessible类似, 他实际上只是一个让客户端获取IRawElementProviderSimple的入口接口而已。 实现Pattern和Properties的都在IRawElementProviderSimple接口里面。他的另外一个目的是给传统的MSAA Client提供接口兼容.
参考:
IAccessibleEx的默认实现在UIAutomationCore.DLL里面. 这里面实现了UIA <-> IAccessibleEx 的bridge
客户端使用Automation首先是获取IAccessible, 然后通过QueryService (不是QueryInterface,因为为了支持Vista的high DPI和MSHTML)获取IAccessibleEx, 然后再通过QueryInterface就可以获取IRawElementProviderSimple了。
中间的IAccessibleEx的作用是handshake, 虽然说它里面也暴露了不少属性,但是具体的实现都是在IRawElementProviderSimple和原来的IAccessible当中。 微软已经故意淡化IAccessibleEx interface了,主要用作内部实现UIA的桥梁, 在MSDN中已经找不到IAccessibleEx的Definition了
拿到IRawElementProviderSimple后,剩下的事情就简单了, 就是直接问这个Interface要对应的provider,然后从provider里面拿pattern就可以了。 详细的介绍可以参考:
这里需要说明的是,AutomationProvider, 既可以在UI Server中实现,也可以在Client中实现。在UI Server中实现的话, 就直接从IRawElementProviderSimple继承,然后用下面的方法回应客户端请求就可以了:
if ((m.Msg == WM_GETOBJECT) && (m.LParam.ToInt32() ==
AutomationInteropProvider.RootObjectId))
{
m.Result = AutomationInteropProvider.ReturnRawElementProvider(
this.Handle, m.WParam, m.LParam,
(IRawElementProviderSimple)this);
return;
}
但是如果都在Server端实现的话,对于传统的Win32 Control, 需要做的修改就太大了。 所以UIAutomation也支持在Client里面实现。凡是客户端的实现,就叫做Provider/Automation Proxy, 具体说来, 就是在ProviderOptions property里面返回ProviderOptions.ClientSideProvider
对于Win32和WinForm的默认实现,都是在client里面的。 具体的assembly是UIAutomationClientSideProvider.DLL。 里面的ProxyHwnd.GetElementProperty实现非常具体代表性:
如果要获取AutomationID, 实现方法是发送WM_GETCONTROLNAME message
如果要获取Name, 逻辑是:
如果是WinForm Control, 就读MSAA里面的Name
否则, 首先尝试调用GetWindowLong和GetWindowText API
如果返回空, 再尝试PostMessage
看到这里,我不小心找到了一个2004年的Automation paper:
这位叫做Brian McMaster的兄弟,现在是Test Architect, 当年在没有Automation只有MSAA的时候, 就教育大家用WM_GETCONTROLNAME message, 区分control tree结构,提防window owner和parent不一致的情况等等。
有了这些了解后,动手来玩UIA就很简单了。WinForm Server side需要做的事情是:
1. override WndProc, 用AutomationInteropProvider.ReturnRawElementProvider方法来回应WM_GETOBJECT消息
2. 实现IRawElementProviderSimple, 并且在接口方法中返回ProviderOptions.ServerSideProvider
3. 实现具体的provider, 比如ISelectionProvider
这些都可以在我上面的link中找到例子代码的。
而UIAutomation的客户端呢, 现在有两组API, managed/unmanaged
managed API在System.Windows.Automation里面。 具体的class是AutomationElement
unmanaged API是一组以UIA开头的API。
目前managed API的内部实现借用了unmanaged API。 而unmanaged API里面用到的client side provider, 其实是在managed的class中实现的。 所以两者关系目前比较复杂。而且有的功能在unmanaged API里面没有问题,但是managed API里面有bug。 在Win8的时候, 微软打算把两者的实现都合并起来, 减小维护成本。
几个有趣的地方是:
如果一个UI Server同时拥有ServerProvider和ClientProvider,那么ServerProvider会覆盖掉同类型的ClientProvider。如果有的Provider只在ClientSide才实现, 那么ClientProvidier会正确返回的。 不用担心实现了ServerProvider导致ClientProvider不工作的情况
[added on 9/24/2009]
Win32和WinForm的ClientProvider有系统的默认实现. ClientProvider的注册是在client side执行的, Server side不需要管. 具体的请参考:
如果要研究系统提供的Client Provider注册过程, 可以用windbg放这样的断点:
sxe ld UIAutomationClientsideProviders*
[/added]
如果使用UIAutomation API, 会发现目前UI Automation里面的Property和Pattern是不能自己随意扩展的。比如你想自己定义一个PatternID和Interface注册进去是不行的。 因为UI Automation返回给你的interface其实是做过手脚的。 MSDN中甚至都说了, 你不能直接使用interface里面的方法:
"The interface returned by this method can only be passed back to UI Automation. Attempting to call a method on the interface will raise an exception"
所以,如果你真的要做,就老实地按照前面介绍的步骤,用COM的标准方法, 先拿MSAA, 然后再自己QueryInterface。 只要能作QI, 什么都解决了。
最后要提的是,前面的介绍都是针对Win32/WinForm的。 对于WPF, 它通过自己的AutomationPeer来实现UIAutomation的Server. 根UIAutomation Client的通信方式也是通过Named Pipe而不是DCOM/Message了。 不过传统的MSAA Client还是可以通过MSAA访问WPF的, 原因在于WPF里面提供了UIAMSAA Bridge, WPF会自己创建Inproc IAccisible Proxy来满足MSAA Client的请求。
最后再贴几个链接:
这两个doc是微软写的, 结果MSDN上却没有连接。 里面详细介绍了UIA的design goal, IAccessibleEx和IRawElementProviderSimple的作用和关系, 基本上可以算一个design spec了。 这两个doc解决了我看代码一年都没看明白的问题。 里面把MSAA, UIA, Provider, Pattern, Proxy, Bridge, 还有依赖调用关系解释得淋漓尽致!