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:
- 8K or larger
- 4K to 8K
- 1 byte to 4K
- 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:
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 )
{
USES_CONVERSION;
lstrcpyn ( m_szFilename, OLE2CT(wszFile), MAX_PATH );
return S_OK;
}
protected:
TCHAR m_szFilename [MAX_PATH];
DWORDLONG m_qwFileSize;
};
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:
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:. 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;
dwFileSizeLo = GetFileSize ( hFile, &dwFileSizeHi );
CloseHandle ( hFile );
if ( (DWORD) -1 == dwFileSizeLo && GetLastError() != NO_ERROR )
return S_FALSE;
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;
}
And here's what our icons look like in action:
If you change GetIconLocation()
so it sets pwFlags
to GIL_SIMULATEDOC
,
then the icons look like this:
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 HICON
s 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;
dwFileSizeLo = GetFileSize ( hFile, &dwFileSizeHi );
CloseHandle ( hFile );
if ( (DWORD) -1 == dwFileSizeLo && GetLastError() != NO_ERROR )
return S_FALSE;
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
HICON
s 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;
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;
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