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) = ''; } 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_accDescr?ption 能取得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》,只是按自己的理解重新編排了一下,如果覺得不符合自己的學習習慣可以看原文。並且我的文章省略了很多東西,呵呵。 |