C# does Shell, Part 1






4.91/5 (91 votes)
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.
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:
- Shell Programmer's Guide
- Shell Basics
- The Shell Namespace
- Getting a Folder's ID
- Getting Information About the Contents of a Folder
- Navigating the Namespace
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:
- Browsing for folders, files, computers or printers.
- Setting the root folder in the dialog box.
- 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.
- Setting you own instructions text.
- Changing the OK button text.
- Events that notify about selection changes and validation.
- 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:
[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:
// 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:
// 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:
// 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:
// 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:
// 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:
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:
// 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:
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:
// 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:
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:
[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:
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 GetDisplayNameOf
to
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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
/// <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:
/// <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:
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:
public BrowseInfoFlag DetailsFlags;
And so now, when you want to set some flags you do something like:
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.
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:
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:
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:
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:
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:
// 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:
[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:
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:
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.