编写 Shell 扩展的完整入门指南 - 第九部分






4.95/5 (39投票s)
2000年10月29日

754755

5769
本教程介绍如何编写一个扩展来定制特定文件类型的图标显示。
目录
引言
欢迎阅读第九部分!本篇文章是另一篇读者请求的文章,它将讨论如何为特定类型的文件(本例中为文本文件)显示自定义图标。附带的示例代码可在任何版本的Windows上运行。
请记住,VC 7(可能还有 VC 8)用户在编译前需要更改一些设置。请参阅 第一部分的 README 部分 以了解详细信息。
众所周知,每种文件类型在资源管理器中都由特定的图标表示。位图显示为画笔图标,HTML页面显示为带有IE标志的纸张图标,等等。资源管理器通过查找注册表,并读取HKEY_CLASSES_ROOT
下与文件类型对应的项来确定使用哪个图标。此方法导致所有同一类型的文件使用同一个图标。
然而,这并不是指定图标的唯一方法。资源管理器允许我们通过编写一个**图标处理程序**扩展来逐个文件地自定义图标。事实上,Windows本身就内置了逐个文件图标的例子。浏览到Windows目录(或任何包含大量EXE文件的目录),你会发现每个EXE都有不同的图标(除了没有图标资源的EXE,它们都显示通用图标)。ICO和CUR文件也是如此,每个文件都有不同的图标。
本文的示例项目是一个图标处理程序扩展,它根据文件大小为文本文件显示4种不同的图标之一。图标显示如下:
- 8K 或更大
- 4K 到 8K
- 1字节 到 4K
- 零字节
扩展接口
你应该对设置步骤很熟悉了,所以我将跳过VC向导的说明。如果你正在使用向导,请创建一个名为TxtFileIcons的新ATL COM应用程序,并带有一个C++实现类CTxtIconShlExt
。
图标处理程序实现两个接口:IPersistFile
和IExtractIcon
。回想一下,IPersistFile
用于初始化一次只处理一个文件的扩展,而IShellExtInit
用于一次处理所有选定文件的扩展。IExtractIcon
有两个方法,它们都参与告诉资源管理器为特定文件使用哪个图标。
请注意,资源管理器为**每个**显示的文件创建一个COM对象。这意味着C++类的每个实例都是为每个文件创建的。因此,你应该避免在扩展中进行耗时的操作,以免让资源管理器界面显得迟钝。
初始化接口
为了将IPersistFile
添加到我们的COM对象中,请打开TxtIconShlExt.h并添加此处粗体显示的行。
#include <comdef.h> #include <shlobj.h> #include <atlconv.h> class CTxtIconShlExt : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CTxtIconShlExt, &CLSID_TxtIconShlExt>, public IPersistFile { BEGIN_COM_MAP(CTxtIconShlExt) COM_INTERFACE_ENTRY(IPersistFile) END_COM_MAP() public: // IPersistFile STDMETHOD(GetClassID)( CLSID* ) { return E_NOTIMPL; } STDMETHOD(IsDirty)() { return E_NOTIMPL; } STDMETHOD(Save)( LPCOLESTR, BOOL ) { return E_NOTIMPL; } STDMETHOD(SaveCompleted)( LPCOLESTR ) { return E_NOTIMPL; } STDMETHOD(GetCurFile)( LPOLESTR* ) { return E_NOTIMPL; } STDMETHOD(Load)( LPCOLESTR wszFile, DWORD /*dwMode*/ ) { USES_CONVERSION; lstrcpyn ( m_szFilename, OLE2CT(wszFile), MAX_PATH ); return S_OK; } protected: TCHAR m_szFilename [MAX_PATH]; // Full path to the file in question. DWORDLONG m_qwFileSize; // File size; used by extraction method 2. };
与其他使用IPersistFile
的扩展一样,唯一需要实现的方法是Load()
,因为这是资源管理器告诉我们正在处理哪个文件的方式。Load()
的实现是内联的,并将文件名复制到m_szFilename
成员变量供以后使用。
IExtractIcon 接口
图标处理程序还实现了IExtractIcon
接口,资源管理器在需要文件图标时会调用它。由于我们的扩展是针对文本文件的,每次在资源管理器窗口或开始菜单中显示文本文件时,资源管理器都会调用IExtractIcon
方法。要将IExtractIcon
添加到我们的COM对象中,请打开TxtIconShlExt.h并添加此处粗体显示的行。
class CTxtIconShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CTxtIconShlExt, &CLSID_TxtIconShlExt>,
public IPersistFile,
public IExtractIcon
{
BEGIN_COM_MAP(CTxtIconShlExt)
COM_INTERFACE_ENTRY(IPersistFile)
COM_INTERFACE_ENTRY(IExtractIcon)
END_COM_MAP()
public:
// IExtractIcon
STDMETHODIMP GetIconLocation(UINT uFlags, LPTSTR szIconFile, UINT cchMax,
int* piIndex, UINT* pwFlags);
STDMETHODIMP Extract(LPCTSTR pszFile, UINT nIconIndex, HICON* phiconLarge,
HICON* phiconSmall, UINT nIconSize);
};
有两种方法可以将图标返回给资源管理器。第一种是GetIconLocation()
可以返回一个文件名/索引对,它指示包含图标的文件以及该文件中的零基图标索引。例如,C:\windows\system\shell32.dll/9是一个可能的返回值,它告诉资源管理器使用shell32.dll中的第9个图标(从0开始计数)。这并不意味着使用资源ID为9的图标,而是意味着按顺序查看资源ID并使用第9个(从最小到最大的ID)。Extract()
不必做任何事情,只需返回S_FALSE
即可告诉资源管理器自己提取图标。
这种方法特别之处在于,资源管理器可能会也可能不会在GetIconLocation()
返回后调用Extract()
。资源管理器维护一个**图标缓存**,其中包含最近使用的图标。如果GetIconLocation()
返回了一个最近使用过的文件名/索引对,并且图标仍在缓存中,资源管理器将使用缓存的图标,而不会调用Extract()
。
第二种方法是从GetIconLocation()
返回一个“不查找缓存”的标志,这会使资源管理器始终调用Extract()
。然后,Extract()
负责加载图标并将图标句柄返回给资源管理器显示。
提取方法 1
调用的第一个IExtractIcon
方法是GetIconLocation()
。此函数查看文件(其名称在IPersistFile::Load()
期间存储)并返回一个文件名/索引对,如上所述。GetIconLocation()
的原型是:
HRESULT IExtractIcon::GetIconLocation ( UINT uFlags, LPTSTR szIconFile, UINT cchMax, int* piIndex, UINT* pwFlags );
参数如下:
uFlags
- 一些可以改变扩展行为的标志。
GIL_ASYNC
用于询问提取过程是否会花费很长时间,如果是,扩展可以请求在后台线程上执行提取,这样资源管理器界面就不会显得迟钝。其他标志GIL_FORSHELL
和GIL_OPENICON
似乎只在命名空间扩展中有意义。就我们而言,我们不会担心这些标志,因为我们的代码执行不会花费很长时间。 szIconFile
,cchMax
szIconFile
是shell提供的一个缓冲区,我们将在此存储要使用的图标所在文件的名称。cchMax
是缓冲区的大小(以字符为单位)。piIndex
- 指向一个
int
的指针,我们将在此存储szIconFile
中文件内的图标索引。 pwFlags
- 指向一个
UINT
的指针,我们可以在其中返回改变资源管理器行为的标志。标志将在下面解释。
GetIconLocation()
填充szIconFile
和piIndex
参数并返回S_OK
。如果决定不再提供自定义图标,它也可以返回S_FALSE
,在这种情况下,资源管理器将回退到通用的“未知文件”图标:。可以在
pwFlags
中返回的标志有:
GIL_DONTCACHE
- 告诉资源管理器不要检查图标缓存,以确定
szIconFile/piIndex
中指定的图标是否最近使用过。结果是,总是调用IExtractIcon::Extract()
。我将在稍后描述提取方法2时,对这个标志有更多的说明。 GIL_NOTFILENAME
- 根据MSDN的说法,这个标志告诉资源管理器在
GetIconLocation()
返回时忽略szIconFile/piIndex
的内容。显然,这就是扩展应该告诉资源管理器始终调用IExtractIcon::Extract()
的方式,但是这个标志对GetIconLocation()
返回后资源管理器做什么没有影响。我将在稍后对此进行更多说明。 GIL_SIMULATEDOC
- 这个标志告诉资源管理器采用扩展返回的图标,将其放入“折角纸张”图标中,并使用**那个**作为文件的图标。我将在下面演示这个标志。
在方法1中,我们的扩展的GetIconLocation()
函数获取文件大小,并根据大小返回一个介于0和3之间的索引。这引出了这种方法的一个缺点——你需要跟踪你的资源ID,并确保它们顺序正确。我们的扩展只有4个图标,所以这种记账工作并不困难,但如果你有更多的图标,或者在项目中添加/删除了一些图标,你必须小心你的资源ID。
这是我们的GetIconLocation()
函数。我们首先打开文件并获取其大小。如果在此过程中发生错误,我们将返回S_FALSE
让资源管理器使用默认图标。
STDMETHODIMP CTxtIconShlExt::GetIconLocation ( UINT uFlags, LPTSTR szIconFile, UINT cchMax, int* piIndex, UINT* pwFlags ) { DWORD dwFileSizeLo, dwFileSizeHi; DWORDLONG qwSize; HANDLE hFile; hFile = CreateFile ( m_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if ( INVALID_HANDLE_VALUE == hFile ) return S_FALSE; // tell the shell to use a default icon dwFileSizeLo = GetFileSize ( hFile, &dwFileSizeHi ); CloseHandle ( hFile ); if ( (DWORD) -1 == dwFileSizeLo && GetLastError() != NO_ERROR ) return S_FALSE; // tell the shell to use a default icon qwSize = DWORDLONG(dwFileSizeHi)<<32 | dwFileSizeLo;
接下来,我们获取我们DLL的路径,因为我们的DLL包含图标。然后将该路径复制到szIconFile
缓冲区。
TCHAR szModulePath[MAX_PATH]; GetModuleFileName ( _Module.GetResourceInstance(), szModulePath, MAX_PATH ); lstrcpyn ( szIconFile, szModulePath, cchMax );
接下来,我们检查文件大小并将piIndex设置为正确的索引。(参见文章顶部了解使用的图标。)
if ( 0 == qwSize ) *piIndex = 0; else if ( qwSize < 4096 ) *piIndex = 1; else if ( qwSize < 8192 ) *piIndex = 2; else *piIndex = 3;
最后,我们将pwFlags
设置为0以获得资源管理器的默认行为。这意味着它会检查其图标缓存,以确定szIconFile/piIndex
指定的图标是否在缓存中。如果是,那么将**不会**调用IExtractIcon::Extract()
。然后我们返回S_OK
表示GetIconLocation()
成功。
*pwFlags = 0; return S_OK; }
由于我们已经告诉资源管理器图标的查找位置,我们的Extract()
实现只需返回S_FALSE
,这告诉资源管理器自己提取图标。我将在下一节讨论Extract()
的参数。
STDMETHODIMP CTxtIconShlExt::Extract (
LPCTSTR pszFile, UINT nIconIndex, HICON* phiconLarge,
HICON* phiconSmall, UINT nIconSize )
{
return S_FALSE; // Tell the shell to do the extracting itself.
}
以下是我们的图标在实际中的样子:
如果你将GetIconLocation()
更改为将pwFlags
设置为GIL_SIMULATEDOC
,那么图标看起来像这样:
请注意,在大图标和图块视图中,使用的是我们图标的**小**版本(16x16版本)。在小图标视图中,资源管理器会将其进一步缩小,这并不算很漂亮。
提取方法 2
方法2涉及我们的扩展自己提取图标,并绕过资源管理器的图标缓存。使用此方法,总是会调用IExtractIcon::Extract()
,它负责加载图标并将两个HICON
返回给资源管理器——一个用于大图标,一个用于小图标。这种方法的优点是您不必担心按顺序排列图标的资源ID。缺点是它绕过了资源管理器的图标缓存,这可能在您进入包含大量文本文件的目录时稍微减慢文件浏览速度。
GetIconLocation()
与方法1类似,但它的工作量少一些,因为它只需要获取文件的大小。
STDMETHODIMP CTxtIconShlExt::GetIconLocation ( UINT uFlags, LPTSTR szIconFile, UINT cchMax, int* piIndex, UINT* pwFlags ) { DWORD dwFileSizeLo, dwFileSizeHi; HANDLE hFile; hFile = CreateFile ( m_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if ( INVALID_HANDLE_VALUE == hFile ) return S_FALSE; // tell the shell to use a default icon dwFileSizeLo = GetFileSize ( hFile, &dwFileSizeHi ); CloseHandle ( hFile ); if ( (DWORD) -1 == dwFileSizeLo && GetLastError() != NO_ERROR ) return S_FALSE; // tell the shell to use a default icon m_qwFileSize = ((DWORDLONG) dwFileSizeHi)<<32 | dwFileSizeLo;
一旦保存了文件大小,我们就将pwFlags
设置为GIL_DONTCACHE
,以告诉资源管理器不要检查其图标缓存。我们必须设置此标志,因为我们没有填充szIconFile/piIndex
,并且需要告诉资源管理器忽略它们。
还包含GIL_NOTFILENAME
标志,尽管在当前版本的shell中它没有作用。其文档目的告诉资源管理器我们没有填充szIconFile/piIndex
,但由于仅传递该标志没有意义(我们将什么都没有提供给资源管理器提取),所以它似乎甚至没有被资源管理器测试。最好还是包含该标志,以防未来版本的shell检查它。
*pwFlags = GIL_NOTFILENAME | GIL_DONTCACHE; return S_OK; }
现在让我们深入了解Extract()
。这是它的原型:
HRESULT IExtractIcon::Extract ( LPCTSTR pszFile, UINT nIconIndex, HICON* phiconLarge, HICON* phiconSmall, UINT nIconSize );
参数如下:
pszFile/nIconIndex
- 指定图标位置的文件名和索引。这些与
GetIconLocation()
返回的值相同。 phiconLarge, phiconSmall
- 指向
HICON
的指针,Extract()
必须将其设置为要使用的大图标和小图标的句柄。这些指针可能为NULL。 nIconSize
- 指示图标所需的尺寸。高字节是小图标的尺寸(高度和宽度总是相同的),低字节是**大**图标的尺寸。在正常情况下,小图标尺寸为16。大图标通常为32或48,具体取决于资源管理器处于哪种视图模式——大图标模式为32,图块模式为48。
在我们的扩展中,我们在GetIconLocation()
中没有填充文件名/索引值,所以我们可以忽略pszFile
和nIconIndex
。我们只需加载两个图标(使用哪个图标取决于文件大小)并将它们返回给资源管理器。
STDMETHODIMP CTxtIconShlExt::Extract ( LPCTSTR pszFile, UINT nIconIndex, HICON* phiconLarge, HICON* phiconSmall, UINT nIconSize ) { UINT uIconID; // Determine which icon to use, depending on the file size. if ( 0 == m_qwFileSize ) uIconID = IDI_ZERO_BYTES; else if ( m_qwFileSize < 4096 ) uIconID = IDI_UNDER_4K; else if ( m_qwFileSize < 8192 ) uIconID = IDI_UNDER_8K; else uIconID = IDI_OVER_8K; // Load the icons! if ( NULL != phiconLarge ) { *phiconLarge = (HICON) LoadImage ( _Module.GetResourceInstance(), MAKEINTRESOURCE(uIconID), IMAGE_ICON, wLargeIconSize, wLargeIconSize, LR_DEFAULTCOLOR ); } if ( NULL != phiconSmall ) { *phiconSmall = (HICON) LoadImage ( _Module.GetResourceInstance(), MAKEINTRESOURCE(uIconID), IMAGE_ICON, wSmallIconSize, wSmallIconSize, LR_DEFAULTCOLOR ); } return S_OK; }
就这样!资源管理器显示了我们返回的图标。
需要注意的是,在使用方法2时,从GetIconLocation()
返回GIL_SIMULATEDOC
标志没有效果。
注册扩展
图标处理程序在它处理的文件类型的注册表项下注册,所以在本例中,它在HKCR\txtfile
下。与其他扩展一样,在txtfile
下有一个ShellEx
项。接下来是一个IconHandler
项,该项的默认值是我们扩展的GUID。就像与拖放处理程序扩展一样,对于特定的文件类型只能有一个图标处理程序,所以GUID作为值存储在IconHandler
项下,而不是在IconHandler
下的子项中。我们还必须将DefaultIcon
项的默认值更改为"%1",才能调用我们的图标处理程序。
这是注册我们扩展的RGS脚本:
HKCR { NoRemove txtfile { NoRemove DefaultIcon = s '%%1' NoRemove ShellEx { ForceRemove IconHandler = s '{DF4F5AE4-E795-4C12-BC26-7726C27F71AE}' } } }
请注意,为了指定"%1"字符串,我们需要在RGS文件中写成"%%1",因为%是用于表示可替换参数(例如,“%MODULE%”)的特殊字符。
我们覆盖了现有的DefaultIcon
值这一事实提出了一个重要问题。如果我们覆盖了旧的DefaultIcon
值,如何正确卸载我们的扩展?答案是我们在DllRegisterServer()
中保存DefaultIcon
的值,并在DllUnregisterServer()
中恢复它。我们**必须**这样做才能干净地卸载,并使文本文件图标恢复到我们出现之前的状态。
请查看注册/注销函数中的代码,了解其工作原理。请注意,我们在调用ATL处理RGS脚本之前进行备份,因为如果我们反过来做,DefaultIcon
将在我们有机会备份之前被覆盖。
版权和许可
本文是受版权保护的材料,©2000-2006 Michael Dunn。我知道这阻止不了人们在网上到处复制它,但我还是要说。如果你有兴趣翻译这篇文章,请给我发邮件告知。我预计不会拒绝任何人翻译的许可,我只是想知道翻译情况,以便在此发布链接。
本文附带的演示代码已发布到公共领域。我这样做是为了让代码惠及所有人。(我并未将本文本身发布到公共领域,因为只在CodeProject上提供本文有助于提高我的知名度和CodeProject网站的知名度。)如果你将演示代码用于你自己的应用程序,发送电子邮件让我知道将表示赞赏(仅为了满足我对人们是否从我的代码中受益的好奇心),但非必需。在您自己的源代码中进行归属也是受欢迎的,但非必需。
修订历史
2000年10月29日:首次发布文章。
2000年11月27日:更新了一些内容。;)
2006年6月3日:更新以涵盖VC 7.1中的更改;添加了在XP上返回48x48图标的代码。
系列导航: « 第八部分