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 接口指針。
圖一
HWND hWndMainWindow;
IAccessible *paccMainWindow = NULL;
HRESULT hr;
//得到標題為"運行"的窗口的句柄
if(NULL == (hWndMainWindow = FindWindow(NULL, "運行")))
{
MessageBox(NULL, "沒有發現窗口!", "錯誤", MB_OK);
}
else
{
//通過窗口句柄得到窗口的 IAccessible 接口指針。
if(S_OK == (hr = AccessibleObjectFromWindow(hWndMainWindow,
OBJID_WINDOW,
IID_IAccessible,
(void**)&paccMainWindow)))
{
//……我們可以通過這個指針paccMainWindow進行操作。
paccMainWindow->Release();
}
}
現在我們已經得到窗口的 IAccessible 接口指針了(paccMainWindow),那麼,我們可以干什麼呢?我們怎麼得到窗口中某個控件的 IAccessible 接口指針呢?我們就以上面的運行窗口為例。看看如何得到文本框的 IAccessible 接口指針!!
首先我們啟動 inspect32.exe,什麼?你不知道這是什麼東西?趕緊先下載個Active Accessibility SDK看看吧……
然後,把鼠標放到所關注的控件上(即上圖中的文本輸入框),你會得到如下信息:
圖二
我們現在主要關注的信息是:Name、Role、Window className。
Name = "打開(O):"
Role = "可編輯文字"
Window className = "Edit"
當開發自定義、owner drawn 或者無窗口的控件時,為同一窗口的每個"角色-名字"指定獨一無二的表示是一個非常好的編程習慣。然而,如果由於某種原因,同一窗口中的2個 UI 元素具有同樣的"角色-名字"對,那麼就需要增加一個參數--windows 類--以唯一的來表示這個元素。
FindChild 函數顯示了一個基於 Active Accessibility 父/子(你可以理解成父窗口/子窗口的關系,只是為了便於理解:-P)導航的搜索例程的實現。這個函數有6個參數。前4個包含傳遞給函數的信息,後2個包含了 IAccessible 接口/子ID對(見附錄)。
下面我們開始取文本輸入框的 IAccessible 接口指針。
IAccessible* paccControl = NULL;//輸入框的 IAccessible 接口
VARIANT varControl; //子ID。
FindChild( paccMainWindow,
"打開(O):",
"可編輯文字",
"Edit",
&paccControl,
&varControl )
第一個參數是先前得到的窗口 IAccessible 接口指針。
第二、三、四個參數分別是名字、角色、類。
後2個為返回參數包含了 IAccessible 接口/子ID對。下面是FindChild的實現。
BOOL FindChild (IAccessible* paccParent,
LPSTR szName, LPSTR szRole,
LPSTR szClass,
IAccessible** paccChild,
VARIANT* pvarChild)
{
HRESULT hr;
long numChildren;
unsigned long numFetched;
VARIANT varChild;
int index;
IAccessible* pCAcc = NULL;
IEnumVARIANT* pEnum = NULL;
IDispatch* pDisp = NULL;
BOOL found = false;
char szObjName[256], szObjRole[256], szObjClass[256], szObjState[256];
//得到父親支持的IEnumVARIANT接口
hr = paccParent -> QueryInterface(IID_IEnumVARIANT, (PVOID*) & pEnum);
if(pEnum)
pEnum -> Reset();
//取得父親擁有的可訪問的子的數目
paccParent -> get_accChildCount(&numChildren);
//搜索並比較每一個子ID,找到名字、角色、類與輸入相一致的。
for(index = 1; index <= numChildren && !found; index++)
{
pCAcc = NULL;
// 如果支持IEnumVARIANT接口,得到下一個子ID
//以及其對應的 IDispatch 接口
if (pEnum)
hr = pEnum -> Next(1, &varChild, &numFetched);
else
{
//如果一個父親不支持IEnumVARIANT接口,子ID就是它的序號
varChild.vt = VT_I4;
varChild.lVal = index;
}
// 找到此子ID對應的 IDispatch 接口
if (varChild.vt == VT_I4)
{
//通過子ID序號得到對應的 IDispatch 接口
pDisp = NULL;
hr = paccParent -> get_accChild(varChild, &pDisp);
}
else
//如果父支持IEnumVARIANT接口可以直接得到子IDispatch 接口
pDisp = varChild.pdispVal;
// 通過 IDispatch 接口得到子的 IAccessible 接口 pCAcc
if (pDisp)
{
hr = pDisp->QueryInterface(IID_IAccessible, (void**)&pCAcc);
hr = pDisp->Release();
}
// Get information about the child
if(pCAcc)
{
//如果子支持IAccessible 接口,那麼子ID就是CHILDID_SELF
VariantInit(&varChild);
varChild.vt = VT_I4;
varChild.lVal = CHILDID_SELF;
*paccChild = pCAcc;
}
else
//如果子不支持IAccessible 接口
*paccChild = paccParent;
//跳過了有不可訪問狀態的元素
GetObjectState(*paccChild,
&varChild,
szObjState,
sizeof(szObjState));
if(NULL != strstr(szObjState, "unavailable"))
{
if(pCAcc)
pCAcc->Release();
continue;
}
//通過get_accName得到Name
GetObjectName(*paccChild, &varChild, szObjName, sizeof(szObjName));
//通過get_accRole得到Role
GetObjectRole(*paccChild, &varChild, szObjRole, sizeof(szObjRole));
//通過WindowFromAccessibleObject和GetClassName得到Class
GetObjectClass(*paccChild, szObjClass, sizeof(szObjClass));
//以上實現代碼比較簡單,大家自己看代碼吧。
//如果這些參數與輸入相符或輸入為NULL
if ((!szName ||
!strcmp(szName, szObjName)) &&
(!szRole ||
!strcmp(szRole, szObjRole)) &&
(!szClass ||
!strcmp(szClass, szObjClass)))
{
found = true;
*pvarChild = varChild;
break;
}
if(!found && pCAcc)
{
// 以這次得到的子接口為父遞歸調用
found = FindChild(pCAcc,
szName,
szRole,
szClass,
paccChild,
pvarChild);
if(*paccChild != pCAcc)
pCAcc->Release();
}
}//End for
// Clean up
if(pEnum)
pEnum -> Release();
return found;
}
// UI元素的狀態也表示成整型形式。因為一個狀態可以有多個值,
//例如可選的、可做焦點的,該整數是反映這些值的位的或操作結果。
//將這些或數轉換成相應的用逗號分割的狀態字符串。
UINT GetObjectState(IAccessible* pacc,
VARIANT* pvarChild,
LPTSTR lpszState,
UINT cchState)
{
HRESULT hr;
VARIANT varRetVal;
*lpszState = 0;
VariantInit(&varRetVal);
hr = pacc->get_accState(*pvarChild, &varRetVal);
if (!SUCCEEDED(hr))
return(0);
DWORD dwStateBit;
int cChars = 0;
if (varRetVal.vt == VT_I4)
{
// 根據返回的狀態值生成以逗號連接的字符串。
for (dwStateBit = STATE_SYSTEM_UNAVAILABLE;
dwStateBit < STATE_SYSTEM_ALERT_HIGH;
dwStateBit <<= 1)
{
if (varRetVal.lVal & dwStateBit)
{
cChars += GetStateText(dwStateBit,
lpszState + cChars,
cchState - cChars);
*(lpszState + cChars++) = '','';
}
}
if(cChars > 1)
*(lpszState + cChars - 1) = ''\0'';
}
else if (varRetVal.vt == VT_BSTR)
{
WideCharToMultiByte(CP_ACP,
0,
varRetVal.bstrVal,
-1,
lpszState,
cchState,
NULL,
NULL);
}
VariantClear(&varRetVal);
return(lstrlen(lpszState));
}
好了!!我們已經成功得到文本框的 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 是怎麼來實現的。
//在文本輸入框輸入"regedit"
if(1 == FindChild (paccMainWindow, "打開(O):",
"可編輯文字",
"Edit",
&paccControl,
&varControl))
{
//在這裡修改文本編輯框的值
hr = paccControl->put_accValue(varControl,
CComBSTR("regedit"));
paccControl->Release();
VariantClear(&varControl);
}
// 找到確定按鈕,並執行默認動作。
if(1 == FindChild (paccMainWindow,
"確定",
"按下按鈕",
"Button",
&paccControl,
&varControl))
{
//這裡執行按鈕的默認動作,即"按下這個按鈕"
hr = paccControl->accDoDefaultAction(varControl);
paccControl->Release();
VariantClear(&varControl);
}
現在,你會發現已經成功啟動了注冊表編輯器!!
模擬鍵盤和鼠標輸入
讓我們假設你需要操作一個新的不完全支持 Windows 消息和 IAccessible 接口方法的 UI 元素。如果它不支持你需要的消息和方法,最簡單的解決辦法就是模擬鍵盤和鼠標輸入。例如,你可以用Tab模擬轉移到期望的控件。
使你能夠實現這些的函數就是 SendInput 一個一般的USER API。雖然不屬於Active Accessibility,把他們聯合使用很自然。
SendInput 接受三個參數:要執行的鼠標鍵盤動作個數、INPUT結構數組和結構數組的大小。每個INPUT結構描述一個要執行的動作。注意,按下一個按鈕和釋放一個按鈕是兩個不同的動作,所以必須創建兩個不同的INPUT結構。
下面的代碼將模擬 ALT+F4 按鍵來關閉窗口。
INPUT input[4];
memset(input, 0, sizeof(input));
//設置模擬鍵盤輸入
input[0].type = input[1].type = input[2].type = input[3].type = INPUT_KEYBOARD;
input[0].ki.wVk = input[2].ki.wVk = VK_MENU;
input[1].ki.wVk = input[3].ki.wVk = VK_F4;
// 釋放按鍵,這非常重要
input[2].ki.dwFlags = input[3].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(4, input, sizeof(INPUT));
具體用法大家還是查MSDN吧,這裡就不羅嗦了!!:)
監視WinEvents
監視 WinEvents 非常像通過 Windows Hook 監視 Windows 消息。最重要的區別就是從另一個進程監視 UI 元素發出的 WinEvents 時,你不需要創建一個單獨的DLL來注入那個進程的地址空間。
監視 WinEvents 有兩種選擇:通過設置 SetWinEventHook 函數的最後一個參數來確定是在上下文之外還是之內監視。如果是在上下文之外,不需要額外的DLL,回調函數運行在目標進程之外。如果是在上下文之內,回調函數必須放在額外的DLL,並注入目標進程的地址空間。第二種方法寫代碼比較麻煩,但是運行效率高。
好,現在回到上面的例子。上面例子能夠執行的前提條件是能夠找到標題為"運行"的窗口。現在可以先檢查運行窗口是否存在,如果不存在就設置WinEvents 鉤子去監視,直到"運行"窗口被創建。看下面代碼:
if(NULL == (hWndMainWindow = FindWindow(NULL, szMainTitle)))
{
hEventHook = SetWinEventHook(
EVENT_MIN, // eventMin ID
EVENT_MAX, // eventMax ID
NULL, // always NULL for outprocess hook
WinCreateNotifyProc, // call back function
0, // idProcess
0, // idThread
// always the same for outproc hook
WINEVENT_SKIPOWNPROCESS | WINEVENT_OUTOFCONTEXT);
}
第一、二個參數用來指定監視事件的范圍。第四個參數是定義的回調函數。
下面是回調函數:
void CALLBACK WinCreateNotifyProc(
HWINEVENTHOOK hEvent,
DWORD event,
HWND hwndMsg,
LONG idObject,
LONG idChild,
DWORD idThread,
DWORD dwmsEventTime
)
{
if( event != EVENT_OBJECT_CREATE)
return;
char bufferName[256];
IAccessible *pacc=NULL;
VARIANT varChild;
VariantInit(&varChild);
//得到觸發事件的 UI 元素的 IAccessible 接口/子ID對
HRESULT hr= AccessibleObjectFromEvent(hwndMsg,
idObject,
idChild,
&pacc,
&varChild);
if(!SUCCEEDED(hr))
{
VariantClear(&varChild);
return;
}
//得到 UI 元素的Name,並比較,如果是"運行"就發送消息給主線程。
GetObjectName(pacc, &varChild, bufferName, sizeof(bufferName));
if(strstr(bufferName, szMainTitle))
PostThreadMessage(GetCurrentThreadId(),
WM_TARGET_WINDOW_FOUND,
0,
0);
return;
}
恩…………,一個應用基本成型了,雖然比較簡單。就先寫這麼多吧,請關注後續介紹。
附錄:
關於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中的相關章節。
本文配套源碼