WinForm引用ActiveX組件,對Com組件的學習

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庫。圖書館的使用者可以發現和使用其功能,而無需了解圖書館的部署和實施細節。

這是官方的定義,當然還有很多細節說明可以看看//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!」但是它不一定聽話,因為之前給它的命令也許還沒有執行完,它會忠誠地等到執行完再回去,當然我們並不關心這些細節。(引用地址://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組件調用引用控制項的功能。