C# 中的 Explorer 列处理程序 Shell 扩展






4.92/5 (33投票s)
本文介绍了如何使用 C# 为 Explorer 的“详细信息”视图编写列处理程序 Shell 扩展
摘要
.Net 开发平台为与使用 COM 编写的非托管代码的互操作性和集成提供了非常丰富的功能。Windows Shell 起源于 Windows 95 用户界面,一直以来都严重依赖 COM,并通过许多 COM 接口公开了几个扩展点。随着 Windows 的 successive 迭代出现,提供了越来越多的扩展功能,尤其是在 Windows 2000 中。其中一项在 Windows 2000 中出现的功能是列处理程序。本文将通过在 C# 中创建列处理程序来演示 COM 互操作技术。
列处理程序简介
除了 Windows 资源管理器“详细信息”视图中常见的“名称”、“大小”、“类型”和“日期”列之外,在运行 Windows XP 时,还可以向视图中添加另外 28 列。有用于数码相机拍摄的照片的列,以及用于音乐曲目的列。将另一个列添加到此列表只需实现 IColumnProvider
COM 接口并在 HKEY_CLASSES_ROOT\Folder\ShellEx\ColumnHandlers
键中注册您的处理程序。因此,列处理程序是较简单的 shell 扩展之一,因为在实现其他 shell 扩展时通常需要其他几个接口。
查找非托管定义
Windows 资源管理器中使用的 COM 接口和结构仅由 C++ 头文件定义。没有类型库或 IDL 可以使用。因此,我们必须从头文件中找到所有信息,并将这些定义精确地按照头文件中的定义移动到托管世界中。如果我们将结构中字段的布局只偏差 1 字节,那么代码可能会拒绝工作,而没有任何故障指示。因此,花时间检查和再次检查这些定义是值得的。
IColumnProvider
的 COM 接口在 ShlObj.h
中定义,如下所示:
DECLARE_INTERFACE_(IColumnProvider, IUnknown) { // IUnknown methods STDMETHOD (QueryInterface)(THIS_ REFIID riid, void **ppv) PURE; STDMETHOD_(ULONG, AddRef)(THIS) PURE; STDMETHOD_(ULONG, Release)(THIS) PURE; // IColumnProvider methods STDMETHOD (Initialize)(THIS_ LPCSHCOLUMNINIT psci) PURE; STDMETHOD (GetColumnInfo)(THIS_ DWORD dwIndex, SHCOLUMNINFO *psci) PURE; STDMETHOD (GetItemData)(THIS_ LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT *pvarData) PURE; };
三个 IColumnProvider 方法包括三个结构,这些结构也在 ShlObj.h 中定义,还有一个结构在 ShObjIdl.idl 中定义。它们是:
typedef struct { ULONG dwFlags; // initialization flags ULONG dwReserved; // reserved for future use. WCHAR wszFolder[MAX_PATH]; // fully qualified folder path (or empty
// if multiple folders) } SHCOLUMNINIT, *LPSHCOLUMNINIT; typedef const SHCOLUMNINIT* LPCSHCOLUMNINIT;
typedef struct { SHCOLUMNID scid; // OUT the unique identifier此外,ShObjIdl.idl 中还定义了另一个结构:
// of this column VARTYPE vt; // OUT the native type of the
// data returned DWORD fmt; // OUT this listview format
// (LVCFMT_LEFT, usually) UINT cChars; // OUT the default width of
// the column, in characters DWORD csFlags; // OUT SHCOLSTATE flags WCHAR wszTitle[MAX_COLUMN_NAME_LEN]; // OUT the title of the column WCHAR wszDescription[MAX_COLUMN_DESC_LEN]; // OUT full description of
// this column } SHCOLUMNINFO, *LPSHCOLUMNINFO; typedef const SHCOLUMNINFO* LPCSHCOLUMNINFO; typedef struct { ULONG dwFlags; // combination of SHCDF_ flags. DWORD dwFileAttributes; // file attributes. ULONG dwReserved; // reserved for future use. WCHAR* pwszExt; // address of file name extension WCHAR wszFile[MAX_PATH]; // Absolute path of file. } SHCOLUMNDATA, *LPSHCOLUMNDATA; typedef const SHCOLUMNDATA* LPCSHCOLUMNDATA;
typedef struct { GUID fmtid; DWORD pid; } SHCOLUMNID, *LPSHCOLUMNID; typedef const SHCOLUMNID* LPCSHCOLUMNID;
手动定义托管元数据
现在我们有了所有的非托管定义,我们需要在托管世界中复制它们。这是通过创建与非托管定义完全匹配的托管元数据来完成的。请注意,我说的是“完全”——如果结构的一部分不完全正确,或者方法上定义了错误的数据类型,那么很可能 shell 扩展将拒绝工作。我稍后会详细讨论这一点。
托管元数据并非使用单独的语言编写。在 COM 编程中,IDL 是与 C++ 互补的元数据语言。然而,在 .Net 编程中,元数据是一种 .Net 语言——对于这个 shell 扩展,我们将使用 C#。
第一步是定义 IColumnProvider
元数据。如果我们查看 shlobj.h
中定义的接口,我们会看到它以三个标准的 IUnknown 方法开头。这些可以忽略,因为 .Net COM 互操作会自动为我们创建它们。因此,我们用 C# 编写的接口是:
[ComVisible(false), ComImport, Guid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IColumnProvider {
[PreserveSig()] int Initialize(LPCSHCOLUMNINIT psci);
[PreserveSig()] int GetColumnInfo(int dwIndex, out SHCOLUMNINFO psci);
/// Note: these objects must be threadsafe! GetItemData _will_ be called
/// simultaneously from multiple threads.
[PreserveSig()]
int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
out object /*VARIANT */ pvarData);
}
请注意 PreserveSig
属性。这可以阻止 COM 互操作将返回值视为 out 参数,并使用返回值作为 COM HRESULT。这些方法仍然引用四个结构,因此让我们用 C# 重写它们:
[ComVisible(false),
StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public class LPCSHCOLUMNINIT {
public uint dwFlags; //ulong
public uint dwReserved; //ulong
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string wszFolder; //[MAX_PATH]; wchar
}
[ComVisible(false), StructLayout(LayoutKind.Sequential)]
public struct SHCOLUMNID {
public Guid fmtid; //GUID
public uint pid; //DWORD
}
[ComVisible(false), StructLayout(LayoutKind.Sequential)]
public class LPCSHCOLUMNID {
public Guid fmtid; //GUID
public uint pid; //DWORD
}
[ComVisible(false), StructLayout(LayoutKind.Sequential,
CharSet=CharSet.Unicode, Pack=1)]
public struct SHCOLUMNINFO {
public SHCOLUMNID scid; //SHCOLUMNID
public ushort vt; //VARTYPE
public LVCFMT fmt; //DWORD
public uint cChars; //UINT
public SHCOLSTATE csFlags; //DWORD
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=80)] //MAX_COLUMN_NAME_LEN
public string wszTitle; //WCHAR
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] //MAX_COLUMN_DESC_LEN
public string wszDescription; //WCHAR
}
[ComVisible(false), StructLayout(LayoutKind.Sequential,
CharSet=CharSet.Unicode)]
public class LPCSHCOLUMNDATA{
public uint dwFlags; //ulong
public uint dwFileAttributes; //dword
public uint dwReserved; //ulong
[MarshalAs(UnmanagedType.LPWStr)]
public string pwszExt; //wchar
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string wszFile; //[MAX_PATH]; wchar
}
细心的读者会发现上面实际上定义了五个结构。更细心的读者会注意到第二个和第三个结构除了一个定义为 struct,另一个定义为 class 之外,其余都相同。这是因为 SHCOLUMNID
被使用了两次——一次作为指向 SHCOLUMNID
的指针,另一次作为内联 SHCOLUMNID
。也就是说,一个是堆分配的,另一个是栈分配的——GetData()
方法将其第一个参数传递为 SHCOLUMNID
的指针,而 SHCOLUMNINFO
结构将其第一个字段作为内联 SHCOLUMNID
。
我之前提到过,您必须将托管元数据与非托管定义精确匹配,我第一次肯定没有正确地得到这些结构!我进行了多次迭代才将其修正。最棘手的结构是 SHCOLUMNINFO
,它以单字节打包定义——这是一个定义大小不是机器字整数倍的字段对齐的规则。我最初在头文件中遗漏了对此的定义。我在这里重现它:
#include <pshpack1.h>
就是这样!pshpack1.h 是一个非常短的头文件,它定义了:
#pragma pack(1)
在这个阶段,我悲观地认为 .Net 元数据无法支持这种过于内存高效的格式选项。如今内存如此便宜,以至于在 Windows 资源管理器窗口中为每个列节省一个字节的麻烦都比不上它的价值。然而,.Net 确实支持打包规则——这证明了 Microsoft COM 互操作团队的完整性。
实现 IColumnProvider
现在我们已经定义了元数据,是时候实现唯一的接口并创建我们自己的列处理程序了。在这个例子中,我选择创建一个新列,其中包含文件的 MD5 校验和值。这是一种检查文件夹中文件唯一性的好方法,即使每个文件都有不同的名称。
为了将来更容易定义列处理程序,我首先创建了 IColumnHandler
接口的抽象实现。这提供了一个基类,该基类定义了在 Windows 注册表中注册列处理程序的基本服务。我们可以简单地从基类派生并重写这三个方法。
Initialize
只返回成功,所以我们将继续介绍 GetColumnInfo
并描述正在发生的事情。Windows 资源管理器将向所有注册的列处理程序询问其列的信息。每个列处理程序可以实现多个列,因此资源管理器将使用索引参数调用 GetColumnInfo
。如果 GetColumnInfo
对特定索引返回 S_FALSE
,则资源管理器将停止调用该方法,并知道它支持多少个列。然后我们创建一个新的 SHCOLUMNINFO
结构,并用我们列的详细信息填充它。此结构将通过 GetColumnInfo
上的 out 参数返回。
public override int GetColumnInfo(int dwIndex, out SHCOLUMNINFO psci) {
psci=new SHCOLUMNINFO();
if(dwIndex!=0)
return S_FALSE;
try {
psci.scid.fmtid=GetType().GUID;
psci.scid.pid=0;
// Cast to a ushort, because a VARTYPE is ushort and a VARENUM is int
psci.vt=(ushort)VarEnum.VT_BSTR;
psci.fmt=LVCFMT.LEFT;
psci.cChars=40;
psci.csFlags=SHCOLSTATE.TYPE_STR;
psci.wszTitle = "MD5 Hash";
psci.wszDescription = "Provides an MD5 Hash of every file";
} catch(Exception e) {
MessageBox.Show(e.Message);
return S_FALSE;
}
return S_OK;
}
当调用 GetItemData()
方法时,会生成 MD5。此方法实际上只包含用于加载文件内容的 System.IO 代码,并使用 MD5CryptoServiceProvider
来生成 MD5 校验和。
public override int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
out object pvarData) {
pvarData=string.Empty;
// Ignore directories
if(((FileAttributes)pscd.dwFileAttributes|FileAttributes.Directory)==
FileAttributes.Directory)
return S_FALSE;
// Only service known columns
if(pscid.fmtid!=GetType().GUID || pscid.pid!=0)
return S_FALSE;
try {
MD5 md5 = new MD5CryptoServiceProvider();
byte[] result;
using(Stream stream=File.OpenRead(pscd.wszFile)) {
result = md5.ComputeHash(stream);
}
StringBuilder output=new StringBuilder(2+(result.Length*2));
foreach(byte b in result) {
output.Append(b.ToString("x2"));
}
pvarData="0x" + output.ToString();
} catch(UnauthorizedAccessException) {
return S_FALSE;
}catch(Exception e) {
MessageBox.Show(e.Message);
return S_FALSE;
}
return S_OK;
}
要测试列处理程序,将调试模式设置为“程序”,将启动应用程序设置为 Windows 资源管理器的完整路径(例如 C:\Windows\Explorer.exe)。这两个设置都可以在项目属性的调试部分找到。您还需要将此程序集注册到 COM——这可以通过在属性对话框的“构建”部分中将“注册 COM 互操作”设置为 true 来实现。此外,DLL 应注册到 GAC 中,因为 Windows 资源管理器不知道如何探测项目文件夹中查找列处理程序 DLL。只需执行命令 gacutil -i MD5ColumnHandler.dll
,或使用 Windows 资源管理器将 DLL 拖放到 GAC (C:\Windows\Assembly) 文件夹中。
最后,如果您想重新启动 Windows 资源管理器实例,您应该使用官方方式关闭资源管理器,而不是从任务管理器中终止它!要官方关闭,请点击“开始”->“关机”(或 Windows XP 上的“关闭计算机”),然后按住 Ctrl+Shift+Alt 键,点击关机对话框中的“取消”。要启动新的 Windows 资源管理器实例,请按住 Ctrl+Shift 键调出任务管理器,然后按 Escape。在任务管理器中,选择“文件”->“新建任务”,然后输入“Explorer.exe”。这将正确重新启动 Windows 资源管理器。
结论
只要拥有托管元数据定义,在 .NET 中编写任何互操作代码都微不足道。这些定义的创建总是被证明是最麻烦和最耗时的部分。如您所见,我们已成功为 Windows 资源管理器创建了一个列处理程序,代码量非常少,尽管 Windows 资源管理器确实倾向于占用更多内存,因为它现在托管着公共语言运行时。只要您的机器有足够的内存,这从来都不是问题。
感谢 Robert Plant 的审阅和建议。