65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2004 年 1 月 20 日

CPOL

20分钟阅读

viewsIcon

104549

探索 .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 是一个完全由可复制类型组成的另一个结构,而 NotificationTypesEventTypes 是映射到整数的枚举。

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;
}

您会在上面的列表中注意到,非托管函数将结构的 dwTypedwEvent 和 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 中的 FileTimeToLocalFileTimeFileTimeToSystemFileTime Windows CE API 函数。

此选项的明显缺点是开发人员必须安装和使用 eMbedded Visual C++,这对许多 Visual Basic 和 .NET Framework 开发人员来说是一项艰巨的任务。因此,我们还将讨论另外两种仅使用 .NET Compact Framework 中的托管代码解决此相同问题的方法。

使用非安全字符串指针

在结构中传递字符串的第二个选项是在 C# 中使用 unsafefixed 关键字(VB 中没有等效项)。虽然此选项允许您仅编写托管代码,但它会禁用公共语言运行时 (CLR) 的代码验证功能,而 CLR 的代码验证功能会验证托管代码只访问已分配的内存,并且所有方法调用都符合方法签名中的参数数量和类型。之所以如此,是因为使用非安全代码意味着您希望使用指针进行直接内存管理。

注意:如我们之前的论文中所述,除了创建不可验证的代码外,完整 .NET Framework 中的非安全代码只能在受信任的环境中执行。然而,在 .NET Compact Framework 的 1.0 版本中,不包含代码访问安全性 (CAS),因此这(目前)不是问题。

fixed 关键字与 unsafe 关键字结合使用,用于确保公共语言运行时 (GC) 的垃圾回收器在非托管函数访问对象时不会尝试解除分配对象。

当然,除了丢失代码验证的缺点之外,此技术不能直接从 VB 使用。但是,您可以在 C# 中创建一个使用非安全代码的程序集,然后从 VB 调用该程序集。

要使用 unsafefixed 关键字,您需要将用指针声明的结构以及使用指针的方法标记为 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 结构,其中包含指向 lpszApplicationlpszArguments 成员的字符数组的指针,因此用 unsafe 关键字标记。

注意:请记住,与完整 .NET Framework 不同,您不需要在 .NET Compact Framework 中使用 StructLayoutAttribute 修饰您的结构,因为所有结构都会自动为 LayoutKind.Sequential

最后,RunApplication 方法以与前一个示例相同的签名声明,但这次该方法创建 CE_NOTIFICATION_TRIGGER 的实例并填充其成员。与前一个示例一样,将几个成员设置为执行给定应用程序所需的值,并使用自定义 DateTimeToSystemTime 方法填充 SYSTEMTIME 结构。

要填充结构中嵌入的字符串指针,该方法首先检查以确保应用程序名称不是 null 或空字符串。如果是这种情况,则抛出 ArgumentNullException。如果不是,则在 fixed 语句中声明指向字符数组的指针,并使用 String 类的 ToCharArray 方法填充。请注意,字符数组将保持固定在内存中,在 fixed 块的作用域内。然后以相同的方式填充参数指针。然后使用指针填充结构的 lpszApplicationlpszArguments 成员,并将结构传递给 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

如列表中所示,LocalAllocLocalFreeLocalRealloc 非托管函数使用 DllImportAttribute 声明,然后分别包装在 AllocHLocalFreeHLocalReAllocHLocal 方法中。最后,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 结构声明 lpszApplicationlpszArguments 成员。此外,由于这些成员需要使用 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 封送处理器和少量自定义封送处理的技术。

注意:此技术的关键是 NOTIFYICONDATAszTip 成员是结构的最后一个成员。如果不是这种情况,则需要更自定义的方法。

要使用此技术,您必须首先在类中进行适当的非托管声明。像以前一样,最佳实践是将其声明为类中的私有函数,然后该类公开 static(VB 中的 Shared)方法来执行功能。在这种情况下,需要声明 DestroyIconRegisterWindowMessageShell_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 成员)必须填充为与最初传入的值相同。

要使用 CreateNotifyIconDestroyNotifyIcon 方法,可以创建以下 FormMessageWindows 类。

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 的应用程序时能够使用和借鉴这些技术。

链接

© . All rights reserved.