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

使用 C# 枚举和托管控制面板小程序。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (17投票s)

2004 年 2 月 16 日

24分钟阅读

viewsIcon

103168

downloadIcon

2554

演示如何使用 C# 和非托管 C++ 枚举和托管 Windows 控制面板小程序。

Sample Image - untitled1.gif

快速浏览以大图标模式显示小程序的“小程序查看器”窗口。

Sample screenshot

现在以详细信息视图查看。请注意为每个小程序显示的名称和描述。

Sample screenshot

最后,请注意一个刚刚通过双击其图标启动的小程序。这表明小程序确实是由小程序引擎托管的。

假设

本文假设读者对编写非托管 C/C++ 动态链接库和从这些库导出函数有基本的了解。对使用 P/Invoke 访问非托管库有基本的了解也将对读者有所帮助,但我会尽量详细解释。

摘要

本文的目的是讨论开发人员在尝试混合非托管和托管代码时可能面临的几个问题。在尝试从 C# 或 VB.NET 等托管语言与当前 Windows API 交互时,这是一个经常遇到的问题。在本文中,我将讨论我所面临的问题以及如何使用托管和非托管代码(C# 和 VC++)的组合来解决这些问题。以下是您通过阅读本文可以学到的一些内容的简要概述:

  • 调用非托管函数指针
  • 在运行时动态加载非托管库
  • 获取非托管函数的函数指针
  • 将 C/C++ 结构和数据类型转换为 CLR 兼容代码
  • 为使用 sizeof 寻找不安全代码的替代方案
  • 在堆栈上分配/释放内存
  • 了解如何以编程方式操作小程序
  • 从外部非托管库中的嵌入资源中提取字符串和图标。

背景

那么,让我们首先讨论我为什么决定写这篇文章和代码。作为一个好奇的程序员,我总是对 Windows 操作系统中功能的底层实现很感兴趣。控制面板小程序一直是一个有点未被发现的话题,原因不明,是的,MSDN 库中确实有关于它们的文档,但很少有好的工作示例。更不用说它们是如何实际工作的了。在我开始写这篇文章之前,我对小程序是如何编写的已经有了相当好的理解,我过去作为一名专业开发人员已经写过几个。然而,直到我进入 shell 开发领域,我才完全好奇 Windows 是如何实现这些非常有用的东西的。

在开发我的 shell 的日子里,我遇到了许多从代码启动控制面板小程序的方法。大多数实现都涉及硬编码对 rundll32.exe 的引用,以调用 Control_RunDLL 函数并使用各种参数来启动控制面板小程序。这总是困扰我,因为这远非动态,你必须提前了解小程序,至少要大致了解如何启动它们。我决定我想要一种像 Windows 一样枚举和托管小程序的方法。

那么,既然已经说了原因,我们来讨论一下小程序到底是什么。顺便说一句,这里提供的所有信息都是我个人对 MSDN 库中找到的文档的剖析。控制面板小程序没什么特别的,它们只是一个带有特殊扩展名 .cpl.dll 文件,并放置在 Windows system 目录中。如果你想编写一个,你必须选择一种可以创建非托管 DLL 并允许导出非托管函数的语言。C# 和 VB 无法做到这一点,所以请选择 C++ 或 Delphi 来实现。这并不难,但超出了本文的范围。

既然我们知道了小程序是什么,一个用 .cpl 扩展名编译的非托管 DLL,那么我们来看看它们是如何工作的。深入研究文档,你会发现 CplApplet 函数。它是小程序必须从其库中导出的唯一函数,用于与 Windows 交互。该函数看起来像这样

LONG CPlApplet(HWND hWnd, UINT uMsg, LPARAM lParam1, LPARAM lParam2);

这个函数与所有窗口背后的 WndProc 函数非常相似。所有与小程序的通信都通过此函数发生。目前你不需要了解这个函数的更多内容。如果你感兴趣,请在 MSDN 库中搜索 CPlApplet,你将有幸像我们其他人被迫做的那样翻译这个奇妙的函数。

好的,我们回顾一下关于小程序的知识。这将是我们发现所有小程序并与之通信的基础。

  • 一个非托管的 Windows 动态链接库
  • 使用 .cpl 扩展名而不是标准的 .dll 扩展名
  • 导出一个名为 CPlApplet 的函数
  • 所有小程序都应位于 Windows System 目录中

问题 #1:动态查找和加载非托管 DLL

正当生活看起来轻松,你觉得这能有多难的时候,第一个问题出现了。我们如何从非托管代码调用这个函数?如你所知,你可以使用 System.Runtime.InteropServices.DllImportAttribute 从 DLL 调用非托管函数,问题在于你必须在编码时知道库的名称。那么,如果我们无法提前定义其入口点,我们如何动态加载非托管 DLL 并调用那个非托管函数呢?

答案在于几个函数:LoadLibraryFreeLibraryGetProcAddress。我们将使用 LoadLibrary 根据文件名加载小程序的 .cpl 文件(也就是一个 .dll),并使用 GetProcAddress 获取 CplApplet 函数的非托管函数指针。一旦我们使用完毕,将使用 FreeLibrary 释放 DLL。这是标准做法,任何做过动态函数指针的人可能都可以跳过一点。然而,我记得这曾经是一个神奇的巫术包,需要一点解释。

我们来看看如何做到这一点。首先,我们需要在 Windows System 目录中搜索所有以 .cpl 结尾的文件。使用 System.IO 命名空间中的方法和类可以很容易地做到这一点。这是将动态发现这些文件的主要方法。让我们来看看。但首先,让我分解一下我们将要使用的类,以及我为实现魔法而创建的类。它们简要来说是……

  • AppletEngine
  • AppletLibrary
  • Applet

AppletEngine 类包含以下方法,可以让我们找到小程序库。

public FileInfo[] FindAppletLibraries(string path) 
{
    DirectoryInfo di = new DirectoryInfo(path);
    if (di != null)
    {
        return di.GetFiles("*.cpl");
    }
    return new FileInfo[] {}; 
}

这将使我们能够返回一个 FileInfo 对象数组,其中包含符合我们搜索条件的文件信息。这些都是相当标准的东西,目前不应该引起任何疑问。如果引起了疑问,请参考 MSDN 上的文档或我的源代码,我相信你会很快明白。

现在我们已经发现了以 .cpl 结尾的文件,我们假设它们都是小程序库。让我们看看如何使用 LoadLibraryGetProcAddress 来加载它们并获取函数指针,以便我们可以与小程序通信。我们只需遍历 FileInfo 对象并对文件名调用 LoadLibrary 来加载 DLL,如果成功,我们就可以调用 GetProcAddress 来返回 CplApplet 函数的函数指针。以下是实现此算法的 AppletLibrary 构造函数中的代码片段。

public AppletLibrary(string path, IntPtr hWndCpl)
{
    _path = path;
    _hWndCpl = hWndCpl;
    _applets = new ArrayList();

    if (!System.IO.File.Exists(path))
      throw new System.IO.FileNotFoundException
          ("No applet could be found in the specified path.", path);

    _library = LoadLibrary(path);
    if (base.IsNullPtr(_library))
      throw new Exception("Failed to load the library '" + _path + "'");
    _appletProc = GetProcAddress(_library, "CPlApplet");
    if (base.IsNullPtr(_appletProc))
      throw new Exception("Failed to load CPlApplet proc for the library '" 
      + _path + "'"); 

    this.Initialize();
  }

让我们讨论一下这段代码片段的作用。首先,它会尝试调用 LoadLibrary,参数是文件的路径,例如 C:\Windows\System\SomeApplet.cpl。该方法将返回一个 IntPtr,它是库的句柄。有关更多信息,请查看 MSDN 文档,我更愿意让创建者来解释。如果函数成功,IntPtr 将不是 IntPtr.Zero。一旦我们有了库的句柄,我们就可以使用该句柄和函数名称调用 GetProcAddress 来获取另一个 IntPtr,它是一个非托管函数指针。

问题 #2:从托管代码调用非托管函数指针

现在我们面临一个相当棘手的问题。如何从托管代码调用非托管函数指针?乍一看,答案似乎很简单,我们使用委托。然而,尽管这个解决方案看起来是正确的,但我未能发现一种在托管代码中为非托管函数指针创建委托的方法。Marshal 类中的几个方法看起来很有希望,特别是 GetUnmanagedThunkForManagedMethodPtr。我承认我在这里失败了,因为我无论如何都无法弄清楚如何使用这个方法。文档没有帮助,我只是厌倦了绞尽脑汁去弄明白。我希望有人能阅读这篇文章并为我接下来要做的事情提出解决方案。顺便说一句,这篇文章的一部分,我将交给你们这些互操作专家来帮助我弄清楚那个方法。我确信它可以做到,只是不值得我再浪费时间去尝试弄明白。如果有人做到了,请告诉我!

我们自己的诡计登场了。我决定最简单的方法是创建一个小型非托管 C++ DLL,让它为我们调用。在 C++ 中调用函数指针就像对托管世界的其他部分声明整数一样简单。因此,我打开了一个 Win32 项目并将其项目类型设置为动态链接库,创建了一个 DLL 来为我完成这项工作。我称之为 AppletProxy.dll,因为它是我们 C# 中的托管代码与小程序导出的非托管函数之间的代理。我不会在这里介绍如何创建非托管 DLL,这超出了本文的范围。如果你真的感兴趣,源代码应该为你提供一个非常简单的示例来学习,一如既往,如果你遇到困难,我可以回答问题。以下是我们将用于调用非托管函数指针的非托管函数的模样。

LONG APIENTRY ForwardCallToApplet(APPLET_PROC pAppletProc, 
    HWND hwndCpl, UINT msg, LPARAM lParam1, LPARAM lParam2)
{
    if (pAppletProc != NULL)
        return pAppletProc(hwndCpl, msg, lParam1, lParam2); 
    // call the unmanaged function pointer, this is the same
    // as calling a regular function, except we’re using
    // the variable instead of a function name
    return 0L;
}

好的,我知道你们很多人都在看这个,心里想,这家伙到底在做什么?我不懂这些语法,那些奇怪的数据类型是怎么回事?我希望情况并非如此,因为如果是这样,你真的应该去打开 MSDN 查找这些数据类型。它们都在文档中随时可用。

此方法将接受一个非托管函数指针并调用它指向的方法并返回结果。

现在我们有了用于调用非托管函数指针的代理函数,您可能想知道如何从托管代码中调用它。我们简单地使用 P/Invoke 并像任何其他 API 一样定义入口点。以下是具体操作方法。

[DllImport("AppletProxy")] 
public static extern int ForwardCallToApplet(IntPtr appletProc, 
    IntPtr hWndCpl, AppletMessages message, IntPtr lParam1, IntPtr lParam2);

我开始尝试调用 API 函数时遇到的一个问题是数据类型的差异。我很难弄清楚 HWNDLPARAM 在托管代码中是如何转换的。这里有一个快速参考,可以帮助你们这些新手在尝试将 C/C++ 函数转换为托管代码时有所帮助。

  • HWND, HANDLE, HINSTANCE, HICON, 任何指针类型都映射到 System.IntPtr
  • DWORDLONGBOOL 映射到 System.Int32 或 C# 中的 int
  • LPSTRLPTSTRLPCTSTR 映射到 System.StringSystem.Text.StringBuilder
  • 大多数时候使用 System.String,但如果 API 需要预初始化任何长度的缓冲区,则使用 System.Text.StringBuilder

我希望这有所帮助,因为我知道有一段时间我总是不断地查看 C 头文件来寻找底层定义,然后又在 MSDN 上做一些研究来弄清楚数据类型在托管代码中应该如何声明。在我看来,最主要的是要记住,如果它以“H”开头,它很可能是一种句柄,可以很好地映射到 System.IntPtr。当然,具体的实现可能会时有不同,但作为一般指导,这些都运行良好。

问题 #3:小程序需要什么?

既然我们已经涵盖了一些棘手的概念,让我们回到一些功能讨论。此时,我们可以加载任何我们想要的小程序,并调用 CplApplet 函数与小程序进行通信。但是,我们到底要向这个函数传递什么才能得到我们想要的结果呢?通过查看 Windows 资源管理器中的 Windows 控制面板,您会注意到 Windows 为小程序提供了几样东西。所有小程序都有图标和文本。它是如何获取这些信息的?答案:通过调用带有特定消息的 CplApplet 函数并获得特定结果。

小应用程序的设计(或应该设计)是为了提供名称和描述以及要显示给用户的图标。让我们看看如何重新创建此功能并与我们的小应用程序进行通信。快速查看 MSDN 上的文档,我们发现了一组用于与小应用程序通信的消息和几个结构。另一个问题正在到来,但它远没有之前任何一个问题那么棘手,但是如果你不知道该怎么做,它可能真的会是一个世界末日的问题。别担心,我会告诉你如何处理它,但我们先看看这些消息和结构是什么样子。

为了与小程序通信,我们将发送一条消息,并可选地发送一个指向结构的指针,以从小程序接收信息。这些结构是我们遇到的最后一个障碍。以下是托管代码中的定义

/// <summary>
/// The standard Control Panel Applet Information structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct CPLINFO
{
    /// <summary>
    /// The resource Id of the icon the applet wishes to display
    /// </summary>
    public int IconResourceId;
    /// <summary>
    /// The resource Id of the name the applet wishes to display
    /// </summary>
    public int NameResourceId;     
    /// <summary>
    /// The resource Id of the information the 
    /// applet wishes to display (aka. Description)
    /// </summary>
    public int InformationResourceId;
    /// <summary>
    /// A pointer to applet defined data
    /// </summary>
    public IntPtr AppletDefinedData;    
    /// <summary>
    /// A simple override to display some debugging information
    /// about the resource ids returned from each applet
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return string.Format(
            "IconResourceId: {0},
             NameResourceId: {1},
             InformationResourceId: {2},
             AppletDefinedData: {3}",
             IconResourceId.ToString(),
             NameResourceId.ToString(),
             InformationResourceId.ToString(),
             AppletDefinedData.ToInt32().ToString("X"));
    }
}
 
/// <summary>
/// The advanced Control Panel Applet Information structure
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct NEWCPLINFO
{
    /// <summary>
    /// The size of the NEWCPLINFO structure 
    /// </summary>
    public int Size;
    /// <summary>
    /// This field is unused
    /// </summary>
    public int Flags;
    /// <summary>
    /// This field is unused
    /// </summary>
    public int HelpContext;
    /// <summary>
    /// A pointer to applet defined data
    /// </summary>
    public IntPtr AppletDefinedData;
    /// <summary>
    /// A handle to an icon that the applet wishes to display
    /// </summary>
    public IntPtr hIcon;
    /// <summary>
    /// An array of chars that contains the name
    /// that the applet wishes to display
    /// </summary>
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]
    public string NameCharArray;
    /// <summary>
    /// An array of chars that contains the information
    /// that the applet wishes to display
    /// </summary>
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=64)]
    public string InfoCharArray;
    /// <summary>
    /// An array of chars that contains the help file that
    /// the applet wishes to display for further help
    /// </summary>
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
    public string HelpFileCharArray;
}

这里实际上有两个难题。第一个在 CPLINFO 结构中。一旦返回给我们,它包含小程序资源文件中资源的整数 ID,这些资源包含名称或描述的字符串,或一个图标。快速浏览文档,我意识到我们可以使用 LoadStringLoadImage 提取此信息。然而,它明确指出在将资源 ID 传递给 LoadStringLoadImage 函数之前,应该对资源 ID 使用 MAKEINTRESOURCE 宏。我翻阅了头文件,发现了一个非常难看的转换。我甚至不想提它,因为我认为 Windows 开发人员这样做只是为了搞垮世界上的其他人,也就是所有不使用 C/C++ 编程的人!它太糟糕了,我不知道如何将其转换为 C#,相信我,我尝试过。这是它在头文件中的样子

#define IS_INTRESOURCE(_r) (((ULONG_PTR)(_r) >> 16) == 0)
#define MAKEINTRESOURCEA(i) (LPSTR)((ULONG_PTR)((WORD)(i)))
#define MAKEINTRESOURCEW(i) (LPWSTR)((ULONG_PTR)((WORD)(i)))
#ifdef UNICODE
#define MAKEINTRESOURCE  MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE  MAKEINTRESOURCEA
#endif // !UNICODE

现在,如果你们中的任何人能把它翻译成 C#,请再次告诉我。我很想知道。我自认为翻译能力还不错,但可能我只是熬夜太晚或者喝了太多激浪,无法达到翻译这个所需的水平。所以,就像我之前对函数指针的解决方案一样,我回到了我的 C DLL,并又创建了一个包装函数,这样我就可以直接使用实际的功能并完成它。以下是包装函数的样子

HICON APIENTRY LoadAppletIcon(HINSTANCE hInstance, int resId)
{
    return ::LoadIcon(hInstance, MAKEINTRESOURCE(resId));
}
 
HANDLE APIENTRY LoadAppletImage(HINSTANCE hInstance, 
                      int resId, int width, int height)
{
    return ::LoadImage(hInstance, MAKEINTRESOURCE(resId), 
        IMAGE_ICON, width, height, LR_DEFAULTCOLOR);
}

好的,现在我们可以从小程序的资源中加载字符串和图标了,我们遇到了最后一个障碍。这可能给我带来了比任何其他问题都多的麻烦,但一旦我理解了如何解决这个问题,它最终却成了最容易解决的问题。正如我之前所说,上面的结构将用于在调用小程序时将信息传递回给我们。CPLINFO 结构非常简单,但 NEWCPLINFO 结构有点不同。有些小程序会公开动态信息,例如根据某些变化的资源。例如,像 Wi-Fi 或某些网络或磁盘资源,描述或图标可能需要更改。因此,我们每次都必须使用 NEWCPLINFO 结构刷新信息。当我开始将该结构翻译成 C# 时,我在文档中发现了以下定义。一个预定义长度的定长 char[]。您可能知道也可能不知道,在托管结构中不能预初始化公共字段。由于这个限制,我不知道如何将我的结构映射到实际的结构。

typedef struct tagNEWCPLINFO {
    DWORD dwSize;
    DWORD dwFlags;
    DWORD dwHelpContext;
    LONG_PTR lpData;
    HICON hIcon;
    TCHAR szName[32];
    TCHAR szInfo[64];
    TCHAR szHelpFile[128];
} NEWCPLINFO, *LPNEWCPLINFO;

szName 字段为例。我们如何在结构中定义一个固定长度的字符数组?答案在于 MarshalAs 属性。我们可以将字段定义为字符串,并让 P/Invoke 服务以特定格式编组数据。这就是我们实现所需编组的方式。我们将字段定义为以固定数组大小编组为空终止字符数组。

[MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)]
public string NameCharArray;

关于字符串转换,有一个小小的旁注。我们是使用 ANSI 还是 Unicode?在大多数情况下,Unicode 是字符串的首选数据类型,但由于我们无法提前知道实际的 struct 使用的是 TCHAR 类型,该类型在编译时会根据 C/C++ 头文件中的各种 #define 映射到适当的类型。我们没有那种便利,但我们有一个解决方案。我们将对我们的结构应用 StructLayout 属性来定义字段的顺序布局,这将使字段保持我们指定的顺序(CLR 倾向于将结构字段优化到它认为最佳的顺序,这在处理指针和非托管代码时可能会给我们带来麻烦,所以我们需要告诉它它需要保持我们定义的字段顺序),以及用于遇到的任何字符串的字符集。这通过以下方式实现:

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct NEWCPLINFO

在各种字符集上摆弄之后,我发现字符串在我的 Windows XP Pro 电脑直到我的 98 测试机上都使用 ANSI 字符集正确显示。我不知道这是不是正确的选择,希望有人能站出来,对这个问题给出最终的肯定或否定。我再次完全接受建议,因为这对我来说也是一次学习经历。我希望在未来 30 年的某个时候,我的所有问题都能得到解答,并在床上睡得安稳,但这只有在我所有的 Windows API 问题都得到解答时才会发生。按照我发现问题的速度,我要么得去微软找份工作,要么就得窃取一些源代码才能得到答案。坦率地说,我现在对这两种选择都持开放态度。我一生中浪费了无数时间试图理解那些创建这些 API 的人的想法,但我又跑题了。

好的,我们即将完成翻译和定义,所以让我们看看如何与小程序通信。小程序期望特定的消息顺序。第一次调用 CPlApplet 应该初始化小程序并让它知道我们正在加载。第二次调用将告诉我们 .cpl 文件中实际包含多少个小程序。这是一对多的关系,因为一个 DLL 可以包含无限数量的小程序。第三次和第四次调用将从小程序查询信息。获取所需信息后,我们可以通过发送双击消息要求小程序显示其 UI(别紧张,这很正常,我已经为消息创建了一个枚举。我根据我的 .NET 喜好重新命名了它们,但如果您感兴趣,请查找 cpl.h 头文件并查看 CPL_INITCPL_GETCOUNTCPL_INQUIRECPL_NEWINQUIRECPL_DBLCLKCPL_CLOSECPL_STOP 消息以获取详细信息)。以下是我从 MSDN 文档翻译过来的消息枚举:

public enum AppletMessages
{
    Initialize = 1,
    /*  This message is sent to indicate CPlApplet() was found. */
    /*  lParam1 and lParam2 are not defined. */
    /*  Return TRUE or FALSE indicating whether 
               the control panel should proceed. */
    GetCount = 2,
    /*  This message is sent to determine the number 
                    of applets to be displayed. */
    /*  lParam1 and lParam2 are not defined. */
    /*  Return the number of applets you wish to display in the control */
    /*  panel window. */
    Inquire = 3,
    /*  This message is sent for information about each applet. */
    /*  A CPL SHOULD HANDLE BOTH THE CPL_INQUIRE 
                   AND CPL_NEWINQUIRE MESSAGES. */
    /*  The developer must not make any assumptions about the order */
    /*  or dependance of CPL inquiries. */
    /*  lParam1 is the applet number to register, a value from 0 to */
    /*  (CPL_GETCOUNT - 1).  lParam2 is a far ptr to a CPLINFO structure. */
    /*  Fill in CPLINFO's IconResourceId, NameResourceId,
          InformationResourceId and AppletDefinedData fields with */
    /*  the resource id for an icon to display, 
             name and description string ids, */
    /*  and a long data item associated with 
             applet #lParam1. This information */
    /*  may be cached by the caller at runtime and/or across sessions. */
    /*  To prevent caching, see CPL_DYNAMIC_RES, above.  */
    Select = 4,
    /*  The CPL_SELECT message has been deleted. */
    DoubleClick = 5,
    /*  This message is sent when the applet's icon has been */
    /*  double-clicked upon. 
        lParam1 is the applet number which was selected. */
    /*  lParam2 is the applet's AppletDefinedData value. */
    /*  This message should initiate the applet's dialog box. */
    Stop = 6,
    /*  This message is sent for each applet when the 
                            control panel is exiting. */
    /*  lParam1 is the applet number.  lParam2 is 
             the applet's AppletDefinedData  value. */
    /*  Do applet specific cleaning up here. */
    Exit = 7,
    /*  This message is sent just before the control panel 
                                       calls FreeLibrary. */
    /*  lParam1 and lParam2 are not defined. */
    /*  Do non-applet specific cleaning up here. */
    NewInquire = 8
    /* Same as CPL_INQUIRE execpt lParam2 is a 
                pointer to a NEWCPLINFO struct. */
    /* A CPL SHOULD HANDLE BOTH THE CPL_INQUIRE 
                AND CPL_NEWINQUIRE MESSAGES. */
    /* The developer must not make any assumptions about the */
    /* order or dependance of CPL inquiries. */
}

我跳过了很多内容,因为它有点无聊。我知道你们都对看到一些正在运行的东西感兴趣。但是如果你好奇,请仔细阅读我的代码中的注释和 MSDN 中的文档,以了解小程序所期望的确切步骤。

因此,让我们首先初始化一个 applet 库,并找出其中有多少个 applet。以下是 AppletLibrary 类中的代码片段,演示了如何实现这一点:

public void Initialize()
{
    if (this.CPlApplet(AppletMessages.Initialize, 
        IntPtr.Zero, IntPtr.Zero) == (int)BOOL.TRUE)
    {
        int count = this.CPlApplet(AppletMessages.GetCount, 
                                     IntPtr.Zero, IntPtr.Zero);
         
//        System.Diagnostics.Trace.WriteLine
//          (string.Format("{0} applets found in '{1}'", count, _path));
                                   
        for(int i = 0; i < count; i++)
        {
            Applet applet = new Applet(this, i);
            System.Diagnostics.Trace.WriteLine(applet.ToString());
            _applets.Add(applet);
        }
    }
}

注意这里的类层次结构。AppletLibrary 包含 Applet。一对多。AppletLibrary 包含一个属性,该属性公开了一个 Applet 对象的 ArrayList。我特意使用 ArrayList,因为我不想让读者被任何自定义集合类弄糊涂。相信我,如果这是生产代码,那将是一个强类型集合类,通过实现 ICollection 等接口,或者继承自 CollectionBase。这再次超出了本文的范围,所以请尝试专注于眼前的问题,而不是我的编码风格或枚举子对象的方式。

现在我们知道一个小程序库中实际有多少个小程序,我们需要从小程序中提取信息,以便我们可以显示其名称、简短描述和图标。如果我们不能像 Windows 那样向用户显示,那有什么用呢,对吧?现在,我们来看看 AppletLibrary 类。我们剩余的讨论就在这里。正如我之前所说,小程序将以结构的形式将信息传递给我们。如果小程序使用的是静态资源,我们将不得不提取它们。所以让我们看看这又是如何通过另一个代码片段实现的,并讨论一下注意事项。我认为这才是有趣的部分,拉一些指针和编组!

在这里我将演示一个使用不安全代码块的小场景,以及另一个类似的、安全的实现相同目的的方法,该方法使用 P/Invoke,不需要将代码标记为不安全。请记住,我们的目标是尽可能地停留在托管世界中,以便我们能够获得垃圾回收和类型安全的好处。有很多时间可以去追踪 bug,因为使用 fixed 语句固定指针并将 byte* 像激浪罐子一样到处抛撒很酷。是的,我能做到,但这有点违背了干净、类型安全的托管语言的目的,所以如果可能的话我会尽量避免它。可能有很多自大的家伙想这样做只是为了耍酷,但相信我,我曾经是那种人,直到截止日期临近,CEO 们问我的应用程序什么时候能停止随机崩溃。指针很酷,不用指针更酷。相信我,托管世界的生活很美好,非常好。

第一个使用不安全代码块的部分实际上需要 unsafe 语句,这只是为了演示我们都习惯的 sizeof() 函数的替代方案。sizeof() 是一种非常标准的方法,用于确定结构在字节中的大小。不幸的是,它必须在不安全代码中使用。它的替代方案是 Marshal.SizeOf,它不需要不安全代码语句。这里,你自己看看。这段代码将调用小程序并请求其信息,然后使用返回的指针强制转换为我们的托管结构。

public void Inquire()
{
    unsafe
    {
        _info = new CPLINFO();
        _infoPtr = Marshal.AllocHGlobal(sizeof(CPLINFO));
        Marshal.StructureToPtr(_info, _infoPtr, true);
        if (!base.IsNullPtr(_infoPtr))
        {
            _appletLibrary.CPlApplet(AppletMessages.Inquire, 
                new IntPtr(_appletIndex), _infoPtr);
            _info = (CPLINFO)Marshal.PtrToStructure(_infoPtr, 
                typeof(CPLINFO));
                          
            if (!this.IsUsingDynamicResources)
            {
                this.ExtractNameFromResources();
                this.ExtractDescriptionFromResources();
                this.ExtractIconFromResources();
            }
            else
            {
                this.NewInquire();
            }
        }
    }
}
 
public void NewInquire()
{
//    unsafe
//    {
          _dynInfo = new NEWCPLINFO();
          _dynInfo.Size = Marshal.SizeOf(_dynInfo); 
          _dynInfoPtr = Marshal.AllocHGlobal(_dynInfo.Size);
          Marshal.StructureToPtr(_dynInfo, _dynInfoPtr, true);
          if (!base.IsNullPtr(_dynInfoPtr))
          {
              _appletLibrary.CPlApplet(AppletMessages.NewInquire, 
                  new IntPtr(_appletIndex), _dynInfoPtr);
              _dynInfo = (NEWCPLINFO)Marshal.PtrToStructure(_dynInfoPtr, 
                  typeof(NEWCPLINFO));
 
              _smallImage = Bitmap.FromHicon(_dynInfo.hIcon);
              _largeImage = Bitmap.FromHicon(_dynInfo.hIcon);
              _name = _dynInfo.NameCharArray.ToString();
              _description = _dynInfo.InfoCharArray.ToString();
          }
//   }
}

为了从小程序返回的指针中获取结构,我们首先必须在堆栈上为该结构分配内存。这可以通过调用 Marshal.AllocHGlobal 实现。请记住,任何时候我们在堆栈上分配内存,我们都必须释放该内存,否则我们将又有一个内存泄漏的糟糕应用程序。这对任何人都没有好处,因为堆栈是每个人共享的有限资源。如果你用完堆栈内存,那么,最好开始考虑重启并解释为什么你的程序运行起来像马拉松中的胖子。它们开始时足够强大,但最终却坐着出租车冲过终点线。那样可不行。由于发生了内存分配,我所有的类都继承自 DisposableObject。这只是我实现 IDisposable 并围绕指针检查添加一些包装器的简单包装器。因此,请查看 AppletLibraryApplet 类中的重写,以了解通过重写 DisposableObject 类中的抽象方法,资源是如何正确释放的。一旦我们从小程序中获取了信息,我们就可以自由地显示它并等待用户打开小程序。你可以通过调用 Applet 类上的 Open 方法以编程方式打开小程序。以下是代码:

public void Open()
{
    IntPtr userData = (this.IsUsingDynamicResources ? 
        _info.AppletDefinedData : _dynInfo.AppletDefinedData);
    int result = _appletLibrary.CPlApplet(AppletMessages.DoubleClick, 
        new IntPtr(_appletIndex), userData);
    if (result != 0)
    {
        System.ComponentModel.Win32Exception e = 
            new System.ComponentModel.Win32Exception();
        System.Diagnostics.Trace.WriteLine(e);
    }
}

请注意,我们将指针传回小程序。每个小程序都可以定义指针中的数据,我们必须在打开和关闭小程序时将其传回。如果发送 CPL_DBLCLK 方法的结果返回 0,则根据 MSDN,一切正常。但是,此调用会一直阻塞,直到小程序的对话框关闭,我见过它失败的情况,结果非零,即使小程序显示了其对话框。我目前正在努力弄清楚这一点,但文档并没有提供太多帮助。我注意到某些小程序根据此调用的结果似乎总是失败,即使它们看起来工作正常。我尝试捕获并查看异常,但大多数时候没有太大帮助。尝试一下,看看你的结果是什么。在跟踪上设置一个断点并启动一个小程序。我真的很好奇我的结果是怎么回事。

我再次希望有人能接手并帮助我解决这个问题。我认为我已经清除了很多相当棘手的问题,我只是讨厌在项目进行到这么远的时候被难倒,然后放弃并说我们只是运气好。顺便说一句,“显示属性”小程序在我的系统上停止工作了。我不知道为什么。我一直在使用这个代码库,但后来我更改了窗口句柄。如果你们不明白我在说什么,请查看文档和源代码,那样你们可能帮不了我,所以不用担心,好吗?哈哈。就像我说的,这应该对我们所有人来说都是一次学习经验,我也在学习。我以前从未在网上看到过任何做我正在尝试做的事情的代码,所以如果你愿意,可以尽情地批评我。如果你能找到另一个例子,我很想看看!说真的,如果除了晦涩的头文件和 MSDN 文档之外,还有其他可以参考的东西,这可能更容易编写。

是时候下载代码了,也许可以投我一票,这样我就知道如何在下一篇文章中改进了。

好吧,如果您还没有下载,那么现在是时候下载源代码和演示项目了。没有什么比运行一个项目并看到一些结果更令人愉快了。所以去玩玩这个项目,并深入了解一些有趣的编码想法吧。我希望这能帮助你们中的一些人克服一些类似的障碍。我写代码写得很开心,在本文中尝试解释它也同样有趣。这是我第一次在网上发布文章,以后可能会有更多,只是需要找时间。我一直因为缺乏时间而迟迟没有发布,而不是因为缺乏想法或知识。敬请期待未来的更多帖子。

另外,这是我的第一篇文章,如果您不介意的话,请给我一些反馈,并可能投上一票。请对我作为文章作者的写作能力宽容一些,因为您可能知道我写的是代码,而不是书籍。所以文章读起来可能不太好,但希望代码是可靠的!:)

我们学到了什么

让我们回顾一下我们已经讨论过的一些有趣概念

  • 调用非托管函数指针
  • 在运行时动态加载非托管库
  • 获取非托管函数的函数指针
  • 将 C/C++ 结构和数据类型转换为 CLR 兼容代码
  • 为使用 sizeof 寻找不安全代码的替代方案
  • 在堆栈上分配/释放内存
  • 了解如何以编程方式操作小程序
  • 从外部非托管库中的嵌入资源中提取字符串和图标

遗留事项和已知问题

  • 线程调用 Open 和 Close,或制作异步包装器
  • 弄清楚为什么某些小程序在关闭后返回 false
  • 找出管理工具小程序到底在哪里,或者它们是如何插入到控制面板命名空间中的(我认为它是一个 Shell 扩展,只是还没有找到它)
  • 弄清楚如何使用 Marshal.GetUnmanagedThunkForManagedMethodPtr
  • 弄清楚如何将 MAKEINTRESOURCE 转换为 C#

历史

版本 1.0.0

使用 C# 枚举和托管控制面板小程序。 - CodeProject - 代码之家
© . All rights reserved.