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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (39投票s)

2000年10月29日

viewsIcon

754755

downloadIcon

5769

本教程介绍如何编写一个扩展来定制特定文件类型的图标显示。

目录

引言

欢迎阅读第九部分!本篇文章是另一篇读者请求的文章,它将讨论如何为特定类型的文件(本例中为文本文件)显示自定义图标。附带的示例代码可在任何版本的Windows上运行。

请记住,VC 7(可能还有 VC 8)用户在编译前需要更改一些设置。请参阅 第一部分的 README 部分 以了解详细信息。

众所周知,每种文件类型在资源管理器中都由特定的图标表示。位图显示为画笔图标,HTML页面显示为带有IE标志的纸张图标,等等。资源管理器通过查找注册表,并读取HKEY_CLASSES_ROOT下与文件类型对应的项来确定使用哪个图标。此方法导致所有同一类型的文件使用同一个图标。

然而,这并不是指定图标的唯一方法。资源管理器允许我们通过编写一个**图标处理程序**扩展来逐个文件地自定义图标。事实上,Windows本身就内置了逐个文件图标的例子。浏览到Windows目录(或任何包含大量EXE文件的目录),你会发现每个EXE都有不同的图标(除了没有图标资源的EXE,它们都显示通用图标)。ICO和CUR文件也是如此,每个文件都有不同的图标。

本文的示例项目是一个图标处理程序扩展,它根据文件大小为文本文件显示4种不同的图标之一。图标显示如下:

 [文件图标 - 1K] - 8K 或更大

 [文件图标 - 1K] - 4K 到 8K

 [文件图标 - 1K] - 1字节 到 4K

 [文件图标 - 1K] - 零字节

扩展接口

你应该对设置步骤很熟悉了,所以我将跳过VC向导的说明。如果你正在使用向导,请创建一个名为TxtFileIcons的新ATL COM应用程序,并带有一个C++实现类CTxtIconShlExt

图标处理程序实现两个接口:IPersistFileIExtractIcon。回想一下,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_FORSHELLGIL_OPENICON似乎只在命名空间扩展中有意义。就我们而言,我们不会担心这些标志,因为我们的代码执行不会花费很长时间。
szIconFile, cchMax
szIconFile是shell提供的一个缓冲区,我们将在此存储要使用的图标所在文件的名称。cchMax是缓冲区的大小(以字符为单位)。
piIndex
指向一个int的指针,我们将在此存储szIconFile中文件内的图标索引。
pwFlags
指向一个UINT的指针,我们可以在其中返回改变资源管理器行为的标志。标志将在下面解释。

GetIconLocation()填充szIconFilepiIndex参数并返回S_OK。如果决定不再提供自定义图标,它也可以返回S_FALSE,在这种情况下,资源管理器将回退到通用的“未知文件”图标: [默认图标 - 2K] 。可以在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.
}

以下是我们的图标在实际中的样子:

 [custom large icons - 24K]

 [custom small icons - 28K]

 [custom tile icons - 26K]

如果你将GetIconLocation()更改为将pwFlags设置为GIL_SIMULATEDOC,那么图标看起来像这样:

 [custom large icons - 24K]

 [custom small icons - 27K]

 [custom tile icons - 28K]

请注意,在大图标和图块视图中,使用的是我们图标的**小**版本(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()中没有填充文件名/索引值,所以我们可以忽略pszFilenIconIndex。我们只需加载两个图标(使用哪个图标取决于文件大小)并将它们返回给资源管理器。

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图标的代码。

系列导航: « 第八部分

© . All rights reserved.