|
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.
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:
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. 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.
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.
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+
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
.
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
SHCOLSTATE_ONBYDEFAULT
SHCOLSTATE_SLOW
SHCOLSTATE_SECONDARYUI
SHCOLSTATE_HIDDEN
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.
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 VARIANT
s.
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 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; // 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; }
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 Author
column.
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' } } } }
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:
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.
|
Home >>
Shell Programming >>
Beginners
Advertise on The Code Project |
Article content copyright Michael Dunn, 2000 everything else © CodeProject, 1999-2001. |