天天看點

C#中COM操作(一)---執行個體化

    用C#做WinForm程式,時間長了難免會遇到和COM元件打交道的地方,用什麼方式建立COM對象也成了我們必須面對的一個問題.據我所知道的建立COM對象的方法一共有以下幾種:

1 使用.NET包裝COM元件

    這是最簡單的就是導入COM元件所在的DLL,讓IDE生成.NET一個IL包裝加到項目中,這樣原來COM裡面所有實作了IDispatch,Dual的COM類型及其相關類型就可以直接在.NET程式裡面使用,比如以前在2003時代,想要寫自己的基于IE的浏覽器,就得手動加入與IWebBrowser2接口相關的DLL,這種方式是大家最常用的,也是最傻瓜化的,是以也沒什麼可解釋的.

    但是這種方式有個至命的缺點---不是所有的COM對象都能用這種方式導出.正如前面所說的,隻有實作了IDispatch,Dual類型的接口才支援被導出,而且面對不同版本的COM或許會生成不一樣的導出DLL,比如說A機器上寫代碼時導入了一個Jet2.6版本的包裝DLL,代碼編譯了拿到B機器上去運作,但是B機器上的Jet版本是2.8的,就可能會出現運作時錯誤.

2 用反射動态建立

    包括使用Type.GetTypeFromCLSID和Type.GetFromProgID兩種方法擷取COM對象的Type再建立.這種方式也好了解,就是說使用這兩個方法之前,必須得知道COM對象的GUID或ProgID,好在這也不是什麼難事,一般我們要使一個COM對象,多多少少都了解一些這個COM對象的GUID或ProgID資訊.用這種方擷取到了一個Type對象後,就可以用.NET裡面通用的反射建立對象的方法來做了.

這裡給出一個建立JetEngine 的COM對象的代碼執行個體:

 1  public   object  GetActiveXObject(Guid clsid)

 2  {

 3      Type t  =  Type.GetTypeFromCLSID(clsid);

 4       if  (t  ==   null )  return   null ;

 5 

 6       return  Activator.CreateInstance(t);

 7  }

 8 

 9  Guid g  =   new  Guid( " DE88C160-FF2C-11D1-BB6F-00C04FAE22DA " );  //  JetEngine

10  object  jet  =  GetActiveXObject(g);

是不是覺得最後調用GetActiveXObject(g)的地方和IE裡面Javascript裡面用new ActiveXOjbect建立COM對象的方法很相像?

3 聲明CoCreateInstance外部函數,用這個函數去建立相應的COM執行個體

    M$在2005裡面包裝的WebBrowser控件内部就是用這個函數去建立的, 使用這種方式建立COM,就跟在C++裡面不什麼兩樣了.有一點需要說明的是,一般我們在代碼中引入外部方法的時候,方法的參數和傳回值的類型不一定是唯一的一種,隻要在邏輯上互相能轉化,一般都可以使用.

比如說如下幾種聲明都是正确的:  

 1  [ return : MarshalAs(UnmanagedType.Interface)]

 2  [DllImport( " ole32.dll " , ExactSpelling = true , PreserveSig = false )]

 3  public   static   extern   object  CoCreateInstance([In]  ref  Guid clsid, 

 4      [MarshalAs(UnmanagedType.Interface)]  object  punkOuter,  int  context, [In]  ref  Guid iid);

 5    

 6  [DllImport( " ole32.dll " , ExactSpelling = true , PreserveSig = false )]

 7  public   static   extern  IntPtr CoCreateInstance([In]  ref  Guid clsid, 

 8      IntPtr punkOuter,  int  context, [In]  ref  Guid iid); 

 9 

10  [DllImport( " ole32.dll " , ExactSpelling = true )]

11  public   static   extern   int  CoCreateInstance([In]  ref  Guid clsid, 

12      IntPtr punkOuter,  int  context, [In]  ref  Guid iid, [Out]  out  IntPtr pVoid); 

13 

14  [DllImport( " ole32.dll " , ExactSpelling = true )]

15  public   static   extern   int  CoCreateInstance([In]  ref  Guid clsid, 

16      [MarshalAs(UnmanagedType.Interface)]  object  punkOuter,  int  context, 

17      [In]  ref  Guid iid, [MarshalAs(UnmanagedType.Interface), Out]  out   object  pVoid); 

 甚至于當你有裡面對應的接口類型的聲明的時候,完全可以把上面的object或IntPtr換成相應的接口類型,前提是你的接口類型的聲明一定要正确.讀者中用C++做過COM的一定對這種方式記憶猶新吧,隻不過這裡不再需要什麼CoInitialize和CoUninitialize,.NET内部自己幫你搞定了.順便提一下,上面例子中的object與IntPtr聲明是相通的,我們可以用Marshal.GetObjectForIUnknown和Marshal.GetIUnknownForObject這兩個方法在object和IntPtr之間互轉,前題當然是這兩種方式所指向的都是COM對象才行.這種方式提供的傳入參數最多,建立對象也最靈活.

3.直接聲明空成員的類

    可能很多程式員對于這個不太了解這是什麼意思,沒關系咱還是"用代碼來說話".

 1  [ComImport, Guid( " DE88C160-FF2C-11D1-BB6F-00C04FAE22DA " )]

 2  public   class  JetEngineClass

 3  {

 4  }

 5 

 6  [ComImport, CoClass( typeof (JetEngineClass)), Guid( " 9F63D980-FF25-11D1-BB6F-00C04FAE22DA " )]

 7  public   interface  IJetEngine

 8  {

 9       void  CompactDatabase(

10          [In, MarshalAs(UnmanagedType.BStr)]  string  SourceConnection, 

11          [In, MarshalAs(UnmanagedType.BStr)]  string  Destconnection

12          );

13       void  RefreshCache([In, MarshalAs(UnmanagedType.Interface)]  object  Connection);

14  }

15 

16  JetEngineClass engine  =   new  JetEngineClass();

17  IJetEngine iengine  =  engine  as  IJetEngine;

18  //  iengine即是所要用的接口的引用

 大家看到了上面聲明的JetEngineClass類隻有一個單單的類聲明,但是沒有一個成員聲明,但是和一般的類聲明有些不一樣的是這個類多了兩個特性(Attribute),把這個類和COM對象聯系在一起的就是這兩個特性了,其中一個是ComImportAttribute,這個特性指明了所作用的類是從COM對象中來的,GuidAttribute指明了COM對象的GUID,也就是說明了建立這個COM需用到的GUID。有了這兩個特性以後,這個類就不是一個普通的類了,當我們使用new去建立執行個體的時候,CLR看到了聲明的這兩特性就知道要建立的是一個COM對象,根據提供的GUID也就能建立出指定的COM對象,并和new傳回的對象執行個體關聯在一起了。

    終上4種方法我們可以看出來,第一種方式隻對特定的COM對象有效,不具有通用性;第二種方式隻需要知道COM對象的CLSID或PROGID就可以了,是我們在.NET裡平時比較常用的建立COM對象的方法;第三種方式需要自己聲明一個外部方法,而且需要傳入若幹的參數,還需要知道COM對象模型,是單線程呢還是多線程,程序内呢還是程序外,兩個字"麻煩"。對CoCreateInstance這個方法不是很熟悉的人來說,用起來就不那麼順手了;第四種方式用起來最像是.NET的方式,也最簡單省事,和其它.NET對象的建立方式最為接近。四種方法各有各有好處,我覺得簡單的COM對象,用第二種和第四種是最好的(我個人來說最喜歡第四種)又不生成額外的程式集;要是COM對象相關的比較多,比如說Excel之類的COM對象,我建議還是用導入類型庫包裝吧,雖然是有可能出現版本問題,但這種應該很容易要求目标機器上運作的COM版和開發的時候一緻的,更何況版本問題也不是100%出現,隻是很少一部分會出這樣的問題。最不推薦的就是第三種方式了,這種方式在我看來唯一用到的地方就是使用IntPtr作為COM對象和接口的指針的時候,或者是想要在建立COM對象的時候,對參數作最靈活的控制的時候. 因為其它三種方式既不能傳回IntPtr指針(其實也可以通過前面提到的的Marshal類的方法把.NET包裝的COM對象轉成指針),也不能提供與直接調用CoCreateInstance函數提供最全面的參數相比對的方式。

    最後提個小問題

1 讀者有興趣的話可以去看看這幾種方式(不包括第三種)生成的COM對象的引用的類型是否是一緻的,也就是用GetType得到的Type是否是一緻的

2 大家猜猜這段代碼運作後,iengine的類型會是什麼(GetType的結果), 會和engine的類型一樣嗎?

1  JetEngineClass engine  =   new  JetEngineClass();

2  IJetEngine iengine  =  engine  as  IJetEngine;

3  //  iengine即是所要用的接口的引用

5  IntPtr p  =  Marshal.GetIUnknownForObject(engine);

6  iengine  =  Marshal.GetObjectForIUnknown(p)  as  IJetEngine;

我這裡就不給出結果了,留給讀者自行去驗證吧。另外如果大家還發現.NET中有其它的建立COM對象的方式也盡指教一二,本人将不甚感激.