C#互操作
一、引言
“為什麼我們需要掌握互操作技術的呢?” 對於這個問題的解釋就是——掌握了.NET平臺下的互操作性技術可以幫助我們在.NET中呼叫非託管的dll和COM元件。
。.NET 平臺下提供了3種互操作性的技術:
- Platform Invoke(P/Invoke),即平臺呼叫,主要用於呼叫C庫函式和Windows API
- C++ Introp, 主要用於Managed C++(託管C++)中呼叫C++類庫
- COM Interop, 主要用於在.NET中呼叫COM元件和在COM中使用.NET程式集。
二、平臺呼叫
使用平臺呼叫的技術可以在託管程式碼中呼叫動態連結庫(Dll)中實現的非託管函式,如Win32 Dll和C/C++ 建立的dll。
2.1 在託管程式碼中通過平臺呼叫來呼叫非託管程式碼的步驟
(1). 獲得非託管函式的資訊,即dll的名稱,需要呼叫的非託管函式名等資訊
(2). 在託管程式碼中對非託管函式進行宣告,並且附加平臺呼叫所需要屬性
(3). 在託管程式碼中直接呼叫第二步中宣告的託管函式
平臺呼叫的過程可以通過下圖更好地理解:
2.2、如何使用平臺呼叫Win32 函式——從例項開始
第一步就需要知道非託管函式宣告,為了找到需要需要呼叫的非託管函式,可以藉助兩個工具——Visual Studio自帶的dumpbin.exe和depends.exe.
- dumpbin.exe 是一個命令列工具,可以用於檢視從非託管DLL中匯出的函式等資訊,可以通過開啟Visual Studio 2010 Command Prompt(中文版為Visual Studio 命令提示(2010)),然後切換到DLL所在的目錄,輸入 dummbin.exe/exports dllName, 如 dummbin.exe/exports User32.dll
- 然而 depends.exe是一個視覺化介面工具,大家可以從 “VS安裝目錄\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\Bin\” 這個路徑找到,然後雙擊 depends.exe 就可以出來一個視覺化介面(如果某些人安裝的VS沒有附帶這個工具,也可以從官方網站下載:http://www.dependencywalker.com/),如下圖:
上圖中 用紅色標示出 MessageBox 有兩個版本,而MessageBoxA 代表的就是ANSI版本,而MessageBoxW 代筆的就是Unicode版本,這也是上面所說的依據。下面就看看 MessageBox的C++宣告的(更多的函式的定義大家可以從MSDN中找到,這裡提供MessageBox的定義在MSDN中的連結:
int WINAPI MessageBox( _In_opt_ HWND hWnd, _In_opt_ LPCTSTR lpText, _In_opt_ LPCTSTR lpCaption, _In_ UINT uType );
現在已經知道了需要呼叫的Win32 API 函式的定義宣告,下面就依據平臺呼叫的步驟,在.NET 中實現對該非託管函式的呼叫,下面就看看.NET中的程式碼的:
using System; // 使用平臺呼叫技術進行互操作性之前,首先需要新增這個名稱空間 using System.Runtime.InteropServices; namespace 平臺呼叫Demo { class Program { // 在託管程式碼中對非託管函式進行宣告,並且附加平臺呼叫所需要屬性 在預設情況下,CharSet為CharSet.Ansi // 指定呼叫哪個版本的方法有兩種——通過DllImport屬性的CharSet欄位和通過EntryPoint欄位指定 在託管函式中宣告注意一定要加上 static 和extern 這兩個關鍵字 //第一種指定方式,通過CharSet欄位指定,在預設情況下CharSet為CharSet.Ansi [DllImport("user32.dll")] public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type); [DllImport("user32.dll")] public static extern int MessageBoxA(IntPtr hWnd, String text, String caption, uint type); // [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int // MessageBox(IntPtr hWnd, String text, String caption, uint type); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int MessageBoxW(IntPtr hWnd, String text, String caption, uint type); // 通過EntryPoint欄位指定 [DllImport("user32.dll", EntryPoint = "MessageBoxA")] public static extern int MessageBox3(IntPtr hWnd, String text, String caption, uint type); [DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode)] public static extern int MessageBox4(IntPtr hWnd, String text, String caption, uint type); static void Main(string[] args) { // 在託管程式碼中直接呼叫宣告的託管函式 使用CharSet欄位指定的方式,要求在託管程式碼中宣告的函式名必須與非託管函式名一樣 否則就會出現找不到入口點的執行時錯誤 // 下面的呼叫都可以執行正確 MessageBox(new IntPtr(0), "Learning Hard", "歡迎", 0); MessageBoxA(new IntPtr(0), "Learning Hard", "歡迎", 0); MessageBoxW(new IntPtr(0), "Learning Hard", "歡迎", 0); //使用指定函式入口點的方式呼叫,OK MessageBox3(new IntPtr(0), "Learning Hard", "歡迎", 0); MessageBox4(new IntPtr(0), "Learning Hard", "歡迎", 0); } } }
2.3使用平臺呼叫技術中,還需要注意下面4點
(1). DllImport屬性的ExactSpelling欄位如果設定為true時,則在託管程式碼中宣告的函式名必須與要呼叫的非託管函式名完全一致,因為從ExactSpelling字面意思可以看出為 "準確拼寫"的意思,當ExactSpelling設定為true時,此時會改變平臺呼叫的行為,此時平臺呼叫只會根據根函式名進行搜尋,而找不到的時候不會新增 A或者W來進行再搜尋,.
[DllImport("user32.dll", ExactSpelling=true)] public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
(2). 如果採用設定CharSet的值來控制呼叫函式的版本時,則需要在託管程式碼中宣告的函式名必須與根函式名一致,否則也會調用出錯
[DllImport("user32.dll")] public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type);
(3). 如果通過指定DllImport屬性的EntryPoint欄位的方式來呼叫函式版本時,此時必須相應地指定與之匹配的CharSet設定,意思就是——如果指定EntryPoint為 MessageBoxW,那麼必須將CharSet指定為CharSet.Unicode,如果指定EntryPoint為 MessageBoxA,那麼必須將CharSet指定為CharSet.Ansi或者不指定,因為 CharSet預設值就是Ansi。
(4). CharSet還有一個可選欄位為——CharSet.Auto, 如果把CharSet欄位設定為CharSet.Auto,則平臺呼叫會針對目標作業系統適當地自動封送字串。在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上,預設值為 Unicode;在 Windows 98 和 Windows Me 上,預設值為 Ansi。
2.3、獲得Win32函式的錯誤資訊
捕捉由託管定義導致的異常演示程式碼:
try { MessageBox1(new IntPtr(0), "Learning Hard", "歡迎", 0); } catch (DllNotFoundException dllNotFoundExc) { Console.WriteLine("DllNotFoundException 異常發生,異常資訊為: " + dllNotFoundExc.Message); } catch (EntryPointNotFoundException entryPointExc) { Console.WriteLine("EntryPointNotFoundException 異常發生,異常資訊為: " + entryPointExc.Message); }
捕獲由Win32函式本身返回異常的演示程式碼如下:要想獲得在呼叫Win32函式過程中出現的錯誤資訊,首先必須將DllImport屬性的SetLastError欄位設定為true,只有這樣,平臺呼叫才會將最後一次呼叫Win32產生的錯誤碼儲存起來,然後會在託管程式碼呼叫Win32失敗後,通過Marshal類的靜態方法GetLastWin32Error獲得由平臺呼叫儲存的錯誤碼,從而對錯誤進行相應的分析和處理。
class Program { // Win32 API // DWORD WINAPI GetFileAttributes( // _In_ LPCTSTR lpFileName //); // 在託管程式碼中對非託管函式進行宣告 [DllImport("Kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)] public static extern uint GetFileAttributes(string filename); static void Main(string[] args) { // 試圖獲得一個不存在檔案的屬性 // 此時呼叫Win32函式會發生錯誤 GetFileAttributes("FileNotexist.txt"); // 在應用程式的Bin目錄下存在一個test.txt檔案,此時呼叫會成功 //GetFileAttributes("test.txt"); // 獲得最後一次獲得的錯誤 int lastErrorCode = Marshal.GetLastWin32Error(); // 將Win32的錯誤碼轉換為託管異常 //Win32Exception win32exception = new Win32Exception(); Win32Exception win32exception = new Win32Exception(lastErrorCode); if (lastErrorCode != 0) { Console.WriteLine("呼叫Win32函式發生錯誤,錯誤資訊為 : {0}", win32exception.Message); } else { Console.WriteLine("呼叫Win32函式成功,返回的資訊為: {0}", win32exception.Message); } Console.Read(); } }
2.4 資料封送
資料封送是——在託管程式碼中對非託管函式進行互操作時,需要通過方法的引數和返回值在託管記憶體和非託管記憶體之間傳遞資料的過程,資料封送處理的過程是由CLR(公共語言執行時)的封送處理服務(即封送拆送器)完成的。
封送時需要處理的資料型別分為兩種——可直接複製到本機結構中的型別(blittable)和非直接複製到本機結構中的型別(non-bittable)。
2.4.1 可直接複製到本機結構中的型別
把在託管記憶體和非託管記憶體中有相同表現形式的資料型別稱為——可直接複製到本機結構中的型別,這些資料型別不需要封送拆送器進行任何特殊的處理就可以在託管和非託管程式碼之間傳遞,
下面列出一些課直接複製到本機結構中的簡單資料型別:
Windows 資料型別 |
非託管資料型別 |
託管資料型別 |
託管資料型別解釋 |
BYTE/Uchar/UInt8 |
unsigned char |
System.Byte |
無符號8位整型 |
Sbyte/Char/Int8 |
char |
System.SByte |
有符號8位整型 |
Short/Int16 |
short |
System.Int16 |
有符號16位整型 |
USHORT/WORD/UInt16/WCHAR |
unsigned short |
System.UInt16 |
無符號16位整型 |
Bool/HResult/Int/Long |
long/int |
System.Int32 |
有符號32位整型 |
DWORD/ULONG/UINT |
unsigned long/unsigned int |
System.UInt32 |
無符號32位整型 |
INT64/LONGLONG |
_int64 |
System.Int64 |
有符號64位整型 |
UINT64/DWORDLONG/ULONGLONG |
_uint64 |
System.UInt64 |
無符號64位整型 |
INT_PTR/hANDLE/wPARAM |
void*/int或_int64 |
System.IntPtr |
有符號指標型別 |
HANDLE |
void* |
System.UIntPtr |
無符號指標型別 |
FLOAT |
float |
System.Single |
單精度浮點數 |
DOUBLE |
double |
System.Double |
雙精度浮點數 |
除了上表列出來的簡單型別之外,還有一些複製型別也屬於可直接複製到本機結構中的資料型別:
(1) 資料元素都是可直接複製到本機結構中的一元陣列,如整數陣列,浮點陣列等
(2)只包含可直接複製到本機結構中的格式化值型別
(3)成員變數全部都是可複製到本機結構中的型別且作為格式化型別封送的類
上面提到的格式化指的是——在型別定義時,成員的記憶體佈局在宣告時就明確指定的型別。在程式碼中用StructLayout屬性修飾被指定的型別,並將StructLayout的LayoutKind屬性設定為Sequential或Explicit,例如:
using System.Runtime.InteropServices; // 下面的結構體也屬於可直接複製到本機結構中的型別 [StructLayout(LayoutKind.Sequential)] public struct Point { public int x; public int y; }
2.4.2 非直接複製到本機結構中的型別
對於這種型別,封送器需要對它們進行相應的型別轉換之後再複製到被呼叫的函式中,下面列出一些非直接複製到本機結構中的資料型別:
Windows 資料型別 |
非託管資料型別 |
託管資料型別 |
託管資料型別解釋 |
Bool |
bool |
System.Boolean |
布林型別 |
WCHAR/TCHAR |
char/ wchar_t |
System.Char |
ANSI字元/Unicode字元 |
LPCSTR/LPCWSTR/LPCTSTR/LPSTR/LPWSTR/LPTSTR |
const char*/const wchar_t*/char*/wchar_t* |
System.String |
ANSI字串/Unicode字串,如果非託管程式碼不需要更新此字串時,此時用String型別在託管程式碼中宣告字串型別 |
LPSTR/LPWSTR/LPTSTR |
Char*/wchar_t* |
System.StringBuilder |
ANSI字串/Unicode字串,如果非託管程式碼需要更新此字串,然後把更新的字串傳回託管程式碼中,此時用StringBuilder型別在託管程式碼中宣告字串 |
除了上表中列出的型別之外,還有很多其他型別屬於非直接複製到本機結構中的型別,例如其他指標型別和控制代碼型別等。
2.4.3、封送字串的處理
封送作為返回值的字串,下面是一段演示程式碼,程式碼中主要是呼叫Win32 GetTempPath函式來獲得返回臨時路徑,此時拆送器就需要把返回的字串封送回託管程式碼中。使用System.StringBuilder託管資料型別。
// 託管函式中的返回值封送回託管函式的例子 class Program { // Win32 GetTempPath函式的定義如下: //DWORD WINAPI GetTempPath( // _In_ DWORD nBufferLength, // _Out_ LPTSTR lpBuffer //); // 主要是注意如何在託管程式碼中定義該函式原型 [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError=true)] public static extern uint GetTempPath(int bufferLength, StringBuilder buffer); static void Main(string[] args) { StringBuilder buffer = new StringBuilder(300); uint tempPath=GetTempPath(300, buffer); string path = buffer.ToString(); if (tempPath == 0) { int errorcode =Marshal.GetLastWin32Error(); Win32Exception win32expection = new Win32Exception(errorcode); Console.WriteLine("呼叫非託管函式發生異常,異常資訊為:" +win32expection.Message); } Console.WriteLine("呼叫非託管函式成功。"); Console.WriteLine("Temp 路徑為:" + buffer); Console.Read(); } }
2.4.4、封送結構體的處理
在我們實際呼叫Win32 API函式時,經常需要封送結構體和類等複製型別,下面就以Win32 函式GetVersionEx為例子來演示如何對作為引數的結構體進行封送處理。
下面是GetVersionEx非託管定義(更多關於該函式的資訊可以參看MSDN連結:http://msdn.microsoft.com/en-us/library/ms885648.aspx ):
BOOL GetVersionEx(
LPOSVERSIONINFO lpVersionInformation
);
引數lpVersionInformation是一個指向 OSVERSIONINFO結構體的指標型別,所以我們在託管程式碼中為函式GetVersionEx函式之前,必須知道 OSVERSIONINFO結構體的非託管定義,然後再在託管程式碼中定義一個等價的結構體型別作為引數。以下是OSVERSIONINFO結構體的非託管定義:
typedef struct _OSVERSIONINFO{ DWORD dwOSVersionInfoSize; //在使用GetVersionEx之前要將此初始化為結構的大小 DWORD dwMajorVersion; //系統主版本號 DWORD dwMinorVersion; //系統次版本號 DWORD dwBuildNumber; //系統構建號 DWORD dwPlatformId; //系統支援的平臺 TCHAR szCSDVersion[128]; //系統補丁包的名稱 WORD wServicePackMajor; //系統補丁包的主版本 WORD wServicePackMinor; //系統補丁包的次版本 WORD wSuiteMask; //標識系統上的程式組 BYTE wProductType; //標識系統型別 BYTE wReserved; //保留,未使用 } OSVERSIONINFO;
知道了OSVERSIONINFO結構體在非託管程式碼中的定義之後, 現在我們就需要在託管程式碼中定義一個等價的結構,並且要保證兩個結構體在記憶體中的佈局相同。託管程式碼中的結構體定義如下:
// 因為Win32 GetVersionEx函式引數lpVersionInformation是一個指向 OSVERSIONINFO的資料結構 // 所以託管程式碼中定義個結構體,把結構體物件作為非託管函式引數 [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)] public struct OSVersionInfo { public UInt32 OSVersionInfoSize; // 結構的大小,在呼叫方法前要初始化該欄位 public UInt32 MajorVersion; // 系統主版本號 public UInt32 MinorVersion; // 系統此版本號 public UInt32 BuildNumber; // 系統構建號 public UInt32 PlatformId; // 系統支援的平臺 // 此屬性用於表示將其封送成內聯陣列 [MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string CSDVersion; // 系統補丁包的名稱 public UInt16 ServicePackMajor; // 系統補丁包的主版本 public UInt16 ServicePackMinor; // 系統補丁包的次版本 public UInt16 SuiteMask; //標識系統上的程式組 public Byte ProductType; //標識系統型別 public Byte Reserved; //保留,未使用 }
從上面的定義可以看出, 託管程式碼中定義的結構體有以下三個方面與非託管程式碼中的結構體是相同的:
- 欄位宣告的順序
- 欄位的型別
- 欄位在記憶體中的大小
並且在上面結構體的定義中,我們使用到了 StructLayout 屬性,該屬性屬於System.Runtime.InteropServices名稱空間(所以在使用平臺呼叫技術必須新增這個額外的名稱空間)。這個類的作用就是允許開發人員顯式指定結構體或類中資料欄位的記憶體佈局,為了保證結構體中的資料欄位在記憶體中的順序與定義時一致,所以指定為 LayoutKind.Sequential(該列舉也是預設值)。
下面就具體看看在託管程式碼中呼叫的程式碼:
using System; using System.ComponentModel; using System.Runtime.InteropServices; namespace 封送結構體的處理 { class Program { // 對GetVersionEx進行託管定義 // 為了傳遞指向結構體的指標並將初始化的資訊傳遞給非託管程式碼,需要用ref關鍵字修飾引數 // 這裡不能使用out關鍵字,如果使用了out關鍵字,CLR就不會對引數進行初始化操作,這樣就會導致呼叫失敗 [DllImport("Kernel32",CharSet=CharSet.Unicode,EntryPoint="GetVersionEx")] private static extern Boolean GetVersionEx_Struct(ref OSVersionInfo osVersionInfo); // 因為Win32 GetVersionEx函式引數lpVersionInformation是一個指向 OSVERSIONINFO的資料結構 // 所以託管程式碼中定義個結構體,把結構體物件作為非託管函式引數 [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)] public struct OSVersionInfo { public UInt32 OSVersionInfoSize; // 結構的大小,在呼叫方法前要初始化該欄位 public UInt32 MajorVersion; // 系統主版本號 public UInt32 MinorVersion; // 系統此版本號 public UInt32 BuildNumber; // 系統構建號 public UInt32 PlatformId; // 系統支援的平臺 // 此屬性用於表示將其封送成內聯陣列 [MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)] public string CSDVersion; // 系統補丁包的名稱 public UInt16 ServicePackMajor; // 系統補丁包的主版本 public UInt16 ServicePackMinor; // 系統補丁包的次版本 public UInt16 SuiteMask; //標識系統上的程式組 public Byte ProductType; //標識系統型別 public Byte Reserved; //保留,未使用 } // 獲得作業系統資訊 private static string GetOSVersion() { // 定義一個字串儲存版本資訊 string versionName = string.Empty; // 初始化一個結構體物件 OSVersionInfo osVersionInformation = new OSVersionInfo(); // 呼叫GetVersionEx 方法前,必須用SizeOf方法設定結構體中OSVersionInfoSize 成員 osVersionInformation.OSVersionInfoSize = (UInt32)Marshal.SizeOf(typeof(OSVersionInfo)); // 呼叫Win32函式 Boolean result = GetVersionEx_Struct(ref osVersionInformation); if (!result) { // 如果呼叫失敗,獲得最後的錯誤碼 int errorcode = Marshal.GetLastWin32Error(); Win32Exception win32Exc = new Win32Exception(errorcode); Console.WriteLine("呼叫失敗的錯誤資訊為: " + win32Exc.Message); // 呼叫失敗時返回為空字串 return string.Empty; } else { Console.WriteLine("呼叫成功"); switch (osVersionInformation.MajorVersion) { // 這裡僅僅討論 主版本號為6的情況,其他情況是一樣討論的 case 6: switch (osVersionInformation.MinorVersion) { case 0: if (osVersionInformation.ProductType == (Byte)0) { versionName = " Microsoft Windows Vista"; } else { versionName = "Microsoft Windows Server 2008"; // 伺服器版本 } break; case 1: if (osVersionInformation.ProductType == (Byte)0) { versionName = " Microsoft Windows 7"; } else { versionName = "Microsoft Windows Server 2008 R2"; } break; case 2: versionName = "Microsoft Windows 8"; break; } break; default: versionName = "未知的作業系統"; break; } return versionName; } } static void Main(string[] args) { string OS=GetOSVersion(); Console.WriteLine("當前電腦安裝的作業系統為:{0}", OS); Console.Read(); } } }
2.4.5、封送類的處理
下面直接通過GetVersionEx函式進行封送類的處理的例子,具體程式碼如下:
using System; using System.ComponentModel; using System.Runtime.InteropServices; namespace 封送類的處理 { class Program { // 對GetVersionEx進行託管定義 // 由於類的定義中CSDVersion為String型別,String是非直接複製到本機結構型別, // 所以封送拆送器需要進行復制操作。 // 為了是非託管程式碼能夠獲得在託管程式碼中物件設定的初始值(指的是OSVersionInfoSize欄位,呼叫函式前首先初始化該值), // 所以必須加上[In]屬性;函式返回時,為了將結果複製到託管物件中,必須同時加上 [Out]屬性 // 這裡不能是用ref關鍵字,因為 OsVersionInfo是類型別,本來就是引用型別,如果加ref 關鍵字就是傳入的為指標的指標了,這樣就會導致呼叫失敗 [DllImport("Kernel32", CharSet = CharSet.Unicode, EntryPoint = "GetVersionEx")] private static extern Boolean GetVersionEx_Struct([In, Out] OSVersionInfo osVersionInfo); // 獲得作業系統資訊 private static string GetOSVersion() { // 定義一個字串儲存作業系統資訊 string versionName = string.Empty; // 初始化一個類物件 OSVersionInfo osVersionInformation = new OSVersionInfo(); // 呼叫Win32函式 Boolean result = GetVersionEx_Struct(osVersionInformation); if (!result) { // 如果呼叫失敗,獲得最後的錯誤碼 int errorcode = Marshal.GetLastWin32Error(); Win32Exception win32Exc = new Win32Exception(errorcode); Console.WriteLine("呼叫失敗的錯誤資訊為: " + win32Exc.Message); // 呼叫失敗時返回為空字串 return string.Empty; } else { Console.WriteLine("呼叫成功"); switch (osVersionInformation.MajorVersion) { // 這裡僅僅討論 主版本號為6的情況,其他情況是一樣討論的 case 6: switch (osVersionInformation.MinorVersion) { case 0: if (osVersionInformation.ProductType == (Byte)0) { versionName = " Microsoft Windows Vista"; } else { versionName = "Microsoft Windows Server 2008"; // 伺服器版本 } break; case 1: if (osVersionInformation.ProductType == (Byte)0) { versionName = " Microsoft Windows 7"; } else { versionName = "Microsoft Windows Server 2008 R2"; } break; case 2: versionName = "Microsoft Windows 8"; break; } break; default: versionName = "未知的作業系統"; break; } return versionName; } } static void Main(string[] args) { string OS = GetOSVersion(); Console.WriteLine("當前電腦安裝的作業系統為:{0}", OS); Console.Read(); } } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public class OSVersionInfo { public UInt32 OSVersionInfoSize = (UInt32)Marshal.SizeOf(typeof(OSVersionInfo)); public UInt32 MajorVersion = 0; public UInt32 MinorVersion = 0; public UInt32 BuildNumber = 0; public UInt32 PlatformId = 0; // 此屬性用於表示將其封送成內聯陣列 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string CSDVersion = null; public UInt16 ServicePackMajor = 0; public UInt16 ServicePackMinor = 0; public UInt16 SuiteMask = 0; public Byte ProductType = 0; public Byte Reserved; } }
三、COM Interop
為了解決在.NET中的託管程式碼能夠呼叫COM元件的問題,.NET 平臺下提供了COM Interop,即COM互操作技術。
在.NET中使用COM物件,主要方法:使用TlbImp工具為COM元件建立一個互操作程式集來繫結早期的COM物件,這樣就可以在程式中新增互操作程式集來呼叫COM物件。
在.NET 中使用COM物件的步驟:
- 找到要使用的COM 元件並註冊它。使用 regsvr32.exe 註冊或登出 COM DLL。
- 在專案中新增對 COM 元件或型別庫的引用。
-
新增引用時,Visual Studio 會用到Tlbimp.exe(型別庫匯入程式),Tlbimp.exe程式將生成一個 .NET Framework 互操作程式集。該程式集又稱為執行時可呼叫包裝 (RCW),其中包含了包裝COM元件中的類和介面。Visual Studio 將生成元件的引用新增至專案。
- 建立RCW中類的例項,這樣就可以使用託管物件一樣來使用COM物件。
在.NET中使用COM元件的過程:
如何在C#中呼叫COM元件——訪問Office 互操作物件
在新建的控制檯程式裡新增”Microsoft.Office.Interop.Word 14.0.0.0 “ 這個引用
Microsoft.Office.Interop.Word.dll 確實是一個.NET程式集,並且它也叫做COM元件的互操作程式集,這個程式集中包含了COM元件中定義的型別的元資料, 託管程式碼通過呼叫互操作程式集中公開的介面或物件來間接地呼叫COM物件和介面的。
關於通過Tlblmp.exe工具來生成互操作程式集步驟,這裡我就不多詳細訴說了,大家可以參考MSDN中這個工具詳細使用說明 :http://msdn.microsoft.com/zh-cn/library/tt0cf3sx(v=VS.80).aspx 。
然而我們也可以使用Visual Studio中內建的支援來完成為COM型別庫建立互操作程式集的工作,我們只需要在VS中為.NET 專案新增對應的COM元件的引用,此時VS就會自動將COM型別庫中的COM型別庫轉化為程式集中的元資料,並在專案的Bin目錄下生成對於的互操作程式集,所以在VS中新增COM引用,其實最後程式中引用的是互操作程式集,然後通過RCW來對COM元件進行呼叫。 然而對於Office中的Microsoft.Office.Interop.Wordd.dll,這個程式集也是互操作程式集,但是它又是主互操作程式集,即PIA(Primary Interop Assemblies)。主互操作程式集是一個由供應商提供的唯一的程式集,為了生成主互操作程式集,可以在使用TlbImp命令是開啟 /primary 選項。
using System; using System.Collections.Generic; using System.Linq; using Excel = Microsoft.Office.Interop.Excel; using Word = Microsoft.Office.Interop.Word; namespace OfficeProgramminWalkthruComplete { class Walkthrough { static void Main(string[] args) { // Create a list of accounts. var bankAccounts = new List<Account> { new Account { ID = 345678, Balance = 541.27 }, new Account { ID = 1230221, Balance = -127.44 } }; // Display the list in an Excel spreadsheet. DisplayInExcel(bankAccounts); // Create a Word document that contains an icon that links to // the spreadsheet. CreateIconInWordDoc(); } static void DisplayInExcel(IEnumerable<Account> accounts) { var excelApp = new Excel.Application(); // Make the object visible. excelApp.Visible = true; // Create a new, empty workbook and add it to the collection returned // by property Workbooks. The new workbook becomes the active workbook. // Add has an optional parameter for specifying a praticular template. // Because no argument is sent in this example, Add creates a new workbook. excelApp.Workbooks.Add(); // This example uses a single workSheet. Excel._Worksheet workSheet = excelApp.ActiveSheet; // Earlier versions of C# require explicit casting. //Excel._Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet; // Establish column headings in cells A1 and B1. workSheet.Cells[1, "A"] = "ID Number"; workSheet.Cells[1, "B"] = "Current Balance"; var row = 1; foreach (var acct in accounts) { row++; workSheet.Cells[row, "A"] = acct.ID; workSheet.Cells[row, "B"] = acct.Balance; } workSheet.Columns[1].AutoFit(); workSheet.Columns[2].AutoFit(); // Call to AutoFormat in Visual C#. This statement replaces the // two calls to AutoFit. workSheet.Range["A1", "B3"].AutoFormat( Excel.XlRangeAutoFormat.xlRangeAutoFormatClassic2); // Put the spreadsheet contents on the clipboard. The Copy method has one // optional parameter for specifying a destination. Because no argument // is sent, the destination is the Clipboard. workSheet.Range["A1:B3"].Copy(); } static void CreateIconInWordDoc() { var wordApp = new Word.Application(); wordApp.Visible = true; // The Add method has four reference parameters, all of which are // optional. Visual C# allows you to omit arguments for them if // the default values are what you want. wordApp.Documents.Add(); // PasteSpecial has seven reference parameters, all of which are // optional. This example uses named arguments to specify values // for two of the parameters. Although these are reference // parameters, you do not need to use the ref keyword, or to create // variables to send in as arguments. You can send the values directly. wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true); } } public class Account { public int ID { get; set; } public double Balance { get; set; } } }
錯誤處理
try { // 如果文件不存在時,就會出現呼叫COM物件失敗的情況 // 開啟Word文件 wordDoc = wordApp.Documents.Open(wordPath); // 向Word中插入文字 Range wordRange = wordDoc.Range(0, 0); wordRange.Text = "這是插入的文字"; // 儲存文件 wordDoc.Save(); } catch(Exception ex) { // 獲得異常相對應的HRESULT值 // 因為COM中根據方法返回的HRESULT來判斷呼叫是否成功的 int HResult = Marshal.GetHRForException(ex); // 設定控制檯的前景色,即輸出文字的顏色 Console.ForegroundColor = ConsoleColor.Red; // 下面把HRESULT值以16進位制輸出 Console.WriteLine("呼叫丟擲異常,異常型別為:{0}, HRESULT= 0x{1:x}", ex.GetType().Name, HResult); Console.WriteLine("異常資訊為:" + ex.Message.Replace('\r', ' ')); } finally { // 關閉文件並 if (wordDoc != null) { wordDoc.Close(); } // 退出Word程式 wordApp.Quit(); }
從上面的結果我們看到了一個 HRESULT值,這個值真是COM程式碼中返回返回的。在COM中,COM方法通過返回 HRESULT 來報告錯誤;.NET 方法則通過引發異常來報告錯誤,為了方便地在託管程式碼中獲得COM程式碼中出現的錯誤和異常資訊,CLR提供了兩者之間的轉換,每一個代表錯誤發生的HRESULT都會被對映到.NET Framework中的一個異常類,對於具體的對映關係可以參考MSDN中 的文章: http://msdn.microsoft.com/zh-cn/library/9ztbc5s1(VS.80).aspx ,