The Code Project View our sponsorsClick here for Dundas Software's TCP/IP Development KitAdvertise on the CodeProject
Home >> Shell Programming >> Beginners

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

A tutorial on adding columns to Explorer's details view via a column handler shell extension. 
 Beginner
 VC6, W2K, ATL
 Posted 11 Sep 2000
 Updated 13 Jun 2001
Articles by this author
Send to a friend
Printer friendly version
Home Latest updates Submit your article About Us Advertise on the Code Project Contact us Discussion Forums
Navigation bar
12 users have rated this article. result:
4.58 out of 5.

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 2000. This type of extension doesn't exist on NT 4 or Win 9x, so you must have Win 2K to run the sample project.

Details View in Windows 2000

Windows 2000 adds a lot of customization options to Explorer's details view. There are 37 different columns you can enable! You can turn on and off columns in two ways. First, there are 8 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. It appears that Explorer does not let extensions add columns to the list in the context menu.

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

Using AppWizard to get started

Run the AppWizard and make a new ATL COM wizard app. We'll call it MP3TagViewer. Click OK to proceed to the first (and only) wizard dialog. Keep all the defaults and click Finish. We now have an empty ATL project that will build a DLL, but we need to add our shell extension COM object. In the ClassView tree, right-click the MP3TagViewer classes item, and pick New ATL Object.

In the ATL Object Wizard, the first panel already has Simple Object selected, so just click Next. On the second panel, enter MP3ColExt in the Short Name edit box and click OK. (The other edit boxes on the panel will be filled in automatically.) This creates a class called CMP3ColExt that contains the basic code for implementing a COM object. We will add our code to this class.

The extension interface

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 initialization, 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 red.

#include <comdef.h>
#include <shlobj.h>
#include <shlguid.h>

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

/////////////////////////////////////////////////////////////////////////////
// CMP3ColExt

class ATL_NO_VTABLE CMP3ColExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CMP3ColExt, &CLSID_MP3ColExt>,
    public IMP3ColExt,
    public IColumnProvider
{
BEGIN_COM_MAP(CMP3ColExt)
    COM_INTERFACE_ENTRY(IMP3ColExt)
    COM_INTERFACE_ENTRY(IColumnProvider)
END_COM_MAP()

public:
    // IColumnProvider 
    STDMETHOD (Initialize)(LPCSHCOLUMNINIT psci) { return S_OK; }
    STDMETHOD (GetColumnInfo)(DWORD dwIndex, SHCOLUMNINFO* psci);
    STDMETHOD (GetItemData)(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData);
};

Notice the IColumnProvider declaration before the class declaration. This is necessary in order for the COM_INTERFACE_ENTRY macro to work. Someone at Microsoft forgot to define a UUID for IColumnProvider in comdef.h, so we need to do the declaration ourselves. ATL has a COM_INTERFACE_ENTRY_IID macro for use in this situation, where there is no UUID assigned to a symbol via the __declspec(uuid()) syntax, however when I used that macro, Explorer ended up passing a bogus pointer to IDispatch::GetTypeInfo(), resulting in the extension crashing.

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

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

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 currently only contains 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 extension 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 > 5 )
        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 = *_Module.pguidVer;   // This is a handy GUID
            psci->scid.pid   = 0;                   // Any ol' ID will do, but using the col # is easiest
            psci->vt         = VT_LPSTR;            // We'll return the data as a string
            psci->fmt        = LVCFMT_LEFT;         // Text will be left-aligned in the column
            psci->csFlags    = SHCOLSTATE_TYPE_STR; // Data should be sorted as strings
            psci->cChars     = 32;                  // Default col width in chars

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

We use our module'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
The docs say that including this flag will cause the column to appear in folder windows by default, until the user disables the column. I have not been able to reproduce this behavior.
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. Explorer only uses one thread to gather data for an extension's columns.
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 (currently) 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 ellipsed.)

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 = *_Module.pguidVer;   // This is a handy GUID
            psci->scid.pid   = 1;                   // Any ol' ID will do, but using the col # is easiest
            psci->vt         = VT_LPSTR;            // We'll return the data as a string
            psci->fmt        = LVCFMT_RIGHT;        // Text will be right-aligned in the column
            psci->csFlags    = SHCOLSTATE_TYPE_INT; // Data should be sorted as ints
            psci->cChars     = 6;                   // Default col width in chars

            lstrcpynW ( psci->wszTitle, L"MP3 Year", MAX_COLUMN_NAME_LEN );
            lstrcpynW ( 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 3 to 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 in the column
            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 incarnation of the loosely-typed variables that VB and other scripting languages have. It has two parts, the type and the data. ATL has a handy CComVariant class, which I'll demonstrate below, 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 == *_Module.pguidVer )
        {
        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 (that is, backed up to tape or some
    // other storage).  
    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 != lstrcmpiW ( 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:

    // WinAmp will pad strings with spaces instead of nulls, so remove 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.

    // Create a VARIANT with the details string, and return it back to the shell.
CComVariant vData ( szField );

    vData.Detach ( pvarData );

    return S_OK;
}

What does it all 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 Author column.

 [New columns in Explorer - 8K]

Registering the shell 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 'MP3 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:

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 MSDN 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 in Win2K this does not work. Only the built-in properties appear in the InfoTips. Here's what a custom InfoTip looks like:

 [Custom InfoTip - 3K]

About Michael Dunn

Michael lives in sunny Los Angeles, California, and is so spoiled by the weather that he will probably never be able to live anywhere else. He graduated from UCLA with a math degree in 1995, 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.

Michael is currently working at Predictive Networks in Westlake Village, CA as an SQA Engineer.

He also enjoys his hobbies of playing pinball, bike riding, and the occasional PlayStation or Dreamcast game. (Game currently in the DC: Confidential Mission.) He is also trying not to forget the languages he's studied: French, Mandarin Chinese, and Japanese.

Click here to visit Michael Dunn's homepage.

[Top] Sign in to vote for this article:     PoorExcellent  

View our sponsorsClick here for Whole Tomato Software - Home of Visual AssistAdvertise on the CodeProject

Hint: For improved responsiveness, use Internet Explorer 4 (or above) with Javascript enabled, choose 'Use DHTML' from the View dropdown and hit 'Set Options'.
 Keyword Filter
 View   Per page   Messages since
New threadMessages 1 to 25 of 25 (Total: 25)First Prev Next Last
Subject 
Author 
Date 
  Something completely different
Duke 11:06 2 Jul 01 
  How to force a column redraw ?
Mickael Pointier 3:52 14 Jun 01 
  Re: How to force a column redraw ?
Michael Dunn 1:29 15 Jun 01 
  Article updated.
Michael Dunn 11:41 16 May 01 
  Could not compile the example !
Anonymous 1:54 2 May 01 
  Re: Could not compile the example !
Michael Dunn 4:14 2 May 01 
  ShellFolder object
keaton 18:47 13 Feb 01 
  Thanks Michael
Alex White 12:05 9 Feb 01 
  Re: Thanks Michael
Michael Dunn 12:59 9 Feb 01 
  Suggestion for part "n"
Jason De Arte 21:37 22 Nov 00 
  It should work in Windows ME also
SoftLion 4:35 21 Nov 00 
  Suggestion
Anonymous 0:34 18 Nov 00 
  A new topic - Adding a menu item or toolbar item to IE
Ray Yang 3:26 20 Oct 00 
  Persistence of a PIDL
Florian W. 13:25 3 Oct 00 
  Re: Persistence of a PIDL
Michael Dunn 14:03 3 Oct 00 
  Idea for another extension...
Andrew Forget 5:52 24 Sep 00 
  Re: Idea for another extension...
Michael Dunn 16:12 24 Sep 00 
  Wish
Mayuresh 22:17 23 Sep 00 
  Re: Wish
Michael Dunn 16:17 24 Sep 00 
  Article Suggestion: Transparent Windows
padgett 12:09 19 Sep 00 
  Extending existing columns
Matthew Ellis 7:09 18 Sep 00 
  Re: Extending existing columns
Michael Dunn 20:40 19 Sep 00 
  Extending existing columns ?
Sergei Zhirikov 6:11 29 Mar 01 
  Article about scrap files
Christian Skovdal Andersen 14:17 14 Sep 00 
  Re: Article about scrap files
Glenn Carr 15:56 18 Sep 00 
Last Visit: 12:00 Friday 1st January, 1999First Prev Next Last
Home >> Shell Programming >> Beginners
Advertise on The Code Project
Article content copyright Michael Dunn, 2000
everything else © CodeProject, 1999-2001.