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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (33投票s)

2003年3月7日

BSD

7分钟阅读

viewsIcon

300414

downloadIcon

2954

本文介绍了如何使用 C# 为 Explorer 的“详细信息”视图编写列处理程序 Shell 扩展

Windows Explorer showing an MD5 column handler shell extension

摘要

.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 
// 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;
此外,ShObjIdl.idl 中还定义了另一个结构:
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) 文件夹中。

Project Properties

最后,如果您想重新启动 Windows 资源管理器实例,您应该使用官方方式关闭资源管理器,而不是从任务管理器中终止它!要官方关闭,请点击“开始”->“关机”(或 Windows XP 上的“关闭计算机”),然后按住 Ctrl+Shift+Alt 键,点击关机对话框中的“取消”。要启动新的 Windows 资源管理器实例,请按住 Ctrl+Shift 键调出任务管理器,然后按 Escape。在任务管理器中,选择“文件”->“新建任务”,然后输入“Explorer.exe”。这将正确重新启动 Windows 资源管理器。

结论

只要拥有托管元数据定义,在 .NET 中编写任何互操作代码都微不足道。这些定义的创建总是被证明是最麻烦和最耗时的部分。如您所见,我们已成功为 Windows 资源管理器创建了一个列处理程序,代码量非常少,尽管 Windows 资源管理器确实倾向于占用更多内存,因为它现在托管着公共语言运行时。只要您的机器有足够的内存,这从来都不是问题。

感谢 Robert Plant 的审阅和建议。

© . All rights reserved.