Click here to Skip to main content
Click here to Skip to main content

Explorer column handler shell extension in C#

, 6 Mar 2003 BSD
Rate this:
Please Sign up or sign in to vote.
This article shows how to write a column handler shell extension for Explorer's 'Detail' view using C#

Windows Explorer showing an MD5 column handler shell extension

Summary

The .Net Development Platform provides very rich facilities for interoperability and integration with unmanaged code written using COM. The Windows Shell, which originated with the Windows 95 user interface has always been heavily based on COM and exposes several extensibility points through a number of COM interfaces. As each successive iteration of Windows has appeared, more and more extension facilities have been provided, particularly with Windows 2000. One of those facilities which appeared in Windows 2000 was the Column Handler. This article will demonstrate techniques for COM Interop by creating a Column Handler in C#.

Introduction to Column Handlers

Besides the usual Name, Size, Type and Date columns in the Details view of Windows Explorer, there are a further 28 columns capable of being added to the view when running Windows XP. There are columns for photographs taken with a digital camera and columns for your music tracks. Adding another column to this list is a matter of implementing the IColumnProvider COM interface and registering your handler in the HKEY_CLASSES_ROOT\Folder\ShellEx\ColumnHandlers key. A Column Handler is therefore one of the simpler shell extensions to implement, as there are usually several other interfaces required when implementing other shell extensions.

Finding the unmanaged definitions

The COM interfaces and structures used within Windows Explorer are defined solely by C++ header files. There are no type libraries nor IDL to work with. Therefore, we must find all the information from the header files and move these definitions into the managed world, precisely as they are defined in the header files. If we deviate the layout of a field within a struct by just 1 byte, then the code may refuse to work without any indication of the fault. Therefore, it is worth spending time checking and double checking the definitions.

The COM interface for an IColumnProvider is defined in ShlObj.h and looks like:

DECLARE_INTERFACE_(IColumnProvider, IUnknown)
{
    // IUnknown methods
    STDMETHOD (QueryInterface)(THIS_ REFIID riid, void **ppv) PURE;
    STDMETHOD_(ULONG, AddRef)(THIS) PURE;
    STDMETHOD_(ULONG, Release)(THIS) PURE;

    // IColumnProvider methods
    STDMETHOD (Initialize)(THIS_ LPCSHCOLUMNINIT psci) PURE;
    STDMETHOD (GetColumnInfo)(THIS_ DWORD dwIndex, SHCOLUMNINFO *psci) PURE;
    STDMETHOD (GetItemData)(THIS_ LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, 
                            VARIANT *pvarData) PURE;
};

The three IColumnProvider methods include three structures which are also defined in ShlObj.h and one structure defined in ShObjIdl.idl. These are:

typedef struct {
    ULONG   dwFlags;              // initialization flags
    ULONG   dwReserved;           // reserved for future use.
    WCHAR   wszFolder[MAX_PATH];  // fully qualified folder path (or empty <BR>                                  // if multiple folders)
} SHCOLUMNINIT, *LPSHCOLUMNINIT;
typedef const SHCOLUMNINIT* LPCSHCOLUMNINIT;


        
typedef struct {
    SHCOLUMNID  scid;                           // OUT the unique identifier <BR>                                                // of this column
    VARTYPE     vt;                             // OUT the native type of the <BR>                                                // data returned
    DWORD       fmt;                            // OUT this listview format <BR>                                                // (LVCFMT_LEFT, usually)
    UINT        cChars;                         // OUT the default width of <BR>                                                // the column, in characters
    DWORD       csFlags;                        // OUT SHCOLSTATE flags
    WCHAR wszTitle[MAX_COLUMN_NAME_LEN];        // OUT the title of the column
    WCHAR wszDescription[MAX_COLUMN_DESC_LEN];  // OUT full description of <BR>                                                // this column
} SHCOLUMNINFO, *LPSHCOLUMNINFO;
typedef const SHCOLUMNINFO* LPCSHCOLUMNINFO;

typedef struct {
    ULONG   dwFlags;             // combination of SHCDF_ flags.
    DWORD   dwFileAttributes;    // file attributes.
    ULONG   dwReserved;          // reserved for future use.
    WCHAR*  pwszExt;             // address of file name extension
    WCHAR   wszFile[MAX_PATH];   // Absolute path of file.
} SHCOLUMNDATA, *LPSHCOLUMNDATA;
typedef const SHCOLUMNDATA* LPCSHCOLUMNDATA;
                
In addition, there is a further structure defined in ShObjIdl.idl
typedef struct {
    GUID fmtid;
    DWORD pid;
} SHCOLUMNID, *LPSHCOLUMNID;
typedef const SHCOLUMNID* LPCSHCOLUMNID;

Manually defining the managed Metadata

Now we have all the unmanaged definitions, we need to duplicate these in the managed world. This is done by creating managed metadata which exactly matches the unmanaged definitions. Note that I said exactly - if part of the structure is not quite correct, or the wrong datatype is defined on a method, then more than likely the shell extension will refuse to work. I will talk more about this later.

Managed Metadata is not written using a separate language. In COM programming, IDL was the metadata language to complement C++. However in .Net programming, the metadata is a .Net language - and for this shell extension, we will use C#.

The first step is to define the IColumnProvider metadata. If we look at the interface defined in shlobj.h, we see that it begins with the three standard IUnknown methods. These can be ignored because .Net COM Interop will automatically create these for us. Therefore, our interface, written in C# is:

[ComVisible(false), ComImport, Guid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IColumnProvider {
    [PreserveSig()] int Initialize(LPCSHCOLUMNINIT psci);
    [PreserveSig()] int GetColumnInfo(int dwIndex, out SHCOLUMNINFO psci);

    /// Note: these objects must be threadsafe! GetItemData _will_ be called
    /// simultaneously from multiple threads.
    [PreserveSig()]
    int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, 
                          out object /*VARIANT */ pvarData);
}
        

Notice the PreserveSig attribute. This stops COM Interop from treating the return value as an out param and uses the return value as the COM HRESULT. These methods still reference the four structures, so let's rewrite those in C#:

[ComVisible(false), <BR> StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public class LPCSHCOLUMNINIT {
    public uint dwFlags; //ulong
    public uint dwReserved; //ulong
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
    public string wszFolder; //[MAX_PATH]; wchar
}

[ComVisible(false), StructLayout(LayoutKind.Sequential)]
public struct SHCOLUMNID {
    public Guid fmtid; //GUID
    public uint pid; //DWORD
}

[ComVisible(false), StructLayout(LayoutKind.Sequential)]
public class LPCSHCOLUMNID {
    public Guid fmtid; //GUID
    public uint pid; //DWORD
}

[ComVisible(false), StructLayout(LayoutKind.Sequential, <BR> CharSet=CharSet.Unicode, Pack=1)]
public struct SHCOLUMNINFO {
    public SHCOLUMNID scid; //SHCOLUMNID
    public ushort vt; //VARTYPE
    public LVCFMT fmt; //DWORD
    public uint cChars; //UINT
    public SHCOLSTATE csFlags;  //DWORD
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=80)] //MAX_COLUMN_NAME_LEN
    public string wszTitle; //WCHAR
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] //MAX_COLUMN_DESC_LEN
    public string wszDescription; //WCHAR
}

[ComVisible(false), StructLayout(LayoutKind.Sequential, <BR> CharSet=CharSet.Unicode)]
public class LPCSHCOLUMNDATA{
    public uint dwFlags; //ulong
    public uint dwFileAttributes; //dword
    public uint dwReserved; //ulong
    [MarshalAs(UnmanagedType.LPWStr)]
    public string pwszExt; //wchar
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
    public string wszFile; //[MAX_PATH]; wchar
}

        

The observant ones reading this will have gathered there are in fact five structures defined above. The even more observant ones will have spotted the second and third structures are identical apart from one is defined as a struct, the other as a class. This is because SHCOLUMNID is used twice - once as a pointer to a SHCOLUMNID, and the other time as an inline SHCOLUMNID. ie One is heap allocated, the other is stack allocated - the method GetData() passes a pointer to a SHCOLUMNID as it's first argument, while the structure SHCOLUMNINFO has an inline SHCOLUMNID as it's first field.

I mentioned previously that you have to match the managed metadata exactly to the unmanaged definitions and I certainly didn't get these structures correct the first time! It took many iterations to get them correct. By far the trickiest structure was SHCOLUMNINFO which is defined with single byte packing - a rule which defines alignment of fields which are not an integral number of machine words in size. I had originally missed the definition for this in the header file. I'll reproduce it here:

#include <pshpack1.h>

That's it! pshpack1.h is a very short header which defines:

#pragma pack(1)

At this stage, I was pessimistic that .Net metadata would be able to support such an overly-memory efficient formatting option. RAM is so cheap these days that saving the odd byte for each column in a Windows Explorer window is more hassle than it's worth. However, .Net does indeed support packing rules - a testament to the completeness of the COM Interop team at Microsoft.

Implementing IColumnProvider

Now we've defined the metadata, it's time to implement the sole interface and create ourselves a Column Handler. For this example, I've chosen to create a new column which will contain the MD5 checksum value for the file. This is a good way of checking uniqueness of the file in a folder, even if each file has a different name.

To make it easier to define column handlers in future, I started by creating an abstract implementation of the IColumnHandler interface. This provides a base class which defines basic services for registering the Column Handler in the Windows Registry. We can simply derive from the base class and override the three methods.

Initialize simply returns success, so we'll move onto GetColumnInfo and describe what is going on. Windows Explorer will ask all registered column handlers for information about their column. Each column handler can implement more than one column, so Explorer will call GetColumnInfo with an index parameter. If S_FALSE is returned from GetColumnInfo for a specific index, then Explorer will stop calling the method and will know how many columns it supports. We then create a new SHCOLUMNINFO structure and fill it in with details about our column. This structure will be returned via the out parameter on GetColumnInfo.

public override int GetColumnInfo(int dwIndex, out SHCOLUMNINFO psci) {
    psci=new SHCOLUMNINFO();

    if(dwIndex!=0)
        return S_FALSE;

    try {
        psci.scid.fmtid=GetType().GUID;
        psci.scid.pid=0;

        // Cast to a ushort, because a VARTYPE is ushort and a VARENUM is int
        psci.vt=(ushort)VarEnum.VT_BSTR;
        psci.fmt=LVCFMT.LEFT;
        psci.cChars=40;

        psci.csFlags=SHCOLSTATE.TYPE_STR;

        psci.wszTitle = "MD5 Hash";
        psci.wszDescription = "Provides an MD5 Hash of every file";
    } catch(Exception e) {
        MessageBox.Show(e.Message);
        return S_FALSE;
    }

    return S_OK;
}

        

The MD5 is generated when the GetItemData() method is called. This method really just contains System.IO code to load the contents of the file and uses the MD5CryptoServiceProvider to generate an MD5 checksum.

public override int GetItemData( LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, <BR>                                 out object pvarData) {
    pvarData=string.Empty;

    // Ignore directories
    if(((FileAttributes)pscd.dwFileAttributes|FileAttributes.Directory)==<BR>                                                     FileAttributes.Directory)
        return S_FALSE;

    // Only service known columns
    if(pscid.fmtid!=GetType().GUID || pscid.pid!=0)
        return S_FALSE;

    try {
        MD5 md5 = new MD5CryptoServiceProvider();
        byte[] result;

        using(Stream stream=File.OpenRead(pscd.wszFile)) {
            result = md5.ComputeHash(stream);
        }

        StringBuilder output=new StringBuilder(2+(result.Length*2));

        foreach(byte b in result) {
            output.Append(b.ToString("x2"));
        }
        pvarData="0x" + output.ToString();

    } catch(UnauthorizedAccessException) {
        return S_FALSE;
    }catch(Exception e) {
        MessageBox.Show(e.Message);
        return S_FALSE;
    }

    return S_OK;
}


        

To test the column handler, set the Debug Mode to "Program" and the Start Application to the full path to Windows Explorer (eg C:\Windows\Explorer.exe). Both these settings can be found in the Project Properties, underneath the debugging section. You will also need to register this assembly with COM - This can be achieved by setting Register for COM Interop to true in the Build section of the Properties dialog. In addition, the DLL should be registered in the GAC, because Windows Explorer doesn't know how to probe inside your project folder for the Column Handler DLL. Simply issue the command gacutil -i MD5ColumnHandler.dll, or drag and drop the DLL into the GAC (C:\Windows\Assembly) folder using Windows Explorer.

Project Properties

Finally, if you want to restart an instance of Windows Explorer, you should use the official way to shut down Explorer, rather than killing it from Task Manager! To shut down officially, hit Start->Shutdown (or Turn Off Computer on Windows XP), then while holding the control shift and alt keys, click cancel on the shutdown dialog. To start a new instance of Windows Explorer, bring up Task Manager by holding the control and shift keys, then hit escape. In Task Manager, choose File->New Task and enter "Explorer.exe". This will have restarted Windows Explorer properly.

Conclusion

Writing any Interop code in .NET trivial as long as you have the managed metadata definitions. The creation of these definitions invariably proves to be the most problematic and time-consuming part. As you can see, we've successfully managed to create a Column Handler for Windows Explorer with very little code, although Windows Explorer does tend to use more RAM because it now hosts the Common Language Runtime. As long as you have a machine with plenty of RAM, this is never a problem.

Thanks to Robert Plant for reviewing and suggestions.

License

This article, along with any associated source code and files, is licensed under The BSD License

Share

About the Author

Richard Birkby
Web Developer
United Kingdom United Kingdom
Richard Birkby is a software engineer from London, UK, specializing in .Net. Richard has coded for many different sized companies from small venture-capital funded start-ups, to multi-national corporations (ie Microsoft). When he's not programming, he enjoys driving his sports car or eating curry (although never at the same time!).
 
Richard helps run CurryPages.com and has several other covert ventures in development. Stay tuned!

Comments and Discussions

 
Questionhow to add a column for file/path length in windows explorer PinmemberMember 435848925-Aug-11 12:19 
AnswerRe: how to add a column for file/path length in windows explorer PinmemberRichard Birkby25-Aug-11 12:21 
Column handlers are not supported in Vista and above.
GeneralRe: how to add a column for file/path length in windows explorer PinmemberMember 435848929-Aug-11 10:05 
Questionhow to uninstall this dll? PinmemberMember 76563983-Apr-11 22:03 
AnswerRe: how to uninstall this dll? PinmemberRichard Birkby4-Apr-11 2:08 
QuestionHow about sort function and default displaying new column Pinmembernamphvn13-Apr-10 6:28 
GeneralWindows 7 and shell extensions with managed code PinmemberTsuda Kageyu21-Nov-09 4:36 
GeneralRe: Windows 7 and shell extensions with managed code PinmemberRichard Birkby21-Nov-09 4:59 
GeneralRe: Windows 7 and shell extensions with managed code PinmemberTsuda Kageyu21-Nov-09 14:43 
GeneralRe: Windows 7 and shell extensions with managed code PinmemberJIANG, Sheng[MVP]8-Jun-10 15:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141223.1 | Last Updated 7 Mar 2003
Article Copyright 2003 by Richard Birkby
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid