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 VIII

0.00/5 (No votes)
31 May 2006 1  
A tutorial on adding columns to Explorer's details view via a column handler shell extension.

Contents

Introduction

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
{
  BEGIN_COM_MAP(CMP3ColExt)
    COM_INTERFACE_ENTRY_IID(IID_IColumnProvider, IColumnProvider)
  END_COM_MAP()
 
public:
  // IColumnProvider 

  STDMETHODIMP Initialize(LPCSHCOLUMNINIT psci) { return S_OK; }
  STDMETHODIMP GetColumnInfo(DWORD dwIndex, SHCOLUMNINFO* psci);
  STDMETHODIMP GetItemData(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd,
                           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.

Initialization

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->scid.pid   = 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 );
    break;

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:

SHCOLSTATE_TYPE_STR, SHCOLSTATE_TYPE_INT, and SHCOLSTATE_TYPE_DATE
Indicate how the column's data should be treated when Explorer sorts on the column. The three possibilities are string, integer, and date.
SHCOLSTATE_ONBYDEFAULT
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).
SHCOLSTATE_SLOW
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.
SHCOLSTATE_SECONDARYUI
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.
SHCOLSTATE_HIDDEN
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->scid.pid   = 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 );
    break;

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->scid.pid   = 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

    break;

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 (
  LPCSHCOLUMNID   pscid,
  LPCSHCOLUMNDATA pscd,
  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 ID3.org.)

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>

 
STDMETHODIMP CMP3ColExt::GetItemData (
  LPCSHCOLUMNID   pscid,
  LPCSHCOLUMNDATA pscd,
  VARIANT*        pvarData )
{
USES_CONVERSION;
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;
    }
  else
    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.  

  if ( pscd->dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_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 )
      m_ID3Cache.pop_back();
 
    // 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';
      break;
      ...
      }

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:

HKCR
{
  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):

HKCR
{
  NoRemove .mp3
  {
    val InfoTip = s 'prop:Type;Author;Title;Comment;
                     {AC146E80-3679-4BCA-9BE4-E36512573E6C},0;
                     {AC146E80-3679-4BCA-9BE4-E36512573E6C},1;
                     {AC146E80-3679-4BCA-9BE4-E36512573E6C},2;Size'
  }
}

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 »

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