C#win32API编程(超详细)(转载)
C# win32 API编程
C# ⽤户经常提出两个问题:“我为什么要另外编写代码来使⽤内置于 Windows 中的功能?在框架中为什么没有相应的内容可以为我完成这⼀任务?”当框架⼩组构建他们的 .NET 部分时,他们评估了为使 .NET 程序员可以使⽤ Win32 ⽽需要完成的⼯作,结果发现 Win32 API 集⾮常庞⼤。他们没有⾜够的资源为所有 Win32 API 编写托管接⼝、加以测试并编写⽂档,因此只能优先处理最重要的部分。许多常⽤操作都有托管接⼝,但是还有许多完整的 Win32 部分没有托管接⼝。
  平台调⽤ (P/Invoke) 是完成这⼀任务的最常⽤⽅法。要使⽤ P/Invoke,您可以编写⼀个描述如何调⽤函数的原型,然后运⾏时将使⽤此信息进⾏调⽤。另⼀种⽅法是使⽤ Managed Extensions to C++ 来包装函数,这部分内容将在以后的专栏中介绍。
  要理解如何完成这⼀任务,最好的办法是通过⽰例。在某些⽰例中,我只给出了部分代码;完整的代码可以通过下载获得。
  简单⽰例
  在第⼀个⽰例中,我们将调⽤ Beep() API 来发出声⾳。⾸先,我需要为 Beep() 编写适当的定义。查看
MSDN 中的定义,我发现它具有以下原型:
BOOL Beep(
 DWORD dwFreq,   // 声⾳频率
 DWORD dwDuration  // 声⾳持续时间
);
  要⽤ C# 来编写这⼀原型,需要将 Win32 类型转换成相应的 C# 类型。由于 DWORD 是 4 字节的整数,因此我们可以使⽤ int 或 uint 作为 C# 对应类型。由于 int 是 CLS 兼容类型(可以⽤于所有 .NET 语⾔),以此⽐ uint 更常⽤,并且在多数情况下,它们之间的区别并不重要。bool 类型与 BOOL 对应。现在我们可以⽤ C# 编写以下原型:
public static extern bool Beep(int frequency, int duration);
  这是相当标准的定义,只不过我们使⽤了 extern 来指明该函数的实际代码在别处。此原型将告诉运⾏时如何调⽤函数;现在我们需要告诉它在何处到该函数。
  我们需要回顾⼀下 MSDN 中的代码。在参考信息中,我们发现 Beep() 是在 kernel32.lib 中定义的。这意味着运⾏时代码包含在kernel32.dll 中。我们在原型中添加 DllImport 属性将这⼀信息告诉运⾏时:
[DllImport("kernel32.dll")]
  这就是我们要做的全部⼯作。下⾯是⼀个完整的⽰例,它⽣成的随机声⾳在⼆⼗世纪六⼗年代的科幻电影中很常见。
using System;
using System.Runtime.InteropServices;
namespace Beep
{
class Class1
 {
   [DllImport("kernel32.dll")]
   public static extern bool Beep(int frequency, int duration);
   static void Main(string[] args)
   {
     Random random = new Random();
     for (int i = 0; i < 10000; i++)
     {
      Beep(random.Next(10000), 100);
}
   }
 }
}
  它的声响⾜以刺激任何听者!由于 DllImport 允许您调⽤ Win32 中的任何代码,因此就有可能调⽤恶意代码。所以您必须是完全受信任的⽤户,运⾏时才能进⾏ P/Invoke 调⽤。
  枚举和常量
  Beep() 可⽤于发出任意声⾳,但有时我们希望发出特定类型的声⾳,因此我们改⽤ MessageBeep()。MSDN 给出了以下原型:
BOOL MessageBeep(
 UINT uType // 声⾳类型
);
  这看起来很简单,但是从注释中可以发现两个有趣的事实。
  ⾸先,uType 参数实际上接受⼀组预先定义的常量。
  其次,可能的参数值包括 -1,这意味着尽管它被定义为 uint 类型,但 int 会更加适合。
  对于 uType 参数,使⽤ enum 类型是合乎情理的。MSDN 列出了已命名的常量,但没有就具体值给出
任何提⽰。由于这⼀点,我们需要查看实际的 API。
  如果您安装了 Visual Studio? 和 C++,则 Platform SDK 位于 \Program Files\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include 下。
  为查这些常量,我在该⽬录中执⾏了⼀个 findstr。
  findstr "MB_ICONHAND" *.h
  它确定了常量位于 winuser.h 中,然后我使⽤这些常量来创建我的 enum 和原型:
public enum BeepType
{
  SimpleBeep = -1,
  IconAsterisk = 0x00000040,
  IconExclamation = 0x00000030,
  IconHand = 0x00000010,
  IconQuestion = 0x00000020,
  Ok = 0x00000000,
}
[DllImport("user32.dll")]
public static extern bool MessageBeep(BeepType beepType);
  现在我可以⽤下⾯的语句来调⽤它: MessageBeep(BeepType.IconQuestion);
处理结构
  有时我需要确定我笔记本的电池状况。Win32 为此提供了电源管理函数。
  搜索 MSDN 可以到 GetSystemPowerStatus() 函数。
BOOL GetSystemPowerStatus(
 LPSYSTEM_POWER_STATUS lpSystemPowerStatus
)
;
  此函数包含指向某个结构的指针,我们尚未对此进⾏过处理。要处理结构,我们需要⽤ C# 定义结构。我们从⾮托管的定义开始:
typedef struct _SYSTEM_POWER_STATUS {
BYTE  ACLineStatus;
BYTE  BatteryFlag;
BYTE  BatteryLifePercent;
BYTE  Reserved1;
DWORD BatteryLifeTime;
DWORD BatteryFullLifeTime;
} SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;
  然后,通过⽤ C# 类型代替 C 类型来得到 C# 版本。
struct SystemPowerStatus
{
  byte ACLineStatus;
  byte batteryFlag;
  byte batteryLifePercent;
  byte reserved1;
  int batteryLifeTime;
  int batteryFullLifeTime;
}
  这样,就可以⽅便地编写出 C# 原型:
[DllImport("kernel32.dll")]
public static extern bool GetSystemPowerStatus(
  ref SystemPowerStatus systemPowerStatus);
  在此原型中,我们⽤“ref”指明将传递结构指针⽽不是结构值。这是处理通过指针传递的结构的⼀般⽅法。
  此函数运⾏良好,但是最好将 ACLineStatus 和 batteryFlag 字段定义为 enum:
  enum ACLineStatus: byte
  {
   Offline = 0,
   Online = 1,
   Unknown = 255,
  }
  enum BatteryFlag: byte
  {
   High = 1,
   Low = 2,
   Critical = 4,
   Charging = 8,
   NoSystemBattery = 128,
   Unknown = 255,
  }
  请注意,由于结构的字段是⼀些字节,因此我们使⽤ byte 作为该 enum 的基本类型。
  字符串
  虽然只有⼀种 .NET 字符串类型,但这种字符串类型在⾮托管应⽤中却有⼏项独特之处。可以使⽤具有内嵌字符数组的字符指针和结构,其中每个数组都需要正确的封送处理。
  在 Win32 中还有两种不同的字符串表⽰:
  ANSI
  Unicode
  最初的 Windows 使⽤单字节字符,这样可以节省存储空间,但在处理很多语⾔时都需要复杂的多字节编码。Windows NT? 出现后,它使⽤双字节的 Unicode 编码。为解决这⼀差别,Win32 API 采⽤了⾮常聪明的做法。它定义了 TCHAR 类型,该类型在 Win9x 平台上是单字节字符,在 WinNT 平台上是双字节 Unicode 字符。对于每个接受字符串或结构(其中包含字符数据)的函数,Win32 API 均定义了该结构的两种版本,⽤ A 后缀指明 Ansi 编码,⽤ W 指明 wide 编码(即 Unicode)。如果您将 C++ 程序编译为单字节,会获得 A 变体,如果编译为 Unicode,则获得 W 变体。Win9x 平台包含 Ansi 版本,⽽ WinNT 平台则包含 W 版本。
  由于 P/Invoke 的设计者不想让您为所在的平台操⼼,因此他们提供了内置的⽀持来⾃动使⽤ A 或 W 版本。如果您调⽤的函数不存在,互操作层将为您查并使⽤ A 或 W 版本。
  通过⽰例能够很好地说明字符串⽀持的⼀些精妙之处。
  简单字符串
  下⾯是⼀个接受字符串参数的函数的简单⽰例:
BOOL GetDiskFreeSpace(
LPCTSTR lpRootPathName,     // 根路径
LPDWORD lpSectorsPerCluster,  // 每个簇的扇区数
LPDWORD lpBytesPerSector,    // 每个扇区的字节数
LPDWORD lpNumberOfFreeClusters, // 可⽤的扇区数
LPDWORD lpTotalNumberOfClusters // 扇区总数
);
  根路径定义为 LPCTSTR。这是独⽴于平台的字符串指针。
  由于不存在名为 GetDiskFreeSpace() 的函数,封送拆收器将⾃动查“A”或“W”变体,并调⽤相应的函数。我们使⽤⼀个属性来告诉封送拆收器,API 所要求的字符串类型。
  以下是该函数的完整定义,就象我开始定义的那样:
[DllImport("kernel32.dll")]
static extern bool GetDiskFreeSpace(
 [MarshalAs(UnmanagedType.LPTStr)]
 string rootPathName,
  ref int sectorsPerCluster,
  ref int bytesPerSector,
  ref int numberOfFreeClusters,
  ref int totalNumberOfClusters);
  不幸的是,当我试图运⾏时,该函数不能执⾏。问题在于,⽆论我们在哪个平台上,封送拆收器在默认情况下都试图查 API 的 Ansi 版本,由于 LPTStr 意味着在 Windows NT 平台上会使⽤ Unicode 字符串,因此试图⽤ Unicode 字符串来调⽤ Ansi 函数就会失败。
  有两种⽅法可以解决这个问题:⼀种简单的⽅法是删除 MarshalAs 属性。如果这样做,将始终调⽤该
函数的 A 版本,如果在您所涉及的所有平台上都有这种版本,这是个很好的⽅法。但是,这会降低代码的执⾏速度,因为封送拆收器要将 .NET 字符串从 Unicode 转换为多字节,然后调⽤函数的 A 版本(将字符串转换回 Unicode),最后调⽤函数的 W 版本。
  要避免出现这种情况,您需要告诉封送拆收器,要它在 Win9x 平台上时查 A 版本,⽽在 NT 平台上时查 W 版本。要实现这⼀⽬的,可以将 CharSet 设置为 DllImport 属性的⼀部分:
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
  在我的⾮正式计时测试中,我发现这⼀做法⽐前⼀种⽅法快了⼤约百分之五。
  对于⼤多数 Win32 API,都可以对字符串类型设置 CharSet 属性并使⽤ LPTStr。但是,还有⼀些不采⽤ A/W 机制的函数,对于这些函数必须采取不同的⽅法。
  字符串缓冲区
  .NET 中的字符串类型是不可改变的类型,这意味着它的值将永远保持不变。对于要将字符串值复制到字符串缓冲区的函数,字符串将⽆效。这样做⾄少会破坏由封送拆收器在转换字符串时创建的临时缓冲区;严重时会破坏托管堆,⽽这通常会导致错误的发⽣。⽆论哪种情况都不可能获得正确的返回值。
  要解决此问题,我们需要使⽤其他类型。StringBuilder 类型就是被设计为⽤作缓冲区的,我们将使⽤它来代替字符串。下⾯是⼀个⽰例:
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern int GetShortPathName(
  [MarshalAs(UnmanagedType.LPTStr)]
  string path,
  [MarshalAs(UnmanagedType.LPTStr)]
  StringBuilder shortPath,
  int shortPathLength);
  使⽤此函数很简单:
StringBuilder shortPath = new StringBuilder(80);
int result = GetShortPathName(@"d:\test.jpg", shortPath, shortPath.Capacity);
string s = shortPath.ToString();
  请注意,StringBuilder 的 Capacity 传递的是缓冲区⼤⼩。
  具有内嵌字符数组的结构
  某些函数接受具有内嵌字符数组的结构。例如,GetTimeZoneInformation() 函数接受指向以下结构的指针:
typedef struct _TIME_ZONE_INFORMATION {
  LONG    Bias;
  WCHAR   StandardName[ 32 ];
  SYSTEMTIME StandardDate;
  LONG    StandardBias;
  WCHAR   DaylightName[ 32 ];
  SYSTEMTIME DaylightDate;
  LONG    DaylightBias;
} TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION;
  在 C# 中使⽤它需要有两种结构。⼀种是 SYSTEMTIME,它的设置很简单:
  struct SystemTime
  {
   public short wYear;
   public short wMonth;
   public short wDayOfWeek;
   public short wDay;
   public short wHour;
   public short wMinute;
   public short wSecond;
   public short wMilliseconds;
  }
  这⾥没有什么特别之处;另⼀种是 TimeZoneInformation,它的定义要复杂⼀些:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct TimeZoneInformation字符串数组怎么转成byte
{
  public int bias;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
  public string standardName;
  SystemTime standardDate;
  public int standardBias;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
  public string daylightName;
  SystemTime daylightDate;
  public int daylightBias;
}
  此定义有两个重要的细节。第⼀个是 MarshalAs 属性:
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
  查看 ByValTStr 的⽂档,我们发现该属性⽤于内嵌的字符数组;另⼀个是 SizeConst,它⽤于设置数组的⼤⼩。
  我在第⼀次编写这段代码时,遇到了执⾏引擎错误。通常这意味着部分互操作覆盖了某些内存,表明结构的⼤⼩存在错误。我使⽤Marshal.SizeOf() 来获取所使⽤的封送拆收器的⼤⼩,结果是 108 字节。
我进⼀步进⾏了调查,很快回忆起⽤于互操作的默认字符类型是Ansi 或单字节。⽽函数定义中的字符类型为 WCHAR,是双字节,因此导致了这⼀问题。
  我通过添加 StructLayout 属性进⾏了更正。结构在默认情况下按顺序布局,这意味着所有字段都将以它们列出的顺序排列。CharSet 的值被设置为 Unicode,以便始终使⽤正确的字符类型。
  经过这样处理后,该函数⼀切正常。您可能想知道我为什么不在此函数中使⽤ CharSet.Auto。这是因为,它也没有 A 和 W 变体,⽽始终使⽤ Unicode 字符串,因此我采⽤了上述⽅法编码。
  具有回调的函数
  当 Win32 函数需要返回多项数据时,通常都是通过回调机制来实现的。开发⼈员将函数指针传递给函数,然后针对每⼀项调⽤开发⼈员的函数。
  在 C# 中没有函数指针,⽽是使⽤“委托”,在调⽤ Win32 函数时使⽤委托来代替函数指针。
  EnumDesktops() 函数就是这类函数的⼀个⽰例:
BOOL EnumDesktops(
 HWINSTA hwinsta,      // 窗⼝实例的句柄
 DESKTOPENUMPROC lpEnumFunc, // 回调函数
 LPARAM lParam        // ⽤于回调函数的值
);
  HWINSTA 类型由 IntPtr 代替,⽽ LPARAM 由 int 代替。DESKTOPENUMPROC 所需的⼯作要多⼀些。下⾯是 MSDN 中的定义:
BOOL CALLBACK EnumDesktopProc(
 LPTSTR lpszDesktop, // 桌⾯名称
 LPARAM lParam    // ⽤户定义的值
);
  我们可以将它转换为以下委托:
delegate bool EnumDesktopProc([MarshalAs(UnmanagedType.LPTStr)] string desktopName,int lParam);
  完成该定义后,我们可以为 EnumDesktops() 编写以下定义:
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern bool EnumDesktops(
  IntPtr windowStation,
  EnumDesktopProc callback,
  int lParam);
  这样该函数就可以正常运⾏了。
  在互操作中使⽤委托时有个很重要的技巧:封送拆收器创建了指向委托的函数指针,该函数指针被传递给⾮托管函数。但是,封送拆收器⽆法确定⾮托管函数要使⽤函数指针做些什么,因此它假定函数指针只需在调⽤该函数时有效即可。
  结果是如果您调⽤诸如 SetConsoleCtrlHandler() 这样的函数,其中的函数指针将被保存以便将来使⽤,您就需要确保在您的代码中引⽤委托。如果不这样做,函数可能表⾯上能执⾏,但在将来的内存回收处理中会删除委托,并且会出现错误。
  其他⾼级函数
  迄今为⽌我列出的⽰例都⽐较简单,但是还有很多更复杂的 Win32 函数。下⾯是⼀个⽰例:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。