上一篇文章,我们说了
这一篇文章,主要讨论一下WPS(或微软Office)的自动化的实现。
一、WPS在类厂中的痕迹
以WPS文字为例,我们在类厂中可以找到kwps.Application,它是可以通过CoCreateInstance唤起WPS文字的实例名:
CLSID为{000209FF-0000-4b30-A977-D214852036FF}接下来,我们通过CLSID {000209FF-0000-4b30-A977-D214852036FF}找到对应的COM实例启动方式:
由上可以知道,对应CLSID存在一个LocalServer32的键,启动的方式是调用wpsoffice.exe /Automation,这也是微软Office系列的启动方式——当我们使用CoCreateInstance创建kwps.Application时,wpsoffice.exe便会被调起,并且带上/Automation参数。
同理,我们也可以找到对应的TypeLib:
TypeLib为{00020905-0000-4b30-A977-D214852036FF}我们通过TypeLib的CLSID,找到了WPS文字的idl生成的接口二进制文件的存放位置:
WPS文字的API接口信息,保存在wpsapi.dll中wpsapi.dll不仅包含了代码信息,也将tlb嵌入了它的资源中。我们通过oleview.exe -> File ->View TypeLib,可以查看到WPS文字所有的API接口,对于微软Office亦是如此。不仅我们可以看到它的接口,甚至还可以看到其反编译生成的idl代码:
通过以上分析,我们知道了以下有用的信息:通过自动化(CoCreateInstance)方式启动WPS文字的路径和方式;以及WPS文字暴露出来的VBA接口。(建议永中office就不要倒腾自己的java接口了,不如老老实实把这个上面的接口统统实现一遍,来实现办公软件领域的兼容。)
既然知道了tlb的位置,我们很容易地可以通过
讲到的方法,生成其它语言兼容的SDK库,例如WPS当前正在推出的js-sdk,将这些接口转化为JS接口,注入cef或v8引擎,实现通过JS来操作WPS。
事实上,在windows下的工业软件,如Office系列,AutoCAD等,都有实现这一整套自动化流程,通过此方法进行分析,可以很方便地进行二次开发。
二、WPS与VBA
WPS(或者说微软Office),能秒杀众多办公软件的原因之一在于,它有一个强大的VBA编辑器:
WPS有一个叫作KDE的模块(安装路径下kde.dll),用来加载、交互微软的VBA编辑器。VBA全称叫作Visual Basic For Applications,也就是为任意(COM实现的)应用提供VB的环境。
WPS从微软购买了VBA编辑器,不过个人版是不带VBA功能的,需要安装企业版,或单独下载VBA,或安装Microsoft Office。当用户启动VBA编辑器时,WPS会首先加载kde.dll,然后kde.dll去寻找VBA编辑器所在的dll。一般VBA编辑器的dll所在的位置是C:\Program Files (x86)\Common Files\Microsoft Shared\VBA\VBA7\VBE7.DLL。
我们简单通过dumpbin来查看一下vbe7.dll的导出函数:
里面几个重要的函数就是DllVbe开头的函数,比如DllVbeInit表示初始化VBA环境,DllVbeTerm表示清理环境等,以及我们非常熟悉的DllReigsterServer,DllGetClassObject等。通过DllVbeInit我们可以拿到一个初始化的函数指针,调用它来完成初始化,并且拿到一个和VBA接口的COM对象指针。
VBA接口指针提供了很多服务,例如查询版本号,或是展示刚刚所截图的VBA编辑器的界面,或通过CreateProject创建一个VBA工程等。
WPS或者Office中创建的文档我们可以叫作内核文档。创建一个docx内核文档,然后在它里面创建了一个宏之后,它便相当于嵌入了VB代码,此时,相当于在内核docx文档中,有一个VBA文档对象的成员,然后VBA编辑器通过操作这个成员,来间接操作内核docx。
内核文档对象 ---持有---> VBA文档对象(IDispatch) <---交互---> VBA
事实上,VBA的原理还是很简单明了的。它只是一个IDE,和你打开记事本写一个vbs中的记事本没有本质区别。只不过,它集成了很多有用的功能,利于解析tlb进行自动补全,提供单步调试、断点等强大的功能。我们可以通过IConnectionPoint来实现VBA和内核之间的事件交互,VBA调用内核对象,本质上就是IDispatch方式的动态调用。举个例子,我们在VBA编辑器中输入一段代码,希望显示当前文档中的表格数量:
Sub Foo()
MsgBox Tables.Count
End Sub
在VBA中看来,这个流程是这样的,由于我们的代码是写在了ThisDocument模块中,意味着它持有一个当前文档的VBA文档对象(IDispatch)。然后我调用了Tables.Count,VB解析它,先要取Tables,再取Count。由于tlb中,Tables定义为一个属性,因此它会通过取属性的方式,从VBA文档对象IDispatch接口中,尝试拿Tables,也就是调用到get_Tables的C++方法中,再从Tables对象中获取Count属性,最后调用VB的内置函数MsgBox,将其取到的值显示出来。
从反编译出来的idl文件中我们也能看出,对外暴露的接口基本上都是dual和oleautomation的,这样就为脚本引擎提供了很大的便利。
无论是WPS还是Office,其流程大体都是如此。所以我们更可以为它设计自己的JS编辑器、Python编辑器等,它用到的内容,都是之前我们讨论过的内容,并且使用到了极致。
以上是WPS、Microsoft Office自动化的基本原理,以及VBA的基本原理。有兴趣的同学可以自己尝试分析一下ET(Excel)或者WPP(Powerpoint),也可以自己尝试写一个IDE,来方便地调用Office或者其它自动化应用,从而灵活地来进行二次开发。