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:
If you select the More... item, Explorer shows a dialog where you can select among all the available
columns:
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>
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:
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
#define _WIN32_WINNT 0x0500
#define _WIN32_IE 0x0500
These #define
s 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 )
{
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:
psci->scid.fmtid = CLSID_MP3ColExt;
psci->scid.pid = 0;
psci->vt = VT_LPSTR;
psci->fmt = LVCFMT_LEFT;
psci->csFlags = SHCOLSTATE_TYPE_STR;
psci->cChars = 32;
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:
psci->scid.fmtid = CLSID_MP3ColExt;
psci->scid.pid = 1;
psci->vt = VT_LPSTR;
psci->fmt = LVCFMT_RIGHT;
psci->csFlags = SHCOLSTATE_TYPE_INT;
psci->cChars = 6;
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:
psci->scid.fmtid = FMTID_SummaryInformation;
psci->scid.pid = 4;
psci->vt = VT_LPSTR;
psci->fmt = LVCFMT_LEFT;
psci->csFlags = SHCOLSTATE_TYPE_STR;
psci->cChars = 32;
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 VARIANT
s.
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];
char szTitle[30];
char szArtist[30];
char szAlbum[30];
char szYear[4];
char szComment[30];
char byGenre;
};
All fields are plain char
s, 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;
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;
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 ( pscd->dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_OFFLINE) )
return S_FALSE;
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.
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 ( !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.
while ( m_ID3Cache.size() > 4 )
m_ID3Cache.pop_back();
CID3CacheEntry entry;
entry.sFilename = szFilename;
CopyMemory ( &entry.rTag, &rTag, sizeof(CID3v1Tag) );
m_ID3Cache.push_front ( entry );
}
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.
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:
if ( bUsingBuiltinCol )
{
switch ( pscid->pid )
{
case 2:
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:
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:
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 »