在 Microsoft .NET Compact Framework 上进行高级 P/Invoke






4.85/5 (8投票s)
探索 .NET Compact Framework 上的高级互操作性。
本文由 MSDN 提供。
摘要
探索 .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 和非托管代码之间没有共同表示的类型(称为可复制类型,并在我们之前的论文中列举)定义,则该结构或类无法完全封送。出于实际目的,这意味着包含字符串指针或固定长度字符缓冲区的结构或类将无法正确封送。
例如,考虑 Windows CE 上可用的用户通知 API。使用此 API,应用程序可以显示通知对话框,或使应用程序在特定时间执行,或响应事件,例如同步,或当 PC 卡更换时。由于 .NET Compact Framework 不包含执行此功能的托管类,因此需要此功能的开发人员将需要使用 P/Invoke 来进行正确的操作系统调用。
要使用 Windows CE 通知 API (CeSetUserNotificationEx
),用于定义什么事件激活通知的结构 CE_NOTIFICATION_TRIGGER
将在托管代码中声明,并直接在 VB.NET 中翻译如下,其中 SYSTEMTIME
是一个完全由可复制类型组成的另一个结构,而 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 中是可复制类型,因为所有字符串都可以视为 Unicode。但是,这仅适用于String
直接传递给非托管函数的情况,而不适用于它在结构或类内部使用的情况。
在完整 .NET Framework 中,封送处理器可以处理这种情况,因为它包含 MarshalAsAttribute
。使用此属性,可以将结构重写为
Private Structure CE_NOTIFICATION_TRIGGER
Dim dwSize As Integer
Dim dwType As NotificationTypes
Dim dwEvent As EventTypes
<MarshalAs(UnmanagedType.LPWStr)> Dim lpszApplication As String
<MarshalAs(UnmanagedType.LPWStr)> 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 层、使用 unsafe
块以及创建一个自定义类来处理字符串指针。
使用 Thunking 层
Thunking 一词在历史上适用于将参数和返回值从 16 位表示形式转换为 32 位表示形式,反之亦然的代码。然而,在其更通用的用法中,该术语仅指创建一些处理数据转换的中间代码。在 .NET Compact Framework 和 P/Invoke 中,thunking 层是一个非托管函数,它接受构成结构的参数,创建非托管结构,并调用适当的函数。这是 Visual Studio .NET 帮助和 MSDN 中提出的在结构中传递复杂对象的技术。
要在上述通知示例中使用此技术,您可以在 eMbedded 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
函数。
从托管代码中,此函数在 C# 中使用 DLLImportAttribute
声明如下。由于 .NET Compact Framework 在将字符串直接传递给非托管函数时可以将其视为可复制类型,因此您的声明可以直接使用字符串
[DLLImport("MyNotification.DLL", SetLastError=true"]
private static extern IntPtr RunApplicationThunk(
string lpszApplication, string lpszArguments,
SYSTEMTIME startTime);
您的非托管 thunking 函数(此处以 C# 形式显示)应封装在一个托管类中,如我们之前的论文中所述,并通过类的 static
(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 函数。
此选项的明显缺点是开发人员必须安装和使用 eMbedded Visual C++,这对许多 Visual Basic 和 .NET Framework 开发人员来说是一项艰巨的任务。因此,我们还将讨论另外两种仅使用 .NET Compact Framework 中的托管代码解决此相同问题的方法。
使用非安全字符串指针
在结构中传递字符串的第二个选项是在 C# 中使用 unsafe
和 fixed
关键字(VB 中没有等效项)。虽然此选项允许您仅编写托管代码,但它会禁用公共语言运行时 (CLR) 的代码验证功能,而 CLR 的代码验证功能会验证托管代码只访问已分配的内存,并且所有方法调用都符合方法签名中的参数数量和类型。之所以如此,是因为使用非安全代码意味着您希望使用指针进行直接内存管理。
注意:如我们之前的论文中所述,除了创建不可验证的代码外,完整 .NET Framework 中的非安全代码只能在受信任的环境中执行。然而,在 .NET Compact Framework 的 1.0 版本中,不包含代码访问安全性 (CAS),因此这(目前)不是问题。
fixed
关键字与 unsafe
关键字结合使用,用于确保公共语言运行时 (GC) 的垃圾回收器在非托管函数访问对象时不会尝试解除分配对象。
当然,除了丢失代码验证的缺点之外,此技术不能直接从 VB 使用。但是,您可以在 C# 中创建一个使用非安全代码的程序集,然后从 VB 调用该程序集。
要使用 unsafe
和 fixed
关键字,您需要将用指针声明的结构以及使用指针的方法标记为 unsafe
。例如,下面显示了使用非安全代码创建相同的 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");
}
}
在此示例中,您会注意到 CeSetUserNotificationEx
Windows CE API 函数首先使用 DllImportAttribute
声明。CE_NOTIFICATION_TRIGGER
被标记为 ref
参数(VB 中的 ByRef
),因为它被定义为结构。这是必需的,因为 .NET Compact Framework 仅在结构声明为 ref
参数时才传递结构的地址。如果 CE_NOTIFICATION_TRIGGER
已声明为类,则 CeSetUserNotificationEx
的参数可以通过值传递,因为 .NET Compact Framework 会自动传递引用类型的地址。尽管在许多情况下,您将非托管函数期望的结构声明为托管代码中的结构还是类并不重要,但 SYSTEMTIME
结构在这种情况下是一个例外。这里 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
的实例并填充其成员。与前一个示例一样,将几个成员设置为执行给定应用程序所需的值,并使用自定义 DateTimeToSystemTime
方法填充 SYSTEMTIME
结构。
要填充结构中嵌入的字符串指针,该方法首先检查以确保应用程序名称不是 null 或空字符串。如果是这种情况,则抛出 ArgumentNullException
。如果不是,则在 fixed
语句中声明指向字符数组的指针,并使用 String
类的 ToCharArray
方法填充。请注意,字符数组将保持固定在内存中,在 fixed
块的作用域内。然后以相同的方式填充参数指针。然后使用指针填充结构的 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
<DllImport("coredll.dll", SetLastError:=True)> _
Private Shared Function LocalAlloc(ByVal uFlags As Integer, _
ByVal uBytes As Integer) As IntPtr
End Function
<DllImport("coredll.dll", SetLastError:=True)> _
Private Shared Function LocalFree(ByVal hMem As IntPtr) As IntPtr
End Function
<DllImport("coredll.dll", SetLastError:=True)> _
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
。
一旦 Memory
类就位,就可以创建一个简单的托管字符串指针结构,如下所示。
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
结构,您可以使用 StringPtr
结构声明 lpszApplication
和 lpszArguments
成员。此外,由于这些成员需要使用 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
是可复制类型,字符数组在运行时也将作为指向数组的 4 字节指针进行封送。和以前一样,完整的 .NET Framework 通过 MarshalAs
属性支持这种情况,在本例中,通过用属性修饰数组并将其构造函数传递 UnmangedType
枚举的 ByValTStr
值。
由于这种行为,您基本上有两种选择。第一种方法涉及创建一个正确总大小的字节数组,然后使用 Marshal
类的方法,或直接通过不安全代码中的指针,将结构的各个字段复制进出字节数组;本质上是手动封送。然后可以创建指向字节数组的指针并将其传递给非托管函数。然而,由于这种技术更复杂,本文的其余部分将重点介绍一种结合使用 .NET Compact Framework 封送处理器和少量自定义封送处理的技术。
注意:此技术的关键是NOTIFYICONDATA
的szTip
成员是结构的最后一个成员。如果不是这种情况,则需要更自定义的方法。
要使用此技术,您必须首先在类中进行适当的非托管声明。像以前一样,最佳实践是将其声明为类中的私有函数,然后该类公开 static
(VB 中的 Shared
)方法来执行功能。在这种情况下,需要声明 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
<DllImport("coredll", SetLastError:=True)> _
Private Shared Function RegisterWindowMessage(ByVal _
lpMessage As String) As Integer
End Function
<DllImport("coredll", SetLastError:=True)> _
Private Shared Function DestroyIcon(ByVal hIcon As IntPtr) As IntPtr
End Function
<DllImport("coredll", SetLastError:=True)> _
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(七个字段每个 4 字节)。然而,通过手动添加数组的大小,计算出正确的总计 92(其他六个字段 24 字节,加上字符数组 64 字节)。SystemDefaultCharSize
用于确保考虑了系统的正确字符大小。
一旦确定了结构的正确大小,就会调用 RegisterWindowMessage
函数来注册一个 Windows 消息句柄,应用程序使用该句柄来确定何时生成图标的事件。然后填充结构的其余部分,包括刚刚计算的大小、图标句柄、将收到通知的窗口句柄、刚刚生成的消息句柄、应用程序定义的标识符,最后是指示哪些其他字段包含有效数据的标志。
此时,结构可以复制到非托管内存中,并返回一个指针。这可以通过使用本文第一部分中所示的 Memory
类的 AllocHLocal
方法来完成。此方法将先前计算的结构总大小作为参数。然后,可以使用 Marshal
类的 StructureToPtr
方法填充返回的 IntPtr
(在本例中为 structPtr
)。此代码依赖于 .NET Compact Framework 封送处理器来正确封送 NOTIFYICONDATA
结构中的可复制类型。
但是,此时,仅初始化了 structPtr
指向的内存的前 24 个字节。要填充剩余的 64 个字节,需要进行一些自定义封送处理。首先,将传入方法的工具提示参数复制到字符数组中。接下来,通过将 NOTIFYICONDATA
结构的大小(在本例中为 24 字节)添加到通过 ToInt32
方法返回的指针本身的整数值,创建一个指向 structPtr
指向的内存区域内正确偏移量的新指针。最后,使用 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
方法,可以创建以下 Form
和 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);
}
}
}
如您所见,Form
包含引用将处理事件的 MessageWindow
类、指向图标的指针以及用于通知的消息标识符的私有字段。创建图标句柄后,Form1_Load
方法创建 EventWindow
类(派生自 MessageWindow
)的实例,并将其和图标指针传递给 CreateNotifyIcon
方法。请注意,该方法是 static
的,并封装在 Atomic.CeApi.Forms
类中,以更好地组织代码。此时,图标将添加到系统托盘中,并准备好接收通知。
但是,要接收这些通知,该方法会返回消息标识符,然后将其放置在 EventWindow
类的 msgId
字段中。这允许 EventWindow
类过滤消息,并且只为与图标通知相关的消息引发事件。或者,消息标识符可以硬编码到 CreateNotifyIcon
方法和 EventWindow
类中,从而使对 RegisterWindowMessage
的调用变得不必要。
然后,在 Form1
中创建一个事件处理程序 eventWnd_MsgProcessed
方法,以处理 eventWindow
类的 MsgProcessedEvent
事件。您会注意到 EventWindow
类定义了相应的委托和事件。重写的 WndProc
方法检查消息标识符是否为通知消息,如果是,则引发 MsgProcessedEvent
,然后由 Form1
捕获。然后,窗体可以执行一些操作,例如将窗体带到前台或显示上下文菜单。
摘要
尽管 P/Invoke 服务使用的 .NET Compact Framework 封送处理器不包含完整 .NET Framework 封送处理器的所有功能,但它仍然可以有效地使用,即使在相当复杂的情况下,也可以使用各种技术。在本白皮书中,我们展示了如何通过使用 thunking 层、指针和非安全代码、创建自己的字符串指针类以及进行一些自定义封送处理来传递结构中的字符串指针和嵌入式字符数组。希望您在创建出色的基于 .NET Compact Framework 的应用程序时能够使用和借鉴这些技术。