MS Active Accessibility 接口技术编程尝试
Microsoft© Active Accessibility 2.0 is a COM-based technology that improves the
way accessibility aids work with applications running on Microsoft Windows?. It
provides dynamic-link libraries that are incorporated into the operating system
as well as a COM interface and application programming elements that provide
reliable methods for exposing information about user interface elements.
基础
Microsoft© Active Accessibility 是一种相对较新的技术(1.0版在1997年5月份推出)。目的是方便身患残疾的人士使用电脑――可用于放大器、屏幕阅读器,以及触觉型鼠标。同样还可以用来开发驱动其它软件的应用程序,其模拟用户输入的能力尤其适合测试软件的开发。
Active Accessibility 的主要思想是提供一种以程序方式访问UI元素信息或操作这些UI元素的功能。支持这种功能的 UI(User Interface) 元素是可访问的。在大多数情况下,这意味着一个UI元素支持 IAccessible 接口。你也可以说在 Active Accessibility 的世界里,一个可访问的UI元素可表示为 IAccessible 接口。
每当你需要得到有关一个元素的信息,在其上执行一个动作,或者使用 Active Accessibility 做其它的什么,你通常需要通过使用代表这个元素的 IAccessible 接口的一种方法或者属性来引用这个元素。
Active Accessibility 原理
Active Accessibility? 的核心功能由 OLEACC.DLL 提供的。每次当你调用一个函数来返回一个 IAccessible 接口指针,其与一个UI元素相对应,OLEACC.DLL就检查此元素是否内在支持 IAccessible。内在的支持意思是该元素的 IAccessible 是用程序实现的。
当一个UI元素不能内在的支持 IAccessible 时,OLEACC.DLL 检查该元素的Windows 类名。如果该类是一个 USER 或者 COMCTL32 支持的类,OLEACC.DLL 就创建一个代理为 UI 元素实现 IAccessible 接口。大多数--但不是全部--COMCTL32 控件都具有被 OLEACC.DLL 支持的 IAccessible 接口。
内在支持 IAccessible 的 UI 元素的例子是定制控件,owner-drawn 和无窗口的控件。因为开发者创建的程序包含这些UI元素,同样就实现了这些元素的接口,他们有责任为这些方法和属性提供正确的支持。
如果你用标准控件,这也意味着你不必重写你的应用,这些应用自动与Active Accessibility兼容。
Active Accessibility 名字是基于 Win32 控件的名字给出的,角色基于控件的功能定义。
如何得到 IAccessible 接口指针
每当你需要有关一个元素的信息,在其上执行一个动作,或者使用 Active Accessibility 做其它的什么,你只需要通过使用代表这个元素的 IAccessible 接口的一种方法或者属性来引用这个元素。
有几种方法取得代表一个可访问 UI 元素的 IAccessible 接口的指针。最普通的方法是使用 Active Accessibility 提供的一种函数,例如 AccessibleObjectFromPoint,AccessibleObjectFromWindow 等等,或者使用 IAccessible 支持的方法,例如 get_accChild,get_accParent。
IAccessible 接口支持允许你得到各 UI 元素信息的属性,而其中对于例子程序最重要的属性是名字、角色和状态。
Active Accessibility SDK提供了一些方便的工具,其中的 Object Inspector 能显示光标指向的UI元素的属性。Object Inspector 显示了Active Accessibility 的世界如何因为具有支持一个选定窗口内的 IAccessible 接口的控制而变得通用了。除了搜索有关元素的信息和通过 IAccessible 接口控制元素以外,Active Accessibility? 还有两种对于例子程序非常有用的特性:监视UI元素发生的事件和模拟键盘、鼠标输入。由可访问的元素激发的事件称为 WinEvents,当可访问的元素创建或者名字、状态、位置或者键盘焦点发生变化时,就激发这些事件(事件机制类似于标准的 Windows 的 hook 机制。监视事件我们将在后面介绍。)。这些事件的清单见文件 WINABLE.H。每个事件的名字以 EVENT_OBJECT 或 EVENT_SYSTEM 开始。
好,我们言归正传,来介绍如何得到 IAccessible 接口指针。前面已经提到过 AccessibleObjectFromWindow 这个 Active Accessibility 提供的函数,从字面上大家可以看出是通过窗口来得到对应的 IAccessible 接口指针。
因为 IAccessible 接口的数量比窗口要多(因为大多数--但不是全部--COMCTL32 控件都有被 OLEACC.DLL 支持的 IAccessible 接口。),使用 Win32 函数来搜索一个窗口将会比使用 Active Accessibility 树搜索与该窗口相应的 IAccessible 接口要占用少得多的时间。这就意味着为了提高性能,你应该使用 FindWindow 和 EnumWindows 这样的 Win32 函数来找到与希望的UI元素最接近的窗口。当然,在权衡 Win32 函数和 Active Accessibility 函数时,上面的规则只是使用它们的一般标准而不能盲目的遵照执行,重要的是理解它们的本来意义。
下面结合代码介绍一下它的用法。
我们来得到下面运行窗口的 IAccessible 接口指针。
图一
01.
HWND
hWndMainWindow;
02.
IAccessible *paccMainWindow = NULL;
03.
HRESULT
hr;
04.
//得到标题为"运行"的窗口的句柄
05.
if
(NULL == (hWndMainWindow = FindWindow(NULL,
"运行"
)))
06.
{
07.
MessageBox(NULL,
"没有发现窗口!"
,
"错误"
, MB_OK);
08.
}
09.
else
10.
{
11.
//通过窗口句柄得到窗口的 IAccessible 接口指针。
12.
if
(S_OK == (hr = AccessibleObjectFromWindow(
13.
hWndMainWindow,
14.
OBJID_WINDOW,
15.
IID_IAccessible,
16.
(
void
**)&
17.
paccMainWindow)))
18.
{
19.
//……我们可以通过这个指针paccMainWindow进行操作。
20.
paccMainWindow->Release();
21.
}
22.
}
现在我们已经得到窗口的 IAccessible 接口指针了(paccMainWindow),那么,我们可以干什么呢?我们怎么得到窗口中某个控件的 IAccessible 接口指针呢?我们就以上面的运行窗口为例。看看如何得到文本框的 IAccessible 接口指针!!
首先我们启动 inspect32.exe,什么?你不知道这是什么东西?赶紧先下载个Active Accessibility SDK看看吧……
然后,把鼠标放到所关注的控件上(即上图中的文本输入框),你会得到如下信息:
图二
我们现在主要关注的信息是:Name、Role、Window className。
1.
Name =
"打开(O):"
2.
Role =
"可编辑文字"
3.
Window className =
"Edit"
当开发自定义、owner drawn 或者无窗口的控件时,为同一窗口的每个"角色-名字"指定独一无二的表示是一个非常好的编程习惯。然而,如果由于某种原因,同一窗口中的2个 UI 元素具有同样的"角色-名字"对,那么就需要增加一个参数--windows 类--以唯一的来表示这个元素。
FindChild 函数显示了一个基于 Active Accessibility 父/子(你可以理解成父窗口/子窗口的关系,只是为了便于理解:-P)导航的搜索例程的实现。这个函数有6个参数。前4个包含传递给函数的信息,后2个包含了 IAccessible 接口/子ID对(见附录)。
下面我们开始取文本输入框的 IAccessible 接口指针。
01.
IAccessible* paccControl = NULL;
//输入框的 IAccessible 接口
02.
VARIANT varControl;
//子ID。
03.
04.
FindChild( paccMainWindow,
05.
"打开(O):"
,
06.
"可编辑文字"
,
07.
"Edit"
,
08.
&paccControl,
09.
&varControl )
第一个参数是先前得到的窗口 IAccessible 接口指针。
第二、三、四个参数分别是名字、角色、类。
后2个为返回参数包含了 IAccessible 接口/子ID对。下面是FindChild的实现。
001.
BOOL
FindChild (IAccessible* paccParent,
002.
LPSTR
szName,
LPSTR
szRole,
003.
LPSTR
szClass,
004.
IAccessible** paccChild,
005.
VARIANT* pvarChild)
006.
{
007.
HRESULT
hr;
008.
long
numChildren;
009.
unsigned
long
numFetched;
010.
VARIANT varChild;
011.
int
index;
012.
IAccessible* pCAcc = NULL;
013.
IEnumVARIANT* pEnum = NULL;
014.
IDispatch* pDisp = NULL;
015.
BOOL
found =
false
;
016.
char
szObjName[256], szObjRole[256], szObjClass[256], szObjState[256];
017.
018.
//得到父亲支持的IEnumVARIANT接口
019.
hr = paccParent -> QueryInterface(IID_IEnumVARIANT, (
PVOID
*) & pEnum);
020.
021.
if
(pEnum)
022.
pEnum -> Reset();
023.
024.
//取得父亲拥有的可访问的子的数目
025.
paccParent -> get_accChildCount(&numChildren);
026.
027.
//搜索并比较每一个子ID,找到名字、角色、类与输入相一致的。
028.
for
(index = 1; index <= numChildren && !found; index++)
029.
{
030.
pCAcc = NULL;
031.
// 如果支持IEnumVARIANT接口,得到下一个子ID
032.
//以及其对应的 IDispatch 接口
033.
if
(pEnum)
034.
hr = pEnum -> Next(1, &varChild, &numFetched);
035.
else
036.
{
037.
//如果一个父亲不支持IEnumVARIANT接口,子ID就是它的序号
038.
varChild.vt = VT_I4;
039.
varChild.lVal = index;
040.
}
041.
042.
// 找到此子ID对应的 IDispatch 接口
043.
if
(varChild.vt == VT_I4)
044.
{
045.
//通过子ID序号得到对应的 IDispatch 接口
046.
pDisp = NULL;
047.
hr = paccParent -> get_accChild(varChild, &pDisp);
048.
}
049.
else
050.
//如果父支持IEnumVARIANT接口可以直接得到子IDispatch 接口
051.
pDisp = varChild.pdispVal;
052.
053.
// 通过 IDispatch 接口得到子的 IAccessible 接口 pCAcc
054.
if
(pDisp)
055.
{
056.
hr = pDisp->QueryInterface(IID_IAccessible, (
void
**)&pCAcc);
057.
hr = pDisp->Release();
058.
}
059.
060.
// Get information about the child
061.
if
(pCAcc)
062.
{
063.
//如果子支持IAccessible 接口,那么子ID就是CHILDID_SELF
064.
VariantInit(&varChild);
065.
varChild.vt = VT_I4;
066.
varChild.lVal = CHILDID_SELF;
067.
068.
*paccChild = pCAcc;
069.
}
070.
else
071.
//如果子不支持IAccessible 接口
072.
*paccChild = paccParent;
073.
074.
//跳过了有不可访问状态的元素
075.
GetObjectState(*paccChild,
076.
&varChild,
077.
szObjState,
078.
sizeof
(szObjState));
079.
if
(NULL !=
strstr
(szObjState,
"unavailable"
))
080.
{
081.
if
(pCAcc)
082.
pCAcc->Release();
083.
continue
;
084.
}
085.
//通过get_accName得到Name
086.
GetObjectName(*paccChild, &varChild, szObjName,
sizeof
(szObjName));
087.
//通过get_accRole得到Role
088.
GetObjectRole(*paccChild, &varChild, szObjRole,
sizeof
(szObjRole));
089.
//通过WindowFromAccessibleObject和GetClassName得到Class
090.
GetObjectClass(*paccChild, szObjClass,
sizeof
(szObjClass));
091.
//以上实现代码比较简单,大家自己看代码吧。
092.
093.
//如果这些参数与输入相符或输入为NULL
094.
if
((!szName ||
095.
!
strcmp
(szName, szObjName)) &&
096.
(!szRole ||
097.
!
strcmp
(szRole, szObjRole)) &&
098.
(!szClass ||
099.
!
strcmp
(szClass, szObjClass)))
100.
{
101.
found =
true
;
102.
*pvarChild = varChild;
103.
break
;
104.
}
105.
if
(!found && pCAcc)
106.
{
107.
// 以这次得到的子接口为父递归调用
108.
found = FindChild(pCAcc,
109.
szName,
110.
szRole,
111.
szClass,
112.
paccChild,
113.
pvarChild);
114.
if
(*paccChild != pCAcc)
115.
pCAcc->Release();
116.
}
117.
}
//End for
118.
119.
// Clean up
120.
if
(pEnum)
121.
pEnum -> Release();
122.
123.
return
found;
124.
}
125.
126.
// UI元素的状态也表示成整型形式。因为一个状态可以有多个值,
127.
//例如可选的、可做焦点的,该整数是反映这些值的位的或操作结果。
128.
//将这些或数转换成相应的用逗号分割的状态字符串。
129.
UINT
GetObjectState(IAccessible* pacc,
130.
VARIANT* pvarChild,
131.
LPTSTR
lpszState,
132.
UINT
cchState)
133.
{
134.
HRESULT
hr;
135.
VARIANT varRetVal;
136.
137.
*lpszState = 0;
138.
139.
VariantInit(&varRetVal);
140.
141.
hr = pacc->get_accState(*pvarChild, &varRetVal);
142.
143.
if
(!SUCCEEDED(hr))
144.
return
(0);
145.
146.
DWORD
dwStateBit;
147.
int
cChars = 0;
148.
if
(varRetVal.vt == VT_I4)
149.
{
150.
// 根据返回的状态值生成以逗号连接的字符串。
151.
for
(dwStateBit = STATE_SYSTEM_UNAVAILABLE;
152.
dwStateBit < STATE_SYSTEM_ALERT_HIGH;
153.
dwStateBit <<= 1)
154.
{
155.
if
(varRetVal.lVal & dwStateBit)
156.
{
157.
cChars += GetStateText(dwStateBit,
158.
lpszState + cChars,
159.
cchState - cChars);
160.
*(lpszState + cChars++) =
''
,
''
;
161.
}
162.
}
163.
if
(cChars > 1)
164.
*(lpszState + cChars - 1) =
''
\0
''
;
165.
}
166.
else
if
(varRetVal.vt == VT_BSTR)
167.
{
168.
WideCharToMultiByte(CP_ACP,
169.
0,
170.
varRetVal.bstrVal,
171.
-1,
172.
lpszState,
173.
cchState,
174.
NULL,
175.
NULL);
176.
}
177.
178.
VariantClear(&varRetVal);
179.
180.
return
(lstrlen(lpszState));
181.
}
好了!!我们已经成功得到文本框的 IAccessible 接口指针了!!现在你可以用这个接口指针为所欲为了!!!呵呵:)
在 IAccessible 接口上执行动作
有了表示一个可访问的 UI 元素的 IAccessible 接口/子ID对,你也有了搜索该元素一个名字(get_accName)、角色(get_accRole)、类和状态(get_accState)的方法。让我们看看你还可以干什么!get_accDescription 能取得UI元素的描述,get_accValue 能取得一个值。
最重要的函数之一是 accDoDefaultAction。每个可访问的UI元素都有一个缺省定义的动作。例如,一个按钮的缺省动作是"按下这个按钮",一个检查框的缺省动作是"不选"。为了确定一个元素的缺省动作,请参考 Active Accessibility 文档或者调用 get_accDefaultAction。
如果我想起动注册表编辑器,该怎么办呢?如果是我们手动做的话,无非是在文本输入框输入"regedit",然后按确定按钮,就这么简单。下面我们来看看用 Active Accessibility 是怎么来实现的。
01.
//在文本输入框输入"regedit"
02.
if
(1 == FindChild (paccMainWindow,
"打开(O):"
,
03.
"可编辑文字"
,
04.
"Edit"
,
05.
&paccControl,
06.
&varControl))
07.
{
08.
//在这里修改文本编辑框的值
09.
hr = paccControl->put_accValue(varControl,
10.
CComBSTR(
"regedit"
));
11.
paccControl->Release();
12.
VariantClear(&varControl);
13.
}
14.
15.
// 找到确定按钮,并执行默认动作。
16.
if
(1 == FindChild (paccMainWindow,
17.
"确定"
,
18.
"按下按钮"
,
19.
"Button"
,
20.
&paccControl,
21.
&varControl))
22.
{
23.
//这里执行按钮的默认动作,即"按下这个按钮"
24.
hr = paccControl->accDoDefaultAction(varControl);
25.
paccControl->Release();
26.
VariantClear(&varControl);
27.
}
现在,你会发现已经成功启动了注册表编辑器!!
模拟键盘和鼠标输入
让我们假设你需要操作一个新的不完全支持 Windows 消息和 IAccessible 接口方法的 UI 元素。如果它不支持你需要的消息和方法,最简单的解决办法就是模拟键盘和鼠标输入。例如,你可以用Tab模拟转移到期望的控件。
使你能够实现这些的函数就是 SendInput 一个一般的USER API。虽然不属于Active Accessibility,把他们联合使用很自然。
SendInput 接受三个参数:要执行的鼠标键盘动作个数、INPUT结构数组和结构数组的大小。每个INPUT结构描述一个要执行的动作。注意,按下一个按钮和释放一个按钮是两个不同的动作,所以必须创建两个不同的INPUT结构。
下面的代码将模拟 ALT+F4 按键来关闭窗口。
01.
INPUT input[4];
02.
memset
(input, 0,
sizeof
(input));
03.
04.
//设置模拟键盘输入
05.
input[0].type = input[1].type = input[2].type = input[3].type = INPUT_KEYBOARD;
06.
input[0].ki.wVk = input[2].ki.wVk = VK_MENU;
07.
input[1].ki.wVk = input[3].ki.wVk = VK_F4;
08.
09.
// 释放按键,这非常重要
10.
input[2].ki.dwFlags = input[3].ki.dwFlags = KEYEVENTF_KEYUP;
11.
12.
SendInput(4, input,
sizeof
(INPUT));
具体用法大家还是查MSDN吧,这里就不罗嗦了!!:)
监视WinEvents
监视 WinEvents 非常像通过 Windows Hook 监视 Windows 消息。最重要的区别就是从另一个进程监视 UI 元素发出的 WinEvents 时,你不需要创建一个单独的DLL来注入那个进程的地址空间。
监视 WinEvents 有两种选择:通过设置 SetWinEventHook 函数的最后一个参数来确定是在上下文之外还是之内监视。如果是在上下文之外,不需要额外的DLL,回调函数运行在目标进程之外。如果是在上下文之内,回调函数必须放在额外的DLL,并注入目标进程的地址空间。第二种方法写代码比较麻烦,但是运行效率高。
好,现在回到上面的例子。上面例子能够执行的前提条件是能够找到标题为"运行"的窗口。现在可以先检查运行窗口是否存在,如果不存在就设置WinEvents 钩子去监视,直到"运行"窗口被创建。看下面代码:
01.
if
(NULL == (hWndMainWindow = FindWindow(NULL, szMainTitle)))
02.
{
03.
hEventHook = SetWinEventHook(
04.
EVENT_MIN,
// eventMin ID
05.
EVENT_MAX,
// eventMax ID
06.
NULL,
// always NULL for outprocess hook
07.
WinCreateNotifyProc,
// call back function
08.
0,
// idProcess
09.
0,
// idThread
10.
// always the same for outproc hook
11.
WINEVENT_SKIPOWNPROCESS | WINEVENT_OUTOFCONTEXT);
12.
}
第一、二个参数用来指定监视事件的范围。第四个参数是定义的回调函数。
下面是回调函数:
01.
void
CALLBACK WinCreateNotifyProc(
02.
HWINEVENTHOOK hEvent,
03.
DWORD
event,
04.
HWND
hwndMsg,
05.
LONG
idObject,
06.
LONG
idChild,
07.
DWORD
idThread,
08.
DWORD
dwmsEventTime
09.
)
10.
{
11.
12.
if
( event != EVENT_OBJECT_CREATE)
13.
return
;
14.
15.
char
bufferName[256];
16.
IAccessible *pacc=NULL;
17.
VARIANT varChild;
18.
VariantInit(&varChild);
19.
//得到触发事件的 UI 元素的 IAccessible 接口/子ID对
20.
HRESULT
hr= AccessibleObjectFromEvent(hwndMsg,
21.
idObject,
22.
idChild,
23.
&pacc,
24.
&varChild);
25.
26.
if
(!SUCCEEDED(hr))
27.
{
28.
VariantClear(&varChild);
29.
return
;
30.
}
31.
//得到 UI 元素的Name,并比较,如果是"运行"就发送消息给主线程。
32.
GetObjectName(pacc, &varChild, bufferName,
sizeof
(bufferName));
33.
if
(
strstr
(bufferName, szMainTitle))
34.
PostThreadMessage(GetCurrentThreadId(),
35.
WM_TARGET_WINDOW_FOUND,
36.
0,
37.
0);
38.
39.
return
;
40.
}
恩…………,一个应用基本成型了,虽然比较简单。就先写这么多吧,请关注后续介绍。
附录:
关于IAccessible 接口/子ID对:
让我们来考虑这样一个控件,他支持 IAccessible 接口并且包含一些子控件,比如 listbox 就包含很多 items 。有两种方法让他可以被访问:第一种,提供listbox的 IAccessible 接口和每一个 item 自己的 IAccessible 接口。另一种是只提供一个控件的 IAccessible 接口,这个接口能够提供基于某种识别方法来访问每一个子控件的功能。
第一种方法,需要为这个控件和每一个子控件创建单独的 COM 对象,这会比第二种方法(每一个子控件不支持自己的 IAccessible 接口,而是通过父接口来访问)增加内存消耗。第二种方法里,通过增加一个参数--子ID--同父的IAccessible 接口一起表示这个子控件。子ID 是一个 VT_I4 型的 VARIANT 值,包含一个由程序决定的独特的值,或只是一个子控件的序号。序号意味着第一个子控件的ID为1,第二个子控件的ID为2,依次增长!
这样,如果一个子控件不支持自己的 IAccessible 接口,而其父控件支持,那么这个子控件可以用它的父控件的 IAccessible 接口/子ID 对来表示。通常,一个支持 IAccessible 接口的父UI元素也是通过这样的 IAccessible 接口/子对表示的,这时候其子ID号为 CHILDID_SELF (就是0)。
记住,子ID号总是相对于 IAccessible 接口的。例如,一个可访问的元素可以同相对于其父 IAccessible 接口的一个非子 CHILDID_SELF 的 ID 及其父IAccessible 接口表示,如果他支持 IAccessible 接口,此元素的子ID就是相对于自己 IAccessible 接口的CHILDID_SELF。
呵呵,翻译的有点别扭,意思就是说,如果这个控件支持 IAccessible 接口,那么它的子ID就是0(CHILDID_SELF),可以用它自己的 IAccessible 接口和0这个对来表示这个控件。如果控件不支持 IAccessible 接口,就用它父控件的 IAccessible 接口,和一个相对于父 IAccessible 接口的子ID来表示。哎呀!!不知道说明白没有。郁闷!!!!
注:
我也是刚开始学习怎么使用MSAA,但是苦于很难找到中文资料。希望这篇文章对大家能有所帮助。由于了解的还很肤浅,错误难免,望谅解!!:)
还有,这篇文章基本编译自Dmitri Klementiev的《Software Driving Software: Active Accessibility-Compliant Apps Give Programmers New Tools to Manipulate Software》,只是按自己的理解重新编排了一下,如果觉得不符合自己的学习习惯可以看原文。并且我的文章省略了很多东西,呵呵。
参考资料:
1、 Dmitri Klementiev写的《Software Driving Software: Active Accessibility-Compliant Apps Give Programmers New Tools to Manipulate Software》及其源程序。http://msdn.microsoft.com/msdnmag/issues/0400/aaccess/default.aspx
2、 MSDN中的相关章节。
正面猛男