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

使用C#创建Shell扩展

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (18投票s)

2001年10月23日

BSD

6分钟阅读

viewsIcon

478868

downloadIcon

6896

演示使用 C# 和 COM 互操作构建 Windows 资源管理器 Shell 扩展。

Shell Hook

引言

C# 和 .NET 被微软誉为未来的 Windows 编程环境。这到底意味着什么?程序员是否被 Windows 屏蔽得如此厉害,以至于尝试做任何有用的事情都困难重重,甚至不可能?它仅仅是另一个 VB 吗?本文将演示一个使用 C# 的 Windows Shell 钩子/扩展,展示了消费 COM 接口以及将最终代码部署为真正 COM 对象有多么容易。

钩入 Shell

Shell 扩展最简单的形式之一是钩入所有 ShellExecuteEx Win32 调用。Windows 资源管理器几乎所有的操作都使用此函数。从“开始”->“运行”对话框到双击文件,所有这些都是通过 ShellExecuteEx 调用的。为了钩入此功能,Shell 使用责任链模式,并将调用每一个实现了 IShellExecuteHook 接口的已注册 COM 组件。IShellExecuteHook 接口只包含一个方法

HRESULT Execute(
    LPSHELLEXECUTEINFO pei
);

要注册此接口的具体实现,我们需要在 Windows 注册表中 HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\ShellExecuteHooks 下的 ShellExecuteHooks 列表中注册我们组件的 CLSID。

那么,既然我们知道了 Shell 对我们的期望,我们如何用 C# 来实现呢?首先,我们需要创建一个实现 IShellExecuteHook 的 C# 对象。查看 C++ 头文件 shlguid.h,我们可以看到 DEFINE_SHLGUID(IID_IShellExecuteHookW, 0x000214FBL, 0, 0);。或者更易读的方式是 000214FB-0000-0000-C000-000000000046。这为我们要实现的接口提供了 GUID。我们现在实际做的是在代码中创建一个该接口的 C# 版本。我们使用特殊的 ComImport 属性将此实现标记为由 COM 定义。我们的 C# 接口现在看起来是这样的

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), 
        Guid("000214FB-0000-0000-C000-000000000046")]
public interface IShellExecuteHook{
    [PreserveSig()]
    int Execute(SHELLEXECUTEINFO sei);
}

请注意 PreserveSig 属性。这可以阻止 COM 互操作将返回值视为 out 参数,而是将返回值用作 COM 的 HRESULT

Execute 方法接受一个参数,即一个 SHELLEXECUTEINFO 结构。经过一番实验,以下 C# 结构满足我们的需求

[StructLayout(LayoutKind.Sequential)]
public class SHELLEXECUTEINFO {
    public int cbSize;
    public int fMask;
    public int hwnd;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string lpVerb;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string lpFile;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string lpParameters;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string lpDirectory;
    public int nShow;
    public int hInstApp;
    public int lpIDList;
    public string lpClass;
    public int hkeyClass;
    public int dwHotKey;
    public int hIcon;
    public int hProcess;
}

对于我们的示例,我们只关心直到 lpParameters 的字段,只要它们是正确的,一切都会正常工作。

现在我们有了一个完全定义的接口,它被声明为一个 COM 接口。作为快速测试,这应该可以顺利编译。我们仍然缺少该接口的具体实现,所以让我们声明一个

[Guid("6156C6FC-4DD9-4f82-8200-0446DABB7F35"), ComVisible(true)]
public class DateParser : IShellExecuteHook {
}

在这里,我们声明 DateParser(我们的 Shell 扩展)实现了 IShellExecuteHook。该类对 COM 可见,并且 Guid 属性指定了 CLSID。填写这唯一的具体方法只是编写纯 C# 代码。在此示例中,我创建了一个方法,该方法可以识别在“开始”->“运行”对话框中输入的日期,并显示一个包含 ISO 8601 等效日期的消息框。代码如下

public int Execute(SHELLEXECUTEINFO sei) {
    try {
        DateTime oTime=DateTime.Parse(sei.lpFile + " " + sei.lpParameters);

        MessageBox.Show(null, "Date '" + sei.lpFile + " " + 
                        sei.lpParameters + "' in ISO 8601 is " + 
                        oTime.ToString("s"), "ISO 8601 Date", 
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
        return S_OK;
    }
    catch(FormatException) {
        return S_FALSE;
    } catch(Exception e) {
        // Unknown exception. Report it to stderr
        Console.Error.WriteLine("Unknown exception parsing Date: " + e.ToString());
    }

    return S_FALSE;
}

Shell 将命令行中第一个以空格分隔的单词视为文件名,并将第一个空格之后的所有内容视为参数。我们想要解析整个命令行,因此我们将文件名和参数连接在一起。

注册

现在我们有了 COM 实现,它需要被注册。COM 组件一直都需要注册,所以这应该不足为奇;但这是 .NET,所以我们的组件需要在两个地方注册。工作方式是,您的 COM 类在通常的位置,即 HKCR\clsid 下注册,并且 InProcServer32 条目设置为 mscoree.dll。就像 MSJava 和 MTS 添加代理来拦截 COM 实例化一样,CLR 也需要拦截这一点。一旦 CLR 获得控制权,它就会使用 progid 键中的值来查找您的 .NET 程序集并实例化您的类。

CLSID entries

progid 键的格式如下:<namespace>.<class>。这显然是个问题,因为在任何地方都没有指向您磁盘上程序集的 文件路径。为了让 CLR 找到您的代码,它需要被注册。这个注册位置称为全局程序集缓存,或 GAC。这是一个所有共享程序集都必须放置的位置,它位于 %windows%\assembly 下的一系列文件夹中。您可以通过 Windows 资源管理器查看它,您将看到一个友好列表,列出了所有放入 GAC 的程序集。在表面之下(通过 DOS),您可以看到它们实际上是如何存储的。GAC 在开发过程中代号为 fusion,这个名称出现在多个地方,例如 DLL 名称。

注册表注册

安装到注册表非常简单

Assembly asm=Assembly.GetExecutingAssembly();
RegistrationServices reg=new RegistrationServices();
reg.RegisterAssembly(asm, 0);

这指示 CLR 将适当的条目添加到 HKCR\clsid 并调用我们预定义的注册函数。我们使用 ComRegisterFunction 属性声明我们的注册函数,并使用 ComUnregisterFunction 属性声明任何反注册函数

[System.Runtime.InteropServices.ComRegisterFunctionAttribute()]
static void RegisterServer(String zRegKey) {
 try {
  RegistryKey root;
  RegistryKey rk;

  root = Registry.LocalMachine;
  rk = root.OpenSubKey(@"Software\Microsoft\Windows\" 
       "CurrentVersion\Explorer\ShellExecuteHooks", true);
  rk.SetValue(clsid, ".Net ISO 8601 Date Parser Shell Extension");
  rk.Close();
 }
 catch(Exception e) {
  System.Console.Error.WriteLine(e.ToString());
 }
}

除了标准的 HKCR\clsid 注册外,我们还需要通过添加 CLSID 作为新值来将自己注册为 ShellExecuteHook

GAC 安装

安装到 GAC 有几种方法。最简单的方法是直接拖放到 %windows%\assembly 文件夹。另一种方法是使用 .NET SDK 提供的 GACUtil.exe 工具。对于最终用户来说,这两种方法都不太直观,所以我们选择以编程方式安装。SDK 中的一些示例包含一个名为 fusioninstall.cs 的文件。顾名思义,它提供了几个使用 PInvoke 函数将程序集安装到 GAC 的函数。这样做很简单

if (FusionInstall.AddAssemblyToCache("DateParser.exe") == 0) {
    Console.WriteLine("DateParser - shell extension successfully registered");
}

Viewing the GAC

强命名

我漏掉了 GAC 安装的最后一个要点。为了防止名称冲突,GAC 要求每个程序集都有一个强名称。也就是说,一个由公钥和程序集版本属性组成的名称。当前的 VB.NET IDE 提供了一个不错的属性页来添加强名称,而对于 C#,则需要手动完成。首先,使用 SDK 中的强命名实用工具 sn.exe -k 生成公钥/私钥对。然后,应使用 key 属性在代码中引用此密钥对

[assembly: AssemblyKeyFile(@"..\..\KeyFile.snk")]

最后,注册已编译的可执行文件只需双击 EXE 即可。这将注册组件,Windows 资源管理器将其加载到其进程空间,并向其发出每一个 ShellExecuteEx 调用。我们的示例将解析传递给它的每个字符串。如果它检测到这是一个日期,将弹出一个 MessageBox,其中包含日期的 ISO 8601 标准格式。尝试在“开始”->“运行”对话框中键入 25 December 2001 来查看 MessageBox。要注销该组件,请使用 /u 作为命令行开关运行 EXE。这不会从 Explorer 进程中释放 DLL - 您需要从任务管理器中终止 Explorer。

Invoking the extension

结论

完成的代码如此之短,真是令人惊讶。可能比等效的 ATL C++ 代码还要短。它表明 C# 和 .NET 具有非常好的 COM 互操作性和遗留集成能力,并且很可能成为 Shell 编程的首选方式,取代 C++。

© . All rights reserved.