用Visual C#呼叫Windows API函式
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;