Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

The Complete Idiot's Guide to Writing Shell Extensions - Part IX

0.00/5 (No votes)
2 Jun 2006 4  
A tutorial on writing an extension to customize the icons displayed for a file type.

Contents

Introduction

Welcome part 9! This article is another Reader Requests one, and will discuss how to display a custom icon for every file of a particular type (in this case, text files). The accompanying sample code will run on any version of Windows.

Remember that VC 7 (and probably VC 8) users will need to change some settings before compiling. See the README section in Part I for the details.

Everyone knows that each file type is represented by a particular icon in Explorer. Bitmaps are shown with the paintbrush icon, HTML pages have a paper-with-IE-logo icon, and so on. Explorer determines what icon to use by looking in the registry, and reading the key under HKEY_CLASSES_ROOT that corresponds to the file type. This method results in one icon being used for all files of a particular type.

However, this is not the only way to specify icons. Explorer lets us customize the icons on a file-by-file basis by writing an icon handler extension. In fact, an example of file-by-file icons is already built in to Windows. Explore to the Windows directory (or any directory that has a lot of EXEs) and you'll see that each EXE has a different icon (except for EXEs without icon resources, which all get a generic icon). ICO and CUR files similarly get a different icon for every file.

This article's sample project is an icon handler extension that shows one of 4 different icons for text files, based on the size of the file. The icons are displayed as follows:

 [file icon - 1K] - 8K or larger

 [file icon - 1K] - 4K to 8K

 [file icon - 1K] - 1 byte to 4K

 [file icon - 1K] - Zero bytes

The Extension Interfaces

You should be familiar with the set-up steps now, so I'll skip the instructions for going through the VC wizards. If you're following along in the wizards, make a new ATL COM app called TxtFileIcons, with a C++ implementation class CTxtIconShlExt.

An icon handler implements two interfaces, IPersistFile and IExtractIcon. Recall that IPersistFile is used to initialize extensions that only operate on one file at a time, as opposed to IShellExtInit which is for extensions that act on all the selected files at once. IExtractIcon has two methods, both of which are involved in telling Explorer which icon to use for a particular file.

Be aware that Explorer creates a COM object for every file displayed. That means that an instance of the C++ class is created for every file. Therefore, you should avoid time-consuming operations in your extension, to avoid making the Explorer interface appear sluggish.

The Initialization Interface

To add IPersistFile to our COM object, open TxtIconShlExt.h and add the lines listed here in bold.

#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.

};

As with other extensions that use IPersistFile, the only method that needs an implementation is Load(), since that's how Explorer tells us what file we're acting on. The implementation of Load() is done inline, and just copies the filename to the m_szFilename member variable for later use.

The IExtractIcon Interface

An icon handler also implements the IExtractIcon interface, which Explorer calls when it needs an icon for a file. Since our extension will be for text files, Explorer will call IExtractIcon methods every time a text file is displayed in an Explorer window or the Start menu. To add IExtractIcon to our COM object, open TxtIconShlExt.h and add the lines listed here in bold:

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);
};

There are two ways to return an icon to Explorer. First, GetIconLocation() can return a filename/index pair which indicates the file that contains the icon, and the zero-based index of the icon in that file. So for example, C:\windows\system\shell32.dll/9 is a possible return value, which tells Explorer to use the 9th icon (counting from 0) in shell32.dll. That doesn't mean use the icon whose resource ID is 9, it means to look at the resource IDs in order and use the 9th one (going from the smallest to largest IDs). Extract() doesn't have to do anything but return S_FALSE to tell Explorer to extract the icon itself.

What's special about this method is that Explorer may or may not call Extract() after GetIconLocation() returns. Explorer keeps an icon cache which holds recently-used icons. If GetIconLocation() returns a filename/index pair that was used recently, and the icon is still in the cache, Explorer will use the cached icon and will not call Extract().

The second method is to return a "don't look in the cache" flag from GetIconLocation(), which makes Explorer always call Extract(). Extract() is then responsible for loading the icons and returning handles to those icons that Explorer will show.

Extraction Method 1

The first IExtractIcon method called is GetIconLocation(). This function looks at the file (whose name was stored during IPersistFile::Load()) and returns a filename/index pair, as discussed above. The prototype for GetIconLocation() is:

HRESULT IExtractIcon::GetIconLocation (
  UINT uFlags, LPTSTR szIconFile, UINT cchMax,
  int* piIndex, UINT* pwFlags );

The parameters are:

uFlags
Some flags that can change the behavior of extensions. GIL_ASYNC is passed to ask if the extraction process will take a long time, and if so, the extension can request that the extraction happen on a background thread, so the Explorer interface won't appear sluggish. The other flags, GIL_FORSHELL and GIL_OPENICON, appear to be meaningful only in namespace extensions. For our purposes, we won't worry about the flags since our code won't take a long time to execute.
szIconFile, cchMax
szIconFile is a buffer provided by the shell in which we'll store the name of the file containing the icon to use. cchMax is the size of the buffer, in characters.
piIndex
Pointer to an int in which we'll store the index of the icon in the file (whose name we put in szIconFile).
pwFlags
Pointer to a UINT in which we can return flags that change Explorer's behavior. The flags are explained below.

GetIconLocation() fills in the szIconFile and piIndex parameters and returns S_OK. It can also return S_FALSE if we decide we don't want to provide a custom icon after all, in which case Explorer will fall back to the generic "unknown file" icon: [def. icon - 2K] . The flags that can be returned in pwFlags are:

GIL_DONTCACHE
Tells Explorer to not check the icon cache to see if the icon specified in szIconFile/piIndex has been used recently. The result is that IExtractIcon::Extract() is always called. I'll have more to say about this flag later, when I describe extraction method 2.
GIL_NOTFILENAME
According to MSDN, this flag tells Explorer to ignore the contents of szIconFile/piIndex when GetIconLocation() returns. Apparently, this is how extensions should tell Explorer to always call IExtractIcon::Extract(), however this flag has no effect on what Explorer does after GetIconLocation() returns. I'll say more about this later.
GIL_SIMULATEDOC
This flag tells Explorer to take the icon returned by the extension, put in it inside the "dog-eared paper" icon, and use that as the icon for the file. I will demonstrate this flag below.

In method 1, our extension's GetIconLocation() function gets the size of the file, and based on the size, returns an index between 0 and 3 inclusive. This brings up one drawback of this method - you need to keep track of your resource IDs and make sure they're in the right order. Our extension only has 4 icons, so this bookkeeping isn't difficult, but if you have many more icons, or if you add/remove some icons from your project, you have to be careful with your resource IDs.

Here's our GetIconLocation() function. We first open the file and get its size. If an error happens along the way, we return S_FALSE to have Explorer use a default icon.

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;

Next, we get the path of our DLL, since our DLL contains the icons. The path is then copied into the szIconFile buffer.

TCHAR szModulePath[MAX_PATH];
 
  GetModuleFileName ( _Module.GetResourceInstance(),
                      szModulePath, MAX_PATH );
 
  lstrcpyn ( szIconFile, szModulePath, cchMax );

Next, we check the file size and set piIndex to the correct index. (See the top of the article for the icons used.)

  if ( 0 == qwSize )
    *piIndex = 0;
  else if ( qwSize < 4096 )
    *piIndex = 1;
  else if ( qwSize < 8192 )
    *piIndex = 2;
  else 
    *piIndex = 3;

Finally, we set pwFlags to 0 to get the default behavior from Explorer. This means that it checks its icon cache to see if the icon specified by szIconFile/piIndex is in the cache. If it is, then IExtractIcon::Extract() will not be called. We then return S_OK to indicate that GetIconLocation() succeeded.

  *pwFlags = 0;
  return S_OK;
}

Since we've told Explorer where to find the icon, our implementation of Extract() just returns S_FALSE, which tells Explorer to extract the icon itself. I'll discuss the parameters of Extract() in the next section.

STDMETHODIMP CTxtIconShlExt::Extract (
  LPCTSTR pszFile, UINT nIconIndex,  HICON* phiconLarge,
  HICON* phiconSmall, UINT nIconSize )
{
  return S_FALSE;    // Tell the shell to do the extracting itself.

}

And here's what our icons look like in action:

 [custom large icons - 24K]

 [custom small icons - 28K]

 [custom tile icons - 26K]

If you change GetIconLocation() so it sets pwFlags to GIL_SIMULATEDOC, then the icons look like this:

 [custom large icons - 24K]

 [custom small icons - 27K]

 [custom tile icons - 28K]

Note that in large icon and tile view, the small version of our icon (the 16x16 version) is used. In small icon view, Explorer shrinks the small icon down even further, which isn't exactly pretty.

Extraction Method 2

Method 2 involves our extension extracting the icons itself, and bypassing Explorer's icon cache. Using this method, IExtractIcon::Extract() is always called, and it is responsible for loading the icons and returning two HICONs to Explorer - one for the large icon, and one for the small icon. The advantage of this method is that you don't have to worry about keeping your icons' resource IDs in order. The downside is that it bypasses Explorer's icon cache, which conceivably might slow down file browsing a bit if you go into a directory with a ton of text files.

GetIconLocation() is similar to method 1, but it has a bit less work to do since it only needs to get the size of the file.

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;

Once we have the file size saved, we set pwFlags to GIL_DONTCACHE to tell Explorer not to check its icon cache. We must set this flag because we don't fill in szIconFile/piIndex and we need to tell Explorer to ignore them.

The GIL_NOTFILENAME flag is included as well, although in current versions of the shell it has no effect. Its documented purpose is to tell Explorer that we didn't fill in szIconFile/piIndex, but since passing that flag by itself is meaningless (we'd be giving Explorer nothing to extract from), it seems like it's not even tested for by Explorer. It's a good idea to include the flag anyway, in case future versions of the shell check for it.

  *pwFlags = GIL_NOTFILENAME | GIL_DONTCACHE;
  return S_OK;
}

Now for an in-depth look at Extract(). Here's its prototype:

HRESULT IExtractIcon::Extract (
  LPCTSTR pszFile, UINT nIconIndex, HICON* phiconLarge,
  HICON* phiconSmall, UINT nIconSize );

The parameters are:

pszFile/nIconIndex
The filename and index specifying the icon location. These are the same as the values returned from GetIconLocation().
phiconLarge, phiconSmall
Pointers to HICONs which Extract() must set to the handles of the large and small icons to use. These pointers may be NULL.
nIconSize
Indicates the desired sizes of the icons. The high word is the dimensions (both height and width, since they're always the same) of the small icon, and the low word holds the dimensions of the of the large icon. Under normal circumstances, the small icon size will be 16. The large icon will usually be 32 or 48, depending on which view mode Explorer is in - 32 for large icon mode, 48 for tile mode.

In our extension, we didn't fill in the name/index values in GetIconLocation(), so we can ignore pszFile and nIconIndex. We just load up the two icons (which icons we use depends on the file size) and return them to Explorer.

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;
}

And that's it! Explorer displays the icons that we return.

One thing to note is that when using method 2, returning the GIL_SIMULATEDOC flag from GetIconLocation() has no effect.

Registering the Extension

An icon handler is registered under the registry key of the file type it handles, so in our case it goes under HKCR\txtfile. As in other extensions, there is a ShellEx key under txtfile. Next is an IconHandler key, and the default value of this key is our extension's GUID. Just as with the drop handler extension, there can be only one icon handler for a particular file type, so the GUID is stored as a value in the IconHandler key, instead of in a subkey under IconHandler. We also have to change the DefaultIcon key's default value to "%1" for our icon handler to be invoked.

Here is the RGS script that registers our extension:

HKCR
{
  NoRemove txtfile
  {
    NoRemove DefaultIcon = s '%%1'
    NoRemove ShellEx
    {
      ForceRemove IconHandler = s '{DF4F5AE4-E795-4C12-BC26-7726C27F71AE}'
    }
  }
}

Note that in order to specify a string of "%1", we need to write "%%1" in the RGS file, since % is a special character used to indicated replaceable parameters (for example, "%MODULE%").

The fact that we overwrite the existing DefaultIcon value raises an important point. How do we properly uninstall our extension if we have overwritten the old DefaultIcon value? The answer is that we save the DefaultIcon value in DllRegisterServer(), and restore it in DllUnregisterServer(). We must do this in order to uninstall cleanly and leave the text file icons the way they were before we came along.

Take a look at the code in the register/unregister functions to see how it works. Note that we make the backup before calling ATL to process the RGS script, since if we did it the other way around, DefaultIcon would be overwritten before we got the chance to make a backup.

Copyright and License

This article is copyrighted material, ©2000-2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.

The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

Oct 29, 2000: Article first published.
Nov 27, 2000: Something updated. ;)
June 3, 2006: Updated to cover changes in VC 7.1; added code for returning 48x48 icons on XP.

Series Navigation: « Part VIII

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here