1. 程式人生 > >模擬點擊Outlook命令欄中的某個按鈕

模擬點擊Outlook命令欄中的某個按鈕

Outlook

幾乎2年沒來這blog了,欣慰的是居然原來的數據還在。。。。
正好今天幫同事解決了一個有意思的小問題,就隨手寫一篇blog,記錄這個事情。

背景是這樣的:
需要通過一個和Outlook沒有關系的單獨exe,來執行Outlook中的某個命令按鈕,而不是靠手動點擊去執行。有這個需求的原因是,他希望在用戶按下按鈕後,起一個線程去工作,但不能block住Outlook導致用戶不能正常操作。當線程結束後,再自動觸發Outlook Addin中的某個按鈕命令做剩余的事情。給Office寫過Addin的都知道,從後臺線程中訪問Outlook對象模型是不被支持的(參見:http://msdn.microsoft.com/en-us/library/office/dd278301(v=office.12).aspx),所以,這就給實現需求帶來了困難。那麽如何實現呢?


難點:
Office界面上的按鈕不是一個普通的窗口,無法拿到句柄去發送WM_COMMAND消息,這就給模擬執行某個按鈕命令帶來了困難。

分析:
用Spy++看了Outlook 2003/2007/2010的界面(其中2003/2007相同,2010的Ribbon則又是另外一種情況),可以看到,在2003/2007中,工具欄的父窗口類名叫“MsoCommandBar”,2010則相對復雜,只能看到“NetUIHWND”這一層。
因為我們無法直接通過窗口拿到按鈕,所以只能另外想辦法——看到NetUIHWND,馬上聯想到DirectUI,繼而就非常感謝微軟提供了IAccessible這個方便的東西。可能有過IAccessible使用經驗的朋友看到這裏,馬上就豁然開朗,沒錯。後面寫的都是用這個東東來完成的。


解決思路:
先通過FindWindow一層層找到你能訪問到的“最後”一個窗口。對於Outlook 2003/2007來說,路徑應該是這樣的:

rctrl_renwnd32 -> MsoCommandBarDock(窗口名MsoDockTop) -> MsoCommandBar(窗口名是你的插件工具欄名稱)

而對於Outlook 2010來說則相對多一些:

rctrl_renwnd32 -> MsoCommandBarDock(窗口名MsoDockTop) -> MsoCommandBar(窗口名Ribbon) -> MsoWorkPane(窗口名Ribbon) -> NUIPane -> NetUIHWND


好了,這一層窗口句柄拿到了,接下來先獲取這一層窗口的IAccessible接口,示例代碼:

IAccessible* accTop = NULL; HRESULT hr = AccessibleObjectFromWindow(hwndTOP, NULL, IID_IAccessible, (LPVOID*)&accTop);
拿到其IAccessible後,還需要繼續遍歷其所有子元素,2003/2007再訪問兩層就能拿到具體的一個按鈕的IAccessible接口,而2010則要訪問至少6層。
往下講就沒什麽可說的了。我貼幾個關鍵的函數吧,請自行建一個Win32進行測試。
1 BOOL FindAccessible(IAccessible* accParent, IAccessible** accToFind, LPCTSTR lpctFindName)
2 {
3 VARIANT* vt_output = NULL;
4 BOOL bRet = FALSE;
5 if(accParent == NULL)
6 return FALSE;
7
8 long lChildCount = 0;
9 HRESULT hr = accParent->get_accChildCount(&lChildCount);
10 if(FAILED(hr) || (lChildCount == 0))
11 return FALSE;
12
13 vt_output = new VARIANT[lChildCount];
14 for(int i=0; i<lChildCount; i++)
15 VariantInit(&vt_output[i]);
16
17 long lNewChildCount = 0;
18 hr = AccessibleChildren(accParent, 0, lChildCount, vt_output, &lNewChildCount);
19 if(FAILED(hr))
20 goto exit;
21
22 for(int j=0; j<lNewChildCount; j++)
23 {
24 if(vt_output[j].vt == VT_DISPATCH)
25 {
26 IDispatch* disp = vt_output[j].pdispVal;
27 hr = disp->QueryInterface(IID_IAccessible, (void**)accToFind);
28 if(FAILED(hr))
29 continue;
30
31 VARIANT vChildID;
32 VariantInit(&vChildID);
33 vChildID.vt = VT_I4;
34 vChildID.lVal = CHILDID_SELF;
35
36 BSTR name;
37 hr = (*accToFind)->get_accName(vChildID, &name);
38 if(FAILED(hr))
39 {
40 SysFreeString(name);
41 continue;
42 }
43
44 if(name == NULL)
45 continue;
46
47 ODF(_T("get_accName=%s\n"), name);
48 if((lpctFindName != NULL) && _tcsicmp(name, lpctFindName) == 0)
49 {
50 //yes, we found!
51 //accToFind now hold the IAccessible pointer we need
52 bRet = TRUE;
53 SysFreeString(name);
54 break;
55 }
56 else
57 {
58 if( (lpctFindName == NULL)
59 && (_tcslen(name) == 0) )
60 {
61 //ok, may be find a NAMELESS object
62 bRet = TRUE;
63 SysFreeString(name);
64 break;
65 }
66 }
67 SysFreeString(name);
68 }
69 }
70 exit:
71 if(vt_output)
72 {
73 for(int k=0; k < lChildCount; k++)
74 VariantClear(&vt_output[k]);
75 delete vt_output;
76 }
77 return bRet;
78 }
上面函數,給出一個父節點的IAccessible和要匹配的子節點名稱,來獲取子節點的IAccessible接口。

下面貼一個調用代碼(for 2003/2007):

1 HWND hwndOutlookWnd = FindWindow(_T("rctrl_renwnd32"), NULL);
2 HWND hwndTopBarDock = FindChildWnd(hwndOutlookWnd, _T("MsoCommandBarDock"), _T("MsoDockTop"));
3 HWND hwndOneClickBar = FindChildWnd(hwndTopBarDock, _T("MsoCommandBar"), _T("你的工具欄名稱"));
4
5 IAccessible* accTop = NULL;
6 HRESULT hr = AccessibleObjectFromWindow(hwndOneClickBar, NULL, IID_IAccessible, (LPVOID*)&accTop);
7 if(FAILED(hr))
8 return FALSE;
9
10 IAccessible* accToFind = NULL;
11 if( FindAccessible(accTop, &accToFind, _T("工具欄名稱")) )
12 {
13 //we found the OneClick toolbar IAccessbile pointer
14 //now we need to find ’PUSH BUTTON’ IAccessible pointer
15 IAccessible* accBtn = NULL;
16 if( FindAccessible(accToFind, &accBtn, _T("PUSH BUTTON")) && (accBtn != NULL) )
17 {
18 //oh yes, we found the button
19 //now do its default action (push down)
20 VARIANT varID;
21 VariantInit(&varID);
22 varID.vt = VT_I4;
23 varID.lVal = CHILDID_SELF;
24 accBtn->accDoDefaultAction(varID);
25 VariantClear(&varID);
26
27 bRet = TRUE;
28 }
29 SAFE_RELEASE_COM_POINTER(accBtn);
30 }
31
32 SAFE_RELEASE_COM_POINTER(accTop);
33 SAFE_RELEASE_COM_POINTER(accToFind);
裏面用到的幾個輔助方法:

1 HWND FindChildWnd(HWND hParent, LPCTSTR lpctClassName, LPCTSTR lpctWndName = NULL)
2 {
3 HWND hChild = NULL;
4 int nCount = 0;
5 while(nCount < 30)
6 {
7 if( NULL != (hChild = ::FindWindowEx(hParent, NULL, lpctClassName, lpctWndName)) )
8 {
9 break;
10 }
11 nCount++;
12 }
13 return hChild;
14 }
1 #define SAFE_RELEASE_COM_POINTER(ptr) \
2 { \
3 if( (ptr) != NULL ) \
4 { \
5 ptr->Release(); \
6 (ptr) = NULL; \
7 } \
8 }

最後要說的是,謝謝你, IAccessible 君。^_^北京整形美容醫院http://www.bj-swjtu.com/

模擬點擊Outlook命令欄中的某個按鈕