C#中使用DLL函数之前,你必须使用DllImport声明要调用的函数:
public class Win32 {
[DllImport("User32.Dll")]
public static extern void SetWindowText(int h, String s);
// 函数原型为:BOOL SetWindowText(HWND hWnd, LPCTSTR lpString);
}
DllImport告诉编译器被调函数的入口在哪里,并且将该入口绑定到类中你声明的函数。你可以给这个类起任意的名字,我给它命名为Win32。你甚至可以将类放到命名空间中,具体参见图一。要编译Win32API.cs,输入:
csc /t:library /out:Win32API.dll Win32API.cs
这样你就拥有了Win32API.dll,并且你可以在任意的C#项目中使用它:
using Win32API;
int hwnd = // get it...
String s = "I'm so cute."
Win32.SetWindowText(hwnd, s);
编译器知道去user32.dll中查找函数SetWindowText,并且在调用前自动将String转换为LPTSTR (TCHAR*)。很惊奇是吧!那么.NET是如何做到的呢?每种C#类型有一个默认的marshal类型,String对应LPTSTR。但你若是试着调用GetWindowText会怎么样呢(此处字符串作为out参数,而不是in参数)?它无法正常调用,是因为String是无法修改的,你必须使用StringBuilder:
using System.Text; // for StringBuilder
public class Win32 {
[DllImport("user32.dll")]
public static extern int GetWindowText(int hwnd,
StringBuilder buf, int nMaxCount);
// 函数原型:int GetWindowText(HWND hWnd, LPTSTR lpString, int nMaxCount);
}
StringBuilder默认的marshal类型是LPTSTR,此时GetWindowText可以修改你的字符串:
int hwnd = // get it...
StringBuilder cb = new StringBuilder(256);
Win32.GetWindowText(hwnd, sb, sb.Capacity);
如果默认的类型转换无法满足你的要求,比如调用函数GetClassName,它总是将参数转换为类型LPSTR (char *),即便在定义Unicode的情况下使用,CLR仍然会将你传递的参数转换为TCHAR类型。不过不用着急,你可以使用MarshalAs覆盖掉默认的类型:
[DllImport("user32.dll")]
public static extern int GetClassName(int hwnd,
[MarshalAs(UnmanagedType.LPStr)] StringBuilder buf,
int nMaxCount);
// 函数原型:int GetClassNameA(HWND hWnd, LPTSTR lpClassName, int nMaxCount);
这样当你调用GetClassName时,.NET将字符串作为ANSI字符传递,而不是宽字符。
结构体和回调函数类型的参数又是如何传递的呢?.NET有一种方法可以处理它们。举个简单的例子,GetWindowRect,这个函数获取窗口的屏幕坐标,C++中我们这么处理:
// in C/C++
RECT rc;
HWND hwnd = FindWindow("foo",NULL);
::GetWindowRect(hwnd, &rc);
你可以使用C#结构体,只需使用另外一种C#属性StructLayout:
[StructLayout(LayoutKind.Sequential)]
public struct RECT {
public int left;
public int top;
public int right;
public int bottom;
}
一旦你定义了上面的结构体,你可以使用下面的函数声明形式 :
[DllImport("user32.dll")]
public static extern int
GetWindowRect(int hwnd, ref RECT rc);
// 函数原型:BOOL GetWindowRect(HWND hWnd, LPRECT lpRect);
使用ref标识很重要,以至于CLR(通用语言运行时)将RECT变量作为引用传递到函数中,而不是无意义的栈拷贝。
定义了GetWindowRect之后,你就可以采用下面的方式调用:
RECT rc = new RECT();
int hwnd = // get it ...
Win32.GetWindowRect(hwnd, ref rc);
注意你同样需要像声明中的那样使用ref关键字。C#结构体默认的marshal类型是LPStruct,因此没有必要使用MarshalAs。
但如果你使用了类RECT而不是结构体RECT,那么你必须使用如下的声明形式:
// if RECT is a class, not struct
[DllImport("user32.dll")]
public static extern int
GetWindowRect(int hwnd,
[MarshalAs(UnmanagedType.LPStruct)] RECT rc);
C#和C++一样,一件事情有很多种实现方式。System.Drawing中已经有Rectangle结构体,用来处理矩形,那有为什么要“重新发明轮子”呢?
[DllImport("user32.dll")]
public static extern int GetWindowRect(int hwnd, ref Rectangle rc);
最后,又是怎样从C#中传递回调函数到非托管代码中的呢?你所要做的就是委托(delegate)
delegate bool EnumWindowsCB(int hwnd, int lparam);
一旦你声明了你的回调函数,那么你需要调用的函数声明为:
[DllImport("user32")]
public static extern int
EnumWindows(EnumWindowsCB cb, int lparam);
// 函数原型:BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
由于上面的委托仅仅是声明了委托类型,你需要在你的类中提供实际的回调函数代码。
// in your class
public static bool MyEWP(int hwnd, int lparam) {
// do something
return true;
}
然后传递给相应的委托变量:
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, 0);
你可能注意到参数lparam。在C语言中,如果你传递参数LPARAM给EnumWindows,Windows将它作为参数调用你的回调函数。通常lparam是包含了你要做的事情的上下文结构体或类指针,记住,在.NET中没有指针的概念!那该怎么做呢?上面的例子中,你可以申明lparam为IntPtr类型,并且使用GCHandle来封装它:
// lparam is IntPtr now
delegate bool EnumWindowsCB(int hwnd, IntPtr lparam);
// wrap object in GCHandle
MyClass obj = new MyClass();
GCHandle gch = GCHandle.Alloc(obj);
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, (IntPtr)gch);
gch.Free();
不要忘了使用完之后手动释放它!有时,你需要按照以前那种方式在C#中释放内存。可以使用GCHandle.Target的方式在你的回调函数中使用“指针”。
public static bool MyEWP(int hwnd, IntPtr param) {
GCHandle gch = (GCHandle)param;
MyClass c = (MyClass)gch.Target;
// ... use it
return true;
图2是将EnumWindows封装到数组中的类。你只需要按如下的方式使用即可,而不要纠结于委托和回调中。
WindowArray wins = new WindowArray();
foreach (int hwnd in wins) {
// do something
}