Click here to Skip to main content
15,892,059 members
Articles / Desktop Programming / ATL

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

Rate me:
Please Sign up or sign in to vote.
4.96/5 (28 votes)
31 May 2006 566.6K   4.1K   133  
A tutorial on adding columns to Explorer's details view via a column handler shell extension.
// MP3ColExt.cpp : Implementation of CMP3ColExt

#include "stdafx.h"
#include "resource.h"
#include "MP3TagViewer.h"
#include "MP3ColExt.h"

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

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;

    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 col # as the ID
            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

            wcsncpy ( psci->wszTitle, L"MP3 Album", MAX_COLUMN_NAME_LEN );
            wcsncpy ( psci->wszDescription, L"Album name of an MP3", MAX_COLUMN_DESC_LEN );
            psci->wszTitle[MAX_COLUMN_NAME_LEN-1] = 0;
            psci->wszDescription[MAX_COLUMN_DESC_LEN-1] = 0;
        break;

        case 1:     // MP3 year - separate column
            psci->scid.fmtid = CLSID_MP3ColExt;     // Use our CLSID as the format ID
            psci->scid.pid   = 1;                   // Use the col # as the ID
            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

            wcsncpy ( psci->wszTitle, L"MP3 Year", MAX_COLUMN_NAME_LEN );
            wcsncpy ( psci->wszDescription, L"Year of an MP3", MAX_COLUMN_DESC_LEN );
            psci->wszTitle[MAX_COLUMN_NAME_LEN-1] = 0;
            psci->wszDescription[MAX_COLUMN_DESC_LEN-1] = 0;
        break;

        case 2:     // MP3 genre - separate column
            psci->scid.fmtid = CLSID_MP3ColExt;     // Use our CLSID as the format ID
            psci->scid.pid   = 2;                   // Use the col # as the ID
            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

            wcsncpy ( psci->wszTitle, L"MP3 Genre", MAX_COLUMN_NAME_LEN );
            wcsncpy ( psci->wszDescription, L"Genre of an MP3", MAX_COLUMN_DESC_LEN );
            psci->wszTitle[MAX_COLUMN_NAME_LEN-1] = 0;
            psci->wszDescription[MAX_COLUMN_DESC_LEN-1] = 0;
        break;

        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;

        case 4:     // MP3 song title - reusing the built-in Title column
            psci->scid.fmtid = FMTID_SummaryInformation;    // predefined FMTID
            psci->scid.pid   = 2;                   // Predefined - title
            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;

        case 5:     // ID3 comment - reusing the built-in Comments column
            psci->scid.fmtid = FMTID_SummaryInformation;    // predefined FMTID
            psci->scid.pid   = 6;                   // Predefined - comments
            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;

        DEFAULT_UNREACHABLE;
        }

    return S_OK;
}

STDMETHODIMP CMP3ColExt::GetItemData (
    LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData )
{
USES_CONVERSION;
LPCTSTR   szFilename = W2CT(pscd->wszFile);
char      szField[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;
        }
    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;

    // 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 bail immediately.
    if ( 0 != wcsicmp ( pscd->pwszExt, L".mp3" ) )
        return S_FALSE;

    // Look for the filename in our cache.
list_ID3Cache::const_iterator it;

    for ( it = m_ID3Cache.begin(); !bCacheHit && it != m_ID3Cache.end(); it++ )
        {
        if ( 0 == lstrcmpi ( szFilename, it->sFilename.c_str() ))
            {
            CopyMemory ( &rTag, &it->rTag, sizeof(CID3v1Tag) );
            bCacheHit = true;
            }
        }

    // 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;

        // 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)

    // Check if we really have an ID3 tag by looking for the signature.
    if ( 0 != strncmp ( rTag.szTag, "TAG", 3 ) )
        return S_FALSE;

    // Format the details string.
    if ( bUsingBuiltinCol )
        {
        switch ( pscid->pid )
            {
            case 2:                     // song title
                CopyMemory ( szField, rTag.szTitle, countof(rTag.szTitle) );
                szField[30] = '\0';
            break;

            case 4:                     // artist
                CopyMemory ( szField, rTag.szArtist, countof(rTag.szArtist) );
                szField[30] = '\0';
            break;

            case 6:                     // comments
                CopyMemory ( szField, rTag.szComment, countof(rTag.szComment) );
                szField[30] = '\0';
            break;

            DEFAULT_UNREACHABLE;
            }
        }
    else
        {
        switch ( pscid->pid )
            {
            case 0:                     // album
                CopyMemory ( szField, rTag.szAlbum, countof(rTag.szAlbum) );
                szField[30] = '\0';
            break;

            case 1:                     // year
                CopyMemory ( szField, rTag.szYear, countof(rTag.szYear) );
                szField[4] = '\0';
            break;

            case 2:                     // genre
                GetGenreName ( rTag, szField, countof(szField) );
                szField[30] = '\0';
            break;

            DEFAULT_UNREACHABLE;
            }
        }

    // WinAmp will pad strings with spaces instead of nulls, so remove any
    // trailing spaces.
    StrTrimA ( szField, " " );

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

    vData.Detach ( pvarData );

    return S_OK;
}


//////////////////////////////////////////////////////////////////////////
//
// GetGenreName() : Returns a textual description given a genre ID.
//
// tag: ID3 tag that contains the ID.
// szField: Buffer to receive the description.
// cchBuffSize: Buffer size in chars.
//
// Note: szField is always an ANSI string.
//
//////////////////////////////////////////////////////////////////////////

void CMP3ColExt::GetGenreName ( const CID3v1Tag& tag, char* szField, 
                                const UINT cchBuffSize )
{
static const char* aszGenres[] = {
    "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", 
    "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
    "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
    "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop",
    "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental",
    "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
    "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
    "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
    "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40",
    "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave",
    "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk",
    "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk",
    "Folk-Rock", "National Folk", "Swing",  "Fast Fusion", "Bebob", "Latin", "Revival",
    "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", 
    "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", 
    "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", 
    "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove",
    "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", 
    "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo",
    "A capella", "Euro-House", "Dance Hall" };

    if ( tag.byGenre < countof(aszGenres) )
        lstrcpynA ( szField, aszGenres[tag.byGenre], cchBuffSize );
    else
        lstrcpynA ( szField, "(Other)", cchBuffSize );
}


//////////////////////////////////////////////////////////////////////////
//
// ReadTagFromFile() : Reads an ID3 tag from a file.
//
// szFilename: Full path to the file.
// pTag: Pointer to a CID3v1Tag that receives the tag, if one is present.
//
// Returns: true/false on success/failure.
//
// Note: This fn does not check if a tag is actually present, it just reads
//       the part of the file where the tag would be.
//
//////////////////////////////////////////////////////////////////////////

bool CMP3ColExt::ReadTagFromFile ( LPCTSTR szFilename, CID3v1Tag* pTag )
{
HANDLE   hfile;
bool     bReadLastAccessTime;
FILETIME ftLastAccess;

    ATLASSERT(!IsBadStringPtr ( szFilename, -1 ));
    ATLASSERT(!IsBadWritePtr ( pTag, sizeof(CID3v1Tag) ));

    // Open the MP3 file.
    hfile = CreateFile ( szFilename, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ,
                         NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );

    if ( INVALID_HANDLE_VALUE == hfile )
        return false;

    // Save the last-accessed time so we can restore it later.  Note that when
    // testing on XP SP2 (and earlier on 2K), by the time this code is called,
    // the file's last-accessed time is already set to the current date/time,
    // so this code here doesn't have any effect.
    bReadLastAccessTime = (0 != GetFileTime ( hfile, NULL, &ftLastAccess, NULL ));

__try
    {
    // Check the file length - it must be >= the size of an ID3v1 tag.
    DWORD dwFileSizeLo, dwFileSizeHi, dwBytesRead;

    dwFileSizeLo = GetFileSize ( hfile, &dwFileSizeHi );

    if ( -1 == dwFileSizeLo && GetLastError() != ERROR_SUCCESS )
        return false;                   // GetFileSize() failed.

    if ( 0 == dwFileSizeHi && dwFileSizeLo < sizeof(CID3v1Tag) )
        return false;                   // File is too small to have a tag.

    // Seek 128 bytes from the end, where the ID3 tag would start.
    if ( INVALID_SET_FILE_POINTER == 
           SetFilePointer ( hfile, -1 * int(sizeof(CID3v1Tag)), NULL, FILE_END ) )
        return false;

    // Read in the data - we don't care if it's really an ID3 tag or not.
    if ( !ReadFile ( hfile, pTag, sizeof(CID3v1Tag), &dwBytesRead, NULL ) )
        return false;

    if ( sizeof(CID3v1Tag) != dwBytesRead )
        return false;
    }
__finally
    {
    ATLASSERT(INVALID_HANDLE_VALUE != hfile);

    // Restore the file's last-accessed time, and close it.
    if ( bReadLastAccessTime )
        SetFileTime ( hfile, NULL, &ftLastAccess, NULL );

    CloseHandle ( hfile );
    }

    return true;
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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


Written By
Software Developer (Senior) VMware
United States United States
Michael lives in sunny Mountain View, California. He started programming with an Apple //e in 4th grade, graduated from UCLA with a math degree in 1994, 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.
Mike has been a a developer at Napster and at his own lil' startup, Zabersoft, a development company he co-founded with offices in Los Angeles and Odense, Denmark. Mike is now a senior engineer at VMware.

He also enjoys his hobbies of playing pinball, bike riding, photography, and Domion on Friday nights (current favorite combo: Village + double Pirate Ship). He would get his own snooker table too if they weren't so darn big! He is also sad that he's forgotten the languages he's studied: French, Mandarin Chinese, and Japanese.

Mike was a VC MVP from 2005 to 2009.

Comments and Discussions