如何使用C#操做WinAPI

Windows API是對Windows操做系統的API函數,在C#中調用Windows API的實質是託管代碼對非託管代碼的調用。html

主要使用的格式就是:算法

using System.Runtime.InteropServices;

namespace TestWinAPI1
{
    class Program
    {
        static void Main(string[] args)
        {
              Beep(100, 100);
        }


        [DllImport("kernel32", CharSet = CharSet.Ansi)]
        public static extern bool Beep(int frequery, int duration);
     }
}

其中的Beep就是Win API的調用,使用[DllImport("kernel32")]屬性進行調用。數據庫

這個函數在MSDN中的本來定義是:編程

C++ 
BOOL WINAPI Beep(
  __in  DWORD dwFreq,
  __in  DWORD dwDuration
);

咱們想要調用BeepAPI,就必須:api

1.將DWORD對應爲C#中的int,相應的參數個數和位置設置正確數組

2.調用的函數名和WinAPI中的函數名一致緩存

這樣,咱們在C#中就可使用Win API對Windows進行操做。安全

 

這裏幾個資源是使用WindowsAPI不可或缺的:數據結構

MSDN:http://msdn.microsoft.com/en-us/library/ee663300(VS.85).aspxapp

推薦的入門教程:http://www.docin.com/p-4510006.html

 

 

使用WINAPI的難點:

 

1.C++中的各個數據類型如何對應到C#中?

使用C#中的那個數據類型對應那個C++的數據類型沒有惟一的規定,可是應該站在內存使用的角度,選擇內存佔用大小一致。

當C++中存在指針的時候,咱們可使用ref來傳遞指針

 

2.若是C++中定義了數據結構如何操做?

咱們也應該在C#中定義與之存儲結構一致的數據結構

 

如下是用WinAPI 模擬鼠標定位和單機左鍵的操做:

代碼

namespace TestWinAPI1
{
    public struct Point
    {
        int x;
        int y;
    }
    class Program
    {
        static void Main(string[] args)
        {
            Point point = new Point();
            bool getResult = GetCursorPos(ref point);
            int setRight = SetCursorPos(27, 881);
            MouseClick("left");
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern bool GetCursorPos(ref Point point);

        [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern IntPtr GetCursor();


        [DllImport("user32")]
        public static extern int SetCursorPos(int x, int y);

        [DllImport("user32.dll")]
        static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, int dwExtraInfo);

        [Flags]
        public enum MouseEventFlags:uint
        {
            LEFTDOWN = 0x00000002,
            LEFTUP = 0x00000004,
            MIDDLEDOWN = 0x00000020,
            MIDDLEUP = 0x00000040,
            MOVE = 0x00000001,
            ABSOLUTE = 0x00008000,
            RIGHTDOWN = 0x00000008,
            RIGHTUP = 0x00000010
        }

         /// <summary>
        /// checks for the currently active window then simulates a mouseclick
        /// </summary>
        /// <param name="button">which button to press (left middle up)</param>
        /// <param name="windowName">the window to send to</param>
        public static void MouseClick(string button, string windowName)
        {
            if (WindowActive(windowName))
                MouseClick(button);
        }

        /// <summary>
        /// simulates a mouse click see http://pinvoke.net/default.aspx/user32/mouse_event.html?diff=y
        /// </summary>
        /// <param name="button">which button to press (left middle up)</param>
        public static void MouseClick(string button)
        {
            switch (button)
            {
                case "left":
                    mouse_event((uint)MouseEventFlags.LEFTDOWN|(uint)MouseEventFlags.LEFTUP, 0, 0, 0, 0);
                    break;
                case "right":
                    mouse_event((uint)MouseEventFlags.RIGHTDOWN, 0, 0, 0, 0);
                    mouse_event((uint)MouseEventFlags.RIGHTUP, 0, 0, 0, 0);
                    break;
                case "middle":
                    mouse_event((uint)MouseEventFlags.MIDDLEDOWN, 0, 0, 0, 0);
                    mouse_event((uint)MouseEventFlags.MIDDLEUP, 0, 0, 0, 0);
                    break;
            }
        }
      }
}

簡要描述:

使用了mouse_event,GetCursorPos,SetCursorPos三個API

mouse_event((uint)MouseEventFlags.LEFTDOWN|(uint)MouseEventFlags.LEFTUP, 0, 0, 0, 0);

表明了單擊左鍵的動做

int setRight = SetCursorPos(27, 881); 中的27,881是屏幕上的絕對位置

詳解

DLL Import 屬性 

如今是更深刻地進行探討的時候了。在對託管代碼進行 P/Invoke 調用時,DllImportAttribute 類型扮演着重要的角色。DllImportAttribute 的主要做用是給 CLR 指示哪一個 DLL 導出您想要調用的函數。相關 DLL 的名稱被做爲一個構造函數參數傳遞給 DllImportAttribute。 

若是您沒法確定哪一個 DLL 定義了您要使用的 Windows API 函數,Platform SDK 文檔將爲您提供最好的幫助資源。在 Windows API 函數主題文字臨近結尾的位置,SDK 文檔指定了 C 應用程序要使用該函數必須連接的 .lib 文件。在幾乎全部的狀況下,該 .lib 文件具備與定義該函數的系統 DLL 文件相同的名稱。例如,若是該函數須要 C 應用程序連接到 Kernel32.lib,則該函數就定義在 Kernel32.dll 中。您能夠在 MessageBeep 中找到有關 MessageBeep 的 Platform SDK 文檔主題。在該主題結尾處,您會注意到它指出庫文件是 User32.lib;這代表 MessageBeep 是從 User32.dll 中導出的。 

可選的 DllImportAttribute 屬性 

除了指出宿主 DLL 外,DllImportAttribute 還包含了一些可選屬性,其中四個特別有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。 

EntryPoint 在不但願外部託管方法具備與 DLL 導出相同的名稱的狀況下,能夠設置該屬性來指示導出的 DLL 函數的入口點名稱。當您定義兩個調用相同非託管函數的外部方法時,這特別有用。另外,在 Windows 中還能夠經過它們的序號值綁定到導出的 DLL 函數。若是您須要這樣作,則諸如「#1」或「#129」的 EntryPoint 值指示 DLL 中非託管函數的序號值而不是函數名。 

CharSet 對於字符集,並不是全部版本的 Windows 都是一樣建立的。Windows 9x 系列產品缺乏重要的 Unicode 支持,而 Windows NT 和 Windows CE 系列則一開始就使用 Unicode。在這些操做系統上運行的 CLR 將Unicode 用於 String 和 Char 數據的內部表示。但也沒必要擔憂 — 當調用 Windows 9x API 函數時,CLR 會自動進行必要的轉換,將其從 Unicode轉換爲 ANSI。 

若是 DLL 函數不以任何方式處理文本,則能夠忽略 DllImportAttribute 的 CharSet 屬性。然而,當 Char 或 String 數據是等式的一部分時,應該將 CharSet 屬性設置爲 CharSet.Auto。這樣可使 CLR 根據宿主 OS 使用適當的字符集。若是沒有顯式地設置 CharSet 屬性,則其默認值爲 CharSet.Ansi。這個默認值是有缺點的,由於對於在 Windows 2000、Windows XP 和 Windows NT® 上進行的 interop 調用,它會消極地影響文本參數封送處理的性能。 

應該顯式地選擇 CharSet.Ansi 或 CharSet.Unicode 的 CharSet 值而不是使用 CharSet.Auto 的惟一狀況是:您顯式地指定了一個導出函數,而該函數特定於這兩種 Win32 OS 中的某一種。ReadDirectoryChangesW API 函數就是這樣的一個例子,它只存在於基於 Windows NT 的操做系統中,而且只支持 Unicode;在這種狀況下,您應該顯式地使用 CharSet.Unicode。 

有時,Windows API 是否有字符集關係並不明顯。一種決不會有錯的確認方法是在 Platform SDK 中檢查該函數的 C 語言頭文件。(若是您沒法確定要看哪一個頭文件,則能夠查看 Platform SDK 文檔中列出的每一個 API 函數的頭文件。)若是您發現該 API 函數確實定義爲一個映射到以 A 或 W 結尾的函數名的宏,則字符集與您嘗試調用的函數有關係。Windows API 函數的一個例子是在 WinUser.h 中聲明的 GetMessage API,您也許會驚訝地發現它有 A 和 W 兩種版本。 

SetLastError 錯誤處理很是重要,但在編程時常常被遺忘。當您進行 P/Invoke 調用時,也會面臨其餘的挑戰 — 處理託管代碼中 Windows API 錯誤處理和異常之間的區別。我能夠給您一點建議。 

若是您正在使用 P/Invoke 調用 Windows API 函數,而對於該函數,您使用 GetLastError 來查找擴展的錯誤信息,則應該在外部方法的 DllImportAttribute 中將 SetLastError 屬性設置爲 true。這適用於大多數外部方法。 

這會致使 CLR 在每次調用外部方法以後緩存由 API 函數設置的錯誤。而後,在包裝方法中,能夠經過調用類庫的 System.Runtime.InteropServices.Marshal 類型中定義的 Marshal.GetLastWin32Error 方法來獲取緩存的錯誤值。個人建議是檢查這些指望來自 API 函數的錯誤值,併爲這些值引起一個可感知的異常。對於其餘全部失敗狀況(包括根本就沒意料到的失敗狀況),則引起在 System.ComponentModel 命名空間中定義的 Win32Exception,並將 Marshal.GetLastWin32Error 返回的值傳遞給它。若是您回頭看一下圖 1 中的代碼,您會看到我在 extern MessageBeep 方法的公共包裝中就採用了這種方法。 

CallingConvention 我將在此介紹的最後也多是最不重要的一個 DllImportAttribute 屬性是 CallingConvention。經過此屬性,能夠給 CLR 指示應該將哪一種函數調用約定用於堆棧中的參數。CallingConvention.Winapi 的默認值是最好的選擇,它在大多數狀況下均可行。然而,若是該調用不起做用,則能夠檢查 Platform SDK 中的聲明頭文件,看看您調用的 API 函數是不是一個不符合調用約定標準的異常 API。 

一般,本機函數(例如 Windows API 函數或 C- 運行時 DLL 函數)的調用約定描述瞭如何將參數推入線程堆棧或從線程堆棧中清除。大多數 Windows API 函數都是首先將函數的最後一個參數推入堆棧,而後由被調用的函數負責清理該堆棧。相反,許多 C-運行時 DLL 函數都被定義爲按照方法參數在方法簽名中出現的順序將其推入堆棧,將堆棧清理工做交給調用者。 

幸運的是,要讓 P/Invoke 調用工做只須要讓外圍設備理解調用約定便可。一般,從默認值 CallingConvention.Winapi 開始是最好的選擇。而後,在 C 運行時 DLL 函數和少數函數中,可能須要將約定更改成 CallingConvention.Cdecl。

C# API

C:\ProgramFiles\MicrosoftVisual Studio .NET\ FrameworkSDK\Samples\ Technologies\ Interop\PlatformInvoke\ WinAPIs\CS目錄下有大量的調用API的例子。1、調用格式using System.Runtime.InteropServices; //引用此名稱空間,簡化後面的代碼//使用DllImportAttribute特性來引入api函數,注意聲明的是空方法,即方法體爲空。[DllImport("user32.dll")]public static extern ReturnType FunctionName(type arg1,type arg2,...);//調用時與調用其餘方法並沒有區別可使用字段進一步說明特性,用逗號隔開,如:[ DllImport( "kernel32", EntryPoint="GetVersionEx" )] DllImportAttribute特性的公共字段以下:一、CallingConvention 指示向非託管實現傳遞方法參數時所用的 CallingConvention 值。CallingConvention.Cdecl : 調用方清理堆棧。它使您可以調用具備 varargs 的函數。CallingConvention.StdCall : 被調用方清理堆棧。它是從託管代碼調用非託管函數的默認約定。二、CharSet 控制調用函數的名稱版本及指示如何向方法封送 String 參數。此字段被設置爲 CharSet 值之一。若是 CharSet 字段設置爲 Unicode,則全部字符串參數在傳遞到非託管實現以前都轉換成 Unicode 字符。這還致使向 DLL EntryPoint 的名稱中追加字母「W」。若是此字段設置爲 Ansi,則字符串將轉換成 ANSI 字符串,同時向 DLL EntryPoint 的名稱中追加字母「A」。大多數 Win32 API 使用這種追加「W」或「A」的約定。若是 CharSet 設置爲 Auto,則這種轉換就是與平臺有關的(在 Windows NT 上爲 Unicode,在 Windows 98 上爲 Ansi)。CharSet 的默認值爲 Ansi。CharSet 字段也用於肯定將從指定的 DLL 導入哪一個版本的函數。CharSet.Ansi 和 CharSet.Unicode 的名稱匹配規則大不相同。對於 Ansi 來講,若是將 EntryPoint 設置爲「MyMethod」且它存在的話,則返回「MyMethod」。若是 DLL 中沒有「MyMethod」,但存在「MyMethodA」,則返回「MyMethodA」。對於 Unicode 來講則正好相反。若是將 EntryPoint 設置爲「MyMethod」且它存在的話,則返回「MyMethodW」。若是 DLL 中不存在「MyMethodW」,但存在「MyMethod」,則返回「MyMethod」。若是使用的是 Auto,則匹配規則與平臺有關(在 Windows NT 上爲 Unicode,在 Windows 98 上爲 Ansi)。若是 ExactSpelling 設置爲 true,則只有當 DLL 中存在「MyMethod」時才返回「MyMethod」。三、EntryPoint 指示要調用的 DLL 入口點的名稱或序號。若是你的方法名不想與api函數同名的話,必定要指定此參數,例如:[DllImport("user32.dll",CharSet="CharSet.Auto",EntryPoint="MessageBox")]public static extern int MsgBox(IntPtr hWnd,string txt,string caption, int type);四、ExactSpelling 指示是否應修改非託管 DLL 中的入口點的名稱,以與 CharSet 字段中指定的 CharSet 值相對應。若是爲 true,則當 DllImportAttribute.CharSet 字段設置爲 CharSet 的 Ansi 值時,向方法名稱中追加字母 A,當 DllImportAttribute.CharSet 字段設置爲 CharSet 的 Unicode 值時,向方法的名稱中追加字母 W。此字段的默認值是 false。五、PreserveSig 指示託管方法簽名不該轉換成返回 HRESULT、而且可能有一個對應於返回值的附加 [out, retval] 參數的非託管簽名。六、SetLastError 指示被調用方在從屬性化方法返回以前將調用 Win32 API SetLastError。 true 指示調用方將調用SetLastError,默認爲 false。運行時封送拆收器將調用 GetLastError 並緩存返回的值,以防其被其餘 API 調用重寫。用戶可經過調用 GetLastWin32Error 來檢索錯誤代碼。2、參數類型:一、數值型直接用對應的就可。(DWORD -> int , WORD -> Int16)二、API中字符串指針類型 -> .net中string三、API中句柄 (dWord) -> .net中IntPtr四、API中結構 -> .net中結構或者類。注意這種狀況下,要先用StructLayout特性限定聲明結構或類公共語言運行庫利用StructLayoutAttribute控制類或結構的數據字段在託管內存中的物理佈局,即類或結構須要按某種方式排列。若是要將類傳遞給須要指定佈局的非託管代碼,則顯式控制類佈局是重要的。它的構造函數中用LayoutKind值初始化 StructLayoutAttribute 類的新實例。 LayoutKind.Sequential 用於強制將成員按其出現的順序進行順序佈局。LayoutKind.Explicit 用於控制每一個數據成員的精確位置。利用 Explicit,每一個成員必須使用 FieldOffsetAttribute 指示此字段在類型中的位置。如:[StructLayout(LayoutKind.Explicit, Size=16, CharSet=CharSet.Ansi)]public class MySystemTime {[FieldOffset(0)]public ushort wYear; [FieldOffset(2)]public ushort wMonth;[FieldOffset(4)]public ushort wDayOfWeek; [FieldOffset(6)]public ushort wDay; [FieldOffset(8)]public ushort wHour; [FieldOffset(10)]public ushort wMinute; [FieldOffset(12)]public ushort wSecond; [FieldOffset(14)]public ushort wMilliseconds; }下面是針對API中OSVERSIONINFO結構,在.net中定義對應類或結構的例子:/*********************************************** API中定義原結構聲明* OSVERSIONINFOA STRUCT* dwOSVersionInfoSize DWORD ?* dwMajorVersion DWORD ?* dwMinorVersion DWORD ?* dwBuildNumber DWORD ?* dwPlatformId DWORD ?* szCSDVersion BYTE 128 dup (?)* OSVERSIONINFOA ENDS** OSVERSIONINFO equ <OSVERSIONINFOA>*********************************************///.net中聲明爲類[ StructLayout( LayoutKind.Sequential )] public class OSVersionInfo { public int OSVersionInfoSize;public int majorVersion; public int minorVersion;public int buildNumber;public int platformId;[ MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )] public String versionString;}//或者//.net中聲明爲結構[ StructLayout( LayoutKind.Sequential )] public struct OSVersionInfo2 {public int OSVersionInfoSize;public int majorVersion; public int minorVersion;public int buildNumber;public int platformId;[ MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )] public String versionString;}此例中用到MashalAs特性,它用於描述字段、方法或參數的封送處理格式。用它做爲參數前綴並指定目標須要的數據類型。例如,如下代碼將兩個參數做爲數據類型長指針封送給 Windows API 函數的字符串 (LPStr):[MarshalAs(UnmanagedType.LPStr)]String existingfile;[MarshalAs(UnmanagedType.LPStr)]String newfile;注意結構做爲參數時候,通常前面要加上ref修飾符,不然會出現錯誤:對象的引用沒有指定對象的實例。[ DllImport( "kernel32", EntryPoint="GetVersionEx" )] public static extern bool GetVersionEx2( ref OSVersionInfo2 osvi ); 3、如何保證使用託管對象的平臺調用成功?若是在調用平臺 invoke 後的任何位置都未引用託管對象,則垃圾回收器可能將完成該託管對象。這將釋放資源並使句柄無效,從而致使平臺invoke 調用失敗。用 HandleRef 包裝句柄可保證在平臺 invoke 調用完成前,不對託管對象進行垃圾回收。例以下面:FileStream fs = new FileStream( "a.txt", FileMode.Open );StringBuilder buffer = new StringBuilder( 5 );int read = 0;ReadFile(fs.Handle, buffer, 5, out read, 0 ); //調用Win API中的ReadFile函數因爲fs是託管對象,因此有可能在平臺調用還未完成時候被垃圾回收站回收。將文件流的句柄用HandleRef包裝後,就能避免被垃圾站回收:[ DllImport( "Kernel32.dll" )]public static extern bool ReadFile( HandleRef hndRef, StringBuilder buffer, int numberOfBytesToRead, out int numberOfBytesRead, ref Overlapped flag );............FileStream fs = new FileStream( "HandleRef.txt", FileMode.Open );HandleRef hr = new HandleRef( fs, fs.Handle );StringBuilder buffer = new StringBuilder( 5 );int read = 0;// platform invoke will hold reference to HandleRef until call endsReadFile( hr, buffer, 5, out read, 0 );我在本身最近的編程中注意到一個趨勢,正是這個趨勢才引出本月的專欄主題。最近,我在基於 Microsoft? .NET Framework 的應用程序中完成了大量的 Win32? Interop。我並非要說個人應用程序充滿了自定義的 interop 代碼,但有時我會在 .NET Framework 類庫中碰到一些次要但又繁絮、不充分的內容,經過調用該 Windows? API,能夠快速減小這樣的麻煩。所以我認爲,.NET Framework 1.0 或 1.1 版類庫中存在任何 Windows 所沒有的功能限制都不足爲怪。畢竟,32 位的 Windows(無論何種版本)是一個成熟的操做系統,爲廣大客戶服務了十多年。相比之下,.NET Framework 倒是一個新事物。隨着愈來愈多的開發人員將生產應用程序轉到託管代碼,開發人員更頻繁地研究底層操做系統以圖找出一些關鍵功能顯得很天然—至少目前是如此。值得慶幸的是,公共語言運行庫 (CLR) 的 interop 功能(稱爲平臺調用 (P/Invoke))很是完善。在本專欄中,我將重點介紹如何實際使用 P/Invoke 來調用 Windows API 函數。當指 CLR 的 COM Interop 功能時,P/Invoke 看成名詞使用;當指該功能的使用時,則將其看成動詞使用。我並不打算直接介紹 COM Interop,由於它比 P/Invoke 具備更好的可訪問性,卻更加複雜,這有點自相矛盾,這使得將 COM Interop 做爲專欄主題來討論不太簡明扼要。走進 P/Invoke首先從考察一個簡單的 P/Invoke 示例開始。讓咱們看一看如何調用 Win32 MessageBeep 函數,它的非託管聲明如如下代碼所示:BOOL MessageBeep(UINT uType // beep type);爲了調用 MessageBeep,您須要在 C# 中將如下代碼添加到一個類或結構定義中:[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);使人驚訝的是,只須要這段代碼就可使託管代碼調用非託管的 MessageBeep API。它不是一個方法調用,而是一個外部方法定義。(另外,它接近於一個來自 C 而 C# 容許的直接端口,所以以它爲起點來介紹一些概念是有幫助的。)來自託管代碼的可能調用以下所示:MessageBeep(0);請注意,如今 MessageBeep 方法被聲明爲 static。這是 P/Invoke 方法所要求的,由於在該 Windows API 中沒有一致的實例概念。接下來,還要注意該方法被標記爲 extern。這是提示編譯器該方法是經過一個從 DLL 導出的函數實現的,所以不須要提供方法體。說到缺乏方法體,您是否注意到 MessageBeep 聲明並無包含一個方法體?與大多數算法由中間語言 (IL) 指令組成的託管方法不一樣,P/Invoke 方法只是元數據,實時 (JIT) 編譯器在運行時經過它將託管代碼與非託管的 DLL 函數鏈接起來。執行這種到非託管世界的鏈接所需的一個重要信息就是導出非託管方法的 DLL 的名稱。這一信息是由 MessageBeep 方法聲明以前的 DllImport 自定義屬性提供的。在本例中,能夠看到,MessageBeep 非託管 API 是由 Windows 中的 User32.dll 導出的。到如今爲止,關於調用 MessageBeep 就剩兩個話題沒有介紹,請回顧一下,調用的代碼與如下所示代碼片斷很是類似:[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);最後這兩個話題是與數據封送處理 (data marshaling) 和從託管代碼到非託管函數的實際方法調用有關的話題。調用非託管 MessageBeep 函數能夠由找到做用域內的extern MessageBeep 聲明的任何託管代碼執行。該調用相似於任何其餘對靜態方法的調用。它與其餘任何託管方法調用的共同之處在於帶來了數據封送處理的須要。C# 的規則之一是它的調用語法只能訪問 CLR 數據類型,例如 System.UInt32 和 System.Boolean。C# 顯然不識別 Windows API 中使用的基於 C 的數據類型(例如 UINT 和 BOOL),這些類型只是 C 語言類型的類型定義而已。因此當 Windows API 函數 MessageBeep 按如下方式編寫時BOOL MessageBeep( UINT uType )外部方法就必須使用 CLR 類型來定義,如您在前面的代碼片斷中所看到的。須要使用與基礎 API 函數類型不一樣但與之兼容的 CLR 類型是 P/Invoke 較難使用的一個方面。所以,在本專欄的後面我將用完整的章節來介紹數據封送處理。樣式在 C# 中對 Windows API 進行 P/Invoke 調用是很簡單的。但若是類庫拒絕使您的應用程序發出嘟聲,應該千方百計調用 Windows 使它進行這項工做,是嗎?是的。可是與選擇的方法有關,並且關係甚大!一般,若是類庫提供某種途徑來實現您的意圖,則最好使用 API 而不要直接調用非託管代碼,由於 CLR 類型和 Win32 之間在樣式上有很大的不一樣。我能夠將關於這個問題的建議歸結爲一句話。當您進行 P/Invoke 時,不要使應用程序邏輯直接屬於任何外部方法或其中的構件。若是您遵循這個小規則,從長遠看常常會省去許多的麻煩。圖 1 中的代碼顯示了我所討論的 MessageBeep 外部方法的最少附加代碼。圖 1 中並無任何顯著的變化,而只是對無包裝的外部方法進行一些普通的改進,這可使工做更加輕鬆一些。從頂部開始,您會注意到一個名爲 Sound 的完整類型,它專用於 MessageBeep。若是我須要使用 Windows API 函數 PlaySound 來添加對播放波形的支持,則能夠重用 Sound 類型。然而,我不會因公開單個公共靜態方法的類型而生氣。畢竟這只是應用程序代碼而已。還應該注意到,Sound 是密封的,並定義了一個空的私有構造函數。這些只是一些細節,目的是使用戶不會錯誤地從 Sound 派生類或者建立它的實例。圖 1 中的代碼的下一個特徵是,P/Invoke 出現位置的實際外部方法是 Sound 的私有方法。這個方法只是由公共 MessageBeep 方法間接公開,後者接受 BeepTypes 類型的參數。這個間接的額外層是一個很關鍵的細節,它提供瞭如下好處。首先,應該在類庫中引入一個將來的 beep 託管方法,能夠重複地經過公共 MessageBeep 方法來使用託管 API,而沒必要更改應用程序中的其他代碼。該包裝方法的第二個好處是:當您進行 P/Invoke 調用時,您放棄了免受訪問衝突和其餘低級破壞的權利,這一般是由 CLR 提供的。緩衝方法能夠保護您的應用程序的其他部分免受訪問衝突及相似問題的影響(即便它不作任何事而只是傳遞參數)。該緩衝方法將由 P/Invoke 調用引入的任何潛在的錯誤本地化。將私有外部方法隱藏在公共包裝後面的第三同時也是最後的一個好處是,提供了向該方法添加一些最小的 CLR 樣式的機會。例如,在圖 1 中,我將 Windows API 函數返回的 Boolean 失敗轉換成更像 CLR 的異常。我還定義了一個名爲 BeepTypes 的枚舉類型,它的成員對應於同該 Windows API 一塊兒使用的定義值。因爲 C# 不支持定義,所以可使用託管枚舉類型來避免幻數向整個應用程序代碼擴散。包裝方法的最後一個好處對於簡單的 Windows API 函數(如 MessageBeep)誠然是微不足道的。可是當您開始調用更復雜的非託管函數時,您會發現,手動將 Windows API 樣式轉換成對 CLR 更加友好的方法所帶來的好處會愈來愈多。越是打算在整個應用程序中重用 interop 功能,越是應該認真地考慮包裝的設計。同時我認爲,在非面向對象的靜態包裝方法中使用對 CLR 友好的參數也並不是不能夠。DLL Import 屬性如今是更深刻地進行探討的時候了。在對託管代碼進行 P/Invoke 調用時,DllImportAttribute 類型扮演着重要的角色。DllImportAttribute 的主要做用是給 CLR 指示哪一個 DLL 導出您想要調用的函數。相關 DLL 的名稱被做爲一個構造函數參數傳遞給 DllImportAttribute。若是您沒法確定哪一個 DLL 定義了您要使用的 Windows API 函數,Platform SDK 文檔將爲您提供最好的幫助資源。在 Windows API 函數主題文字臨近結尾的位置,SDK 文檔指定了 C 應用程序要使用該函數必須連接的 .lib 文件。在幾乎全部的狀況下,該 .lib 文件具備與定義該函數的系統 DLL 文件相同的名稱。例如,若是該函數須要 C 應用程序連接到 Kernel32.lib,則該函數就定義在 Kernel32.dll 中。您能夠在 MessageBeep 中找到有關 MessageBeep 的 Platform SDK 文檔主題。在該主題結尾處,您會注意到它指出庫文件是 User32.lib;這代表 MessageBeep 是從 User32.dll 中導出的。可選的 DllImportAttribute 屬性除了指出宿主 DLL 外,DllImportAttribute 還包含了一些可選屬性,其中四個特別有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。EntryPoint 在不但願外部託管方法具備與 DLL 導出相同的名稱的狀況下,能夠設置該屬性來指示導出的 DLL 函數的入口點名稱。當您定義兩個調用相同非託管函數的外部方法時,這特別有用。另外,在 Windows 中還能夠經過它們的序號值綁定到導出的 DLL 函數。若是您須要這樣作,則諸如「#1」或「#129」的 EntryPoint 值指示 DLL 中非託管函數的序號值而不是函數名。CharSet 對於字符集,並不是全部版本的 Windows 都是一樣建立的。Windows 9x 系列產品缺乏重要的 Unicode 支持,而 Windows NT 和 Windows CE 系列則一開始就使用 Unicode。在這些操做系統上運行的 CLR 將Unicode 用於 String 和 Char 數據的內部表示。但也沒必要擔憂—當調用 Windows 9x API 函數時,CLR 會自動進行必要的轉換,將其從 Unicode轉換爲 ANSI。若是 DLL 函數不以任何方式處理文本,則能夠忽略 DllImportAttribute 的 CharSet 屬性。然而,當 Char 或 String 數據是等式的一部分時,應該將 CharSet 屬性設置爲 CharSet.Auto。這樣可使 CLR 根據宿主 OS 使用適當的字符集。若是沒有顯式地設置 CharSet 屬性,則其默認值爲 CharSet.Ansi。這個默認值是有缺點的,由於對於在 Windows 2000、Windows XP 和 Windows NT? 上進行的 interop 調用,它會消極地影響文本參數封送處理的性能。應該顯式地選擇 CharSet.Ansi 或 CharSet.Unicode 的 CharSet 值而不是使用 CharSet.Auto 的惟一狀況是:您顯式地指定了一個導出函數,而該函數特定於這兩種 Win32 OS 中的某一種。ReadDirectoryChangesW API 函數就是這樣的一個例子,它只存在於基於 Windows NT 的操做系統中,而且只支持 Unicode;在這種狀況下,您應該顯式地使用 CharSet.Unicode。有時,Windows API 是否有字符集關係並不明顯。一種決不會有錯的確認方法是在 Platform SDK 中檢查該函數的 C 語言頭文件。(若是您沒法確定要看哪一個頭文件,則能夠查看 Platform SDK 文檔中列出的每一個 API 函數的頭文件。)若是您發現該 API 函數確實定義爲一個映射到以 A 或 W 結尾的函數名的宏,則字符集與您嘗試調用的函數有關係。Windows API 函數的一個例子是在 WinUser.h 中聲明的 GetMessage API,您也許會驚訝地發現它有 A 和 W 兩種版本。SetLastError 錯誤處理很是重要,但在編程時常常被遺忘。當您進行 P/Invoke 調用時,也會面臨其餘的挑戰—處理託管代碼中 Windows API 錯誤處理和異常之間的區別。我能夠給您一點建議。若是您正在使用 P/Invoke 調用 Windows API 函數,而對於該函數,您使用 GetLastError 來查找擴展的錯誤信息,則應該在外部方法的 DllImportAttribute 中將 SetLastError 屬性設置爲 true。這適用於大多數外部方法。這會致使 CLR 在每次調用外部方法以後緩存由 API 函數設置的錯誤。而後,在包裝方法中,能夠經過調用類庫的 System.Runtime.InteropServices.Marshal 類型中定義的 Marshal.GetLastWin32Error 方法來獲取緩存的錯誤值。個人建議是檢查這些指望來自 API 函數的錯誤值,併爲這些值引起一個可感知的異常。對於其餘全部失敗狀況(包括根本就沒意料到的失敗狀況),則引起在 System.ComponentModel 命名空間中定義的 Win32Exception,並將 Marshal.GetLastWin32Error 返回的值傳遞給它。若是您回頭看一下圖 1 中的代碼,您會看到我在 extern MessageBeep 方法的公共包裝中就採用了這種方法。CallingConvention 我將在此介紹的最後也多是最不重要的一個 DllImportAttribute 屬性是 CallingConvention。經過此屬性,能夠給 CLR 指示應該將哪一種函數調用約定用於堆棧中的參數。CallingConvention.Winapi 的默認值是最好的選擇,它在大多數狀況下均可行。然而,若是該調用不起做用,則能夠檢查 Platform SDK 中的聲明頭文件,看看您調用的 API 函數是不是一個不符合調用約定標準的異常 API。一般,本機函數(例如 Windows API 函數或 C- 運行時 DLL 函數)的調用約定描述瞭如何將參數推入線程堆棧或從線程堆棧中清除。大多數 Windows API 函數都是首先將函數的最後一個參數推入堆棧,而後由被調用的函數負責清理該堆棧。相反,許多 C-運行時 DLL 函數都被定義爲按照方法參數在方法簽名中出現的順序將其推入堆棧,將堆棧清理工做交給調用者。幸運的是,要讓 P/Invoke 調用工做只須要讓外圍設備理解調用約定便可。一般,從默認值 CallingConvention.Winapi 開始是最好的選擇。而後,在 C 運行時 DLL 函數和少數函數中,可能須要將約定更改成 CallingConvention.Cdecl。數據封送處理數據封送處理是 P/Invoke 具備挑戰性的方面。當在託管和非託管代碼之間傳遞數據時,CLR 遵循許多規則,不多有開發人員會常常遇到它們直至可將這些規則記住。除非您是一名類庫開發人員,不然在一般狀況下沒有必要掌握其細節。爲了最有效地在 CLR 上使用 P/Invoke,即便只偶爾須要 interop 的應用程序開發人員仍然應該理解數據封送處理的一些基礎知識。在本月專欄的剩餘部分中,我將討論簡單數字和字符串數據的數據封送處理。我將從最基本的數字數據封送處理開始,而後介紹簡單的指針封送處理和字符串封送處理。封送數字和邏輯標量Windows OS 大部分是用 C 編寫的。所以,Windows API 所用到的數據類型要麼是 C 類型,要麼是經過類型定義或宏定義從新標記的 C 類型。讓咱們看看沒有指針的數據封送處理。簡單起見,首先重點討論的是數字和布爾值。當經過值向 Windows API 函數傳遞參數時,須要知道如下問題的答案:? 數據從根本上講是整型的仍是浮點型的?? 若是數據是整型的,則它是有符號的仍是無符號的?? 若是數據是整型的,則它的位數是多少?? 若是數據是浮點型的,則它是單精度的仍是雙精度的?有時答案很明顯,但有時卻不明顯。Windows API 以各類方式從新定義了基本的 C 數據類型。圖 2 列出了 C 和 Win32 的一些公共數據類型及其規範,以及一個具備匹配規範的公共語言運行庫類型。一般,只要您選擇一個其規範與該參數的 Win32 類型相匹配的 CLR 類型,您的代碼就可以正常工做。不過也有一些特例。例如,在 Windows API 中定義的 BOOL 類型是一個有符號的 32 位整型。然而,BOOL 用於指示 Boolean 值 true 或 false。雖然您不用將 BOOL 參數做爲 System.Int32 值封送,可是若是使用 System.Boolean 類型,就會得到更合適的映射。字符類型的映射相似於 BOOL,由於有一個特定的 CLR 類型 (System.Char) 指出字符的含義。在瞭解這些信息以後,逐步介紹示例多是有幫助的。依然採用 beep 主題做爲例子,讓咱們來試一下 Kernel32.dll 低級 Beep,它會經過計算機的揚聲器發生嘟聲。這個方法的 Platform SDK 文檔能夠在 Beep 中找到。本機 API 按如下方式進行記錄:BOOL Beep(DWORD dwFreq, // FrequencyDWORD dwDuration // Duration in milliseconds);在參數封送處理方面,您的工做是瞭解什麼 CLR 數據類型與 Beep API 函數所使用的 DWORD 和 BOOL 數據類型相兼容。回顧一下圖 2 中的圖表,您將看到 DWORD 是一個 32 位的無符號整數值,如同 CLR 類型 System.UInt32。這意味着您可使用 UInt32 值做爲送往 Beep 的兩個參數。BOOL 返回值是一個很是有趣的狀況,由於該圖表告訴咱們,在 Win32 中,BOOL 是一個 32 位的有符號整數。所以,您可使用 System.Int32 值做爲來自 Beep 的返回值。然而,CLR 也定義了 System.Boolean 類型做爲 Boolean 值的語義,因此應該使用它來替代。CLR 默認將 System.Boolean 值封送爲 32 位的有符號整數。此處所顯示的外部方法定義是用於 Beep 的結果 P/Invoke 方法:[DllImport("Kernel32.dll", SetLastError=true)]static extern Boolean Beep(UInt32 frequency, UInt32 duration);指針參數許多 Windows API 函數將指針做爲它們的一個或多個參數。指針增長了封送數據的複雜性,由於它們增長了一個間接層。若是沒有指針,您能夠經過值在線程堆棧中傳遞數據。有了指針,則能夠經過引用傳遞數據,方法是將該數據的內存地址推入線程堆棧中。而後,函數經過內存地址間接訪問數據。使用託管代碼表示此附加間接層的方式有多種。在 C# 中,若是將方法參數定義爲 ref 或 out,則數據經過引用而不是經過值傳遞。即便您沒有使用 Interop 也是這樣,但只是從一個託管方法調用到另外一個託管方法。例如,若是經過 ref 傳遞 System.Int32 參數,則在線程堆棧中傳遞的是該數據的地址,而不是整數值自己。下面是一個定義爲經過引用接收整數值的方法的示例:void FlipInt32(ref Int32 num){num = -num;}這裏,FlipInt32 方法獲取一個 Int32 值的地址、訪問數據、對它求反,而後將求反過的值賦給原始變量。在如下代碼中,FlipInt32 方法會將調用程序的變量 x 的值從 10 更改成 -10:Int32 x = 10;FlipInt32(ref x);在託管代碼中能夠重用這種能力,將指針傳遞給非託管代碼。例如,FileEncryptionStatus API 函數以 32 位無符號位掩碼的形式返回文件加密狀態。該 API 按如下所示方式進行記錄:BOOL FileEncryptionStatus(LPCTSTR lpFileName, // file nameLPDWORD lpStatus // encryption status);請注意,該函數並不使用它的返回值返回狀態,而是返回一個 Boolean 值,指示調用是否成功。在成功的狀況下,實際的狀態值是經過第二個參數返回的。它的工做方式是調用程序向該函數傳遞指向一個 DWORD 變量的指針,而該 API 函數用狀態值填充指向的內存位置。如下代碼片斷顯示了一個調用非託管 FileEncryptionStatus 函數的可能外部方法定義:[DllImport("Advapi32.dll", CharSet=CharSet.Auto)]static extern Boolean FileEncryptionStatus(String filename, out UInt32 status);該定義使用 out 關鍵字來爲 UInt32 狀態值指示 by-ref 參數。這裏我也能夠選擇 ref 關鍵字,實際上在運行時會產生相同的機器碼。out 關鍵字只是一個 by-ref 參數的規範,它向 C# 編譯器指示所傳遞的數據只在被調用的函數外部傳遞。相反,若是使用 ref 關鍵字,則編譯器會假定數據能夠在被調用的函數的內部和外部傳遞。託管代碼中 out 和 ref 參數的另外一個很好的方面是,地址做爲 by-ref 參數傳遞的變量能夠是線程堆棧中的一個本地變量、一個類或結構的元素,也能夠是具備合適數據類型的數組中的一個元素引用。調用程序的這種靈活性使得 by-ref 參數成爲封送緩衝區指針以及單數值指針的一個很好的起點。只有在我發現 ref 或 out 參數不符合個人須要的狀況下,我纔會考慮將指針封送爲更復雜的 CLR 類型(例如類或數組對象)。若是您不熟悉 C 語法或者調用 Windows API 函數,有時很難知道一個方法參數是否須要指針。一個常見的指示符是看參數類型是不是以字母 P 或 LP 開頭的,例如 LPDWORD 或 PINT。在這兩個例子中,LP 和 P 指示參數是一個指針,而它們指向的數據類型分別爲 DWORD 或 INT。然而,在有些狀況下,能夠直接使用 C 語言語法中的星號 (*) 將 API 函數定義爲指針。如下代碼片斷展現了這方面的示例:void TakesAPointer(DWORD* pNum);能夠看到,上述函數的惟一一個參數是指向 DWORD 變量的指針。當經過 P/Invoke 封送指針時,ref 和 out 只用於託管代碼中的值類型。當一個參數的 CLR 類型使用 struct 關鍵字定義時,能夠認爲該參數是一個值類型。Out 和 ref 用於封送指向這些數據類型的指針,由於一般值類型變量是對象或數據,而在託管代碼中並無對值類型的引用。相反,當封送引用類型對象時,並不須要 ref 和 out 關鍵字,由於變量已是對象的引用了。若是您對引用類型和值類型之間的差異不是很熟悉,請查閱 2000 年 12 月發行的 MSDN? Magazine,在 .NET 專欄的主題中能夠找到更多信息。大多數 CLR 類型都是引用類型;然而,除了 System.String 和 System.Object,全部的基元類型(例如 System.Int32 和 System.Boolean)都是值類型。封送不透明 (Opaque) 指針:一種特殊狀況有時在 Windows API 中,方法傳遞或返回的指針是不透明的,這意味着該指針值從技術角度講是一個指針,但代碼卻不直接使用它。相反,代碼將該指針返回給 Windows 以便隨後進行重用。一個很是常見的例子就是句柄的概念。在 Windows 中,內部數據結構(從文件到屏幕上的按鈕)在應用程序代碼中都表示爲句柄。句柄其實就是不透明的指針或有着指針寬度的數值,應用程序用它來表示內部的 OS 構造。少數狀況下,API 函數也將不透明指針定義爲 PVOID 或 LPVOID 類型。在 Windows API 的定義中,這些類型意思就是說該指針沒有類型。當一個不透明指針返回給您的應用程序(或者您的應用程序指望獲得一個不透明指針)時,您應該將參數或返回值封送爲 CLR 中的一種特殊類型— System.IntPtr。當您使用 IntPtr 類型時,一般不使用 out 或 ref 參數,由於 IntPtr 意爲直接持有指針。不過,若是您將一個指針封送爲一個指針,則對 IntPtr 使用 by-ref 參數是合適的。在 CLR 類型系統中,System.IntPtr 類型有一個特殊的屬性。不像系統中的其餘基類型,IntPtr 並無固定的大小。相反,它在運行時的大小是依底層操做系統的正常指針大小而定的。這意味着在 32 位的 Windows 中,IntPtr 變量的寬度是 32 位的,而在 64 位的 Windows 中,實時編譯器編譯的代碼會將 IntPtr 值看做 64 位的值。當在託管代碼和非託管代碼之間封送不透明指針時,這種自動調節大小的特色十分有用。請記住,任何返回或接受句柄的 API 函數其實操做的就是不透明指針。您的代碼應該將 Windows 中的句柄封送成 System.IntPtr 值。您能夠在託管代碼中將 IntPtr 值強制轉換爲 32 位或 64 位的整數值,或將後者強制轉換爲前者。然而,當使用 Windows API 函數時,由於指針應是不透明的,因此除了存儲和傳遞給外部方法外,不能將它們另作它用。這種「只限存儲和傳遞」規則的兩個特例是當您須要向外部方法傳遞 null 指針值和須要比較 IntPtr 值與 null 值的狀況。爲了作到這一點,您不能將零強制轉換爲 System.IntPtr,而應該在 IntPtr 類型上使用 Int32.Zero 靜態公共字段,以便得到用於比較或賦值的 null 值。封送文本在編程時常常要對文本數據進行處理。文本爲 interop 製造了一些麻煩,這有兩個緣由。首先,底層操做系統可能使用 Unicode 來表示字符串,也可能使用 ANSI。在極少數狀況下,例如 MultiByteToWideChar API 函數的兩個參數在字符集上是不一致的。第二個緣由是,當須要進行 P/Invoke 時,要處理文本還須要特別瞭解到 C 和 CLR 處理文本的方式是不一樣的。在 C 中,字符串實際上只是一個字符值數組,一般以 null 做爲結束符。大多數 Windows API 函數是按照如下條件處理字符串的:對於 ANSI,將其做爲字符值數組;對於 Unicode,將其做爲寬字符值數組。幸運的是,CLR 被設計得至關靈活,當封送文本時問題得以輕鬆解決,而不用在乎 Windows API 函數指望從您的應用程序獲得的是什麼。這裏是一些須要記住的主要考慮事項:? 是您的應用程序向 API 函數傳遞文本數據,仍是 API 函數向您的應用程序返回字符串數據?或者兩者兼有?? 您的外部方法應該使用什麼託管類型?? API 函數指望獲得的是什麼格式的非託管字符串?咱們首先解答最後一個問題。大多數 Windows API 函數都帶有 LPTSTR 或 LPCTSTR 值。(從函數角度看)它們分別是可修改和不可修改的緩衝區,包含以 null 結束的字符數組。「C」表明常數,意味着使用該參數信息不會傳遞到函數外部。LPTSTR 中的「T」代表該參數能夠是 Unicode 或 ANSI,取決於您選擇的字符集和底層操做系統的字符集。由於在 Windows API 中大多數字符串參數都是這兩種類型之一,因此只要在 DllImportAttribute 中選擇 CharSet.Auto,CLR 就按默認的方式工做。然而,有些 API 函數或自定義的 DLL 函數採用不一樣的方式表示字符串。若是您要用到一個這樣的函數,就能夠採用 MarshalAsAttribute 修飾外部方法的字符串參數,並指明一種不一樣於默認 LPTSTR 的字符串格式。有關 MarshalAsAttribute 的更多信息,請參閱位於 MarshalAsAttribute Class 的 Platform SDK 文檔主題。如今讓咱們看一下字符串信息在您的代碼和非託管函數之間傳遞的方向。有兩種方式能夠知道處理字符串時信息的傳遞方向。第一個也是最可靠的一個方法就是首先理解參數的用途。例如,您正調用一個參數,它的名稱相似 CreateMutex 並帶有一個字符串,則能夠想像該字符串信息是從應用程序向 API 函數傳遞的。同時,若是您調用 GetUserName,則該函數的名稱代表字符串信息是從該函數向您的應用程序傳遞的。除了這種比較合理的方法外,第二種查找信息傳遞方向的方式就是查找 API 參數類型中的字母「C」。例如,GetUserName API 函數的第一個參數被定義爲 LPTSTR 類型,它表明一個指向 Unicode 或 ANSI 字符串緩衝區的長指針。可是 CreateMutex 的名稱參數被類型化爲 LTCTSTR。請注意,這裏的類型定義是同樣的,但增長一個字母「C」來代表緩衝區爲常數,API 函數不能寫入。一旦明確了文本參數是隻用做輸入仍是用做輸入/輸出,就能夠肯定使用哪一種 CLR 類型做爲參數類型。這裏有一些規則。若是字符串參數只用做輸入,則使用 System.String 類型。在託管代碼中,字符串是不變的,適合用於不會被本機 API 函數更改的緩衝區。若是字符串參數能夠用做輸入和/或輸出,則使用 System.StringBuilder 類型。StringBuilder 類型是一個頗有用的類庫類型,它能夠幫助您有效地構建字符串,也正好能夠將緩衝區傳遞給本機函數,由本機函數爲您填充字符串數據。一旦函數調用返回,您只須要調用 StringBuilder 對象的 ToString 就能夠獲得一個 String 對象。GetShortPathName API 函數能很好地用於顯示何時使用 String、何時使用 StringBuilder,由於它只帶有三個參數:一個輸入字符串、一個輸出字符串和一個指明輸出緩衝區的字符長度的參數。圖 3 所示爲加註釋的非託管 GetShortPathName 函數文檔,它同時指出了輸入和輸出字符串參數。它引出了託管的外部方法定義,也如圖 3 所示。請注意第一個參數被封送爲 System.String,由於它是一個只用做輸入的參數。第二個參數表明一個輸出緩衝區,它使用了 System.StringBuilder。小結本月專欄所介紹的 P/Invoke 功能足夠調用 Windows 中的許多 API 函數。然而,若是您大量用到 interop,則會最終發現本身封送了很複雜的數據結構,甚至可能須要在託管代碼中經過指針直接訪問內存。實際上,本機代碼中的 interop 能夠是一個將細節和低級比特藏在裏面的真正的潘多拉盒子。CLR、C# 和託管 C++ 提供了許多有用的功能;也許之後我會在本專欄介紹高級的 P/Invoke 話題。同時,只要您以爲 .NET Framework 類庫沒法播放您的聲音或者爲您執行其餘一些功能,您能夠知道如何向原始而優秀的 Windows API 尋求一些幫助。API(應用編程接口)是程序與處理器接口的命令集。最經常使用的就是在外部調用微軟WINDOWS內部的進程。WINDOWS API包括成千的你可使用的函數、結構、常量。這些函數是用C語言寫的,在使用他們以前,你必須聲明。定義Dll的進程將至關的複雜,甚至比VB還複雜。你可使用API Viewer工具獲得API函數的聲明,可是必須注意的是,它的參數類型跟C#的不同。大部分的高級語言都支持API,微軟函數類庫(MFC)封裝了大部分的Win32 API。ODBC API對提升數據庫的操做速度大有好處。使用API,能夠請求更底層的系統服務。API從簡單的對話框到複雜的加密運算都提供支持。開發者應該知道如何在他們程序中使用API API有許多類型,(針對不一樣的操做系統、處理器…………)OS specific API: 操做系統特有API: 每種操做系統都有一套公用API和專有API。好比:Windows NT 支持MS-DOS, Win16, Win32, POSIX (便攜式操做系統接口),OS/2 console API ;同時Windows 95 supports MS-DOS, Win16 和Win32 API。Win16 和 Win32 API: WIN16 是基於16位的處理器,並使用16位的值,它是一個獨立的平臺。好比:你能夠運行TSR 程序在MS-DOS環境下。WIN32 是基於32位的處理器,並使用32位的值。他可用於任何操做系統,它的使用範圍更廣。Win32 API has 32 prefix after the library name e.g. KERNEL32, USER32 etc? Win32 API的DLL通常都具備32的後綴,好比:KERNEL32, USER32等。全部的API都在下面3個DLL中實現的。Kernel User GDI 1. KERNEL 它的庫名是:KERNEL32.DLL,它是操做系統管理的API集Process loading. 加載進程Context switching. File I/O. 文件操做Memory management. 內存管理好比:GlobalMemoryStatus 函數得到目前系統物理虛擬內存的使用信息。2. USER 在WIN32下,它的庫名是 USER32.DLL This allows managing the entire user interfaces such as 它管理所有的用戶界面,好比:Windows 窗口Menus 菜單Dialog Boxes 對話框Icons etc., 圖標等好比:DrawIcon 畫一個圖標在指定的設備上。3. GDI (Graphical Device Interface) 這個DLL是GDI32.dll,它負責圖像的輸出,使用GDI繪出窗口,菜單,對話框It can create Graphical Output. 輸出圖像好比:CreateBitmap 函數建立一個指定寬度、高度和顏色格式的位圖。C#中API的工具對初學者是至關不錯的。在C#使用中使用API以前,你應該先知道C#中如何使用結構、類型轉換,安全與不安全代碼等。使用複雜的api以前,咱們先用一個簡單的MessageBox API做爲列子。打開一個C#工程,增長一個按鈕,在按鈕的點擊事件中,咱們將顯示一個信息框。增長使用外部庫的命名空間:using System.Runtime.InteropServices; 下面聲明API [DllImport("User32.dll")] DllImport屬性用來指定包含外部方法的動態鏈接庫的位置。 "User32.dll"指出了庫名,static 指明它不屬於特定的對象。extern 指明是一個外部的方法。帶有DllImport 屬性的方法必須帶有修飾符extern 。MessageBox 是一個漢數名,帶四個參數返回一個int型值。許多API使用結構來傳遞、返回參數,這樣能夠減小複雜度。它也容許使用象MessageBox 函數那樣,使用固定的參數。在按鈕的點擊事件中增長下面代碼:protected void button1_Click (object sender, System.EventArgs e) { MessageBox (0,"API Message Box","API Demo",0); } 編譯並運行程序,點擊按鈕之後,你就能夠看到一個由API調用的信息框。Using structure 使用結構API中常用複雜的結構。不過一旦你明白了他們,將是很簡單的。In next example we will use GetSystemInfo API which returns information about the current system. 下面的列子,咱們用GetSystemInfo API獲得當前系統的信息。第一步:增長一個C#窗口,並在上面增長一個按鈕,在窗口代碼頁增長一個命名空間:using System.Runtime.InteropServices; 聲明 GetSystemInfo 的參數結構: [StructLayout(LayoutKind.Sequential)] public struct SYSTEM_INFO { public uint dwOemId; public uint dwPageSize; public uint lpMinimumApplicationAddress; public uint lpMaximumApplicationAddress; public uint dwActiveProcessorMask; public uint dwNumberOfProcessors; public uint dwProcessorType; public uint dwAllocationGranularity; public uint dwProcessorLevel; public uint dwProcessorRevision; }聲明API函數:[DllImport("kernel32")] static extern void GetSystemInfo(ref SYSTEM_INFO pSI); ref是一個標誌參量傳遞形式的關鍵字,它使傳入傳出的變量指向同一個變量(傳址傳遞)在按鈕點擊事件中增長下面的代碼,protected void button1_Click (object sender, System.EventArgs e) { try {SYSTEM_INFO pSI = new SYSTEM_INFO(); GetSystemInfo(ref pSI); Once you retrieve the structure perform operations on required parameter 好比:listBox1.Items.Insert(0,pSI.dwActiveProcessorMask.ToString()); } catch(Exception er) { MessageBox.Show (er.Message); } }用Visual C#調用Windows API函數北京機械工業學院研00級(100085)冉林倉 Api函數是構築Windws應用程序的基石,每一種Windows應用程序開發工具,它提供的底層函數都間接或直接地調用了Windows API函數,同時爲了實現功能擴展,通常也都提供了調用WindowsAPI函數的接口,也就是說具有調用動態鏈接庫的能力。Visual C#和其它開發工具同樣也可以調用動態連接庫的API函數。.NET框架自己提供了這樣一種服務,容許受管轄的代碼調用動態連接庫中實現的非受管轄函數,包括操做系統提供的Windows API函數。它可以定位和調用輸出函數,根據須要,組織其各個參數(整型、字符串類型、數組、和結構等等)跨越互操做邊界。下面以C#爲例簡單介紹調用API的基本過程: 動態連接庫函數的聲明  動態連接庫函數使用前必須聲明,相對於VB,C#函數聲明顯得更加羅嗦,前者經過 Api Viewer粘貼之後,能夠直接使用,然後者則須要對參數做些額外的變化工做。 動態連接庫函數聲明部分通常由下列兩部分組成,一是函數名或索引號,二是動態連接庫的文件名。  譬如,你想調用User32.DLL中的MessageBox函數,咱們必須指明函數的名字MessageBoxA或MessageBoxW,以及庫名字User32.dll,咱們知道Win32 API對每個涉及字符串和字符的函數通常都存在兩個版本,單字節字符的ANSI版本和雙字節字符的UNICODE版本。 下面是一個調用API函數的例子: [DllImport("KERNEL32.DLL", EntryPoint="MoveFileW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool MoveFile(String src, String dst);  其中入口點EntryPoint標識函數在動態連接庫的入口位置,在一個受管轄的工程中,目標函數的原始名字和序號入口點不只標識一個跨越互操做界限的函數。並且,你還能夠把這個入口點映射爲一個不一樣的名字,也就是對函數進行重命名。重命名能夠給調用函數帶來種種便利,經過重命名,一方面咱們不用爲函數的大小寫傷透腦筋,同時它也能夠保證與已有的命名規則保持一致,容許帶有不一樣參數類型的函數共存,更重要的是它簡化了對ANSI和Unicode版本的調用。CharSet用於標識函數調用所採用的是Unicode或是ANSI版本,ExactSpelling=false將告訴編譯器,讓編譯器決定使用Unicode或者是Ansi版本。其它的參數請參考MSDN在線幫助.  在C#中,你能夠在EntryPoint域經過名字和序號聲明一個動態連接庫函數,若是在方法定義中使用的函數名與DLL入口點相同,你不須要在EntryPoint域顯示聲明函數。不然,你必須使用下列屬性格式指示一個名字和序號。[DllImport("dllname", EntryPoint="Functionname")] [DllImport("dllname", EntryPoint="#123")] 值得注意的是,你必須在數字序號前加「#」 下面是一個用MsgBox替換MessageBox名字的例子: [C#] using System.Runtime.InteropServices; public class Win32 { [DllImport("user32.dll", EntryPoint="MessageBox")] public static extern int MsgBox(int hWnd, String text, String caption, uint type); } 許多受管轄的動態連接庫函數指望你可以傳遞一個複雜的參數類型給函數,譬如一個用戶定義的結構類型成員或者受管轄代碼定義的一個類成員,這時你必須提供額外的信息格式化這個類型,以保持參數原有的佈局和對齊。C#提供了一個StructLayoutAttribute類,經過它你能夠定義本身的格式化類型,在受管轄代碼中,格式化類型是一個用StructLayoutAttribute說明的結構或類成員,經過它可以保證其內部成員預期的佈局信息。佈局的選項共有三種:佈局選項 描述 LayoutKind.Automatic 爲了提升效率容許運行態對類型成員從新排序。 注意:永遠不要使用這個選項來調用不受管轄的動態連接庫函數。 LayoutKind.Explicit 對每一個域按照FieldOffset屬性對類型成員排序 LayoutKind.Sequential 對出如今受管轄類型定義地方的不受管轄內存中的類型成員進行排序。 傳遞結構成員 下面的例子說明如何在受管轄代碼中定義一個點和矩形類型,並做爲一個參數傳遞給User32.dll庫中的PtInRect函數, 函數的不受管轄原型聲明以下: BOOL PtInRect(const RECT *lprc, POINT pt); 注意你必須經過引用傳遞Rect結構參數,由於函數須要一個Rect的結構指針。 [C#] using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] public struct Point { public int x; public int y; } [StructLayout(LayoutKind.Explicit] public struct Rect { [FieldOffset(0)] public int left; [FieldOffset(4)] public int top; [FieldOffset(8)] public int right; [FieldOffset(12)] public int bottom; } class Win32API { [DllImport("User32.dll")] public static extern Bool PtInRect(ref Rect r, Point p); } 相似你能夠調用GetSystemInfo函數得到系統信息: ? using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] public struct SYSTEM_INFO { public uint dwOemId; public uint dwPageSize; public uint lpMinimumApplicationAddress; public uint lpMaximumApplicationAddress; public uint dwActiveProcessorMask; public uint dwNumberOfProcessors; public uint dwProcessorType; public uint dwAllocationGranularity; public uint dwProcessorLevel; public uint dwProcessorRevision; } [DllImport("kernel32")] static extern void GetSystemInfo(ref SYSTEM_INFO pSI); SYSTEM_INFO pSI = new SYSTEM_INFO(); GetSystemInfo(ref pSI); 類成員的傳遞 一樣只要類具備一個固定的類成員佈局,你也能夠傳遞一個類成員給一個不受管轄的動態連接庫函數,下面的例子主要說明如何傳遞一個sequential順序定義的MySystemTime類給User32.dll的GetSystemTime函數, 函數用C/C++調用規範以下: void GetSystemTime(SYSTEMTIME* SystemTime); 不像傳值類型,類老是經過引用傳遞參數. [C#] [StructLayout(LayoutKind.Sequential)] public class MySystemTime { public ushort wYear; public ushort wMonth; public ushort wDayOfWeek; public ushort wDay; public ushort wHour; public ushort wMinute; public ushort wSecond; public ushort wMilliseconds; } class Win32API { [DllImport("User32.dll")] public static extern void GetSystemTime(MySystemTime st); } 回調函數的傳遞: 從受管轄的代碼中調用大多數動態連接庫函數,你只需建立一個受管轄的函數定義,而後調用它便可,這個過程很是直接。 若是一個動態連接庫函數須要一個函數指針做爲參數,你還須要作如下幾步: 首先,你必須參考有關這個函數的文檔,肯定這個函數是否須要一個回調;第二,你必須在受管轄代碼中建立一個回調函數;最後,你能夠把指向這個函數的指針做爲一個參數創遞給DLL函數,. 回調函數及其實現: 回調函數常常用在任務須要重複執行的場合,譬如用於枚舉函數,譬如Win32 API 中的EnumFontFamilies(字體枚舉), EnumPrinters(打印機), EnumWindows (窗口枚舉)函數. 下面以窗口枚舉爲例,談談如何經過調用EnumWindow 函數遍歷系統中存在的全部窗口分下面幾個步驟: 1. 在實現調用前先參考函數的聲明 BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARMAM IParam) 顯然這個函數須要一個回調函數地址做爲參數. 2. 建立一個受管轄的回調函數,這個例子聲明爲表明類型(delegate),也就是咱們所說的回調,它帶有兩個參數hwnd和lparam,第一個參數是一個窗口句柄,第二個參數由應用程序定義,兩個參數均爲整形。  當這個回調函數返回一個非零值時,標示執行成功,零則暗示失敗,這個例子老是返回True值,以便持續枚舉。 3. 最後建立以表明對象(delegate),並把它做爲一個參數傳遞給EnumWindows 函數,平臺會自動地把表明轉化成函數可以識別的回調格式。[C#] using System; using System.Runtime.InteropServices; public delegate bool CallBack(int hwnd, int lParam); public class EnumReportApp { [DllImport("user32")] public static extern int EnumWindows(CallBack x, int y); public static void Main() { CallBack myCallBack = new CallBack(EnumReportApp.Report); EnumWindows(myCallBack, 0); } public static bool Report(int hwnd, int lParam) { Console.Write("窗口句柄爲"); Console.WriteLine(hwnd); return true; } } 指針類型參數傳遞:  在Windows API函數調用時,大部分函數採用指針傳遞參數,對一個結構變量指針,咱們除了使用上面的類和結構方法傳遞參數以外,咱們有時還能夠採用數組傳遞參數。 下面這個函數經過調用GetUserName得到用戶名 BOOL GetUserName( LPTSTR lpBuffer, // 用戶名緩衝區 LPDWORD nSize // 存放緩衝區大小的地址指針 );   [DllImport("Advapi32.dll", EntryPoint="GetComputerName", ExactSpelling=false, SetLastError=true)] static extern bool GetComputerName ( [MarshalAs(UnmanagedType.LPArray)] byte[] lpBuffer,   [MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );  這個函數接受兩個參數,char * 和int *,由於你必須分配一個字符串緩衝區以接受字符串指針,你可使用String類代替這個參數類型,固然你還能夠聲明一個字節數組傳遞ANSI字符串,一樣你也能夠聲明一個只有一個元素的長整型數組,使用數組名做爲第二個參數。上面的函數能夠調用以下:byte[] str=new byte[20]; Int32[] len=new Int32[1]; len[0]=20; GetComputerName (str,len); MessageBox.Show(System.Text.Encoding.ASCII.GetString(str));  最後須要提醒的是,每一種方法使用前必須在文件頭加上:  using System.Runtime.InteropServices;

相關文章
相關標籤/搜索