Microsoft .NET Compact Framework 上的高级 P/Invoke

Microsoft .NET Compact Framework 上的高级 P/Invoke
发布日期 : 10/29/2004 | 更新日期 : 10/29/2004
Jon Box,Dan Fox
Quilogy
编著:
Jonathan Wells
Microsoft Corporation
适用于:
Microsoft_ .NET Compact Framework 1.0
Microsoft_ Visual Studio_ .NET 2003
摘要:探讨 .NET Compact Framework 的高级互操作性。
本页内容
简介
封送处理复杂类型
封送处理结构中的字符串
封送处理结构中的定长字符串
小结

简介
在上一篇文章“Microsoft .NET Compact Framework 上的 P/Invoke 和封送处理简介”中,我们讨论了 Microsoft .NET Compact Framework 和 Microsoft .NET Framework 中的平台调用服务如何允许托管代码调用驻留在非托管 DLL 中的函数,进而允许自定义以及操作系统 (Windows CE) API 可由为上述任何一种框架编写的应用程序访问。虽然此服务的很多功能在这两种框架中都是一样的,但由于 .NET Compact Framework 是完整的 .NET Framework 的一个子集,所以存在一些差异,有些差异我们已经在上一篇文章中进行了探讨。在本白皮书中,我们将集中讨论在封送处理结构时产生的两个特定问题,以及在 .NET Compact Framework 中如何对它们进行处理。
返回页首

封送处理复杂类型
正如上一篇文章中所提及的,.NET Compact Framework 中的封送拆收器和完整的 .NET Framework 中的封送拆收器之间的一个主要差异是:较轻型的 .NET Compact Framework 封送拆收器不能封送处理结构或类中的复杂对象(引用类型)。这就意味着,如果结构或类中有字段定义为在 .NET Compact Framework 和非托管代码之间不存在通用表示形式的类型(称为 blittable 类型,这些类型在上一篇文章中列出), 则该结构或类不能被完全地进行封送处理。从实际的角度来说,这意味着包含字符串指针或者定长字符缓冲的结构或类均不能被正确地封送处理。
作为一个例子,请考虑 Windows CE 上可用的用户通知 API。使用此 API,应用程序可以在特定的时间,或者在响应某个事件(比如同步)时,或者在更换 PC 卡时,显示通知对话框或引发某个应用程序的执行。因为 .NET Compact Framework 不包括执行此功能的托管类,所以需要该功能的开发人员就需要使用 P/Invoke 进行正确的操作系统调用。
要使用 Windows CE 通知 API (CeSetUserNotificationEx),用于定义什么事件激活此通知的结构 ( CE_NOTIFICATION_TRIGGER) 需要在托管代码中进行声明,并在 VB .NET 中进行如下的直接转换,其中 SYSTEMTIME 是另一个完全由 blittable 类型组成的结构, NotificationTypes 和 EventTypes 是映射至整数的枚举。
Private Structure CE_NOTIFICATION_TRIGGER
Dim dwSize As Integer
Dim dwType As NotificationTypes
Dim dwEvent As EventTypes
Dim lpszApplication As String
Dim lpszArguments As String
Dim startTime As SYSTEMTIME
Dim endTime As SYSTEMTIME
End Structure
不幸的是,用于指定要执行的应用程序及其命令行参数的两个字符串值在非托管代码中定义为指向以 null 终止的 Unicode 字符串 (WCHAR *) 的指针。因此,.NET Compact Framework 封送拆收器不能正确地封送处理该结构,因为 String 是引用类型 (System.String)。
注 正如我们上一篇文章所提及的, System.String 在 .NET Compact Framework 中是 blittable 类型,因为所有字符串均可视为 Unicode。但是,这只在将 String 直接传递给非托管函数时才适用,字符串用在结构或类中时并非如此。
在完整的 .NET Framework 中,封送拆收器可以处理这种情况,因为它包含 MarshalAsAttribute。使用此属性,该结构可被重写为:
Private Structure CE_NOTIFICATION_TRIGGER
Dim dwSize As Integer
Dim dwType As NotificationTypes
Dim dwEvent As EventTypes
Dim lpszApplication As String
Dim lpszArguments As String
Dim startTime As SYSTEMTIME
Dim endTime As SYSTEMTIME
End Structure
因此,在完整的 .NET Framework 中,结构中引用的字符串将被封送处理为以 null 终止的 Unicode 字符串,正如非托管函数所期望的那样。因为 .NET Compact Framework 不包括这种行为,所以您需要解决这个问题。
返回页首

封送处理结构中的字符串
要允许结构和类中的字符串指针被 .NET Compact Framework 正确地封送处理,有三个主要的解决方法:调用 thunking 层,使用不安全块,以及创建一个处理字符串指针的自定义类。
使用 thunking 层
术语 thunking 以前表示将参数和返回值从 16 位转换为 32 位表示方式(或者相反)的代码。但是,在更一般的用法中,该术语仅是指创建一些处理数据转换的中间代码。对于 .NET Compact Framework 和 P/Invoke,thunking 层是一个非托管函数,该函数接受构成该结构的参数,创建非托管结构,并调用适当的函数。这就是 Visual Studio .NET 帮助和 MSDN 中描述的在结构中传递复杂对象的方法。
要在上文的通知示例中使用该方法,您可以在嵌入式 Visual C++ 中创建一个非托管 DLL,该 DLL 导出一个接受 CE_NOTIFICATION_TRIGGER 结构的所有参数的函数。例如,您可以在 C 中创建一个使用通知服务( CeSetUserNotificationEx 函数)安排应用程序在特定时间运行的非托管函数,如下所示:
HANDLE RunApplicationThunk (WCHAR *lpszApplication,
WCHAR *lpszArguments, SYSTEMTIME startTime)
{
HANDLE returnCode;
CE_NOTIFICATION_TRIGGER trigger;
// populate the structure
trigger.dwSize = sizeof(trigger);
trigger.dwType = 2; //CNT_TIME
trigger.dwEvent = 0; //NONE
trigger.lpszApplication = lpszApplication;
trigger.lpszArguments = lpszArguments;
trigger.startTime = startTime;
trigger.endTime = 0; //empty, not used

// call the native Windows CE API function
returnCode = CeSetUserNotificationEx(0,&trigger, 0);
return returnCode;
}
注意,在上面的列表中,非托管函数默认设定了该结构的 dwType、dwEvent 和 endTime 成员,并且将这些参数传递给了 CeSetUserNotificationEx 函数。
在托管代码中,此函数使用 DLLImportAttribute 在 C# 中以如下方式进行声明。因为当字符串直接传递给非托管函数时 .NET Compact Framework 将其视为 blittable 类型,所以您的声明只需简单地使用字符串:
[DLLImport("MyNotification.DLL", SetLastError=true"]
private static extern IntPtr RunApplicationThunk(
string lpszApplication, string lpszArguments,
SYSTEMTIME startTime);
您的非托管 thunking 函数(以 C# 显示如下)应该封装到一个托管类中,正如我们的上一篇文章中所讨论的,并且通过该类的一个静态(VB 中的 shared)方法进行公开。
public static void RunApplication(string application, string arguments,
DateTime startTime)
{
// translate the DateTime values into managed SYSTEMTIME structures
SYSTEMTIME startStruct = DateTimeToSystemTime( start );

try
{
// call the thunking layer
IntPtr hNotify = RunApplicationThunk(application, arguments,
startStruct);
// handle errors
if(hNotify == IntPtr.Zero)
{
int errorNum = Marshal.GetLastWin32Error();
HandleCeError(New WinCeException("Could not set notification ",
errorNum), "RunApplication");
}
}
catch (Exception ex)
{
HandleCeError(ex, "RunApplication");
}
}
注意在这种情况下,您甚至不需要在托管代码中声明 CE_NOTIFICATION_TRIGGER 结构,因为托管方法可以编写为接受所有适当的参数,这样就更完全地封装了底层的操作系统交互。或者,您可以创建此结构的托管版本,如上文所示,然后只需将该结构传递给方法。如果遇到错误(返回句柄是零),则将创建一个自定义异常并以从操作系统获取的错误代码进行填充,正如上一篇文章中所讨论的一样。
注您可以看到,用于将托管代码中的 DateTime 变量转换为 SYSTEMTIME 结构的代码没有给出,它包括在自定义的方法 DateTimeToSystemTime 中。该方法调用位于 coredll.dll 中的 FileTimeToLocalFileTime 和 FileTimeToSystemFileTime Windows CE API 函数。
这种方法的明显不足就是开发人员必须安装和使用嵌入式 Visual C++,对于很多 Visual Basic 和 .NET Framework 开发人员而言,这是一个令人生畏的任务。正是由于这个原因,我们还将讨论解决相同问题的其它两种方法,这两种方法只使用 .NET Compact Framework 中的托管代码。
使用不安全字符串指针
传递结构中字符串的第二种方法是使用 C# 中的 unsafe 和 fixed 关键字(在 VB 中没有相对应的关键字)。虽然这种方法允许您只编写托管代码,但它是以禁用公共语言运行库的代码验证功能为代价的,该功能用于验证托管代码只访问分配的内存,并且所有的方法调用在参数数量和类型上都符合方法签名。使用不安全代码就意味着您希望使用指针对内存进行直接管理。
注正如上一篇文章中所述,除了创建无法验证的代码,完整的 .NET Framework 中的不安全代码只能在受信任的环境中执行。但 .NET Compact Framework 1.0 版中没有包括代码访问安全性 (CAS),所以目前这还不是问题。
fixed 关键字与 unsafe 关键字一起使用,它用于确保公共语言运行库的垃圾回收器 (GC) 在对象被非托管函数访问时不会试图释放该对象。
当然,除了丧失了代码验证功能以外,这种方法的另一个不足就是不能在 VB 中直接使用。但是,您可以在 C# 中创建一个使用不安全代码的程序集,然后从 VB 中调用该程序集。
要使用 unsafe 和 fixed 关键字,您需要将声明了指针的结构以及使用了指针的方法标记为不安全的。例如,可以使用不安全代码创建相同的 RunApplication 方法,如下所示。
[DllImport("coredll.dll",SetLastError=true)]
private static extern IntPtr CeSetUserNotificationEx(
IntPtr h,
ref CE_NOTIFICATION_TRIGGER nt,
IntPtr un
);

private unsafe struct CE_NOTIFICATION_TRIGGER
{
public uint dwSize;
public CNT_TYPE NotificationTypes; //enumeration
public NOTIFICATION_EVENT EventTypes; //enumeration
public char *lpszApplication;
public char *lpszArguments;
public SYSTEMTIME startTime;
public SYSTEMTIME endTime;
}

public static unsafe void RunApplication( string application,
string arguments, DateTime start )
{
CE_NOTIFICATION_TRIGGER nt = new CE_NOTIFICATION_TRIGGER();
nt.dwSize = (uint)Marshal.SizeOf( typeof(CE_NOTIFICATION_TRIGGER) );
nt.dwType = NotificationTypes.Time;
nt.dwEvent = EventTypes.None;
nt.startTime = DateTimeToSystemTime( start );
nt.endTime = new SYSTEMTIME(); //skip this member

try
{
if ((application == null) || (application.Length == 0))
{
throw new ArgumentNullException();
}
fixed (char *pApp = application.ToCharArray( ))
{
if ((arguments == null) || (arguments.Length == 0) )
{
arguments = " ";
}
fixed (char *pArgs = arguments.ToCharArray())
{
nt.lpszApplication = pApp;
nt.lpszArguments = pArgs;
// call the native function
IntPtr hNotify = CeSetUserNotificationEx(IntPtr.Zero,
ref nt, IntPtr.Zero);

if( hNotify == IntPtr.Zero )
{
int errorNum = Marshal.GetLastWin32Error();
HandleCeError(New WinCeException(
"Could not set notification ", errorNum),
"RunApplication");
}
}
}
}
catch (Exception ex)
{
HandleCeError(ex, " RunApplication");
}
}
在这个例子中,您可以看到我们首先使用 DllImportAttribute 对 CeSetUserNotificationEx Windows CE API 函数进行了声明。 CE_NOTIFICATION_TRIGGER 被标记为 ref 参数(在 VB 中为 ByRef ),因为它被定义为结构。这样做是有必要的,因为只有将结构声明为 ref 参数,.NET Compact Framework 才传递结构的地址。如果 CE_NOTIFICATION_TRIGGER 被声明为类,则 CeSetUserNotificationEx 的参数可能是通过值传递的,因为 .NET Compact Framework 自动传递引用类型的地址。虽然,在很多情况下,在托管代码中将非托管函数所期望的结构声明为结构或声明为类都没有关系,但这里的 SYSTEMTIME structure 是一个例外。此处, SYSTEMTIME 必须声明为结构,因为 CeSetUserNotificationEx 函数期望 CE_NOTIFICATION_TRIGGER 内联包含整个结构。如果 SYSTEMTIME 声明为类,则只能封送处理指向该类的 4 字节的指针。
接着,声明了 CE_NOTIFICATION_TRIGGER 结构,它具有指向成员 lpszApplication 和 lpszArguments 的字符数组的指针,因此使用 unsafe 关键字对它进行了标记。
注记住,与完整的 .NET Framework 不同,您不需要使用 .NET Compact Framework 中的 StructLayoutAttribute 修饰您的结构,因为所有的结构都自动为 LayoutKind.Sequential。
最后,RunApplication 方法以与前面示例中相同的签名进行声明,只是此时该方法创建了一个 CE_NOTIFICATION_TRIGGER 的实例并继而填充其成员。如前面示例中所示,一些成员被设置为执行给定应用程序所需的值,而 SYSTEMTIME DateTimeToSystemTime 方法进行填充。
要填充嵌入于该结构中的字符串指针,该方法首先进行检查以确保应用程序名非空而且不是空字符串。如果为空或者是空字符串,则会引发 ArgumentNullException 。如果都不是,则字符数组的指针会在固定语句中声明,并且使用 String 类的 ToCharArray 方法进行填充。注意,字符数组会固定于固定块范围内的内存中。然后参数指针以同样的方式进行填充。该结构的 lpszApplication 和 lpszArguments 成员以传递给 CeSetUserNotificationEx 函数的指针和结构进行填充。
注在这个例子中, CeSetUserNotificationEx 函数声明的第三个参数为 IntPtr ,它事实上需要一个 CE_USER_NOTIFICATION 类型的结构。这个声明是必需的,这样 RunApplication 方法就可以将 IntPtr.Zero 传递为该函数的这一参数。对于 CeSetUserNotificationEx 的其他用法,您需要真正地传递该结构(例如弹出一个通知对话框)。在这些情况下,您可以进行 CeSetUserNotificationEx 的第二次声明以重载第一次的声明。编译器将根据参数选择适当的声明。
如果有错误发生(函数返回的句柄为空),则自定义的 WinCeException 将被创建并传递给错误处理程序。
使用托管字符串指针
最后一种您可以用来在 .NET Compact Framework 中封送处理结构中字符串的方法是创建您自己的托管字符串指针类。这种方法的优点是它在 C# 和 VB 中都可以使用,并且一旦字符串指针类创建完毕就可以在各种情况下使用。但是,它确实需要与 Windows CE 操作系统的内存分配 API 进行一些交互。
首先,您可以创建一个声明和调用必要的内存管理 API 的托管类。该类可以公开一些共享的方法以分配非托管的内存块,释放该内存,调整非托管内存块的大小,将托管字符串复制到非托管内存中。Memory 类的 VB 版显示如下。
Public Class Memory
_
Private Shared Function LocalAlloc(ByVal uFlags As Integer, _
ByVal uBytes As Integer) As IntPtr
End Function

_
Private Shared Function LocalFree(ByVal hMem As IntPtr) As IntPtr
End Function

_
Private Shared Function LocalReAlloc(ByVal hMem As IntPtr, _
ByVal uBytes As Integer, ByVal fuFlags As Integer) As IntPtr
End Function

Private Const LMEM_FIXED As Integer = 0
Private Const LMEM_MOVEABLE As Integer = 2
Private Const LMEM_ZEROINIT As Integer = &H40
Private Const LPTR = (LMEM_FIXED Or LMEM_ZEROINIT)

???? Allocates a block of memory using LocalAlloc
Public Shared Function AllocHLocal(ByVal cb As Integer) As IntPtr
Return LocalAlloc(LPTR, cb)
End Function

???? Frees memory allocated by AllocHLocal
Public Shared Sub FreeHLocal(ByVal hlocal As IntPtr)
If Not hlocal.Equals(IntPtr.Zero) Then
If Not IntPtr.Zero.Equals(LocalFree(hlocal)) Then
Throw New Win32Exception(Marshal.GetLastWin32Error())
End If
hlocal = IntPtr.Zero
End If
End Sub

???? Resizes a block of memory previously allocated with AllocHLocal
Public Shared Function ReAllocHLocal(ByVal pv As IntPtr, _
ByVal cb As Integer) As IntPtr
Dim newMem As IntPtr = LocalReAlloc(pv, cb, LMEM_MOVEABLE)
If newMem.Equals(IntPtr.Zero) Then
Throw New OutOfMemoryException
End If
Return newMem
End Function

???? Copies the contents of a managed string to unmanaged memory
Public Shared Function StringToHLocalUni( _
ByVal s As String) As IntPtr
If s Is Nothing Then
Return IntPtr.Zero
Else
Dim nc As Integer = s.Length
Dim len As Integer = 2 * (1 + nc)
Dim hLocal As IntPtr = AllocHLocal(len)
If hLocal.Equals(IntPtr.Zero) Then
Throw New OutOfMemoryException
Else
Marshal.Copy(s.ToCharArray(), 0, hLocal, s.Length)
Return hLocal
End If
End If
End Function
End Class
正如列表中所示,非托管函数 LocalAlloc, LocalFree,和 LocalRealloc 使用 DllImportAttribute 进行声明,并分别包装于 AllocHLocal, FreeHLocal和 ReAllocHLocal 方法中。最后, StringToHLocalUni 方法为给定的字符串分配适当大小的非托管内存块(每个字符 2 个字节,因为 .NET Compact Framework 只支持 Unicode),然后将字符数组复制到指向非托管块的 IntPtr 中。
一旦内存类创建完成,就可以创建一个简单的托管字符串指针结构,如下所示。
Public Structure StringPtr
Private szString As IntPtr

Public Sub New(ByVal s As String)
Me.szString = Memory.StringToHLocalUni(s)
End Sub

Public Overrides Function ToString() As String
Return Marshal.PtrToStringUni(Me.szString)
End Function

Public Sub Free()
Memory.FreeHLocal(Me.szString)
End Sub
End Structure
正如您所见, StringPtr 结构包含了私有的 IntPtr 以跟踪非托管内存中的字符串指针。接下来,该指针通过调用 Memory 类的 StringToHLocalUni 方法(将托管字符串传递给该方法)在构造函数中进行填充。然后, ToString 方法重写 System.ToString 方法以返回给定指针的托管字符串,而 Free 方法则释放为该字符串分配的非托管内存。
然后, Memory 类和 StringPtr 结构都可以放置于程序集中,并能够从需要托管字符串指针的各个智能设备项目中进行引用。
要在 CE_NOTIFICATION_TRIGGER 结构的情况下使用使用 StringPtr 结构,您可以对该结构的 lpszApplication 和 lpszArguments 成员使用 StringPtr 结构进行声明。另外,因为这些成员需要使用 StringPtr 类的 Free 方法进行释放,因此 CE_NOTIFICATION_TRIGGER 实现 IDisposable 接口是非常合理的。然后 Dispose 方法可以用来调用这两个成员的 Free 方法。您还可以将公共的构造函数添加到该类中,不仅用于创建 StringPtr 成员,而且还对建立通知所需要的其它成员进行默认设置。该结构的 C# 代码如下所示。
private struct CE_NOTIFICATION_TRIGGER: IDisposable
{
public uint dwSize;
public CNT_TYPE dwType;
public NOTIFICATION_EVENT dwEvent;
public StringPtr lpszApplication;
public StringPtr lpszArguments;
public SYSTEMTIME startTime;
public SYSTEMTIME endTime;

public CE_NOTIFICATION_TRIGGER( string application, string arguments,
DateTime start )
{
dwSize = (uint)Marshal.SizeOf( typeof(CE_NOTIFICATION_TRIGGER) );
dwType = CNT_TYPE.CNT_TIME;
dwEvent = NOTIFICATION_EVENT.NONE;

lpszApplication = new StringPtr( application );
lpszArguments = new StringPtr( arguments );

startTime = DateTimeToSystemTime( start );
endTime = new SYSTEMTIME();
}

//other constructors possible here

public void Dispose()
{
lpszApplication.Free();
lpszArguments.Free();
}
}
最后, RunApplication 方法可以简单地创建一个 CE_NOTIFICATION_TRIGGER 结构的新实例并调用 CeSetUserNotificationEx 函数。因为该结构实现了 IDisposable 接口,所以在 C# 中,您可以使用 using 语句以确保编译器在调用非托管函数之后自动调用 Dispose 方法。
public static void RunApplication( string application, string arguments,
DateTime start )
{
CE_NOTIFICATION_TRIGGER nt =
new CE_NOTIFICATION_TRIGGER( application, arguments, start );
using( nt )
{
IntPtr hNotify = CeSetUserNotificationEx(
IntPtr.Zero, ref nt, IntPtr.Zero );

if( hNotify == IntPtr.Zero )
{
int errorNum = Marshal.GetLastWin32Error();
HandleCeError(New WinCeException(
"Could not set notification ", errorNum),
"RunApplication");
}
}
}
返回页首

封送处理结构中的定长字符串
使用 .NET Compact Framework 的 P/Invoke 服务时会引起的第二种情况涉及到封送处理结构中的固定长度字符串或字符数组。例如,用来将应用程序图标放置于系统栏中或者从其中移除的 Shell_NotifyIcon 函数接受一个指向在 Windows CE SDK 中定义的结构的指针,如下所示:
typedef struct _NOTIFYICONDATA {
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
WCHAR szTip[64];
} NOTIFYICONDATA, *PNOTIFYICONDATA;
您可以看到该结构中最后一个字段(该字段保存了当光标处于图标上方时显示的工具提示的文本)被定义为 64 个元素的字符数组。在 VB .NET 中对该结构的直接转换如下所示:
Private Structure NOTIFYICONDATA
Public cbSize As Integer
Public hWnd As IntPtr
Public uID As Integer
Public uFlags As Integer
Public uCallbackMessage As Integer
Public hIcon As IntPtr
Public szTip() As Char

Public Sub New(ByVal toolTip As String)
szTip = toolTip.ToCharArray
End Sub
End Structure
不幸的是,这个简单的转换不能正常工作,因为虽然 System.Char 是 blittable 类型,字符数组在运行时却作为 4 字节的数组指针进行封送处理。如前所述,完整的 .NET Framework 通过 MarshalAs 属性确实支持这种情况,在这个例子中,通过使用此属性修饰该数组并将 UnmangedType 枚举的 ByValTStr 值传递给它的构造函数。
因为存在这种情况,所以本质上您有两种选择。第一种是创建一个正确总长度的字节数组,然后使用 Marshal 类的方法,或直接通过不安全代码中的指针,将该结构的各个字段复制到此字节数组中或从其中取出;基本上这是手工进行封送处理的。然后可以创建字节数组的指针并传递给非托管函数。但是,因为这种方法较复杂,所以本文的以下部分将集中介绍一种联合使用 .NET Compact Framework 的封送拆收器以及少量自定义封送处理的方法。
注这种方法的关键是 NOTIFYICONDATA 的 szTip 成员是该结构的最后一个成员。如果情况不是如此,则将需要自定义程度更高的方法。
要使用这种方法,您首先必须在您的类中进行适当的非托管声明。如前所述,一种最佳做法是在一个类中将这些函数声明为私有函数,然后让该类公开静态方法(VB 中的共享方法)以执行该功能。在这种情况下, DestroyIcon, RegisterWindowMessage,和 Shell_NotifyIcon 函数,以及 NOTIFYICONDATA 结构和一些常量都需要进行声明,如下所示。
Private Structure NOTIFYICONDATA
Public cbSize As Integer
Public hWnd As IntPtr
Public uID As Integer
Public uFlags As Integer
Public uCallbackMessage As Integer
Public hIcon As IntPtr
End Structure

_
Private Shared Function RegisterWindowMessage(ByVal lpMessage As String) As Integer
End Function

_
Private Shared Function DestroyIcon(ByVal hIcon As IntPtr) As IntPtr
End Function

_
Private Shared Function Shell_NotifyIcon( _
ByVal dwMessage As TrayConstants, _
ByVal lpData As IntPtr) As Boolean
End Function

Private Enum TrayConstants As Integer
NIM_ADD = 0
NIM_DELETE = 2
NIF_ICON = 2
NIF_MESSAGE = 1
NIF_TIP = 4
NIF_ALL = NIF_ICON Or NIF_MESSAGE Or NIF_TIP
End Enum
此处需要注意的关键一点是 szTip 字段不包括在 NOTIFYICONDATA 结构中,因为它需要手动进行封送处理。另外,RegisterWindowMessage 函数将被用以创建唯一的消息标识符来填充 uCallbackMessage 字段,并最终在通知图标被用户激活时通知应用程序。DestroyIcon 函数用于释放系统栏中图标所使用的内存。最后,您可以看到,Shell_NotifyIcon 方法被声明为接受一个 TrayConstants 类型的参数(该参数部分定义了该函数将执行哪种操作,比如添加或移除图标)和一个需要用包含 NOTIFYICONDATA 结构的指针进行填充的非托管指针。
然后对 Shell_NotifyIcon 函数的调用可以包装在一个名为 CreateNotifyIcon 的共享方法中,如下所示。
???? Sets up the notify icon and returns the message to hook events for
Public Shared Function CreateNotifyIcon( _
ByVal notifyWindow As MessageWindow, _
ByVal icon As IntPtr, _
ByVal Tooltip As String) As Integer

Dim structPtr As IntPtr

???? Create the structure
Dim nid As New NOTIFYICONDATA

???? Calculate the size of the structure needed
Dim size As Integer = Marshal.SizeOf(nid) + _
(64 * Marshal.SystemDefaultCharSize)

Try
???? Register the callback event
Dim msg As Integer = RegisterWindowMessage("NotifyIconMsg")

???? Fill in the fields of the structure
With nid
.cbSize = size
.hIcon = icon
.hWnd = notifyWindow.Hwnd
.uCallbackMessage = msg
.uID = 1 ????Application defined identifier
.uFlags = TrayConstants.NIF_ALL
End With

???? Allocate the memory
structPtr = Memory.AllocHLocal(size)

???? Copy the structure to the pointer
Marshal.StructureToPtr(nid, structPtr, False)

???? Add the tooltip
Dim arrTooltip() As Char = Tooltip.ToCharArray()
Dim toolOffset As New IntPtr(structPtr.ToInt32() + _
Marshal.SizeOf(nid))
Marshal.Copy(arrTooltip, 0, toolOffset, arrTooltip.Length)

???? Call the function
Dim ret As Boolean = Shell_NotifyIcon( _
TrayConstants.NIM_ADD, structPtr)

If ret = False Then
???? Go get the error
Dim errorNum As Integer = Marshal.GetLastWin32Error()
Throw New WinCeException("Could not set the notify icon ", _
errorNum)
Else
???? Success!
Return msg
End If

Catch ex As Exception
HandleCeError(ex, "CreateNotifyIcon", False)
Finally
???? Free memory
Memory.FreeHLocal(structPtr)
End Try

End Function
您可以看到,此方法接受一个图标被激活时需要通知的窗口(该窗口被定义为 Microsoft.WindowsCe.Forms 命名空间中的 MessageWindow 对象),一个指向放置于系统栏中的图标(实际上是图标句柄)的非托管指针,以及要显示的工具提示的文本。然后,此方法返回 Windows 消息句柄, MessageWindow 可以使用该句柄查询事件,如发生在通知图标上的单击。
注 一个创建图标指针的技巧是使用 ExtractIconEx Windows CE 函数从可执行文件或 DLL 中提取图标句柄。
首先,此方法声明了一个用于保存该结构指针的非托管指针,创建了一个 NOTIFYICONDATA 结构的新实例,并将其放置于引用变量 nid 中。然后,非托管函数所期望的该结构的正确大小可以通过将封送拆收器计算出的大小(Marshal.SizeOf)与工具提示文本的大小(在本例中是一个 64 元素的数组)相加得到。在这个特定的例子中,如果 szTip 字段包括于该结构中,则 SizeOf 方法将返回 28(7 个字段,每个字段 4 字节)。但是,通过手动添加数组的大小,计算出正确的总数为 92(其它 6 个字段为 24 字节,加上字符数组的 64 字节)。 SystemDefaultCharSize 用以确保该系统的正确字符大小已经考虑在内。
一旦该结构的正确大小被确定之后,调用 RegisterWindowMessage 函数注册 Windows 消息句柄,应用程序使用该句柄确定图标事件生成的时间。然后,填充该结构的其余部分,包括刚才计算出的大小,图标句柄,将被通知的窗口的句柄,刚生成的消息句柄,应用程序定义的标识符,以及指示其他哪些字段包含有效数据的标志。
此时,该结构可以被复制到非托管内存中,并返回一个指针。这可以用本文第一节中所述的 Memory 类的 AllocHLocal 方法实现。将先前计算出的该结构的总大小传递给此方法。然后,返回的 IntPtr (在本例中是 structPtr )可以使用 Marshal 类的 StructureToPtr 方法进行填充。此代码依赖 .NET Compact Framework 的封送拆收器来正确的封送处理 NOTIFYICONDATA 结构中的 blittable 类型。
但是,此时 structPtr 指向的内存只有最初的 24 字节被初始化。要填充其余的 64 字节,需要进行一些自定义的封送处理。首先,将传入到该方法中的工具提示参数复制到字符数组中。接下来,一个通过 structPtr 指向内存区中正确偏移位置的新指针被创建,方法是将 NOTIFYICONDATA 结构的大小(在本例中为 24 字节)添加到由 ToInt32 方法返回的指针本身的整数值中。最后, Marshal 类的 Copy 方法被用来将字符数组的内容复制到新指针中。
然后,调用 Shell_NotifyIcon 函数将图标添加到任务栏中。如果该函数返回 false,则可以调用 Marshal 类的 GetLastWin32Error 方法(正如在上一篇文章中我们所讨论的)并引发自定义异常。如果调用成功,则返回挂钩的消息句柄。如果 PInvoke 服务引发异常,我们便使用自定义的 HandleCeError 方法(也在上一篇文章中讨论过)来执行相应的操作。
但是,注意 Finally 块包含了对 Memory 类的 FreeHLocal 方法的调用,这样为该结构分配的内存总是能被释放。
注那些需要封送处理引用类型和字符数组(类似于在本文中所显示)的开发人员也可能想投入时间为 .NET Compact Framework 创建自定义的 MarshalAs 属性。使用这种方法允许您将所有的内存和指针操作放置于属性代码中,使得调用非托管函数的代码更加清晰,并提供了一种集中处理这些情况的方式。事实上,在本文撰写时,一名 .NET Compact Framework 社区的开发人员已经开始开发这样的实用工具。更多信息,请参见 Compact Framework 公共新闻组 (microsoft.public.dotnet.framework.compactframework)。
为完整起见,相关的 DestroyNotifyIcon 方法也在此给出。它使用相同的方法,但是当然,使用 TrayConstants 枚举的 NIM_DELETE 值以将该图标从任务栏移除。它也调用 DestroyIcon 非托管函数释放该图标的句柄。
???? Clear the notify icon and deallocates the icon
Public Shared Sub DestroyNotifyIcon(ByVal notifyWindow As MessageWindow, _
ByVal icon As IntPtr)

Dim structPtr As IntPtr
Dim nid As New NOTIFYICONDATA
Dim size As Integer = Marshal.SizeOf(nid) + _
(64 * Marshal.SystemDefaultCharSize)

nid.cbSize = size
nid.hWnd = notifyWindow.Hwnd
nid.uID = 1

Try
???? Allocate the memory
structPtr = Memory.AllocHLocal(size)
???? Create the pointer to the structure and remove the icon
Marshal.StructureToPtr(nid, structPtr, False)
Dim ret As Boolean = Shell_NotifyIcon( _
TrayConstants.NIM_DELETE, structPtr)
If ret = False Then
???? Go get the error
Dim errorNum As Integer = Marshal.GetLastWin32Error()
Throw New WinCeException("Could not destroy icon ", errorNum)
End If
Catch ex As Exception
HandleCeError(ex, "DestroyNotifyIcon", False)
Finally
???? Free memory
Memory.FreeHLocal(structPtr)
???? Also we need to destroy the icon to recover resources
DestroyIcon(icon)
End Try
End Sub
注要使移除该图标时 Shell_NotifyIcon 函数返回 true,该应用程序的标识符(结构的 uID 成员)必须被填充为与最初传递时同样的值。
要使用 CreateNotifyIcon 和 DestroyNotifyIcon 方法,可以创建下面的窗体和 MessageWindows 类。
using Atomic.CeApi;
public class Form1 : System.Windows.Forms.Form
{
private EventWindow eventWnd;
private IntPtr hIcon;
private int iconMsg;

public Form1()
{
this.Text = "Form1";
this.Load += new System.EventHandler(this.Form1_Load);
this.Closed += new System.EventHandler(this.Form1_Closed);
}

private void Form1_Load(object sender, System.EventArgs e)
{
hIcon = IntPtr.Zero;
// Get the icon handle here

// Create the MessageWindow to process messages
eventWnd = new EventWindow();

// Place the icon in the tray
iconMsg = Forms.CreateNotifyIcon(eventWnd,
hIcon,"My application tooltip");

// Set up event handler to handle incoming messages
eventWnd.msgId = iconMsg;
eventWnd.MsgProcssedEvent += new
EventWindow.MsgProcessedEventHandler(
eventWnd_MsgProcessed);
}

private void Form1_Closed(object sender, System.EventArgs e)
{
// Clean up the icon
Forms.DestroyNotifyIcon(eventWnd,hIcon);
}

private void eventWnd_MsgProcessed (ref Message msg)
{
// Process the message
}
}

// Class that receives messages from tray icon
public class EventWindow: MessageWindow
{
public delegate void MsgProcessedEventHandler(ref Message msg);
public event MsgProcessedEventHandler MsgProcessedEvent;
public int msgId

protected override void WndProc(ref Message m)
{
base.WndProc (ref m);
if (m.Msg == this.msgId)
{
MsgProcessedEvent(ref m);
}
}
}
您可以看到,该窗体包括了私有字段(该字段引用了处理事件的 MessageWindow 类),指向图标的指针以及用于通知的消息的标识符。创建图标的句柄之后,Form1_Load 方法创建了 EventWindow 类(派生于 MessageWindow)的实例,并将它和图标指针传递给 CreateNotifyIcon 方法。注意,该方法是静态的,并且封装在 Atomic.CeApi.Forms 类中,这样可以更好地组织代码。此时图标将被添加到系统栏中并准备好接收通知。
但是,要接收那些通知,该方法需返回消息标识符,然后该消息标识符被置于 EventWindow 类的 msgId 字段中。这就允许 EventWindow 类筛选消息,并且只为与该图标通知相关的消息引发事件。或者,该消息标识符可以硬编码于 CreateNotifyIcon 方法和 EventWindow 类中,这样就不必调用 RegisterWindowMessage 了。
然后事件处理程序(即 eventWnd_MsgProcessed 方法)在 Form1 中被创建以处理 eventWindow 类的 MsgProcessedEvent 事件。您可以看到,EventWindow 类定义了相应的委派和事件。被替代后的 WndProc 方法检查消息标识符是不是通知消息,如果是则引发 MsgProcessedEvent,此事件将由 Form1 捕获。然后该窗体执行一些操作,如将该窗体置于前台或显示上下文菜单。

本文转自
http://msdn2.microsoft.com/zh-cn/library/aa446529.aspx
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值