Click here to Skip to main content
12,819,402 members (33,570 online)
Click here to Skip to main content
Add your own
alternative version


128 bookmarked
Posted 10 Sep 2000

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

, 31 May 2006
Rate this:
Please Sign up or sign in to vote.
A tutorial on adding columns to Explorer's details view via a column handler shell extension.



The Reader Requests portion of the Idiot's Guide continues! In this part, I'll tackle the topic of adding columns to Explorer's details view on Windows Me, 2000, or later. This type of extension doesn't exist on NT 4 or 95/98, so you must have one of the newer OSes to run the sample project.

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.

Windows Me and 2000 added a lot of customization options to Explorer's details view. On Windows 2000, there are 37 different columns you can enable! You can turn on and off columns in two ways. First, there is a short list of columns that appear in a context menu when you right-click a column header:

 [Header control context menu - 3K]

If you select the More... item, Explorer shows a dialog where you can select among all the available columns:

 [Default column settings dlg - 10K]

Explorer lets us put our own data in some of these columns, and even add columns to this list, with a column handler extension.

The sample project for this article is a column handler for MP3 files that shows the various fields of the ID3 tag (version 1 tags only) that can be stored in the MP3s.

The Extension Interface

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 MP3TagViewer, with a C++ implementation class CMP3ColExt.

A column handler only implements one interface, IColumnProvider. There is no separate initialization through IShellExtInit or IPersistFile as in other extensions. This is because a column handler is an extension of the folder, and has nothing to do with the current selection. Both IShellExtInit and IPersistFile carry with them the notion of something being selected. There is an initialization step, but it's done through a method of IColumnProvider.

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

#include <comdef.h>
#include <shlobj.h>
#include <shlguid.h>
// CMP3ColExt
class CMP3ColExt :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CMP3ColExt, &CLSID_MP3ColExt>,
  public IColumnProvider
    COM_INTERFACE_ENTRY_IID(IID_IColumnProvider, IColumnProvider)
  // IColumnProvider 
  STDMETHODIMP Initialize(LPCSHCOLUMNINIT psci) { return S_OK; }
                           VARIANT* pvarData);

Notice the COM_INTERFACE_ENTRY_IID macro in the interface map. In previous extensions, we've used COM_INTERFACE_ENTRY, which requies the interface to have an associated GUID via the __declspec(uuid) syntax. Since comdef.h doesn't define a GUID for IColumnProvider, we can't use COM_INTERFACE_ENTRY. COM_INTERFACE_ENTRY_IID exists for just this situation, so we can explicitly specify both the IID and the interface name. An alternate solution is to add this line before the class declaration:

struct __declspec(uuid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1")) IColumnProvider;

which will satisfy the requirements to make COM_INTERFACE_ENTRY work.

We also need to make some changes to stdafx.h. Since we're using Windows 2000 features, we need to #define a few symbols so we have access to declarations and prototypes related to those features:

#define WINVER       0x0500    // Enable W2K/98 features
#define _WIN32_WINNT 0x0500    // Enable W2K  features
#define _WIN32_IE    0x0500    // Enable IE 5+ features

These #defines need to be placed before all #include lines.


IColumnProvider has three methods. The first is Initialize(), which has this prototype:

HRESULT IColumnProvider::Initialize ( LPCSHCOLUMNINIT psci );

The shell passes us a SHCOLUMNINIT struct, which contains just one tidbit of info, the full path of the folder being viewed in Explorer. For our purposes, we don't need that info, so our Initialize() implementation just returns S_OK.

Enumerating the New Columns

When Explorer sees that our column handler is registered, it calls the extension to get info about each of the columns the extension implements. This is done through the GetColumnInfo() method, which has this prototype:

HRESULT IColumnProvider::GetColumnInfo ( DWORD dwIndex, SHCOLUMNINFO* psci );

dwIndex is a 0-based counter and indicates which column Explorer is interested in. The other parameter is an SHCOLUMNINFO struct which our extension fills in with the parameters of the column.

The first member of SHCOLUMNINFO is another struct, SHCOLUMNID. An SHCOLUMNID is a GUID/DWORD pair, where the GUID is called the "format ID" and the DWORD is the "property ID." This pair of numbers uniquely identifies any column on the system. It is possible to reuse an existing column (for example, Author), in which case the format ID and property ID are set to predefined values. If an extension adds new columns, it can use its own CLSID for the format ID (since the CLSID is guaranteed to be unique), and a simple counter for the property ID.

Our extension will use both methods. We'll reuse the Author, Title, and Comments columns, and add three more: MP3 Album, MP3 Year, and MP3 Genre.

Here's the beginning of our GetColumnInfo() method:

STDMETHODIMP CMP3ColExt::GetColumnInfo (
  DWORD dwIndex, SHCOLUMNINFO* psci )
  // We have 6 columns, so if dwIndex is 6 or greater, return S_FALSE to
  // indicate we've enumerated all our columns.
  if ( dwIndex >= 6 )
    return S_FALSE;

If dwIndex is 6 or larger, we return S_FALSE to stop the enumeration. Otherwise, we fill in the SHCOLUMNINFO struct. For dwIndex values 0 to 2, we will return data about one of our new columns. For values 3 to 5, we'll return data about one of the built-in columns that we're reusing. Here's how we specify the first custom column, which shows the album name field of the ID3 tag:

switch ( dwIndex )
  case 0:     // MP3 Album - separate column
    psci->scid.fmtid = CLSID_MP3ColExt;     // Use our CLSID as the format ID
    psci->   = 0;                   // Use the column # as the ID
    psci->vt         = VT_LPSTR;            // We'll return the data as a string
    psci->fmt        = LVCFMT_LEFT;         // Text will be left-aligned
    psci->csFlags    = SHCOLSTATE_TYPE_STR; // Data should be sorted as strings
    psci->cChars     = 32;                  // Default col width in chars

    wcsncpy ( psci->wszTitle, L"MP3 Album", MAX_COLUMN_NAME_LEN );
    wcsncpy ( psci->wszDescription, L"Album name of an MP3", MAX_COLUMN_DESC_LEN );

IMPORTANT: Previous versions of this article stored _Module.pguidVer in the fmtid member. This is totally wrong, because that GUID is always the same in all binaries built with the same version of ATL. If there are two extensions installed that both use _Module.pguidVer and the same property IDs, their columns will clobber each other.

We use the extension's GUID for the format ID, and the column number for the property ID. The vt member of the SHCOLUMNINIT struct indicates what type of data we will return to Explorer. VT_LPSTR indicates a C-style string. The fmt member can be one of the LVCFMT_* constants, and indicates the text alignment for the column. In this case the text will be left-aligned.

The csFlags member contains a few flags about the column. However, not all flags seem to be implemented by the shell. Here are the flags and an explanation of their effects:

Indicate how the column's data should be treated when Explorer sorts on the column. The three possibilities are string, integer, and date.
Here is a description of this flag's effect from Dave Anderson of Microsoft Developer Support (as quoted in this forum comment):
[The behavior] depends on how you have your shell browser configured. If you use the "Remember each folder's view settings" folder option, the colums displayed in a particular shell view may be restored from the registry, so in this case the SHCOLSTATE_ONBYDEFAULT flag has no effect. Resetting the all folder view settings should allow your columns to be on by default. You can do this in the Folder Options dialog in Explorer (or through Control Panel).
According to the docs, including this flag indicates that the column's data take a while to gather, and Explorer will call the extension on one or more background threads so that the Explorer UI will remain responsive. I have seen no difference in my testing when this flag is present. On Windows 2000, Explorer only uses one thread to gather data for an extension's columns. On XP, it uses a few different threads, but I didn't see any difference in the number of threads when I added or removed SHCOLSTATE_SLOW.
The docs say that passing this flag prevents a column from appearing in the header control's context menu. That implies that if you don't include this flag, the column will appear on the context menu. However, no additional columns ever appear on the context menu, so for now this flag has no effect.
Passing this flag prevents the column from appearing in the Column Settings dialog. Since there is no way of enabling a hidden column, this flag renders a column useless.

The cChars member holds the default width for a column in characters. Set this to the maximum of the lengths of the column name and the longest string you expect to show in the column. You should also add 2 or 3 to this number to ensure that the column is actually wide enough to display all of the text. (If you don't add this little bit of padding, the default width of the column may not be wide enough, and the text can get truncated.)

The final two members are unicode strings that hold the column name (the text that's shown in the header control) and a description of the column. Currently, the description is not used by the shell, and the user never sees it.

Columns 1 and 2 are pretty similar, however column 1 illustrates a point about the data type and sorting method. This column shows the year, and here's the code that defines it:

case 1:     // MP3 year - separate column
  psci->scid.fmtid = CLSID_MP3ColExt;     // Use our CLSID as the format ID
  psci->   = 1;                   // Use the column # as the ID
  psci->vt         = VT_LPSTR;            // We'll return the data as a string
  psci->fmt        = LVCFMT_RIGHT;        // Text will be right-aligned
  psci->csFlags    = SHCOLSTATE_TYPE_INT; // Data should be sorted as ints
  psci->cChars     = 6;                   // Default col width in chars

  wcsncpy ( psci->wszTitle, L"MP3 Year", MAX_COLUMN_NAME_LEN );
  wcsncpy ( psci->wszDescription, L"Year of an MP3", MAX_COLUMN_DESC_LEN );

Notice that the vt member is VT_LPSTR, meaning that we will pass a string to Explorer, but the csFlags member is SHCOLSTATE_TYPE_INT, meaning that when the data is sorted, it should be sorted numerically. While it's of course possible to return a number instead of a string, the ID3 tag stores the year as a string, so this column definition saves us the trouble of converting the year to a number.

When dwIndex is between 3 and 5, we return info about a built-in column that we are reusing. Column 3 shows the Artist ID3 field in the Author column:

case 3:     // MP3 artist - reusing the built-in Author column
  psci->scid.fmtid = FMTID_SummaryInformation;  // predefined FMTID
  psci->   = 4;                   // Predefined - author
  psci->vt         = VT_LPSTR;            // We'll return the data as a string
  psci->fmt        = LVCFMT_LEFT;         // Text will be left-aligned
  psci->csFlags    = SHCOLSTATE_TYPE_STR; // Data should be sorted as strings
  psci->cChars     = 32;                  // Default col width in chars

FMTID_SummaryInformation is a predefined symbol, and the Author field ID (4) is listed in the MSDN documentation. See the page "The Summary Information Property Set" for a complete list. When reusing a column, we don't return a title or description, since the shell already takes care of that.

Finally, after the end of the switch statement, we return S_OK to indicate that we filled in the SHCOLUMNINFO struct.

Displaying Data in the Columns

The last IColumnProvider method is GetItemData(), which Explorer calls to get the data to be shown in a column for a file. The prototype is:

HRESULT IColumnProvider::GetItemData (
  VARIANT*        pvarData );

The SHCOLUMNID struct indicates which column Explorer needs data for. It will contain the same info that we gave Explorer in our GetColumnInfo() method. The SHCOLUMNDATA struct contains details about the file or directory, including its path. We can use this info to decide if we want to provide any data for the file or directory. pvarData points at a VARIANT, in which we'll store the actual data for Explorer to show. VARIANT is the C version of the loosely-typed variables that Visual Basic and scripting languages have. It has two parts, the type and the data. ATL has a handy CComVariant class that handles all the mucking about with initializing and setting VARIANTs.

Sidebar - Handling ID3 Tags

Now would be a good time to show how our extension will read and store ID3 tag information. An ID3v1 tag is a fixed-length structure appended to the end of an MP3 file, and looks like this:

struct CID3v1Tag
  char szTag[3];      // Always 'T','A','G'
  char szTitle[30];
  char szArtist[30];
  char szAlbum[30];
  char szYear[4];
  char szComment[30];
  char byGenre;

All fields are plain chars, and the strings are not necessarily null-terminated, which requires a bit of special handling. The first field, szTag, contains the characters "TAG" to identify the ID3 tag. byGenre is a number that identifies the song's genre. (There is a predefined list of genres and their numerical IDs, available from

We will also need an additional structure that holds an ID3 tag and the name of the file that the tag came from. This struct will be used in a cache that I'll explain shortly.

#include <string>
#include <list>
typedef std::basic_string<TCHAR> tstring;  // a TCHAR string
struct CID3CacheEntry
  tstring   sFilename;
  CID3v1Tag rTag;
typedef std::list<CID3CacheEntry> list_ID3Cache;

A CID3CacheEntry object holds a filename and the ID3 tag stored in that file. A list_ID3Cache is a linked list of CID3CacheEntry structures.

OK, back to the extension. Here's the beginning of our GetItemData() function. We first check the SHCOLUMNID struct to make sure we're being called for one of our own columns.

#include <atlconv.h>
  VARIANT*        pvarData )
LPCTSTR   szFilename = OLE2CT(pscd->wszFile);
char      szField[31];
TCHAR     szDisplayStr[31];
bool      bUsingBuiltinCol = false;
CID3v1Tag rTag;
bool      bCacheHit = false;
  // Verify that the format id and column numbers are what we expect.
  if ( pscid->fmtid == CLSID_MP3ColExt )
    if ( pscid->pid > 2 )
      return S_FALSE;

If the format ID is our own GUID, the property ID must be 0, 1, or 2, since those are the IDs we used back in GetColumnInfo(). If, for some reason, the ID is out of this range, we return S_FALSE to tell the shell that we have no data for it, and the column should appear empty.

We next compare the format ID with FMTID_SummaryInformation, and then check the property ID to see if it's a property that we provide.

else if ( pscid->fmtid == FMTID_SummaryInformation )
  bUsingBuiltinCol = true;

  if ( pscid->pid != 2 && pscid->pid != 4 && pscid->pid != 6 )
    return S_FALSE;
  return S_FALSE;

Next, we check the attributes of the file whose name we were passed. If it's actually a directory, or if the file is offline (that is, it's been moved to another storage medium like tape), we bail out. We also check the file extension, and return if it isn't .MP3.

// If we're being called with a directory (instead of a file), we can
// bail immediately. Also bail if the file is offline.
  return S_FALSE;

// Check the file extension.  If it's not .MP3, we can return.
if ( 0 != wcsicmp ( pscd->pwszExt, L".mp3" ) )
  return S_FALSE;

At this point, we've determined we want to operate on the file. Here's where our ID3 tag cache comes into use. The MSDN docs say that the shell will group calls to GetItemData() by file, meaning that it will try to call GetItemData() with the same filename in consecutive calls. We can take advantage of that behavior and cache the ID3 tag for a particular file, so that we don't have to read the tag from the file again on subsequent calls.

We first iterate through the cache (stored as a member variable, m_ID3Cache), comparing the cached filenames with the filename passed to the function. If we find the name in our cache, we grab the associated ID3 tag.

  // Look for the filename in our cache.
list_ID3Cache::const_iterator it, itEnd;
  for ( it = m_ID3Cache.begin(), itEnd = m_ID3Cache.end();
        !bCacheHit && it != itEnd; it++ )
    if ( 0 == lstrcmpi ( szFilename, it->sFilename.c_str() ))
      CopyMemory ( &rTag, &it->rTag, sizeof(CID3v1Tag) );
      bCacheHit = true;

If bCacheHit is false after that loop, we need to read the file and see if it has an ID3 tag. The helper function ReadTagFromFile() does the dirty work of reading the last 128 bytes of the file, and returns TRUE on success or FALSE if a file error occurred. Note that ReadTagFromFile() returns whatever the last 128 bytes are, regardless of whether they are really an ID3 tag.

// If the file's tag wasn't in our cache, read the tag from the file.
if ( !bCacheHit )
  if ( !ReadTagFromFile ( szFilename, &rTag ) )
    return S_FALSE;

So now we have an ID3 tag. We check the size of our cache, and if it contains 5 entries, the oldest is removed to make room for the new entry. (5 is just an arbitrary small number.) We create a new CID3CacheEntry object and add it to the list.

// We'll keep the tags for the last 5 files cached - remove the oldest
// entries if the cache is bigger than 4 entries.
while ( m_ID3Cache.size() > 4 )

// Add the new ID3 tag to our cache.
CID3CacheEntry entry;

entry.sFilename = szFilename;
CopyMemory ( &entry.rTag, &rTag, sizeof(CID3v1Tag) );

m_ID3Cache.push_front ( entry );
}   // end if(!bCacheHit)

Our next step is to test the first three signature bytes to determine if an ID3 tag is present. If not, we can return immediately.

// Check if we really have an ID3 tag by looking for the signature.
if ( 0 != StrCmpNA ( rTag.szTag, "TAG", 3 ) )
  return S_FALSE;

Next, we read the field from the ID3 tag that corresponds to the property that the shell is requesting. This involves just testing the property IDs. Here is one example, for the Title field:

// Format the details string.
if ( bUsingBuiltinCol )
  switch ( pscid->pid )
    case 2:  // song title
      CopyMemory ( szField, rTag.szTitle, countof(rTag.szTitle) );
      szField[30] = '\0';

Notice that our szField buffer is 31 chars long, 1 longer than the longest ID3v1 field. This way we know we'll always end up with a properly null-terminated string. The bUsingBuiltinCol flag was set earlier when we tested the FMTID/PID pair. We need that flag because the PID alone isn't enough to identify a column - the Title and MP3 Genre columns both have PID 2.

At this point, szField contains the string we read from the ID3 tag. WinAmp's ID3 tag editor pads strings with spaces instead of null characters, so we correct for this by removing any trailing spaces:

StrTrimA ( szField, " " );

And finally, we create a CComVariant object and store the szDisplayStr string in it. Then we call CComVariant::Detach() to copy the data from the CComVariant into the VARIANT provided by Explorer.

CComVariant vData ( szField );
  vData.Detach ( pvarData );
  return S_OK;

What Does It Look Like?

Our new columns appear at the end of the list in the Column Settings dialog:

 [Column settings dlg with new columns - 10K]

Here's what the columns look like. The files are being sorted by our custom MP3 Album column.

Registering the Extension

Since column handlers extend folders, they are registered under the HKCR\Folders key. Here is the section to add to the RGS file that registers our column handler extension:

  NoRemove Folder
    NoRemove Shellex
      NoRemove ColumnHandlers
        ForceRemove {AC146E80-3679-4BCA-9BE4-E36512573E6C} = s 'ID3v1 viewer column ext'

An Extra Goodie - Infotips

Another interesting thing a column handler can do is customize the infotip for a file type. This RGS script creates a custom infotip for MP3 files (the text here has been broken into several lines to prevent horizontal scrolling; it must be all one line in the actual RGS file):

  NoRemove .mp3
    val InfoTip = s 'prop:Type;Author;Title;Comment;

Notice that the Author, Title, and Comment fields appear in the prop: string. When you hover the mouse over an MP3 file, Explorer will call our extension to get stings to show for those fields. The docs say that our custom fields can appear in infotips as well (that's why our GUID and property IDs appear in the string above), however I could not get this to work on Windows 2000; only the built-in properties appear in the infotips. Here's what a custom infotip looks like:

 [Custom InfoTip - 3K]

Also note that this customization may not work on XP, because XP introduced some new file type registry keys. On my XP system, the infotip information is kept in HKCR\SystemFileAssociations\audio.

To Be Continued...

Coming up in Part IX, we'll see another new type of extension, the icon handler, that can customize the icons shown for a particular file type.

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

Sept 11, 2000: Article first published.
June 13, 2001: Something updated. ;)
June 2, 2006: Updated to cover changes in VC 7.1, Win Me, and XP. Sample code works on Me.

Series Navigation: « Part VII | Part IX »


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


About the Author

Michael Dunn
Software Developer (Senior) VMware
United States United States
Michael lives in sunny Mountain View, California. He started programming with an Apple //e in 4th grade, graduated from UCLA with a math degree in 1994, and immediately landed a job as a QA engineer at Symantec, working on the Norton AntiVirus team. He pretty much taught himself Windows and MFC programming, and in 1999 he designed and coded a new interface for Norton AntiVirus 2000.
Mike has been a a developer at Napster and at his own lil' startup, Zabersoft, a development company he co-founded with offices in Los Angeles and Odense, Denmark. Mike is now a senior engineer at VMware.

He also enjoys his hobbies of playing pinball, bike riding, photography, and Domion on Friday nights (current favorite combo: Village + double Pirate Ship). He would get his own snooker table too if they weren't so darn big! He is also sad that he's forgotten the languages he's studied: French, Mandarin Chinese, and Japanese.

Mike was a VC MVP from 2005 to 2009.

You may also be interested in...

Comments and Discussions

QuestionAdding columns to Network view Pin
earweed21-Feb-13 6:41
memberearweed21-Feb-13 6:41 
GeneralHint: For InfoTips don't use "," comma Pin
starktdh1-Oct-10 2:06
memberstarktdh1-Oct-10 2:06 
Hi all,

for the InfoTips based on ColumnHandlers, do not use "," comma to add them to a Registry Value, such as "InfoTip". Example:
{CLSID},1 will call ColumnID 0 handler
{CLSID}1 will call ColumnID 1 handler

@Michael, perhaps you mention the "InfoTips with Columnhandler" way of implementing it, in the description of the InfoTip handler (is it a chapter earlier?). For example right were you describe, that the infotip handlers won't work für "*" or all "AllFilesystemObjects"...

Finally let me say: great articles! Thanks a lot,

Generalcolumn doesn't not display as default Pin
Vuong Do Quoc20-Apr-10 17:56
memberVuong Do Quoc20-Apr-10 17:56 
QuestionHow about sort function and default displaying new column Pin
namphvn13-Apr-10 6:15
membernamphvn13-Apr-10 6:15 
QuestionCan we show\ hide a column Pin
Javed Akhtar Ansari29-Oct-08 22:47
memberJaved Akhtar Ansari29-Oct-08 22:47 
QuestionVista? Pin
Hans Dietrich27-Oct-08 19:12
mvpHans Dietrich27-Oct-08 19:12 
AnswerRe: Vista? Pin
Javed Akhtar Ansari29-Oct-08 22:44
memberJaved Akhtar Ansari29-Oct-08 22:44 
AnswerRe: Vista? Pin
umeca7412-Feb-10 22:59
memberumeca7412-Feb-10 22:59 
QuestionSHCOLSTATE_HIDDEN not working as documented Pin
Javed Akhtar Ansari23-Oct-08 4:28
memberJaved Akhtar Ansari23-Oct-08 4:28 
Script21-Jan-08 1:38
memberScript21-Jan-08 1:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170308.1 | Last Updated 1 Jun 2006
Article Copyright 2000 by Michael Dunn
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid