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:\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;