不是我变态,我也是被逼的。客户喜欢Word 2007里的Custom Task Pane,希望在侧边栏上放一些界面。但是他们刚刚从Word 97升级到2003,完全没有可能升级到更高的版本。我之前给他们做过一个DEMO,是用ActionsPane技术实现的。他们觉得挺好,就要这个。但是我不喜欢ActionsPane,所以只能自己想办法喽。为什么不喜欢ActionsPane?理由有很多:
1、绑定到Template,部署困难
2、排版有问题
3、稳定性不好,.NET控件是以ActiveX形式放置在上面的
很快,我就注意到Word 2003不是没有TaskPane的:
那么我可不可以把我的东西放上去呢?那我们来查查MSDN吧。确实还有这么一个Interface,就叫TaskPane:
http://msdn.microsoft.com/de-de/library/microsoft.office.interop.word.taskpane_members(VS.80).aspx
但是很遗憾,我们在上面找不到任何线索,可以把我们控件加上去的。但是我们可以用Application.TaskPanes[xxx].Visible来控制特定TaskPane的开关。也算有点收获啦。
然后我们又注意到,这个TaskPane怎么看起来这么像Command Bar?它还真的就是一个CommandBar。用Application.CommandBars["Task Pane"].Visible,我们也可以开关TaskPane(不过必须要在TaskPane已经打开过一次的前提下)。而且MsoControlType里有也有一条msoControlWorkPane。嗯,看起来有戏?。。。门都没有。MSDN上明说了,Work pane. Cannot be created through the object model。
显然,用微软官方API的办法是死路一条。但是也不是完全没有收获。
1、通过Application.TaskPanes[xxx]我们可以可靠的开关TaskPane上的某一页
2、我们知道Application.CommandBars["Task Pane"]对应的就是TaskPane所在的Command Bar。
曾经有一个让我恨之入骨的Office开发库,名字叫Addin-Express(大家不要用啊,谁用谁后悔)。当年我们用它就是因为它可以在Outlook里做类似的事情,在Email Composer上凭空多一块区域出来(类似于Outlook 2007的Form Region)。它的实现原理其实就是用一个独立的WinForm窗口,然后用为win32 api SetParent把窗口“融合”到Outlook上去。同时通过SetWindowLong的办法拦截目标窗口的WndProc(窗口事件处理函数,所有的GUI事件的起点),达到你缩放我也缩放的目的。这次我决定不用Addin-Express来做(它也没实现这个功能),但是可以利用其原理。
在.NET中拦截WndProc的最简单的办法是用NativeWindow这个类。只要override WndProc那个方法就可以了 。
protected override void WndProc(ref Message message) {
// Your code
base.WndProc(ref message);
}
}
虽然知道大概的原理,不过还有一个难点一个疑问需要解决。那就是,到底以哪个Window来Parent?而且,这个Parent在隐藏之后再重现出来会不会被重新创建。如果父窗口每次都会被销毁,那我们也没法用SetParent大法了。这个时候就是Spy++出场的时候了。
高亮的那个窗口就是我们的目标了。而且经过试验证明,窗口SetParent到它身上之后,一直都在。 注意,如果设在NUIPane上是不行的,必须是在NetUIHWND上。这个窗口的ClassName是固定的,但是XML Structure那个TaskPane的窗口ClassName不一样,不过也是一样可以SetParent的。
不过还有一个问题。我如何得到这个目标窗口的句柄呢?我甚至都没法得到当前Word窗口的句柄。这个时候就需要依赖CommandBar的一个特性了,Accessible。这是Windows用来服务残障人士的API。对于我们来说,就是CommandBar对象都实现了IAccessible这个Interface,然后我们就可以:
WindowFromAccessibleObject是OleAcc这个DLL上的一个API。用来获得Accssible对象的窗口句柄。利用Accessibility API可以做很多有意思的事情,比如http://blogs.officezealot.com/whitechapel/archive/2005/04/10/4514.aspx。这里就用了相反的一个操作,AccessibleObjectFromWindow来获得Excel的对象,当年在写Excel的UDF的时候帮了我的大忙。
还有一个难点就是,如果TaskPane没有打开我怎么SetParent。这个时候当然是利用前面提到的Application.TaskPanes[xxx]的方法来得到接口,并设置Visible了。不过还是有一个时间差。你设置下Visible之后,是一个异步地打开TaskPane的过程。所以当你接下来做SetParent的时候,那边可能还没有完成窗口的初始化。我现在只能用一个新线程Sleep(300)来完成这个任务。并不是完全不可靠,也不是完全可靠。最完美的解决方案还是升级到Word 2007。
知道了上述的机要。实现Word 2003下的Custom Task Pane就是体力活了。用GetWindowRect来缩放,用GetPixel来融合背景色。最终的效果是这个样子的。
而且能缩能放,能关闭能打开,能Dock能Undock。遗憾的是必须要牺牲掉Word内建的TaskPane中的某一个。不过好在Word 2003中有那么多人们用不着的功能,牺牲掉一两个,没谁会注意到的,对吧?:)