Click here to Skip to main content
15,861,168 members
Articles / Programming Languages / C#
Article

C# does Shell, Part 1

Rate me:
Please Sign up or sign in to vote.
4.91/5 (93 votes)
31 Jan 2003Ms-PL20 min read 526.6K   12.2K   383   82
This article introduces shell programming using C#. It includes developing several utility classes for working with the shell and a class that wraps the extensible Browse for Folder dialog.

Sample Image

Introduction

This article introduces shell programming with C#. I'll be honest with you, this part has little fun in it, but it is essential that some aspects of the shell programming using C# will be clear before moving to the real stuff.

If you want more details about shell programming I suggest you search msdn.microsoft.com using "Shell Programming" as the search term.

While writing this article I realized it's to complicate to explain the shell and then explain how to do it in C#. you MUST read the following MSDN articles, they explain the basics of the shell, I promise that it will be worth it:

This article assumes that you read those article.

So, what is shell programming? In a few words, shell programming means using the Windows platform and extending the Windows shell. I'll give some example for using and extending the shell. Ever used the "open file" dialog? Well, how do you think this dialog is the same in most of the applications? this dialog and more (open, save, font, color, printer) are dialogs that comes with windows and can be used with a set of API functions, called Shell API. I'll give another example for using the shell API. suppose you want to find the Windows directory or the My Documents directory or even the folder where you can put the files that are waiting to be written to CD (exists only on XP). In the past you used the environment variables but not all the information is there and its not the way Microsoft encourages. The correct way is using the Shell API to get this information.

So far so good, using the Shell API is nice but extending the shell is a totally different story. Extending the shell means that you can put your own menu commands on the context menu when you right click a file in the explorer (WinZip does it, allowing you to select files and compress them from the explorer). Developing shell extensions is the way you integrate your application into the Windows platform. More examples for shell extensions includes: customize a file class's icon and shortcut menu; customize the appearance of a folder; integrate your application's cleanup procedures with the shell's disk cleanup manager; customize the way Webview displays the contents of a folder, creating custom Explorer bars - tool bands and desk bands; using the Active Desktop object; creating Control Panel applications; and many more...

Well then, lets get to work.

Main Goals

In this article we will describe some of the basic functions and interfaces that involves the shell and we will see how to use them with C#. Then I'll introduce a library I have written called ShellLib that wraps almost everything in a nice way - ready for use. The article is not suppose to explain all the shell specifications and features. I'm not going to rewrite MSDN.

Interfaces we will review in the first section: IMalloc, IShellFolder

Functions we will review in the first section: SHGetMalloc, SHGetFolderLocation, SHGetPathFromIDList, SHGetFolderPath, SHParseDisplayName, SHGetDesktopFolder, SHBindToParent, StrRetToBSTR, StrRetToBuf

Interfaces we will review in the second section: IFolderFilterSite, IFolderFilter

Functions we will review in the second section: SHBrowseForFolder

The second section of the article talks about the class I've written that wraps the 'Browse for Folder' dialog, its supports:

  1. Browsing for folders, files, computers or printers.
  2. Setting the root folder in the dialog box.
  3. Using the new dialog style allowing drag and drop capability within the dialog box, reordering, shortcut menus, new folders, delete, and other shortcut menu commands.
  4. Setting you own instructions text.
  5. Changing the OK button text.
  6. Events that notify about selection changes and validation.
  7. Customize filtering, including an example of a custom filter for specific file types.

Note: the picture above shows an example of this window, with a custom filter for showing only txt and bmp files.

Note 2: Main parts pf the article is using functions that exists only on win2k and XP, the Custom Filtering exists ONLY on XP.

Section 1: Getting to Know the Basics of Shell

Interface: IMalloc

MSDN Description: The IMalloc interface allocates, frees, and manages memory.
Related API: SHGetMalloc - Retrieves a pointer to the Shell's IMalloc interface.

C# definition:

C#
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000002-0000-0000-C000-000000000046")]
public interface IMalloc 
{
    // Allocates a block of memory.
    // Return value: a pointer to the allocated memory block.
    [PreserveSig]
    IntPtr Alloc(
        UInt32 cb);        // Size, in bytes, of the memory block to be allocated. 
        
    // Changes the size of a previously allocated memory block.
    // Return value:  Reallocated memory block 
    [PreserveSig]
    IntPtr Realloc(
        IntPtr pv,         // Pointer to the memory block to be reallocated.
        UInt32 cb);        // Size of the memory block (in bytes) to be reallocated.

    // Frees a previously allocated block of memory.
    [PreserveSig]
    void Free(
        IntPtr pv);        // Pointer to the memory block to be freed.

    // This method returns the size (in bytes) of a memory block previously
    // allocated with IMalloc::Alloc or IMalloc::Realloc.
    // Return value: The size of the allocated memory block in bytes 
    [PreserveSig]
    UInt32 GetSize(
        IntPtr pv);        // Pointer to the memory block for which the size
                        // is requested.

    // This method determines whether this allocator was used to allocate
    // the specified block of memory.
    // Return value: 1 - allocated 0 - not allocated by this IMalloc instance. 
    [PreserveSig]
    Int16 DidAlloc(
        IntPtr pv);        // Pointer to the memory block

    // This method minimizes the heap as much as possible by releasing unused
    // memory to the operating system, 
    // coalescing adjacent free blocks and committing free pages.
    [PreserveSig]
        void HeapMinimize();
}

Explaining the code: The ComImport attribute indicates that the interface was previously defined in COM. The InterfaceType attribute says that the interface inherits the well-known IUnknown interface. Then comes the Guid attribute, this attribute Supplies an explicit Guid when it is known. In this case I took the guid from the header file objidl.h.

A few words: the common use of this interface is when you get a PIDL from a function and you need to free its memory. In this case you need to use SHGetMalloc to get the interface and use the free function to free the memory of the PIDL. There is an example of using this interface in the following section, and also in the library ShellLib, in the class ShellFunctions, method GetMalloc.

Function: SHGetMalloc

MSDN Description: Retrieves a pointer to the Shell's IMalloc interface.

C# definition:

C#
// Retrieves a pointer to the Shell's IMalloc interface.
[DllImport("shell32.dll")]
public static extern Int32 SHGetMalloc(
    out IntPtr hObject);    // Address of a pointer that receives the Shell's
                            // IMalloc interface pointer. 

Example of usage:

C#
// Get IMalloc interface
IntPtr ptrRet;
SHGetMalloc(out ptrRet);

System.Type mallocType = System.Type.GetType("IMalloc");
Object obj = Marshal.GetTypedObjectForIUnknown(ptrRet,mallocType);
IMalloc pMalloc = (IMalloc)obj;
 
// Get a PIDL
IntPtr pidlRoot;
SHGetFolderLocation(IntPtr.zero,CSIDL_WINDOWS,IntPtr.Zero,0,out pidlRoot);
 
// Use the IMalloc object to free PIDL
if (pidlRoot != IntPtr.Zero)
pMalloc.Free(pidlRoot);

// Free the IMalloc object
System.Runtime.InteropServices.Marshal.ReleaseComObject(pMalloc);

Explaining the code: Well the code starts by using the SHGetMalloc to get the interface pointer, then it get a normal .net object to use the interface. Afterwards it uses the SHGetFolderLocation (will be discussed later) to receive a PIDL, and finally it uses the interface free method to release the PIDL memory. Not forgetting to release the interface itself when we finish using it.

Function: SHGetFolderLocation

MSDN Description: Retrieves the path of a folder as an ITEMIDLIST structure (PIDL).

C# definition:

C#
// Retrieves the path of a folder as an PIDL.
[DllImport("shell32.dll")]
public static extern Int32 SHGetFolderLocation(
    IntPtr hwndOwner,         // Handle to the owner window.
    Int32 nFolder,            // A CSIDL value that identifies the folder to be
                            // located.
    IntPtr hToken,            // Token that can be used to represent a particular
                            // user.
    UInt32 dwReserved,        // Reserved.
    out IntPtr ppidl);        // Address of a pointer to an item identifier list
                            // structure 
                              // specifying the folder's location relative to the
                            // root of the namespace 
                              // (the desktop). 

Example of usage:

C#
// Get a PIDL
IntPtr pidlRoot;
SHGetFolderLocation(IntPtr.zero,CSIDL_WINDOWS,IntPtr.Zero,0,out pidlRoot);

A few words: this function return the PIDL of the requested special folder. the requested folder is specified in the nFolder parameter. In the library ShellLib there is an enum called CSIDL that contains all the possible values for this parameter, the enum will be reviewed later when I'll introduce the library.

Function: SHGetPathFromIDList

MSDN Description: Converts an item identifier list to a file system path.

C# definition:

C#
// Converts an item identifier list to a file system path. 
[DllImport("shell32.dll")]
public static extern Int32 SHGetPathFromIDList(
    IntPtr pidl,                // Address of an item identifier list that
                                // specifies a file or directory location
                                // relative to the root of the namespace (the
                                // desktop). 
    StringBuilder pszPath);        // Address of a buffer to receive the file system
                                // path.

Example of usage:

C#
IntPtr pidlRoot;
SHGetFolderLocation(IntPtr.zero,CSIDL_WINDOWS,IntPtr.Zero,0,out pidlRoot);

System.Text.StringBuilder path = new System.Text.StringBuilder(256);
SHGetPathFromIDList(pidlRoot,path); 

Explaining the code: first we get the PIDL of the windows folder, then we create a buffer for the result and get the path of the PIDL. Note this function will work only on PIDL's that represents file or folders in the file system.

Function: SHGetFolderPath

MSDN Description: Takes the CSIDL of a folder and returns the pathname.

C# definition:

C#
// Takes the CSIDL of a folder and returns the pathname.
[DllImport("shell32.dll")]
public static extern Int32 SHGetFolderPath(
    IntPtr hwndOwner,            // Handle to an owner window.
    Int32 nFolder,                // A CSIDL value that identifies the folder whose
                                // path is to be retrieved.
    IntPtr hToken,                // An access token that can be used to represent
                                // a particular user.
    UInt32 dwFlags,                // Flags to specify which path is to be returned.
                                // It is used for cases where 
                                 // the folder associated with a CSIDL may be moved
                                // or renamed by the user. 
    StringBuilder pszPath);        // Pointer to a null-terminated string which will
                                // receive the path.

Example of usage:

C#
System.Text.StringBuilder path = new System.Text.StringBuilder(256);
SHGetFolderPath(IntPtr.Zero,CSIDL_WINDOWS,IntPtr.Zero,SHGFP_TYPE_CURRENT,path); 
Explaining the code: well, this is quite simple, this function just do the work quicker. but the two examples are identical.

Function: SHParseDisplayName

MSDN Description: Translates a Shell namespace object's display name into an item identifier list and returns the attributes of the object. This function is the preferred method to convert a string to a pointer to an item identifier list (PIDL).

C# definition:

C#
// Translates a Shell namespace object's display name into an item 
// identifier list and returns the attributes of the object. This function is 
// the preferred method to convert a string to a pointer to an item identifier 
// list (PIDL). 
[DllImport("shell32.dll")]
public static extern Int32 SHParseDisplayName(
    [MarshalAs(UnmanagedType.LPWStr)]
    String pszName,                // Pointer to a zero-terminated wide string that
                                // contains the display name 
                                   // to parse. 
    IntPtr pbc,                    // Optional bind context that controls the parsing
                                // operation. This parameter 
                                   // is normally set to NULL.
    out IntPtr ppidl,            // Address of a pointer to a variable of type
                                // ITEMIDLIST that receives the item
                                   // identifier list for the object.
    UInt32 sfgaoIn,                // ULONG value that specifies the attributes to
                                // query.
    out UInt32 psfgaoOut);        // Pointer to a ULONG. On return, those attributes
                                // that are true for the 
                                   // object and were requested in sfgaoIn will be set. 

Example of usage:

C#
ShellLib.IMalloc pMalloc;
pMalloc = ShellLib.ShellFunctions.GetMalloc();

IntPtr pidlRoot;
String sPath = @"c:\temp\divx";
uint iAttribute;

ShellLib.ShellApi.SHParseDisplayName(sPath,IntPtr.Zero,out pidlRoot,0,
    out iAttribute);

if (pidlRoot != IntPtr.Zero)
    pMalloc.Free(pidlRoot);

System.Runtime.InteropServices.Marshal.ReleaseComObject(pMalloc);

Explaining the code: Suppose you want a PIDL of the my documents folder, we already seen how this is done, we got a function called

SHGetFolderLocation 
which return us all the PIDL's of the special folders. What if I want a PIDL which represents C:\temp\Divx? in this case we will use the SHParseDisplayName function. the example is quite simple, I set a string with the folder I want and call the SHParseDisplayName, the result is return in the pidlRoot variable. and finally I'm not forgetting to free the PIDL memory when I finish using it.

Interface: IShellFolder

MSDN Description: The IShellFolder interface is used to manage folders. It is exposed by all Shell namespace folder objects.
Related API's: SHGetDesktopFolder - Retrieves the IShellFolder interface for the desktop folder, which is the root of the Shell's namespace. SHBindToParent - This function takes the fully-qualified pointer to an item identifier list (PIDL) of a namespace object, and returns a specified interface pointer on the parent object.

C# definition:

C#
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214E6-0000-0000-C000-000000000046")]
public interface IShellFolder 
{
    // Translates a file object's or folder's display name into an item identifier list.
    // Return value: error code, if any
    [PreserveSig]
    Int32 ParseDisplayName( 
        IntPtr hwnd,            // Optional window handle
        IntPtr pbc,                 // Optional bind context that controls the
                                // parsing operation. This parameter is 
                                // normally set to NULL. 
        [MarshalAs(UnmanagedType.LPWStr)] 
        String pszDisplayName,    // Null-terminated UNICODE string with the
                                // display name.
        ref UInt32 pchEaten,    // Pointer to a ULONG value that receives the
                                // number of characters of the 
                                // display name that was parsed.
        out IntPtr ppidl,           // Pointer to an ITEMIDLIST pointer that receives
                                // the item identifier list for 
                                // the object.
        ref UInt32 pdwAttributes); // Optional parameter that can be used to
                                // query for file attributes.
                                // this can be values from the SFGAO enum
        
    // Allows a client to determine the contents of a folder by creating an item
    // identifier enumeration object and returning its IEnumIDList interface.
    // Return value: error code, if any
    [PreserveSig]
    Int32 EnumObjects( 
        IntPtr hwnd,            // If user input is required to perform the
                                // enumeration, this window handle 
                                // should be used by the enumeration object as
                                // the parent window to take 
                                // user input.
        Int32 grfFlags,             // Flags indicating which items to include in the
                                // enumeration. For a list 
                                   // of possible values, see the SHCONTF enum. 
        out IntPtr ppenumIDList); // Address that receives a pointer to the
                                // IEnumIDList interface of the 
                                // enumeration object created by this method. 

    // Retrieves an IShellFolder object for a subfolder.
    // Return value: error code, if any
    [PreserveSig]
    Int32 BindToObject( 
        IntPtr pidl,            // Address of an ITEMIDLIST structure (PIDL)
                                // that identifies the subfolder.
        IntPtr pbc,                // Optional address of an IBindCtx interface on
                                // a bind context object to be 
                                // used during this operation.
        Guid riid,                  // Identifier of the interface to return. 
        out IntPtr ppv);        // Address that receives the interface pointer.
        
    // Requests a pointer to an object's storage interface. 
    // Return value: error code, if any
    [PreserveSig]
    Int32 BindToStorage( 
        IntPtr pidl,            // Address of an ITEMIDLIST structure that
                                // identifies the subfolder relative 
                                // to its parent folder. 
        IntPtr pbc,                // Optional address of an IBindCtx interface on a
                                // bind context object to be 
                                // used during this operation.
        Guid riid,                  // Interface identifier (IID) of the requested
                                // storage interface.
        out IntPtr ppv);        // Address that receives the interface pointer specified by riid.
        
    // Determines the relative order of two file objects or folders, given their
    // item identifier lists. Return value: If this method is successful, the
    // CODE field of the HRESULT contains one of the following values (the code
    // can be retrived using the helper function GetHResultCode): Negative A
    // negative return value indicates that the first item should precede
    // the second (pidl1 < pidl2). 

    // Positive A positive return value indicates that the first item should
    // follow the second (pidl1 > pidl2).  Zero A return value of zero
    // indicates that the two items are the same (pidl1 = pidl2). 
    [PreserveSig]
    Int32 CompareIDs( 
        Int32 lParam,               // Value that specifies how the comparison
                                // should be performed. The lower 
                                   // Sixteen bits of lParam define the sorting rule.
                                // The upper sixteen bits of 
                                // lParam are used for flags that modify the
                                // sorting rule. values can be from 
                                // the SHCIDS enum
        IntPtr pidl1,               // Pointer to the first item's ITEMIDLIST structure.
        IntPtr pidl2);              // Pointer to the second item's ITEMIDLIST structure.

    // Requests an object that can be used to obtain information from or interact
    // with a folder object.
    // Return value: error code, if any
    [PreserveSig]
    Int32 CreateViewObject( 
        IntPtr hwndOwner,           // Handle to the owner window.
        Guid riid,                  // Identifier of the requested interface. 
        out IntPtr ppv);        // Address of a pointer to the requested interface. 

    // Retrieves the attributes of one or more file objects or subfolders. 
    // Return value: error code, if any
    [PreserveSig]
    Int32 GetAttributesOf( 
        UInt32 cidl,            // Number of file objects from which to retrieve
                                // attributes. 
        [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)]
        IntPtr[] apidl,            // Address of an array of pointers to ITEMIDLIST
                                // structures, each of which 
                                // uniquely identifies a file object relative to
                                // the parent folder.
        ref UInt32 rgfInOut);    // Address of a single ULONG value that, on entry,
                                // contains the attributes that 
                                // the caller is requesting. On exit, this value
                                // contains the requested 
                                // attributes that are common to all of the
                                // specified objects. this value can
                                // be from the SFGAO enum
       
    // Retrieves an OLE interface that can be used to carry out actions on the
    // specified file objects or folders.
    // Return value: error code, if any
    [PreserveSig]
    Int32 GetUIObjectOf( 
        IntPtr hwndOwner,        // Handle to the owner window that the client
                                // should specify if it displays 
                                // a dialog box or message box.
        UInt32 cidl,            // Number of file objects or subfolders specified
                                // in the apidl parameter. 
        IntPtr[] apidl,            // Address of an array of pointers to ITEMIDLIST
                                // structures, each of which 
                                // uniquely identifies a file object or subfolder
                                // relative to the parent folder.
        Guid riid,                // Identifier of the COM interface object to return.
        ref UInt32 rgfReserved,    // Reserved. 
        out IntPtr ppv);        // Pointer to the requested interface.

    // Retrieves the display name for the specified file object or subfolder. 
    // Return value: error code, if any
    [PreserveSig]
    Int32 GetDisplayNameOf(
        IntPtr pidl,            // Address of an ITEMIDLIST structure (PIDL)
                                // that uniquely identifies the file 
                                // object or subfolder relative to the parent folder. 
        UInt32 uFlags,              // Flags used to request the type of display name
                                // to return. For a list of 
                                // possible values, see the SHGNO enum. 
        out ShellApi.STRRET pName); // Address of a STRRET structure in which to
                                // return the display name.
        
    // Sets the display name of a file object or subfolder, changing the item
    // identifier in the process.
    // Return value: error code, if any
    [PreserveSig]
    Int32 SetNameOf( 
        IntPtr hwnd,            // Handle to the owner window of any dialog or
                                // message boxes that the client 
                                // displays.
        IntPtr pidl,            // Pointer to an ITEMIDLIST structure that uniquely
                                // identifies the file object
                                // or subfolder relative to the parent folder. 
        [MarshalAs(UnmanagedType.LPWStr)] 
        String pszName,            // Pointer to a null-terminated string that
                                // specifies the new display name. 
        UInt32 uFlags,            // Flags indicating the type of name specified by
                                // the lpszName parameter. For a list of possible
                                // values, see the description of the SHGNO enum. 
        out IntPtr ppidlOut);   // Address of a pointer to an ITEMIDLIST structure
                                // which receives the new ITEMIDLIST. 
}

Example of usage:

C#
int retVal;

ShellLib.IMalloc pMalloc;
pMalloc = ShellLib.ShellFunctions.GetMalloc();

IntPtr pidlSystem;
retVal = ShellLib.ShellApi.SHGetFolderLocation(
                IntPtr.Zero,
                (int)ShellLib.ShellApi.CSIDL.CSIDL_SYSTEM,
                IntPtr.Zero,
                0,
                out pidlSystem);

IntPtr ptrParent;
IntPtr pidlRelative = IntPtr.Zero;
retVal = ShellLib.ShellApi.SHBindToParent(
                pidlSystem,
                ShellLib.ShellGUIDs.IID_IShellFolder,
                out ptrParent,
                ref pidlRelative);

System.Type shellFolderType = ShellLib.ShellFunctions.GetShellFolderType();
Object obj = System.Runtime.InteropServices.Marshal.GetTypedObjectForIUnknown(
    ptrParent,shellFolderType);
ShellLib.IShellFolder ishellParent = (ShellLib.IShellFolder)obj;

ShellLib.ShellApi.STRRET ptrString;
retVal = ishellParent.GetDisplayNameOf(pidlRelative,
    (uint)ShellLib.ShellApi.SHGNO.SHGDN_NORMAL, out ptrString);

System.Text.StringBuilder strDisplay = new System.Text.StringBuilder(256);
retVal = ShellLib.ShellApi.StrRetToBuf(ref ptrString ,pidlSystem,strDisplay,
    (uint)strDisplay.Capacity);

System.Runtime.InteropServices.Marshal.ReleaseComObject(ishellParent);
pMalloc.Free(pidlSystem);
System.Runtime.InteropServices.Marshal.ReleaseComObject(pMalloc); 

Explaining the code: in this sample I've called SHGetFolderLocation in order to get the system's PIDL. Then I call SHBindToObject which gives me the IShellFolder of the parent. Then I use the interface method GetDisplayNameOfto get the display name of the system's PIDL. The result is a structure called STRRET. in order for me to convert this struct into a normal string I need to call StrRetToBuf or StrRetToBstr, I've picked the first, only because this example is a rewrite of the C++ sample in the msdn articles. Then we need to release all the com objects we have used. That's it. Only took 6 hours to make this code work.

Function: SHGetDesktopFolder

MSDN Description: Retrieves the IShellFolder interface for the desktop folder, which is the root of the Shell's namespace.

C# definition:

C#
// Retrieves the IShellFolder interface for the desktop folder,
//which is the root of the Shell's namespace. 
[DllImport("shell32.dll")]
public static extern Int32 SHGetDesktopFolder(
    out IntPtr ppshf);        // Address that receives an IShellFolder interface
                            // pointer for the 
                            // desktop folder.

Example of usage:

public static IShellFolder GetDesktopFolder()
{    
    IntPtr ptrRet;
    ShellApi.SHGetDesktopFolder(out ptrRet);

    System.Type shellFolderType = System.Type.GetType("ShellLib.IShellFolder");
    Object obj = Marshal.GetTypedObjectForIUnknown(ptrRet,shellFolderType);
    IShellFolder ishellFolder = (IShellFolder)obj;

    return ishellFolder;
}

{ 
    ... 

    ShellLib.IShellFolder pShellFolder;
    pShellFolder = ShellLib.ShellFunctions.GetDesktopFolder();3

    IntPtr pidlRoot;
    ShellLib.ShellApi.SHGetFolderLocation(
                                IntPtr.Zero,
                                (short)ShellLib.ShellApi.CSIDL.CSIDL_SYSTEM,
                                IntPtr.Zero,
                                0,
                                out pidlRoot);

    ShellLib.ShellApi.STRRET ptrDisplayName;
    pShellFolder.GetDisplayNameOf(
                        pidlRoot,
                        (uint)ShellLib.ShellApi.SHGNO.SHGDN_NORMAL 
                        | (uint)ShellLib.ShellApi.SHGNO.SHGDN_FORPARSING,
                        out ptrDisplayName);

    String sDisplay;
    ShellLib.ShellApi.StrRetToBSTR(ref ptrDisplayName,pidlRoot,out sDisplay);

    System.Runtime.InteropServices.Marshal.ReleaseComObject(pShellFolder);
}

Explaining the code: well, here I've made a function called GetDesktopFolder which returns the IShellFolder of the Desktop folder. then I'm doing something similar to what I've done in the previous example. I get the display name, this time in a different format and convert the returning STRRET struct into a string with a different API.

Function: SHBindToParent

MSDN Description: This function takes the fully-qualified pointer to an item identifier list (PIDL) of a namespace object, and returns a specified interface pointer on the parent object.

C# definition:

C#
// This function takes the fully-qualified pointer to an item
// identifier list (PIDL) of a namespace object, and returns a specified
// interface pointer on the parent object.
[DllImport("shell32.dll")]
public static extern Int32 SHBindToParent(
    IntPtr pidl,            // The item's PIDL. 
    [MarshalAs(UnmanagedType.LPStruct)]
    Guid riid,                  // The REFIID of one of the interfaces exposed by
                            // the item's parent object. 
    out IntPtr ppv,            // A pointer to the interface specified by riid. You
                            // must release the object when 
                            // you are finished. 
    ref IntPtr ppidlLast);    // The item's PIDL relative to the parent folder. This
                            // PIDL can be used with many
                            // of the methods supported by the parent folder's
                            // interfaces. If you set ppidlLast 
                            // to NULL, the PIDL will not be returned. 

A few words: I will not give an example of using this function cause I already gave one in the IShellFolder section.

Function: StrRetToBSTR

MSDN Description: Accepts a STRRET structure returned by IShellFolder::GetDisplayNameOf that contains or points to a string, and then returns that string as a BSTR.

C# definition:

C#
// Accepts a STRRET structure returned by
// ShellFolder::GetDisplayNameOf that contains or points to a string, and then
// returns that string as a BSTR.
[DllImport("shlwapi.dll")]
public static extern Int32 StrRetToBSTR(
    ref STRRET pstr,    // Pointer to a STRRET structure.
    IntPtr pidl,        // Pointer to an ITEMIDLIST uniquely identifying a file
                        // object or subfolder relative
                        // to the parent folder.
    [MarshalAs(UnmanagedType.BStr)]
    out String pbstr);    // Pointer to a variable of type BSTR that contains the
                        // converted string.

Function: StrRetToBuf

MSDN Description: Takes a STRRET structure returned by IShellFolder::GetDisplayNameOf, converts it to a string, and places the result in a buffer.

C# definition:

C#
// Takes a STRRET structure returned by IShellFolder::GetDisplayNameOf,
// converts it to a string, and places the result in a buffer. 
[DllImport("shlwapi.dll")]
public static extern Int32 StrRetToBuf(
    ref STRRET pstr,    // Pointer to the STRRET structure. When the function
                        // returns, this pointer will no
                        // longer be valid.
    IntPtr pidl,        // Pointer to the item's ITEMIDLIST structure.
    StringBuilder pszBuf, // Buffer to hold the display name. It will be returned
                        // as a null-terminated
                        // string. If cchBuf is too small, the name will be
                        // truncated to fit. 
    UInt32 cchBuf);        // Size of pszBuf, in characters. If cchBuf is too small,
                        // the string will be 
                        // truncated to fit. 

Section 2: ShellBrowseForFolderDialog - A Class that Wraps a Dialog

How do we get the Browse for Folder dialog? the answer relies in a Shell API function called: SHBrowseForFolder, this function according to the MSDN: Displays a dialog box that enables the user to select a Shell folder. So how do we use it? first, we need its C# declaration:

C#
// Displays a dialog box that enables the user to select a Shell folder. 
[DllImport("shell32.dll")]
public static extern IntPtr SHBrowseForFolder(
    ref BROWSEINFO lbpi);    // Pointer to a BROWSEINFO structure that contains
                            // information used to display 
                            // the dialog box. 

That's nice, the return value is a PIDL to the selected shell item (remember, can be a file, folder or a virtual folder), later in the class we will see how we return the display name of the selected PIDL. and the BROWSEINFO structure has specific details on the looks and feels of the dialog box. the BROWSEINFO also needs a declaration:

C#
// Contains parameters for the SHBrowseForFolder function and
// receives information about the folder selected 
// by the user.
[StructLayout(LayoutKind.Sequential)]
public struct BROWSEINFO
{
    public IntPtr hwndOwner;                // Handle to the owner window for the
                                            // dialog box.
    public IntPtr pidlRoot;                    // Pointer to an item identifier list
                                            // (PIDL) specifying the location of
                                            // the root folder from which to start
                                            // browsing.
    [MarshalAs(UnmanagedType.LPStr)]        // Address of a buffer to receive the
                                            // display name of the 
    public String pszDisplayName;            // folder selected by the user.
    [MarshalAs(UnmanagedType.LPStr)]        // Address of a null-terminated string
                                            // that is displayed 
    public String lpszTitle;                // above the tree view control in the
                                            // dialog box.
    public UInt32 ulFlags;                    // Flags specifying the options for the
                                            // dialog box.
    [MarshalAs(UnmanagedType.FunctionPtr)]    // Address of an application-defined
                                            // function that the 
    public BrowseCallbackProc lpfn;               // dialog box calls when an event occurs.
    public Int32 lParam;                    // Application-defined value that the
                                            // dialog box passes to 
                                              // the callback function
    public Int32 iImage;                    // Variable to receive the image
                                            // associated with the selected folder.
}

The hwndOwner is where you put he handle to the owner window of the dialog, the dialog is modal relative to this owner window. the pidlRoot is a PIDL that specify the root folder in the dialog. pszDisplayName is where the short display name of the selected item is returned. lpszTitle is the instructions text you can define in the dialog box. ulFlags is where you set all the little details of the dialog like if you want the dialog to include files, or if you want the dialog to return only printers, or computer, if you want the dialog to have the new dialog style, if you the 'New folder' button in the dialog, all this options and more are defined in this flags member. lpfn is a delegate function, the dialog is calling this function with some events like selection changes or initialization of the dialog, so you can define a function and respond to these events. These are the important members.

So, all you need to do, is create a BROWSEINFO struct, fill it with your data, and call the function. Offcourse, you need to take care of the PIDL's and numeric flags and CALLBACK's events. Good thing I've did it for you, lets see what I've did, and how it works.

Setting main members

The class ShellBrowseForFolderDialog has several main properties which should be set, here they are:

C#
/// <summary> Handle to the owner window for the dialog box. </summary>
public IntPtr hwndOwner;

/// <summary> Select the root type </summary>
public RootTypeOptions RootType;

/// <summary> valid only if RootType is RootTypeOptions.ByPath </summary>
public string RootPath;

/// <summary> valid only if RootType is RootTypeOptions.BySpecialFolder </summary>
public ShellApi.CSIDL RootSpecialFolder;

/// <summary> 
/// Address of a null-terminated string that is displayed above the tree view 
/// control in the dialog box. 
/// </summary>
public string Title;

/// <summary> Token that can be used to represent a particular user. </summary>
public IntPtr UserToken; 

hwndOwner is the handle to the owner window, this will normally be set to the form's this.Handle property. it can also be IntPtr.Zero, in this case the dialog parent window will be the desktop.

RootType can be one of the following RootTypeOptions enum values: BySpecialFolder, ByPath. If you set it to BySpecialFolder then you should also set the RootSpecialFolder property, this property is an enum of all the Special folders, (for a discussion of the special folders look at the first section under SHGetFolderLocation), doing this will set the root folder of the dialog to the selected special folder. But what if you want to set the root folder to a specific path? In this case you should set the RootType property to ByPath, and then set the RootPath property to the path you want. note that the dialog will act according to the value in RootType, setting the RootType to BySpecialFolder will ignore the value in RootPath.

Title is where you set the instruction text in the dialog box, you can put up to 3 lines of text, separated by '\n'.

Finally, the UserToken. most of the time it will be IntPtr.Zero, which is the default value, but let us suppose that you want the root folder in the dialog box will be the My Documents of a specific user? in this case you need to obtain the user token and set this property, not forgetting to set the RootType to BySpecialFolder and the RootSpecialFolder to the My Documents value.

Setting Flag Members

Remember the flags member of the BROWSEINFO structure? well, I've decided that it best way is to separate it to many Boolean flags, with convenient default values, and when I need to set the flags member I'll use a function to create the proper flags value according to all the Boolean properties. So, here are the Boolean flags:

C#
/// <summary> 
Only return computers. If the user selects anything other than a computer,
/// the OK button is grayed. </summary>
public bool BrowseForComputer;

/// <summary> 
Only return printers. If the user selects anything other than a printer, the
/// OK button is grayed.</summary>
public bool BrowseForPrinter;

/// <summary> The browse dialog box will display files as well as folders.
/// </summary>
public bool IncludeFiles;

/// <summary> 
/// The browse dialog box can display URLs. The BIF_USENEWUI and
/// BIF_BROWSEINCLUDEFILES flags must also be set. If these three flags are not
/// set, the browser dialog box will reject URLs. Even when these flags are set,
/// the browse dialog box will only display URLs if the folder that contains the 
/// selected item supports them. When the folder's IShellFolder::GetAttributesOf
/// method is called to request the selected item's attributes, the folder must
/// set the SFGAO_FOLDER attribute flag. Otherwise, the browse dialog box will
/// not display the URL. </summary>
public bool IncludeUrls;

/// <summary> Do not include network folders below the domain level in the 
/// dialog box's tree view control. </summary>
public bool DontGoBelowDomain;

/// <summary> Include an edit control in the browse dialog box that allows the 
/// user to type the name of an item. </summary>
public bool EditBox;

/// <summary> Use the new user interface. Setting this flag provides the user 
/// with a larger dialog box that can be resized. The dialog box has several new 
/// capabilities including: drag and drop capability within the dialog box, 
/// reordering, shortcut menus, new folders, delete, and other shortcut menu 
/// commands. </summary>
public bool NewDialogStyle;

/// <summary> Do not include the New Folder button in the browse dialog box.
/// </summary>
public bool NoNewFolderButton;

/// <summary> When the selected item is a shortcut, return the PIDL of the 
/// shortcut itself rather than its target. </summary>
public bool NoTranslateTargets;

/// <summary> Only return file system ancestors. An ancestor is a subfolder that 
/// is beneath the root folder in the namespace hierarchy. If the user selects an 
/// ancestor of the root folder that is not part of the file system, the OK button 
/// is grayed. </summary>
public bool ReturnOnlyFileSystemAncestors;

/// <summary> Only return file system directories. If the user selects folders 
/// that are not part of the file system, the OK button is grayed. </summary>
public bool ReturnOnlyFileSystemDirs;

/// <summary> The browse dialog box can display shareable resources on remote 
/// systems. It is intended for applications that want to expose remote shares on a 
/// local system. The BIF_USENEWUI flag must also be set. </summary>
public bool Shareable;

/// <summary> Include a status area in the dialog box. The callback function can 
/// set the status text by sending messages to the dialog box. </summary>
public bool StatusText;

/// <summary> When combined with BIF_NEWDIALOGSTYLE, adds a usage hint to the 
/// dialog box in place of the edit box. BIF_EDITBOX overrides this flag. </summary>
public bool UsageHint;

/// <summary> Use the new user interface, including an edit box. This flag is 
/// equivalent to BIF_EDITBOX | BIF_NEWDIALOGSTYLE. </summary>
public bool UseNewUI;

/// <summary> If the user types an invalid name into the edit box, the browse 
/// dialog box will call the application's BrowseCallbackProc with the 
/// BFFM_VALIDATEFAILED message. This flag is ignored if BIF_EDITBOX is not 
/// specified. </summary>
public bool Validate;

Well, I'm not going to go over the flags one by one cause they are quite simple and has a quick explanation near them. Also you can just test what happens by setting the flag. Another important code about the flags is the function that gives me the flags value when I need to call the SHBrowseForFolder function. Here it is:

C#
private UInt32 GetFlagsValue()
{
    UInt32 flags = 0;

    if (BrowseForComputer)            flags |= (uint)ShellApi.BIF.BIF_BROWSEFORCOMPUTER;
    if (BrowseForPrinter)             flags |= (uint)ShellApi.BIF.BIF_BROWSEFORPRINTER;
    if (IncludeFiles)                flags |= (uint)ShellApi.BIF.BIF_BROWSEINCLUDEFILES;
    if (IncludeUrls)                flags |= (uint)ShellApi.BIF.BIF_BROWSEINCLUDEURLS;
    if (DontGoBelowDomain)             flags |= (uint)ShellApi.BIF.BIF_DONTGOBELOWDOMAIN;
    if (EditBox)                    flags |= (uint)ShellApi.BIF.BIF_EDITBOX;
    if (NewDialogStyle)                flags |= (uint)ShellApi.BIF.BIF_NEWDIALOGSTYLE;
    if (NoNewFolderButton)            flags |= (uint)ShellApi.BIF.BIF_NONEWFOLDERBUTTON;
    if (NoTranslateTargets)            flags |= (uint)ShellApi.BIF.BIF_NOTRANSLATETARGETS;
    if (ReturnOnlyFileSystemAncestors) flags |= (uint)ShellApi.BIF.BIF_RETURNFSANCESTORS;
    if (ReturnOnlyFileSystemDirs)    flags |= (uint)ShellApi.BIF.BIF_RETURNONLYFSDIRS;
    if (Shareable)                    flags |= (uint)ShellApi.BIF.BIF_SHAREABLE;
    if (StatusText)                    flags |= (uint)ShellApi.BIF.BIF_STATUSTEXT;
    if (UsageHint)                    flags |= (uint)ShellApi.BIF.BIF_UAHINT;
    if (UseNewUI)                    flags |= (uint)ShellApi.BIF.BIF_USENEWUI;
    if (Validate)                    flags |= (uint)ShellApi.BIF.BIF_VALIDATE;

    return flags;
}

Update: As you have seen, the code in this section is quite ugly, and a little bird told me that it could be done in a nice simple way. So I've changed the code and the thanks goes to leppie for his great suggestion. So the changes are: the BIF enum has now the [Flags] attribute, and also I've changed its name to BrowseInfoFlags. And in the dialog class there is a member called DetailsFlags defined like this:

C#
public BrowseInfoFlag DetailsFlags;

And so now, when you want to set some flags you do something like:

C#
DetailsFlags = BrowseInfoFlag.BIF_BROWSEINCLUDEFILES 
                | BrowseInfoFlag.BIF_EDITBOX 
                | BrowseInfoFlag.BIF_NEWDIALOGSTYLE 
                | BrowseInfoFlag.BIF_SHAREABLE
                | BrowseInfoFlag.BIF_STATUSTEXT
                | BrowseInfoFlag.BIF_USENEWUI
                | BrowseInfoFlag.BIF_VALIDATE;

Showing the Dialog and Getting the Result

After we set the class properties we will now want to show the dialog, and then getting the selected item has a normal path. in the ShowDialog method, the first thing we do is getting the PIDL of the root folder, if we use special folders we need to call the SHGetFolderLocation to receive the Special Folder PIDL, and if we want a specific path we use the SHParseDisplayName in order to get the PIDL of the specified path.

Then we create a BROWSEINFO struct and fill it with the info from the properties. Then we call SHBrowseForFolder which displays the dialog box, and when we select an item returns us the PIDL of the selected item. But we want to return the display name of this item, not the PIDL. So what we do is asking for the IShellFolder interface of the Desktop item and using the interface method GetDisplayNameOf to get the display name of our PIDL, the reason we need the IShellFolder of the Desktop item is that the selected PIDL is relative to the Desktop PIDL. We are not finished yet, cause GetDisplayNameOf returns a STRRET structure. In order to get a normal string value we need to use the StrRetToBSTR function.

We are almost done. If we want to be good developers so we need to free the PIDL's that return to us. To do that we need to Get the IMalloc interface of the shell and calling its Free method to free the PIDL's. And not forgetting to release the IMalloc object itself.

The following code is what I do, go thru it and see that it's clear. the part when I set the function pointer of the BROWSEINFO struct to a class delegate will be explained later.

C#
public void ShowDialog()
{
    m_FullName = "";
    m_DisplayName = "";

    // Get shell's memory allocator, it is needed to free some memory later
    IMalloc pMalloc;
    pMalloc = ShellFunctions.GetMalloc();

    IntPtr pidlRoot;
    
    if (RootType == RootTypeOptions.BySpecialFolder)
    {
        ShellApi.SHGetFolderLocation(hwndOwner,(int)RootSpecialFolder,UserToken,
            0,out pidlRoot);
    }
    else // m_RootType = RootTypeOptions.ByPath
    {
        uint iAttribute;
        ShellApi.SHParseDisplayName(RootPath,IntPtr.Zero,out pidlRoot,0,
            out iAttribute);
    }
                            
    ShellApi.BROWSEINFO bi = new ShellApi.BROWSEINFO();
    
    bi.hwndOwner = hwndOwner;
    bi.pidlRoot = pidlRoot;
    bi.pszDisplayName = new String(' ',256);
    bi.lpszTitle = Title;
    bi.ulFlags = (uint)DetailsFlags; 
    bi.lParam = 0;
    bi.lpfn = new ShellApi.BrowseCallbackProc(this.myBrowseCallbackProc);
    
    // Show dialog
    IntPtr pidlSelected;
    pidlSelected = ShellLib.ShellApi.SHBrowseForFolder(ref bi);

    // Save the display name
    m_DisplayName = bi.pszDisplayName.ToString();

    IShellFolder isf = ShellFunctions.GetDesktopFolder();

    ShellApi.STRRET ptrDisplayName;
    isf.GetDisplayNameOf(
        pidlSelected,
        (uint)ShellApi.SHGNO.SHGDN_NORMAL | 
        (uint)ShellApi.SHGNO.SHGDN_FORPARSING,
        out ptrDisplayName);
    
    String sDisplay;
    ShellLib.ShellApi.StrRetToBSTR(ref ptrDisplayName,pidlRoot,out sDisplay);
    m_FullName = sDisplay;

    if (pidlRoot != IntPtr.Zero)
        pMalloc.Free(pidlRoot);
    
    if (pidlSelected != IntPtr.Zero)
        pMalloc.Free(pidlSelected);
    
    Marshal.ReleaseComObject(isf);
    Marshal.ReleaseComObject(pMalloc);
}

Using messages and events

Well, remember the line that set a delegate function to the BROWSEINFO struct? the delegate function type is declared as follows:

C#
public delegate Int32 BrowseCallbackProc(IntPtr hwnd, UInt32 uMsg, Int32 lParam, Int32 lpData);

This is the way the Shell gives you notification of events. What you need to do is declare a function of this type, Set the function pointer to your function, and when an event occurs the shell will notify your function with the correct message. There are 4 types of messages:

BFFM_INITIALIZED: Initialized - notify you when the dialog completes its initialization, giving you the change to initialize your stuff.

BFFM_IUNKNOWN: IUnknown - Gives you a iunknown interface pointer, letting you set custom filtering to the dialox box, this will be described later on.

BFFM_SELCHANGED: SelChanged - notify you when the user changes his selection.

BFFM_VALIDATEFAILED: ValidateFailed - If you set the flag of the EditBox, meaning you let the user enter string, and the user enters an invalid string (the folder he entered does not exists) the shell will notify you of this. (actually there is 2 messages for this, one for the ansi version and one for the Unicode version, in my testing I got only one of them, I guess its platform specific).

So, these are the notification you can receive. In my class what I've done is I don't let you specify that function cause this is ugly. what I do is defining 4 delegates properties, so if you want to get a notificaton you only need to create an event handler and set the class to use your event handler. This also allows me to translate the pointers to normal strings and passing your function normal values. The delegates and their arguments are declared as follows:

C#
public class InitializedEventArgs : EventArgs
{
    public InitializedEventArgs(IntPtr hwnd)
    {
        this.hwnd = hwnd;
    }
    public readonly IntPtr hwnd;
}

public class IUnknownEventArgs : EventArgs
{
    public IUnknownEventArgs(IntPtr hwnd, IntPtr iunknown)
    {
        this.hwnd = hwnd;
        this.iunknown = iunknown;
    }
    public readonly IntPtr hwnd;
    public readonly IntPtr iunknown;
}

public class SelChangedEventArgs : EventArgs
{
    public SelChangedEventArgs(IntPtr hwnd, IntPtr pidl)
    {
        this.hwnd = hwnd;
        this.pidl = pidl;
    }
    public readonly IntPtr hwnd;
    public readonly IntPtr pidl;
}

public class ValidateFailedEventArgs : EventArgs
{
    public ValidateFailedEventArgs(IntPtr hwnd, string invalidSel)
    {
        this.hwnd = hwnd;
        this.invalidSel = invalidSel;
    }
    public readonly IntPtr hwnd;
    public readonly string invalidSel;
}


public delegate void InitializedHandler(ShellBrowseForFolderDialog sender,
    InitializedEventArgs args);
public delegate void IUnknownHandler(ShellBrowseForFolderDialog sender,
    IUnknownEventArgs args);
public delegate void SelChangedHandler(ShellBrowseForFolderDialog sender,
    SelChangedEventArgs args);
public delegate int ValidateFailedHandler(ShellBrowseForFolderDialog sender,
    ValidateFailedEventArgs args);  

public event InitializedHandler OnInitialized;
public event IUnknownHandler OnIUnknown;
public event SelChangedHandler OnSelChanged;
public event ValidateFailedHandler OnValidateFailed;

The code for my CALLBACK function that calls your delegated function is as follows:

C#
private Int32 myBrowseCallbackProc(IntPtr hwnd, UInt32 uMsg,
    Int32 lParam, Int32 lpData)
{
    switch ((BrowseForFolderMessages)uMsg)
    {
        case BrowseForFolderMessages.BFFM_INITIALIZED:
            System.Diagnostics.Debug.WriteLine("BFFM_INITIALIZED");

            if (OnInitialized != null)
            {
                InitializedEventArgs args = new InitializedEventArgs(hwnd);
                OnInitialized(this,args);
            }

            break;

        case BrowseForFolderMessages.BFFM_IUNKNOWN:
            System.Diagnostics.Debug.WriteLine("BFFM_IUNKNOWN");

            if (OnIUnknown != null)
            {
                IUnknownEventArgs args = new IUnknownEventArgs(hwnd,(IntPtr)lParam);
                OnIUnknown(this,args);
            }

            break;

        case BrowseForFolderMessages.BFFM_SELCHANGED:
            System.Diagnostics.Debug.WriteLine("BFFM_SELCHANGED");
            
            if (OnSelChanged != null)
            {
                SelChangedEventArgs args = new SelChangedEventArgs(hwnd,(IntPtr)lParam);
                OnSelChanged(this,args);
            }
        
            break;
        
        case BrowseForFolderMessages.BFFM_VALIDATEFAILEDA:
            System.Diagnostics.Debug.WriteLine("BFFM_VALIDATEFAILEDA");
        
            if (OnValidateFailed != null)
            {
                string failedSel = Marshal.PtrToStringAnsi((IntPtr)lParam);
                ValidateFailedEventArgs args = new ValidateFailedEventArgs(hwnd,failedSel);
                return OnValidateFailed(this,args);
            }
            break;

        case BrowseForFolderMessages.BFFM_VALIDATEFAILEDW:
            System.Diagnostics.Debug.WriteLine("BFFM_VALIDATEFAILEDW");
            
            if (OnValidateFailed != null)
            {
                string failedSel = Marshal.PtrToStringUni((IntPtr)lParam);
                ValidateFailedEventArgs args = new ValidateFailedEventArgs(hwnd,failedSel);
                return OnValidateFailed(this,args);
            }
                                
            break;
    }

    return 0;
}

Another type of messages are specific messages you can send to the shell dialog window with the help of the API function: SendMessage. The messages let you control a bit the dialog, and can be sent only from your event handlers, that's why you receive the handle to the shell dialog window in all of the events. The messages are:

BFFM_ENABLEOK: EnableOk - this message tells the shell dialog to enable or disable the ok button, for example you can respond to the selection change event and if the selection is now "my_secret_file.txt" then you can send a message to disable the ok button.

BFFM_SETEXPANDED: SetExpanded - with this message you can tell the shell dialog to expand a folder, for example you can respond to the initialized event and set the windows folder to be expanded, event though you can still choose from other folders.

BFFM_SETSELECTION: SetSelection - similar to the previous message but this message selects an item, and does not expand it.

BFFM_SETSTATUSTEXT: SetStatusText - when you use the old dialog style you can set the status text with this message.

BFFM_SETOKTEXT: SetOkText - this message let you set the ok button text, for example you can respond to the initialize event and set the buttons text to "KABOOM!" if you like.. another idea is to set the button text on respond to the selection changed event.

So, how do you send these events? you will probably need to declare the

SendMessage 
API and start dealing with raw win32 code.. luckily, I've did it for you, all you need to do is call the following methods. Don't forget, you can call this methods only from your event handlers. Here are the methods:

C#
public void EnableOk(IntPtr hwnd, bool Enabled)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_ENABLEOK, 0, Enabled ? 1 : 0); 
}

public void SetExpanded(IntPtr hwnd, string path)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETEXPANDED, 1, path);
}

public void SetOkText(IntPtr hwnd, string text)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETOKTEXT, 0, text);
}

public void SetSelection(IntPtr hwnd, string path)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETSELECTIONW, 1, path);
}

public void SetStatusText(IntPtr hwnd, string text)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETSTATUSTEXTW, 1, text);
}

Note that sometimes I use the SendMessage function with the last parameter as a number and sometimes as a string, this is not done by magic, I had to declare two times the SendMessage function, as shown here:

C#
// The SendMessage function sends the specified message to a 
// window or windows. It calls the window procedure for the specified 
// window and does not return until the window procedure has processed the message. 
[DllImport("User32.dll")]
public static extern Int32 SendMessage(
    IntPtr hWnd,               // handle to destination window
    UInt32 Msg,                // message
    UInt32 wParam,             // first message parameter
    Int32 lParam               // second message parameter
    );

[DllImport("User32.dll")]
public static extern Int32 SendMessage(
    IntPtr hWnd,               // handle to destination window
    UInt32 Msg,                // message
    UInt32 wParam,             // first message parameter
    [MarshalAs(UnmanagedType.LPWStr)]
    String lParam              // second message parameter
    );

Custom Filtering

What is custom filtering? I'll start with an example, suppose I want that the dialog will display only txt or bmp files, and off course all the folders. Sure, I can handle the election changed event, and disable the ok button if the selected file is not a bmp or txt, but this is not what I've asked for. what I want is not seeing at all other files accept the bmp and txt files. This is custom filtering, meaning you can choose for each item if you want it displayed or not.

In order to achieve custom filtering you need to follow these steps:

1. Create an object that inherits the interface IFolderFilter. this intrface has two methods: GetEnumFlags - this function is called by the shell when it wants to know what KIND of items you want to display, here you return if you want only folders or only files or both, this is a minimal filtering. the other function: ShouldShow is called by the shell for each and every item , before it is displayed, letting you decide if you want it to be displayed or not. in this function you get the PIDL of the specific item so you could check all you want about the item, for example you can check whether it is a file or a folder and if its a file you can check if this file has a valid extension (bmp or txt). Heck, you can also check the size of the file and let the dialog display only file that are bigger then 666K..

After you create a class that inherits the IFolderFilter and you write your own GetEnumFlags and ShouldShow functions you can move to the next step.

2. On response to the IUnknown event (I told you we will discuss it later) you query the interface for the IFolderFilterSite interface, this interface has only one function: SetFilter. This function receive an instance of the FolderFilter object you created in step one, that is how the shell known who to call when it need to decide whether to display the items or not.

That's it, after doing those two steps, every time the shell will need to know whether to display an item in the shell dialog it will call your

ShouldShow 
function in your predefined class.

Note, the IFolderFilter and IFolderFilterSite interfaces are declared in the source code added to this article, also added is an example of a custom filter, the filter I've made has a property of type string array (string[]) this will hold the valid file extension allowed to be displayed, here is the code:

C#
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("9CC22886-DC8E-11d2-B1D0-00C04F8EEB3E")]
public interface IFolderFilter
{
    // Allows a client to specify which individual items should be enumerated.
    // Note: The host calls this method for each item in the folder. Return S_OK,
    // to have the item enumerated. 
    // Return S_FALSE to prevent the item from being enumerated.
    [PreserveSig] 
    Int32 ShouldShow(
        [MarshalAs(UnmanagedType.Interface)]Object psf,                
                        // A pointer to the folder's IShellFolder interface.
        IntPtr pidlFolder,    // The folder's PIDL.
        IntPtr pidlItem);    // The item's PIDL.

    // Allows a client to specify which classes of objects in a Shell folder 
    // should be enumerated.
    [PreserveSig] 
    Int32 GetEnumFlags( 
        [MarshalAs(UnmanagedType.Interface)]Object psf,                
                        // A pointer to the folder's IShellFolder interface.
        IntPtr pidlFolder,    // The folder's PIDL.
        IntPtr phwnd,        // A pointer to the host's window handle.
        out UInt32 pgrfFlags); // One or more SHCONTF values that specify which
                        // classes of objects to enumerate.
    
};

Finally you need to respond to the IUnknown event in order to use this filter, here it is:

C#
private void IUnknownEvent(
    ShellLib.ShellBrowseForFolderDialog sender,
    ShellLib.ShellBrowseForFolderDialog.IUnknownEventArgs args)
{
    IntPtr iFolderFilterSite;

    if (args.iunknown == IntPtr.Zero)
        return;

    System.Runtime.InteropServices.Marshal.QueryInterface(
        args.iunknown,
        ref ShellLib.ShellGUIDs.IID_IFolderFilterSite,
        out iFolderFilterSite);
    
    Object obj = System.Runtime.InteropServices.Marshal.GetTypedObjectForIUnknown(
        iFolderFilterSite,
        ShellLib.ShellFunctions.GetFolderFilterSiteType());
    ShellLib.IFolderFilterSite folderFilterSite = (ShellLib.IFolderFilterSite)obj;
                
    ShellLib.FilterByExtension filter = new ShellLib.FilterByExtension();
    
    string[] ext = new string[2];
    ext[0] = "bmp";
    ext[1] = "txt";
                
    filter.ValidExtension = ext;
    
    folderFilterSite.SetFilter(filter);
}

Using the Class

Using the class is quite easy and there is practically no shell stuff involved, here is a sample:

C#
private void button5_Click(object sender, System.EventArgs e)
{
    ShellLib.ShellBrowseForFolderDialog folderDialog =
        new ShellLib.ShellBrowseForFolderDialog();
    folderDialog.hwndOwner = this.Handle;
    // Scenario A - take the defaults

    // Scenario B - select from a special folder
    //folderDialog.RootType = ShellLib.ShellBrowseForFolderDialog.RootTypeOptions.
    // BySpecialFolder;
    //folderDialog.RootSpecialFolder = ShellLib.ShellApi.CSIDL.CSIDL_WINDOWS;
    
    // Scenario C - select from a specific path
    //folderDialog.RootType = ShellLib.ShellBrowseForFolderDialog.RootTypeOptions.
    //ByPath;
    //folderDialog.RootPath = @"c:\temp\divx";
    
    folderDialog.Title = "Hello CodeProject readers!";
    folderDialog.Title += "\n";
    folderDialog.Title += "This is my extensible Shell dialog.";
    folderDialog.Title += "\n";
    folderDialog.Title += "Please select a bmp or a txt file:";

    // register events
    folderDialog.OnInitialized += 
        new ShellLib.ShellBrowseForFolderDialog.InitializedHandler(
            this.InitializedEvent);
    folderDialog.OnIUnknown += 
        new ShellLib.ShellBrowseForFolderDialog.IUnknownHandler(
            this.IUnknownEvent);
    folderDialog.OnSelChanged += 
        new ShellLib.ShellBrowseForFolderDialog.SelChangedHandler(
            this.SelChangedEvent);
    folderDialog.OnValidateFailed += 
        new ShellLib.ShellBrowseForFolderDialog.ValidateFailedHandler(
            this.ValidateFailedEvent);

    folderDialog.ShowDialog();
    MessageBox.Show("Display Name: " + folderDialog.DisplayName + "\nFull Name: "
        + folderDialog.FullName );
}

That's it. This was a long article but necessary to start using the shell, I promise that future articles will be more fun and less boring functions and interfaces..

If you have any comments, I will be pleased to know them. I hope you liked it and please don't forget to vote.

History

25/01/2003 - Article first released.
26/01/2003 - Article and Code updated.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer (Senior) Verint
Israel Israel
Arik Poznanski is a senior software developer at Verint. He completed two B.Sc. degrees in Mathematics & Computer Science, summa cum laude, from the Technion in Israel.

Arik has extensive knowledge and experience in many Microsoft technologies, including .NET with C#, WPF, Silverlight, WinForms, Interop, COM/ATL programming, C++ Win32 programming and reverse engineering (assembly, IL).

Comments and Discussions

 
QuestionSet of broken links in this article Pin
ralpitto9-May-16 6:59
ralpitto9-May-16 6:59 
SuggestionRe: Set of broken links in this article Pin
Maciej Los9-May-16 9:19
mveMaciej Los9-May-16 9:19 
BugGetUIObjectOf AccessViolationException Pin
M Klenotic5-Jun-15 6:32
M Klenotic5-Jun-15 6:32 
QuestionInsert as Control Pin
Suplanus20-Mar-11 23:33
Suplanus20-Mar-11 23:33 
GeneralThanks! Pin
Manfred Duerst6-Aug-10 0:01
Manfred Duerst6-Aug-10 0:01 
QuestionHow to determine that ShellBrowseForFolderDialog was canceled? Pin
YuriGubanov19-Aug-09 0:19
YuriGubanov19-Aug-09 0:19 
AnswerRe: How to determine that ShellBrowseForFolderDialog was canceled? Pin
#realJSOP17-Jan-10 4:12
mve#realJSOP17-Jan-10 4:12 
GeneralRe: How to determine that ShellBrowseForFolderDialog was canceled? Pin
menzer13-Jul-12 8:25
menzer13-Jul-12 8:25 
QuestionHow to pass initial path to ShellBrowseForFolderDialog? Pin
YuriGubanov19-Aug-09 0:10
YuriGubanov19-Aug-09 0:10 
Generalselect more than 15 files in Windows Explorer (Vista) Pin
robertosari3-Aug-09 3:38
robertosari3-Aug-09 3:38 
QuestionHow can I get pidl of control panel? Pin
imadulhaq28-Jan-09 18:07
imadulhaq28-Jan-09 18:07 
GeneralMissing Shell Programmer's Guide Article Pin
Member 42420909-Jun-08 19:55
Member 42420909-Jun-08 19:55 
GeneralRe: Missing Shell Programmer's Guide Article Pin
Jay Thompson13-Jun-08 14:06
Jay Thompson13-Jun-08 14:06 
GeneralRe: Missing Shell Programmer's Guide Articles Pin
Tatworth21-Aug-08 0:53
Tatworth21-Aug-08 0:53 
GeneralRe: Missing Shell Programmer's Guide Articles Pin
ralpitto8-May-16 5:04
ralpitto8-May-16 5:04 
GeneralRe: Missing Shell Programmer's Guide Articles Pin
ralpitto9-May-16 6:55
ralpitto9-May-16 6:55 
QuestionHow to create a virtual shell folder and how can we keep our own context menu for that. Pin
Intuitive00718-Apr-08 0:33
Intuitive00718-Apr-08 0:33 
Generalbeyond .net frameworks FolderBrowserDialog Pin
Vivekv29-Feb-08 12:27
Vivekv29-Feb-08 12:27 
GeneralDLL created with your article's code Pin
sunny7426-Oct-07 7:00
sunny7426-Oct-07 7:00 
GeneralRe: DLL created with your article's code Pin
J4amieC29-Oct-07 4:43
J4amieC29-Oct-07 4:43 
GeneralRe: DLL created with your article's code Pin
Azad Giordano Ratzki18-Dec-07 13:38
professionalAzad Giordano Ratzki18-Dec-07 13:38 
QuestionWhat about the license? Pin
unintended9-Jul-07 23:15
unintended9-Jul-07 23:15 
GeneralException occurs - System.Runtime.InteropServices.Marshal.ReleaseComObject(ishellParent); Pin
pkwan721-Mar-06 19:06
pkwan721-Mar-06 19:06 
GeneralThanks! Pin
User 366041-Mar-06 23:52
User 366041-Mar-06 23:52 
GeneralCommenting Pin
BC3Tech14-Feb-06 6:36
BC3Tech14-Feb-06 6:36 

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

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