几乎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 君。^_^