使用C#创建Shell扩展






4.88/5 (18投票s)
演示使用 C# 和 COM 互操作构建 Windows 资源管理器 Shell 扩展。
引言
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 程序集并实例化您的类。
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");
}
强命名
我漏掉了 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。
结论
完成的代码如此之短,真是令人惊讶。可能比等效的 ATL C++ 代码还要短。它表明 C# 和 .NET 具有非常好的 COM 互操作性和遗留集成能力,并且很可能成为 Shell 编程的首选方式,取代 C++。