SunSpring

1、WinForm引用Adobe PDF Reader

工作中写WinForm程序经常会引用第三方的组件,包括引用Com组件,做了一个桌面程序需要展示PDF,看了些其它的开源组件对PDF的兼容性都不是很好,有些看着PDF是正常的但是复制出来的字有很多乱码。然后就直接引用了adboe pdf reader来显示,测试了不同pdf兼容性算是不错的。那如何引用呢?

  • 在工具栏选择项

  • 添加Com组件
    找到Adobe PDF Reader勾选,然后点击确定之后组件就被添加到工具箱里面了。

  • 使用Com组件
    新建一个窗体或者用户控件,将刚才添加的Adobe PDF Reader 组件拖入到窗体中就可以像winform控件一样操作该控件了。

在该窗体类中生成了一个AxAcroPDFLib.AxAcroPDF的控件,进入该控件类可以看到控件类对外提供的方法,包括用于加载显示pdf的 LoadFile 方法,gotoFirstPage 等翻页的方法。

而该控件有一个父类AxHost类,进入Axhost类有一个摘要:

包装 ActiveX 控件,并将它们作为功能完整的 Windows 窗体控件公开

对此我陷入了沉思,ActiveX控件到底是什么,com组件如何被使用,AxAxAcroPDFLib.AxAcroPDF类是如何生成的,Winform和Com如何互操作?于是我进行了一番资料查找和学习。

2、ActiveX控件

ActiveX控件技术基于由COM,可连接对象,复合文档,属性页,OLE自动化,对象持久性以及系统提供的字体和图片对象组成的基础。
控件本质上是一个COM对象,它公开IUnknown接口,客户端可以通过该对象获取指向其其他接口的指针。控件可以通过IClassFactory2和自我注册来支持许可。
也就是说ActiveX控件是基于COM对象的,使用COM技术让不同语言编写的控件可以进行互相调用,而如何编写ActiveX控件呢,可以使用ATL 和 MFC,但是两个我都没使用过!并且没编写过,所以我就略过,只先了解其概念。既然它是基于COM,那接下来看看COM是什么东东。

3、COM技术

Microsoft组件对象模型(COM)定义了一个二进制互操作性标准,用于创建在运行时进行交互的可重用软件库。您可以使用COM库,而无需将其编译到应用程序中。COM是许多Microsoft产品和技术(例如Windows Media Player和Windows Server)的基础。
COM定义了适用于许多操作系统和硬件平台的二进制标准。对于网络计算,COM为在不同硬件平台上运行的对象之间的交互定义了标准的有线格式和协议。COM独立于实现语言,这意味着您可以使用其他编程语言(例如C ++和.NET Framework中的编程语言)创建COM库。
COM规范提供了支持跨平台软件重用的所有基本概念:
组件之间的函数调用的二进制标准。
将功能强类型分组到接口中的规定。
提供多态性,功能发现和对象生存期跟踪的基本接口。
唯一标识组件及其接口的机制。
组件加载器,可从部署中创建组件实例。
COM具有多个部分,这些部分可以一起工作以创建由可重用组件构建的应用程序:
一个主机系统提供了一个运行时环境符合的COM规范。
定义要素合同的接口和实现接口的组件。
为系统提供组件的服务器,以及使用组件提供的功能的客户端。
一个注册表,用于跟踪组件在本地和远程主机上的部署位置。
一个服务控制管理器,可以在本地和远程主机上找到组件,并将服务器连接到客户端。
一种结构化的存储协议,它定义了如何导航主机文件系统上文件的内容。
跨主机和平台启用代码重用对于COM至关重要。可重用的接口实现被称为组件,组件对象或COM对象。组件实现一个或多个COM接口。
您可以通过设计库实现的接口来定义自定义COM库。图书馆的使用者可以发现和使用其功能,而无需了解图书馆的部署和实施细节。

这是官方的定义,当然还有很多细节说明可以看看https://docs.microsoft.com/zh-cn/windows/win32/com/com-technical-overview 其中包括实现的定义和方式,对象和接口、接口实现、IUnknown接口等等。

那是如何实现如何调用呢,引用一段有趣的概括性的描述:

COM主要是一套给C/C++用的接口,当然为了微软的野心,它也被推广到了VB、Delphi以及其他一大堆奇奇怪怪的平台上。它主要为了使用dll发布基于interface的接口。我们知道dll的接口是为了C设计的,它导出的基本都是C的函数,从原理上来说,将dll加载到内存之后,会告诉你一组函数的地址,你自己call进去就可以调用相应的函数。
但是对于C++来说这个事情就头疼了,现在假设你有一个类,我们知道使用一个类的第一步是创建这个类:new MyClass()。这里直接就出问题了,new方法通过编译器计算MyClass的大小来分配相应的内存空间,但是如果库升级了,相应的类可能会增加新的成员,大小就变了,那么使用旧的定义分配出来的空间就不能在新的库当中使用。
要解决这问题,我们必须在dll当中导出一个CreateObject的方法,用来代替构造函数,然后返回一个接口。然而,接口的定义在不同版本当中也是有可能会变化的,为了兼容以前的版本同时也提供新功能,还需要让这个对象可以返回不同版本的接口。接口其实是一个只有纯虚函数的C++类,不过对它进行了一些改造来兼容C和其他一些编程语言。
在这样改造之后,出问题的还有析构过程~MyClass()或者说delete myClass,因为同一个对象可能返回了很多个接口,有些接口还在被使用,如果其中一个被人delete了,其他接口都会出错,所以又引入了引用计数,来让许多人可以共享同一个对象。
其实到此为止也并不算是很奇怪的技术,我们用C++有的时候也会使用Factory方法来代替构造函数实现某些特殊的多态,也会用引用计数等等。COM技术的奇怪地方在于微软实在是脑洞太大了,它们构造了一个操作系统级别的Factory,规定所有人的Interface都统一用UUID来标识,以后想要哪个Interface只要报出UUID来就行了。这样甚至连链接到特定的dll都省了。
这就好比一个COM程序员,只要他在Windows平台上,调用别的库就只要首先翻一下魔导书,查到了一个用奇怪文字写的“Excel = {xxx-xxx-xxxx...}”的记号,然后它只要对着空中喊一声:“召唤,Excel!CoCreateInstance, {xxx-xxx-xxxx...}”
然后呼的从魔法阵里面窜出来了一个怪物,它长什么样我们完全看不清,因为这时候它的类型是IUnknow,这是脑洞奇大无比的微软为所有接口设计的一个基类。我们需要进一步要求它变成我们能控制的接口形态,于是我们再喊下一条指令:
“变身,Excel 2003形态!QueryInterface, {xxx-xxx-xxxx...}”
QueryInterface使用的是另一个UUID,用来表示不同版本的接口。于是怪物就变成了我们需要的Excel 2003接口,虽然我们不知道它实际上是2003还是2007还是更高版本。
等我们使唤完这只召唤兽,我们就会对它说“回去吧,召唤兽!Release!”但是它不一定听话,因为之前给它的命令也许还没有执行完,它会忠诚地等到执行完再回去,当然我们并不关心这些细节。(引用地址:https://www.zhihu.com/question/49433640)

从这个概括理解,所有的COM类其实都继承了IUnknown,当我们拿到IUnknown接口后还需要转成我们需要使用的类型,而这个类型如果用强转可能会出错,但是微软认为,直接由用户来转型是不安全的需要唯一的一个标识符来确定一个类,那么这个标识符就是GUID。类ID就叫作CLSID,接口ID就叫作IID,还需要一个转型的函数叫QueryInterface。QueryInterface作为IUnknown中的一个纯虚函数,做的事情其实很简单,判断自己能不能转成某个GUID所指向的类而已。如果不可以,则返回E_NOTIMPL,可以的话返回S_OK,并将转换后的指针作为参数返回。
COM组件并不需要名字,或者说不需要UUID,因为我们总是使用他里面的接口,而不是直接使用COM组件,所以接口也要UUID。说了这么多,COM架构这么复杂,肯定需要一个中间层,或者说摆渡人,这就是COM Library(一堆dll) + 注册表。A应用通知COM Library,并输入接口的UUID,由COM Library装入B应用的该组件对应的dll,并把接口指针返回给A应用,指针里指示的是一堆函数指针,由这些指针,可以调用到B应用里的函数功能。

注:上面有时说的UUID,有时说的GUDI,UUID即是GUID值。

4、Aximp.exe(Windows 窗体 ActiveX 控件导入程序)

有了上面的ActiveX控件和Com组件的介绍,我们再回到开始我们如何导入的ActiveX控件。
ActiveX 控件导入程序将 ActiveX 控件的 COM 类型库中的类型定义转换为 Windows 窗体控件。
Windows 窗体只能承载 Windows 窗体控件,即从 Control 派生的类。 Aximp.exe 生成可承载于 Windows 窗体上的 ActiveX 控件的包装器类。 这使你得以使用适用于其他 Windows 窗体控件的同一设计时支持和编程方法。
若要承载 ActiveX 控件,必须生成从 AxHost 派生的包装器控件。 此包装器控件包含基础 ActiveX 控件的一个实例。 它知道如何与 ActiveX 控件通信,但它显示为 Windows 窗体控件。 这个生成的控件承载 ActiveX 控件并将其属性、方法和事件作为生成的控件的属性、方法和事件公开。
由此可见当我们再工具箱里面选择添加com组件后实际隐含执行了该导入程序,为我们生成了对应的AxAcroPDFLib.AxAcroPDF包装器控件。而AxAcroPDFLib则如同第三点中讲的那样就是COM Library。

5、验证

既然AxAcroPDFLib 是摆渡人(互操作程序集) 那么我们可以看到这个COM Library的引用

有了互操作程序那么这个互操作程序必然是去调用COM组件,调用COM组件那么UUID呢?将这个程序集放到Dnspy反编译可以看到在ClsidAttribute标记有{ca8a9780-280d-11cf-a24d-444553540000},构造函数里面有UUID。

然后我们打开注册表查询下对应的值和注册表的情况。

6、总结

所以通过上面的概念了解和猜想验证,基本清楚了com的设计和想法,以及ActiveX控件的调用过程。

  1. Activex控件时COM实现的一种方式。
  2. Activex控件通过VS工具引用时调用了Aximp.exe 。
  3. Aximp.exe程序生成了互操作程序集AxAcroPDFLib,同时生成可承载于 Windows 窗体上的 ActiveX 控件的从 AxHost 派生的包装器控件。
  4. 调用AxAcroPDF方法时通过com组件调用引用控件的功能。

分类:

技术点:

相关文章: