![]() |
Desktop Development »
Shell and IE programming »
Beginners
Beginner
The Complete Idiot's Guide to Writing Shell Extensions - Part IXBy Michael DunnA tutorial on writing an extension to customize the icons displayed for a file type. |
VC6, VC7.1Win2K, WinXP, ATL, GDI, VS.NET2003, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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
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.
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.
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.
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:
uFlagsGIL_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, cchMaxszIconFile 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.
piIndexint in which we'll store the index of the icon in the file (whose name we put in
szIconFile).
pwFlagsUINT 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_DONTCACHEszIconFile/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_NOTFILENAMEszIconFile/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_SIMULATEDOCIn 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]](ShellExtGuide9/ShellExtGuide9_6.png)
![[custom small icons - 28K]](ShellExtGuide9/ShellExtGuide9_7.png)
![[custom tile icons - 26K]](ShellExtGuide9/xptilemode.png)
If you change GetIconLocation() so it sets pwFlags to GIL_SIMULATEDOC,
then the icons look like this:
![[custom large icons - 24K]](ShellExtGuide9/ShellExtGuide9_8.png)
![[custom small icons - 27K]](ShellExtGuide9/ShellExtGuide9_9.png)
![[custom tile icons - 28K]](ShellExtGuide9/xptilemodedoc.png)
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.
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/nIconIndexGetIconLocation().
phiconLarge, phiconSmallHICONs which Extract() must set to the handles of the large and small
icons to use. These pointers may be NULL.
nIconSizeIn 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.
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.
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.
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
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 2 Jun 2006 Editor: Chris Maunder |
Copyright 2000 by Michael Dunn Everything else Copyright © CodeProject, 1999-2009 Web22 | Advertise on the Code Project |