對在C#中的消息應用的資料收集

給你提供一點:C#中調用Windows API的要點,來自【CSDN - 文檔中心】
在.Net Framework SDK文檔中,關於調用Windows API的指示比較零散,並且其中稍全面一點的是針對Visual Basic .net講述的。本文將C#中調用API的要點彙集如下,希望給未在C#中使用過API的朋友一點幫助。另外如果安裝了Visual Studio .net的話,在C:/Program Files/Microsoft Visual Studio .NET/FrameworkSDK/Samples/Technologies/Interop/PlatformInvoke/WinAPIs/CS目錄下有大量的調用API的例子。
一、調用格式
using System.Runtime.InteropServices; //引用此名稱空間,簡化後面的代碼
...
//使用DllImportAttribute特性來引入api函數,注意聲明的是空方法,即方法體爲空。
[DllImport("user32.dll")]
public static extern ReturnType FunctionName(type arg1,type arg2,...);
//調用時與調用其他方法並無區別

可以使用字段進一步說明特性,用逗號隔開,如:
[ DllImport( "kernel32", EntryPoint="GetVersionEx" )]
DllImportAttribute特性的公共字段如下:
1、CallingConvention 指示向非託管實現傳遞方法參數時所用的 CallingConvention 值。
  CallingConvention.Cdecl : 調用方清理堆棧。它使您能夠調用具有 varargs 的函數。
  CallingConvention.StdCall : 被調用方清理堆棧。它是從託管代碼調用非託管函數的默認約定。
2、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”。

3、EntryPoint 指示要調用的 DLL 入口點的名稱或序號。
  如果你的方法名不想與api函數同名的話,一定要指定此參數,例如:
[DllImport("user32.dll",CharSet="CharSet.Auto",EntryPoint="MessageBox")]
public static extern int MsgBox(IntPtr hWnd,string txt,string caption, int type);

4、ExactSpelling 指示是否應修改非託管 DLL 中的入口點的名稱,以與 CharSet 字段中指定的 CharSet 值相對應。如果爲 true,則當 DllImportAttribute.CharSet 字段設置爲 CharSet 的 Ansi 值時,向方法名稱中追加字母 A,當 DllImportAttribute.CharSet 字段設置爲 CharSet 的 Unicode 值時,向方法的名稱中追加字母 W。此字段的默認值是 false。
5、PreserveSig 指示託管方法簽名不應轉換成返回 HRESULT、並且可能有一個對應於返回值的附加 [out, retval] 參數的非託管簽名。
6、SetLastError 指示被調用方在從屬性化方法返回之前將調用 Win32 API SetLastError。 true 指示調用方將調用 SetLastError,默認爲 false。運行時封送拆收器將調用 GetLastError 並緩存返回的值,以防其被其他 API 調用重寫。用戶可通過調用 GetLastWin32Error 來檢索錯誤代碼。

二、參數類型:
1、數值型直接用對應的就可。(DWORD -> int , WORD -> Int16)
2、API中字符串指針類型 -> .net中string
3、API中句柄 (dWord)  -> .net中IntPtr
4、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 );

三、如何保證使用託管對象的平臺調用成功?
    如果在調用平臺 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 ends
        ReadFile( hr, buffer, 5, out read, 0 );

======================================================================================================
說來說去,還是覺得API的功能是最強大的,但是.NET FCL,MFC等對API的封裝之後也使得程序的開發變得更加容易。本模塊的主要原理還是使用API,查找指定類型,窗口文本的窗口對象,獲取該對象的指針。然後操作該對象。


實例1:

創建一個C#Windows Form應用程序,向窗口中添加一個按鈕button1,添加事件相應函數:
複製  保存private void button1_Click(object sender, System.EventArgs e)
{
    MessageBox.Show("This is button1 click!");
}

實例2:

創建一個C# Windows Form應用程序,添加一個按鈕控件button1

1:添加using System.Runtime.InteropServices;

2: 添加對API的引用:
複製  保存[DllImport("user32.dll")]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("user32.dll")]
public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr PostMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
3:添加button1的相應函數:
複製  保存private void button1_Click(object sender, System.EventArgs e)
{
    IntPtr hwnd_win;          // 存放實例1中的Form1窗口的窗口句柄
    IntPtr hwnd_button;     // 存放實例1中的Form1中的button1控件的窗口句柄

    // 參數1:窗口類型,參數2:窗口名稱

    hwnd_win = FindWindow("WindowsForms10.Window.8.app3", "Form1"); // 得到Form1窗口的句柄。


    // 參數1:父窗口句柄,   參數2:子窗口指針;參數3:窗口類型;參數4:窗口文本

    hwnd_button = FindWindowEx(hwnd_win, new IntPtr(0), "WindowsForms10.BUTTON.app3", "button1");

    // 定義待發送的消息 

    const int BM_CLICK = 0x00F5;
    Message msg = Message.Create(hwnd_button, BM_CLICK, new IntPtr(0), new IntPtr(0));


    // 向Form1窗口的button1控件發送BM_CLICK消息  

    PostMessage(msg.HWnd, msg.Msg, msg.WParam, msg.LParam);
}

總結:

其實C#幕後還是採用的消息處理機制,本創許也充分利用了Windows的消息處理機之。

附帶一個獲取窗口類型的技巧:使用SPY ++就可以獲取任何窗口的窗口類型。

所有的類似於WM_CHAR,WM_COMMAND等消息的值,可以在.Net目錄下的WinUser.h文件中查詢到


===============================================================================================

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
{
  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 函數。下面是一個示例:

DWORD SetEntriesInAcl(
 ULONG cCountOfExplicitEntries,      // 項數
 PEXPLICIT_ACCESS pListOfExplicitEntries, // 緩衝區
 PACL OldAcl,               // 原始 ACL
 PACL *NewAcl               // 新 ACL
);

  前兩個參數的處理比較簡單:ulong 很簡單,並且可以使用 UnmanagedType.LPArray 來封送緩衝區。

  但第三和第四個參數有一些問題。問題在於定義 ACL 的方式。ACL 結構僅定義了 ACL 標頭,而緩衝區的其餘部分由 ACE 組成。ACE 可以具有多種不同類型,並且這些不同類型的 ACE 的長度也不同。

  如果您願意爲所有緩衝區分配空間,並且願意使用不太安全的代碼,則可以用 C# 進行處理。但工作量很大,並且程序非常難調試。而使用 C++ 處理此 API 就容易得多。

  屬性的其他選項

  DLLImport 和 StructLayout 屬性具有一些非常有用的選項,有助於 P/Invoke 的使用。下面列出了所有這些選項:

  DLLImport

  CallingConvention

  您可以用它來告訴封送拆收器,函數使用了哪些調用約定。您可以將它設置爲您的函數的調用約定。通常,如果此設置錯誤,代碼將不能執行。但是,如果您的函數是 Cdecl 函數,並且使用 StdCall(默認)來調用該函數,那麼函數能夠執行,但函數參數不會從堆棧中刪除,這會導致堆棧被填滿。

  CharSet

  控制調用 A 變體還是調用 W 變體。

  EntryPoint

  此屬性用於設置封送拆收器在 DLL 中查找的名稱。設置此屬性後,您可以將 C# 函數重新命名爲任何名稱。

  ExactSpelling

  將此屬性設置爲 true,封送拆收器將關閉 A 和 W 的查找特性。

  PreserveSig

  COM 互操作使得具有最終輸出參數的函數看起來是由它返回的該值。此屬性用於關閉這一特性。

  SetLastError

  確保調用 Win32 API SetLastError(),以便您找出發生的錯誤。

  StructLayout

  LayoutKind

  結構在默認情況下按順序佈局,並且在多數情況下都適用。如果需要完全控制結構成員所放置的位置,可以使用 LayoutKind.Explicit,然後爲每個結構成員添加 FieldOffset 屬性。當您需要創建 union 時,通常需要這樣做。

  CharSet

  控制 ByValTStr 成員的默認字符類型。

  Pack

  設置結構的壓縮大小。它控制結構的排列方式。如果 C 結構採用了其他壓縮方式,您可能需要設置此屬性。

  Size

  設置結構大小。不常用;但是如果需要在結構末尾分配額外的空間,則可能會用到此屬性。

  從不同位置加載

  您無法指定希望 DLLImport 在運行時從何處查找文件,但是可以利用一個技巧來達到這一目的。
     DllImport 調用 LoadLibrary() 來完成它的工作。如果進程中已經加載了特定的 DLL,那麼即使指定的加載路徑不同,LoadLibrary() 也會成功。

  這意味着如果直接調用 LoadLibrary(),您就可以從任何位置加載 DLL,然後 DllImport LoadLibrary() 將使用該 DLL。

  由於這種行爲,我們可以提前調用 LoadLibrary(),從而將您的調用指向其他 DLL。如果您在編寫庫,可以通過調用 GetModuleHandle() 來防止出現這種情況,以確保在首次調用 P/Invoke 之前沒有加載該庫。

  P/Invoke 疑難解答

  如果您的 P/Invoke 調用失敗,通常是因爲某些類型的定義不正確。以下是幾個常見問題:

  1.long != long。在 C++ 中,long 是 4 字節的整數,但在 C# 中,它是 8 字節的整數。

  2.字符串類型設置不正確。


==================================================================================================
Net平臺在消息處理上的編程和Windows C++有很大的不同,Net對消息處理更加面向對象,但對於截獲某些Window消息的處理並不讓人很適應, 這裏根據我們遇到的一些需求寫了點東西供大家參考.

  有這樣一個需求, 一個文本框, 用戶要求只能輸入數字, 不可以輸入字符, 但又不願意在保存的時候提醒, 而是直接讓字符輸不進來.

這時我們就要截獲字符輸入消息, 避免讓它顯示在文本框裏, 看下面的代碼:

public class MyTextBox:TextBox
 {
  private const int WM_CHAR=0x0102;  //定義在WinUser.h中, 位於這個目錄:.../Vc7/PlatformSDK/Include

  public override bool PreProcessMessage(ref Message msg)
  {
   if(msg.Msg==WM_CHAR)  
   {
    Keys keyCode=(Keys)(int)msg.WParam & Keys.KeyCode; 
    if(keyCode<Keys.D0 || keyCode>Keys.D9)
     if(keyCode!=Keys.Back && keyCode!=Keys.Delete)
           return true;  // 這行代碼可以截住輸入, 從而避免顯示
   }  

   return base.PreProcessMessage (ref msg);
  }

}

這裏講兩個方法:PreProcessMessage 和 WndProc. 它們都是Control的虛方法, 可以被覆蓋從而爲我所用. 這兩個方法只對於本控件有效, 與別的控件, 無論是子控件還是父控件.統統無關. Net裏的消息如果當前控件不處理它, 它就被丟棄, 不會傳給父控件. 它不再沿用VC的消息機制, 這點要記住.

PreProcessMessage:  Preprocesses input messages within the message loop before they are dispatched.

WndProc: Processes Windows messages.

我們可以看到, 這兩個方法的分工不同, 前一個是處理那些輸入消息,比如鍵盤上的輸入鍵. 後一個是處理單純的消息, 比如鼠標點擊,激活等.

這裏一個例子是截獲雙擊事件:

private const int WM_LBUTTONDBLCLK=0x0203;

protected override void WndProc(ref Message m)
  {
   if(m.Msg==WM_LBUTTONDBLCLK)
   {
    System.Diagnostics.Debug.WriteLine("wndproc");
    return;
   }
   base.WndProc(ref m);
  }

在這裏, 處理完事件後, 用return來截獲, 如果不加return這句代碼, 消息將會繼續得到處理, 如果你又定義了一個雙擊的事件處理方法後, 這個方法將被觸發:

private void textBox1_DoubleClick(object sender, System.EventArgs e)
  {
   System.Diagnostics.Debug.WriteLine("double click");
  }

Net 裏的每個控件都定義了十分豐富的消息事件, 一般情況下都可以滿足你的需求, 所以WndProc重寫並不普遍.這一切由你來權衡, 有的人想把所有的消息處理放到一起, 用這種方式也未嘗不可.

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章