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:
- 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
{
[PreserveSig]
IntPtr Alloc(
UInt32 cb);
[PreserveSig]
IntPtr Realloc(
IntPtr pv,
UInt32 cb);
[PreserveSig]
void Free(
IntPtr pv);
[PreserveSig]
UInt32 GetSize(
IntPtr pv);
[PreserveSig]
Int16 DidAlloc(
IntPtr pv);
[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:
[DllImport("shell32.dll")]
public static extern Int32 SHGetMalloc(
out IntPtr hObject);
Example of usage:
IntPtr ptrRet;
SHGetMalloc(out ptrRet);
System.Type mallocType = System.Type.GetType("IMalloc");
Object obj = Marshal.GetTypedObjectForIUnknown(ptrRet,mallocType);
IMalloc pMalloc = (IMalloc)obj;
IntPtr pidlRoot;
SHGetFolderLocation(IntPtr.zero,CSIDL_WINDOWS,IntPtr.Zero,0,out pidlRoot);
if (pidlRoot != IntPtr.Zero)
pMalloc.Free(pidlRoot);
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:
[DllImport("shell32.dll")]
public static extern Int32 SHGetFolderLocation(
IntPtr hwndOwner,
Int32 nFolder,
IntPtr hToken,
UInt32 dwReserved,
out IntPtr ppidl);
Example of usage:
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:
[DllImport("shell32.dll")]
public static extern Int32 SHGetPathFromIDList(
IntPtr pidl,
StringBuilder pszPath);
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:
[DllImport("shell32.dll")]
public static extern Int32 SHGetFolderPath(
IntPtr hwndOwner,
Int32 nFolder,
IntPtr hToken,
UInt32 dwFlags,
StringBuilder pszPath);
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:
[DllImport("shell32.dll")]
public static extern Int32 SHParseDisplayName(
[MarshalAs(UnmanagedType.LPWStr)]
String pszName,
IntPtr pbc,
out IntPtr ppidl,
UInt32 sfgaoIn,
out UInt32 psfgaoOut);
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
{
[PreserveSig]
Int32 ParseDisplayName(
IntPtr hwnd,
IntPtr pbc,
[MarshalAs(UnmanagedType.LPWStr)]
String pszDisplayName,
ref UInt32 pchEaten,
out IntPtr ppidl,
ref UInt32 pdwAttributes);
[PreserveSig]
Int32 EnumObjects(
IntPtr hwnd,
Int32 grfFlags,
out IntPtr ppenumIDList);
[PreserveSig]
Int32 BindToObject(
IntPtr pidl,
IntPtr pbc,
Guid riid,
out IntPtr ppv);
[PreserveSig]
Int32 BindToStorage(
IntPtr pidl,
IntPtr pbc,
Guid riid,
out IntPtr ppv);
[PreserveSig]
Int32 CompareIDs(
Int32 lParam,
IntPtr pidl1,
IntPtr pidl2);
[PreserveSig]
Int32 CreateViewObject(
IntPtr hwndOwner,
Guid riid,
out IntPtr ppv);
[PreserveSig]
Int32 GetAttributesOf(
UInt32 cidl,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)]
IntPtr[] apidl,
ref UInt32 rgfInOut);
[PreserveSig]
Int32 GetUIObjectOf(
IntPtr hwndOwner,
UInt32 cidl,
IntPtr[] apidl,
Guid riid,
ref UInt32 rgfReserved,
out IntPtr ppv);
[PreserveSig]
Int32 GetDisplayNameOf(
IntPtr pidl,
UInt32 uFlags,
out ShellApi.STRRET pName);
[PreserveSig]
Int32 SetNameOf(
IntPtr hwnd,
IntPtr pidl,
[MarshalAs(UnmanagedType.LPWStr)]
String pszName,
UInt32 uFlags,
out IntPtr ppidlOut);
}
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:
[DllImport("shell32.dll")]
public static extern Int32 SHGetDesktopFolder(
out IntPtr ppshf);
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:
[DllImport("shell32.dll")]
public static extern Int32 SHBindToParent(
IntPtr pidl,
[MarshalAs(UnmanagedType.LPStruct)]
Guid riid,
out IntPtr ppv,
ref IntPtr ppidlLast);
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:
[DllImport("shlwapi.dll")]
public static extern Int32 StrRetToBSTR(
ref STRRET pstr,
IntPtr pidl,
[MarshalAs(UnmanagedType.BStr)]
out String pbstr);
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:
[DllImport("shlwapi.dll")]
public static extern Int32 StrRetToBuf(
ref STRRET pstr,
IntPtr pidl,
StringBuilder pszBuf,
UInt32 cchBuf);
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:
[DllImport("shell32.dll")]
public static extern IntPtr SHBrowseForFolder(
ref BROWSEINFO lbpi);
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:
[StructLayout(LayoutKind.Sequential)]
public struct BROWSEINFO
{
public IntPtr hwndOwner;
public IntPtr pidlRoot;
[MarshalAs(UnmanagedType.LPStr)]
public String pszDisplayName;
[MarshalAs(UnmanagedType.LPStr)]
public String lpszTitle;
public UInt32 ulFlags;
[MarshalAs(UnmanagedType.FunctionPtr)]
public BrowseCallbackProc lpfn;
public Int32 lParam;
public Int32 iImage;
}
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:
public IntPtr hwndOwner;
public RootTypeOptions RootType;
public string RootPath;
public ShellApi.CSIDL RootSpecialFolder;
public string Title;
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:
Only return computers. If the user selects anything other than a computer,
public bool BrowseForComputer;
Only return printers. If the user selects anything other than a printer, the
public bool BrowseForPrinter;
public bool IncludeFiles;
public bool IncludeUrls;
public bool DontGoBelowDomain;
public bool EditBox;
public bool NewDialogStyle;
public bool NoNewFolderButton;
public bool NoTranslateTargets;
public bool ReturnOnlyFileSystemAncestors;
public bool ReturnOnlyFileSystemDirs;
public bool Shareable;
public bool StatusText;
public bool UsageHint;
public bool UseNewUI;
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 = "";
IMalloc pMalloc;
pMalloc = ShellFunctions.GetMalloc();
IntPtr pidlRoot;
if (RootType == RootTypeOptions.BySpecialFolder)
{
ShellApi.SHGetFolderLocation(hwndOwner,(int)RootSpecialFolder,UserToken,
0,out pidlRoot);
}
else
{
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);
IntPtr pidlSelected;
pidlSelected = ShellLib.ShellApi.SHBrowseForFolder(ref bi);
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:
[DllImport("User32.dll")]
public static extern Int32 SendMessage(
IntPtr hWnd,
UInt32 Msg,
UInt32 wParam,
Int32 lParam
);
[DllImport("User32.dll")]
public static extern Int32 SendMessage(
IntPtr hWnd,
UInt32 Msg,
UInt32 wParam,
[MarshalAs(UnmanagedType.LPWStr)]
String lParam
);
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
{
[PreserveSig]
Int32 ShouldShow(
[MarshalAs(UnmanagedType.Interface)]Object psf,
IntPtr pidlFolder,
IntPtr pidlItem);
[PreserveSig]
Int32 GetEnumFlags(
[MarshalAs(UnmanagedType.Interface)]Object psf,
IntPtr pidlFolder,
IntPtr phwnd,
out UInt32 pgrfFlags);
};
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;
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:";
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.
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).