|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
IntroductionThe 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 InterfaceYou 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 A column handler only implements one interface, To add #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 struct __declspec(uuid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1")) IColumnProvider; which will satisfy the requirements to make We also need to make some changes to stdafx.h. Since we're using Windows 2000 features, we need to #define WINVER 0x0500 // Enable W2K/98 features #define _WIN32_WINNT 0x0500 // Enable W2K features #define _WIN32_IE 0x0500 // Enable IE 5+ features These Initialization
HRESULT IColumnProvider::Initialize ( LPCSHCOLUMNINIT psci ); The shell passes us a Enumerating the New ColumnsWhen 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 HRESULT IColumnProvider::GetColumnInfo ( DWORD dwIndex, SHCOLUMNINFO* psci );
The first member of 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 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 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;
We use the extension's GUID for the format ID, and the column number for the property ID. The The
The 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 When 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;
Finally, after the end of the switch statement, we return Displaying Data in the ColumnsThe last HRESULT IColumnProvider::GetItemData ( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData ); The Sidebar - Handling ID3 TagsNow 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 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 OK, back to the extension. Here's the beginning of our #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
We next compare the format ID with 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 // 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 We first iterate through the cache (stored as a member variable, // 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 // 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 // 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 At this point, StrTrimA ( szField, " " ); And finally, we create a 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 ExtensionSince column handlers extend folders, they are registered under the HKCR
{
NoRemove Folder
{
NoRemove Shellex
{
NoRemove ColumnHandlers
{
ForceRemove {AC146E80-3679-4BCA-9BE4-E36512573E6C} = s 'ID3v1 viewer column ext'
}
}
}
}
An Extra Goodie - InfotipsAnother 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 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 LicenseThis 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 HistorySept 11, 2000: Article first published. | ||||||||||||||||||||