编写外壳扩展的 Complete Idiot's Guide - 第 VIII 部分






4.96/5 (28投票s)
2000年9月11日

597078

4091
关于如何通过列处理器 Shell 扩展为 Explorer 的详细信息视图添加列的教程。
目录
引言
《傻瓜指南》的“读者请求”部分还在继续!在这一部分,我将介绍如何在 Windows Me、2000 或更高版本的 Explorer 的详细信息视图中添加列。这种类型的扩展在 NT 4 或 95/98 上不存在,所以你必须拥有较新的操作系统才能运行示例项目。
请记住,VC 7(可能还有 VC 8)用户在编译前需要更改一些设置。请参阅 第一部分的 README 部分 以了解详细信息。
Windows Me 和 2000 为 Explorer 的详细信息视图添加了许多自定义选项。在 Windows 2000 上,有 37 种不同的列可供启用!你可以通过两种方式打开和关闭列。首先,当你右键单击列标题时,会出现一个上下文菜单,其中包含一小部分列。
如果选择“更多...”项,Explorer 将显示一个对话框,你可以在其中选择所有可用列。
Explorer 允许我们将自己的数据放入其中的一些列,甚至可以通过 **列处理器扩展** 将列添加到此列表中。
本文的示例项目是一个 MP3 文件的列处理器,它显示 MP3 文件中存储的 ID3 标签(仅限版本 1 标签)的各种字段。
扩展接口
你应该熟悉设置步骤了,所以我将跳过 VC 向导的说明。如果你正在按照向导操作,请创建一个名为 *MP3TagViewer* 的新 ATL COM 应用程序,其中包含一个 C++ 实现类 `CMP3ColExt`。
列处理器仅实现一个接口,即 `IColumnProvider`。与其他扩展不同,它没有通过 `IShellExtInit` 或 `IPersistFile` 进行单独的初始化。这是因为列处理器是文件夹的扩展,与当前选择无关。`IShellExtInit` 和 `IPersistFile` 都带有选中项的概念。确实存在一个初始化步骤,但它是通过 `IColumnProvider` 的一个方法完成的。
为了将 `IColumnProvider` 添加到我们的 COM 对象中,请打开 *MP3ColExt.h* 并添加此处以粗体显示的行。
#include <comdef.h> #include <shlobj.h> #include <shlguid.h> ///////////////////////////////////////////////////////////////////////////// // CMP3ColExt class CMP3ColExt : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CMP3ColExt, &CLSID_MP3ColExt>, public IColumnProvider { BEGIN_COM_MAP(CMP3ColExt) COM_INTERFACE_ENTRY_IID(IID_IColumnProvider, IColumnProvider) END_COM_MAP() public: // IColumnProvider STDMETHODIMP Initialize(LPCSHCOLUMNINIT psci) { return S_OK; } STDMETHODIMP GetColumnInfo(DWORD dwIndex, SHCOLUMNINFO* psci); STDMETHODIMP GetItemData(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData); };
请注意接口映射中的 `COM_INTERFACE_ENTRY_IID` 宏。在以前的扩展中,我们使用了 `COM_INTERFACE_ENTRY`,它要求接口通过 `__declspec(uuid)` 语法拥有一个关联的 GUID。由于 *comdef.h* 没有为 `IColumnProvider` 定义 GUID,因此我们不能使用 `COM_INTERFACE_ENTRY`。`COM_INTERFACE_ENTRY_IID` 存在于这种情况下,因此我们可以显式指定 IID 和接口名称。另一种解决方案是在类声明之前添加此行
struct __declspec(uuid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1")) IColumnProvider;
这将满足使 `COM_INTERFACE_ENTRY` 生效的要求。
我们还需要对 *stdafx.h* 进行一些更改。由于我们使用的是 Windows 2000 功能,因此我们需要 `#define
` 几个符号,以便能够访问与这些功能相关的声明和原型。
#define WINVER 0x0500 // Enable W2K/98 features #define _WIN32_WINNT 0x0500 // Enable W2K features #define _WIN32_IE 0x0500 // Enable IE 5+ features
这些 `#define
` 需要放在所有 `#include
` 行之前。
初始化
`IColumnProvider` 有三个方法。第一个是 `Initialize()`,其原型如下:
HRESULT IColumnProvider::Initialize ( LPCSHCOLUMNINIT psci );
Shell 向我们传递一个 `SHCOLUMNINIT` 结构,其中只包含一个信息片段,即 Explorer 中正在查看的文件夹的完整路径。对我们来说,不需要这些信息,所以我们的 `Initialize()` 实现只返回 `S_OK`。
枚举新列
当 Explorer 检测到我们的列处理器已注册时,它会调用该扩展以获取有关该扩展实现的每列的信息。这是通过 `GetColumnInfo()` 方法完成的,其原型如下:
HRESULT IColumnProvider::GetColumnInfo ( DWORD dwIndex, SHCOLUMNINFO* psci );
`dwIndex` 是一个从 0 开始的计数器,表示 Explorer 感兴趣的是哪一列。另一个参数是一个 `SHCOLUMNINFO` 结构,我们的扩展会用该列的参数填充它。
`SHCOLUMNINFO` 的第一个成员是另一个结构 `SHCOLUMNID`。`SHCOLUMNID` 是一个 GUID/`DWORD` 对,其中 GUID 称为“格式 ID”,`DWORD` 称为“属性 ID”。这对数字唯一标识系统中的任何列。可以重用现有列(例如,作者),在这种情况下,格式 ID 和属性 ID 会设置为预定义值。如果扩展添加新列,它可以使用自己的 CLSID 作为格式 ID(因为 CLSID 保证是唯一的),并使用简单的计数器作为属性 ID。
我们的扩展将同时使用这两种方法。我们将重用“作者”、“标题”和“注释”列,并添加另外三列:“MP3 专辑”、“MP3 年份”和“MP3 流派”。
这是我们 `GetColumnInfo()` 方法的开头。
STDMETHODIMP CMP3ColExt::GetColumnInfo ( DWORD dwIndex, SHCOLUMNINFO* psci ) { // We have 6 columns, so if dwIndex is 6 or greater, return S_FALSE to // indicate we've enumerated all our columns. if ( dwIndex >= 6 ) return S_FALSE;
如果 `dwIndex` 为 6 或更大,则返回 `S_FALSE` 以停止枚举。否则,我们填充 `SHCOLUMNINFO` 结构。对于 `dwIndex` 值 0 到 2,我们将返回有关我们新列之一的数据。对于值 3 到 5,我们将返回有关我们正在重用的内置列之一的数据。以下是我们如何指定第一个自定义列,它显示 ID3 标签的专辑名称字段。
switch ( dwIndex ) { case 0: // MP3 Album - separate column psci->scid.fmtid = CLSID_MP3ColExt; // Use our CLSID as the format ID psci->scid.pid = 0; // Use the column # as the ID psci->vt = VT_LPSTR; // We'll return the data as a string psci->fmt = LVCFMT_LEFT; // Text will be left-aligned psci->csFlags = SHCOLSTATE_TYPE_STR; // Data should be sorted as strings psci->cChars = 32; // Default col width in chars wcsncpy ( psci->wszTitle, L"MP3 Album", MAX_COLUMN_NAME_LEN ); wcsncpy ( psci->wszDescription, L"Album name of an MP3", MAX_COLUMN_DESC_LEN ); break;
重要:本文的早期版本将 `_Module.pguidVer` 存储在 `fmtid` 成员中。这是完全错误的,因为使用相同版本 ATL 构建的所有二进制文件中,该 GUID 始终相同。如果安装了两个都使用 `_Module.pguidVer` 和相同属性 ID 的扩展,它们的列将会互相冲突。
我们使用扩展的 GUID 作为格式 ID,并使用列号作为属性 ID。`SHCOLUMNINIT` 结构中的 `vt` 成员指示我们将返回给 Explorer 的数据类型。`VT_LPSTR` 表示 C 样式字符串。`fmt` 成员可以是 `LVCFMT_*` 常量之一,表示列中文本的对齐方式。在这种情况下,文本将左对齐。
`csFlags` 成员包含有关该列的一些标志。但是,Shell 似乎并未实现所有标志。以下是标志及其作用的说明。
SHCOLSTATE_TYPE_STR
、SHCOLSTATE_TYPE_INT
和SHCOLSTATE_TYPE_DATE
- 指示当 Explorer 按列排序时,应如何处理列的数据。三种可能性是字符串、整数和日期。
SHCOLSTATE_ONBYDEFAULT
- 以下是 Microsoft 开发者支持部门的 Dave Anderson 对此标志作用的描述(引自 此论坛评论):
- [行为] 取决于你的 Shell 浏览器配置方式。如果你使用“记住每个文件夹的视图设置”文件夹选项,则特定 Shell 视图中显示的列可能会从注册表中恢复,因此在这种情况下,`SHCOLSTATE_ONBYDEFAULT` 标志无效。重置所有文件夹视图设置应该可以让你的列默认启用。你可以在 Explorer 中的“文件夹选项”对话框中(或通过控制面板)执行此操作。
SHCOLSTATE_SLOW
- 根据文档,包含此标志表示收集列数据需要一些时间,Explorer 将在其中一个或多个后台线程上调用扩展,以便 Explorer UI 保持响应。我在测试中没有发现存在此标志时有任何区别。在 Windows 2000 上,Explorer 只使用一个线程来收集扩展列的数据。在 XP 上,它使用几个不同的线程,但我添加或删除 `SHCOLSTATE_SLOW` 时没有看到线程数量的差异。
SHCOLSTATE_SECONDARYUI
- 文档说,传递此标志可防止列出现在标题控件的上下文菜单中。这意味着如果你不包含此标志,该列将出现在上下文菜单中。但是,上下文菜单中从未出现额外的列,所以目前此标志无效。
SHCOLSTATE_HIDDEN
- 传递此标志可防止列出现在“列设置”对话框中。由于无法启用隐藏的列,因此此标志会使列无用。
`cChars` 成员保存列的默认宽度(以字符为单位)。将其设置为列名长度和预计显示在列中的最长字符串长度中的较大值。你还应该在此数字上加 2 或 3,以确保列的宽度足够显示所有文本。(如果你不添加这个小填充,列的默认宽度可能不够,文本可能会被截断。)
最后两个成员是 Unicode 字符串,分别保存列名(显示在标题控件中的文本)和列的描述。目前,Shell 不使用描述,用户也永远看不到它。
列 1 和 2 非常相似,但列 1 阐明了有关数据类型和排序方法的要点。此列显示年份,以下是定义它的代码。
case 1: // MP3 year - separate column psci->scid.fmtid = CLSID_MP3ColExt; // Use our CLSID as the format ID psci->scid.pid = 1; // Use the column # as the ID psci->vt = VT_LPSTR; // We'll return the data as a string psci->fmt = LVCFMT_RIGHT; // Text will be right-aligned psci->csFlags = SHCOLSTATE_TYPE_INT; // Data should be sorted as ints psci->cChars = 6; // Default col width in chars wcsncpy ( psci->wszTitle, L"MP3 Year", MAX_COLUMN_NAME_LEN ); wcsncpy ( psci->wszDescription, L"Year of an MP3", MAX_COLUMN_DESC_LEN ); break;
请注意,`vt` 成员是 `VT_LPSTR`,这意味着我们将传递一个字符串给 Explorer,而 `csFlags` 成员是 `SHCOLSTATE_TYPE_INT`,这意味着当数据排序时,应按数字排序。虽然当然可以将数字而不是字符串返回,但 ID3 标签将年份存储为字符串,因此此列定义为我们省去了将年份转换为数字的麻烦。
当 `dwIndex` 在 3 到 5 之间时,我们返回有关我们正在重用的一列内置列的信息。列 3 显示“作者”列中的艺术家 ID3 字段。
case 3: // MP3 artist - reusing the built-in Author column psci->scid.fmtid = FMTID_SummaryInformation; // predefined FMTID psci->scid.pid = 4; // Predefined - author psci->vt = VT_LPSTR; // We'll return the data as a string psci->fmt = LVCFMT_LEFT; // Text will be left-aligned psci->csFlags = SHCOLSTATE_TYPE_STR; // Data should be sorted as strings psci->cChars = 32; // Default col width in chars break;
`FMTID_SummaryInformation` 是一个预定义符号,并且作者字段 ID (4) 列在 MSDN 文档中。请参阅 “摘要信息属性集” 页面以获取完整列表。重用列时,我们不返回标题或描述,因为 Shell 已经处理了这些。
最后,在 switch 语句结束后,我们返回 `S_OK`,表示我们已填充 `SHCOLUMNINFO` 结构。
在列中显示数据
最后一个 `IColumnProvider` 方法是 `GetItemData()`,Explorer 调用它来获取用于显示文件中某列的数据。原型如下:
HRESULT IColumnProvider::GetItemData ( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData );
`SHCOLUMNID` 结构指示 Explorer 需要哪一列的数据。它将包含我们在 `GetColumnInfo()` 方法中提供给 Explorer 的相同信息。`SHCOLUMNDATA` 结构包含有关文件或目录的详细信息,包括其路径。我们可以使用此信息来决定是否为文件或目录提供数据。`pvarData` 指向一个 `VARIANT`,我们将在此 `VARIANT` 中存储要显示给 Explorer 的实际数据。`VARIANT` 是 Visual Basic 和脚本语言中的松散类型变量的 C 版本。它有两个部分:类型和数据。ATL 有一个方便的 `CComVariant` 类,可以处理初始化和设置 `VARIANT` 的所有繁琐工作。
侧边栏 - 处理 ID3 标签
现在是时候展示我们的扩展将如何读取和存储 ID3 标签信息了。ID3v1 标签是一个固定长度的结构,附加在 MP3 文件末尾,如下所示:
struct CID3v1Tag
{
char szTag[3]; // Always 'T','A','G'
char szTitle[30];
char szArtist[30];
char szAlbum[30];
char szYear[4];
char szComment[30];
char byGenre;
};
所有字段都是普通的 `char
`,并且字符串不一定以 null 结尾,这需要一些特殊的处理。第一个字段 `szTag` 包含字符“TAG”以标识 ID3 标签。`byGenre` 是一个数字,用于标识歌曲的流派。(ID3.org 提供了预定义的流派列表及其数字 ID。)
我们还需要一个额外的结构来保存 ID3 标签以及标签来自的文件名。该结构将用于我稍后会解释的缓存中。
#include <string> #include <list> typedef std::basic_string<TCHAR> tstring; // a TCHAR string struct CID3CacheEntry { tstring sFilename; CID3v1Tag rTag; }; typedef std::list<CID3CacheEntry> list_ID3Cache;
`CID3CacheEntry` 对象保存文件名和存储在该文件中的 ID3 标签。`list_ID3Cache` 是 `CID3CacheEntry` 结构的链表。
好了,回到扩展。这是我们 `GetItemData()` 函数的开头。我们首先检查 `SHCOLUMNID` 结构,以确保我们不是为我们自己的列调用的。
#include <atlconv.h> STDMETHODIMP CMP3ColExt::GetItemData ( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData ) { USES_CONVERSION; LPCTSTR szFilename = OLE2CT(pscd->wszFile); char szField[31]; TCHAR szDisplayStr[31]; bool bUsingBuiltinCol = false; CID3v1Tag rTag; bool bCacheHit = false; // Verify that the format id and column numbers are what we expect. if ( pscid->fmtid == CLSID_MP3ColExt ) { if ( pscid->pid > 2 ) return S_FALSE; }
如果格式 ID 是我们自己的 GUID,则属性 ID 必须是 0、1 或 2,因为这些是我们早在 `GetColumnInfo()` 中使用的 ID。如果出于某种原因,ID 超出了此范围,我们则返回 `S_FALSE`,告知 Shell 我们没有数据,该列应显示为空。
接下来,我们将格式 ID 与 `FMTID_SummaryInformation` 进行比较,然后检查属性 ID 以确定它是否是我们提供的属性。
else if ( pscid->fmtid == FMTID_SummaryInformation ) { bUsingBuiltinCol = true; if ( pscid->pid != 2 && pscid->pid != 4 && pscid->pid != 6 ) return S_FALSE; } else return S_FALSE;
接下来,我们检查我们要操作的文件的属性。如果它实际上是一个目录,或者如果文件处于*离线*状态(即,它已被移动到磁带等其他存储介质),则我们退出。我们还检查文件扩展名,如果不是 `.MP3`,则返回。
// If we're being called with a directory (instead of a file), we can // bail immediately. Also bail if the file is offline. if ( pscd->dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_OFFLINE) ) return S_FALSE; // Check the file extension. If it's not .MP3, we can return. if ( 0 != wcsicmp ( pscd->pwszExt, L".mp3" ) ) return S_FALSE;
此时,我们已确定我们要处理该文件。这就是我们的 ID3 标签缓存发挥作用的地方。MSDN 文档说,Shell 会按文件分组调用 `GetItemData()`,这意味着它会在连续调用中尝试使用相同的文件名调用 `GetItemData()`。我们可以利用这种行为,缓存特定文件的 ID3 标签,以便在后续调用中不必再次从文件中读取标签。
我们首先遍历缓存(存储为成员变量 `m_ID3Cache`),将缓存的文件名与传递给函数的 [文件名] 进行比较。如果我们在缓存中找到该名称,则获取关联的 ID3 标签。
// Look for the filename in our cache.
list_ID3Cache::const_iterator it, itEnd;
for ( it = m_ID3Cache.begin(), itEnd = m_ID3Cache.end();
!bCacheHit && it != itEnd; it++ )
{
if ( 0 == lstrcmpi ( szFilename, it->sFilename.c_str() ))
{
CopyMemory ( &rTag, &it->rTag, sizeof(CID3v1Tag) );
bCacheHit = true;
}
}
如果在该循环之后 `bCacheHit` 为 false,我们需要读取文件并查看它是否具有 ID3 标签。辅助函数 `ReadTagFromFile()` 可以完成读取文件最后 128 字节的繁重工作,成功时返回 TRUE,发生文件错误时返回 FALSE。请注意,`ReadTagFromFile()` 返回文件的最后 128 字节,而不管它们是否真的是 ID3 标签。
// If the file's tag wasn't in our cache, read the tag from the file.
if ( !bCacheHit )
{
if ( !ReadTagFromFile ( szFilename, &rTag ) )
return S_FALSE;
现在我们有了一个 ID3 标签。我们检查缓存的大小,如果缓存包含 5 个条目,则最旧的条目会被移除,为新条目腾出空间。(5 只是一个任意小的数字。)我们创建一个新的 `CID3CacheEntry` 对象并将其添加到列表中。
// We'll keep the tags for the last 5 files cached - remove the oldest // entries if the cache is bigger than 4 entries. while ( m_ID3Cache.size() > 4 ) m_ID3Cache.pop_back(); // Add the new ID3 tag to our cache. CID3CacheEntry entry; entry.sFilename = szFilename; CopyMemory ( &entry.rTag, &rTag, sizeof(CID3v1Tag) ); m_ID3Cache.push_front ( entry ); } // end if(!bCacheHit)
我们的下一步是测试前三个签名字节以确定是否存在 ID3 标签。如果没有,我们可以立即返回。
// Check if we really have an ID3 tag by looking for the signature. if ( 0 != StrCmpNA ( rTag.szTag, "TAG", 3 ) ) return S_FALSE;
接下来,我们从 ID3 标签中读取与 Shell 请求的属性相对应的字段。这只需要测试属性 ID。以下是一个示例,用于 Title 字段。
// Format the details string. if ( bUsingBuiltinCol ) { switch ( pscid->pid ) { case 2: // song title CopyMemory ( szField, rTag.szTitle, countof(rTag.szTitle) ); szField[30] = '\0'; break; ... }
请注意,我们的 `szField` 缓冲区是 31 个字符长,比最长的 ID3v1 字段长 1 个字符。这样我们就能确保始终得到一个正确以 null 结尾的字符串。`bUsingBuiltinCol` 标志是在我们早先测试 FMTID/PID 对时设置的。我们需要该标志,因为仅凭 PID 并不足以识别列——Title 和 MP3 Genre 列都有 PID 2。
此时,`szField` 包含我们从 ID3 标签读取的字符串。WinAmp 的 ID3 标签编辑器用空格而不是 null 字符填充字符串,因此我们通过删除任何尾随空格来纠正此问题。
StrTrimA ( szField, " " );
最后,我们创建一个 `CComVariant` 对象,并将 `szDisplayStr` 字符串存储在其中。然后,我们调用 `CComVariant::Detach()` 将数据从 `CComVariant` 复制到 Explorer 提供的 `VARIANT` 中。
CComVariant vData ( szField ); vData.Detach ( pvarData ); return S_OK; }
它看起来怎么样?
我们的新列将出现在“列设置”对话框的列表末尾。
这是这些列的样子。文件正按我们自定义的“MP3 专辑”列进行排序。
注册扩展
由于列处理器扩展了文件夹,因此它们在 `HKCR\Folders` 键下注册。以下是需要添加到 RGS 文件中以注册我们的列处理器扩展的部分。
HKCR { NoRemove Folder { NoRemove Shellex { NoRemove ColumnHandlers { ForceRemove {AC146E80-3679-4BCA-9BE4-E36512573E6C} = s 'ID3v1 viewer column ext' } } } }
额外福利 - 信息提示
列处理器还可以做的另一件有趣的事情是自定义文件类型的信息提示。此 RGS 脚本为 MP3 文件创建自定义信息提示(此处文本已分成多行以防止水平滚动;在实际的 RGS 文件中,它必须是单行)。
HKCR { NoRemove .mp3 { val InfoTip = s 'prop:Type;Author;Title;Comment; {AC146E80-3679-4BCA-9BE4-E36512573E6C},0; {AC146E80-3679-4BCA-9BE4-E36512573E6C},1; {AC146E80-3679-4BCA-9BE4-E36512573E6C},2;Size' } }
请注意,“作者”、“标题”和“注释”字段出现在“prop:”字符串中。当鼠标悬停在 MP3 文件上时,Explorer 将调用我们的扩展以获取要显示在这些字段中的字符串。文档说我们的自定义字段也可以出现在信息提示中(这就是为什么我们的 GUID 和属性 ID 出现在上面的字符串中),但我无法在 Windows 2000 上让它工作;只有内置属性出现在信息提示中。以下是自定义信息提示的外观。
另请注意,此自定义功能在 XP 上可能不起作用,因为 XP 引入了一些新的文件类型注册表项。在我的 XP 系统上,信息提示信息保存在 `HKCR\SystemFileAssociations\audio` 中。
待续...
在第九部分中,我们将看到另一种新型扩展:图标处理器,它可以自定义特定文件类型显示的图标。
版权和许可
本文是版权材料,©2000-2006 Michael Dunn。我知道这不会阻止人们在网上到处复制它,但我还是必须说。如果你有兴趣翻译本文,请给我发电子邮件告知。我不打算拒绝任何人翻译的许可,我只是想知道翻译情况,以便在此处发布链接。
本文附带的演示代码已发布到公共领域。我之所以这样发布,是因为代码可以造福所有人。(我不将本文本身设为公共领域,因为只在 CodeProject 上提供本文有助于提高我的知名度和 CodeProject 网站的流量。)如果你在自己的应用程序中使用演示代码,发送电子邮件告知我将不胜感激(仅为了满足我对人们是否从我的代码中受益的好奇心),但不是必需的。在你的源代码中注明出处也受欢迎,但不是必需的。
修订历史
2000年9月11日:首次发布文章。
2001年6月13日:更新了某些内容。;)
2006年6月2日:更新以涵盖 VC 7.1、Win Me 和 XP 的更改。示例代码适用于 Me。