Introduction
Have you ever tried to change anything in the folder browser dialog of C#?
In this article, I will show you how you can make changes to the dialog. For example, what if we want to disable the �Make New Folder� button every time a CD drive is selected? Or what if we want the user to select only from existing folders, except one folder that is to be allowed to make child folders?
Background
.NET has a FolderBrowserDialog
control which is, basically, a wrapper for the Win32 function SHBrowseForFolder
. The only issue here is that this class is a sealed
class, so we can�t derive from it and add functionality. This control doesn�t provide any event we can register to and execute our code there. So basically, if you are satisfied with the control as it is, then you are OK. But once you need to do a small change, you have to figure out how the control really works.
In this article, we will see, step by step, how we can disable the �Make New Folder� every time a CD drive is selected.
To achieve our goal, we will have to overcome two main issues:
- Getting the control to fire an event every time a selection is changed.
- Disabling the button.
Using the code
The first thing we are going to do is create an ExtendedFolderBrowser
class. This class will hold a private class, InternalFolderBrowser
, which will be derived from CommonDialog
. InternalFolderBrowser
is, basically, an implementation of the folder browser dialog, but this implementation will allow us to extend the functionality and add our new add-ins in the parent class. When deriving from the CommonDialog
class, we need to implement two methods: Reset
and Rundialog
.
private class InternalFolderBrowser : CommonDialog
{
private string m_selectedPath = null;
private Environment.SpecialFolder m_rootFolder;
public event EventHandler SelectedFolderChanged;
private string m_descriptionText = String.Empty;
public override void Reset()
{
m_rootFolder = Environment.SpecialFolder.Desktop;
m_selectedPath = string.Empty;
}
protected override bool RunDialog(System.IntPtr hwndOwner)
{
IntPtr ptr1 = IntPtr.Zero;
bool flag1 = false;
Win32API.SHGetSpecialFolderLocation(hwndOwner,
(int)m_rootFolder, ref ptr1);
if (ptr1 == IntPtr.Zero)
{
Win32API.SHGetSpecialFolderLocation(hwndOwner, 0, ref ptr1);
if (ptr1 == IntPtr.Zero)
{
throw new Exception("FolderBrowserDialogNoRootFolder");
}
}
Application.OleRequired();
IntPtr ptr2 = IntPtr.Zero;
try
{
Win32API.BROWSEINFO browseinfo1 = new Win32API.BROWSEINFO();
IntPtr ptr3 = Marshal.AllocHGlobal((int)
(260 * Marshal.SystemDefaultCharSize));
IntPtr ptr4 = Marshal.AllocHGlobal((int)
(260 * Marshal.SystemDefaultCharSize));
Win32API.BrowseCallbackProc proc1 =
new Win32API.BrowseCallbackProc(
this.FolderBrowserDialog_BrowseCallbackProc);
browseinfo1.pidlRoot = ptr1;
browseinfo1.hwndOwner = hwndOwner;
browseinfo1.pszDisplayName = ptr3;
browseinfo1.lpszTitle = m_descriptionText;
browseinfo1.ulFlags = 0x40;
browseinfo1.lpfn = proc1;
browseinfo1.lParam = IntPtr.Zero;
browseinfo1.iImage = 0;
ptr2 = Win32API.SHBrowseForFolder(browseinfo1);
string s = Marshal.PtrToStringAuto(ptr3);
if (ptr2 != IntPtr.Zero)
{
Win32API.SHGetPathFromIDList(ptr2, ptr4);
this.m_selectedPath = Marshal.PtrToStringAuto(ptr4);
Marshal.FreeHGlobal(ptr4);
Marshal.FreeHGlobal(ptr3);
flag1 = true;
}
}
finally
{
Win32API.IMalloc malloc1 = GetSHMalloc();
malloc1.Free(ptr1);
if (ptr2 != IntPtr.Zero)
{
malloc1.Free(ptr2);
}
}
return flag1;
}
When we create the BROWSEINFO
struct, we pass a delegate to the function that will be called on every change in the control:
private int FolderBrowserDialog_BrowseCallbackProc(IntPtr hwnd,
int msg, IntPtr lParam, IntPtr lpData)
{
switch (msg)
{
case Win32API.BFFM_INITIALIZED:
if (m_selectedPath != string.Empty)
{
Win32API.SendMessage(new HandleRef(null, hwnd),
0x467, 1, m_selectedPath);
}
break;
case Win32API.BFFM_SELCHANGED:
{
IntPtr ptr1 = lParam;
if (ptr1 != IntPtr.Zero)
{
IntPtr ptr2 = Marshal.AllocHGlobal((int)
(260 * Marshal.SystemDefaultCharSize));
bool flag1 = Win32API.SHGetPathFromIDList(ptr1, ptr2);
m_selectedPath = Marshal.PtrToStringAuto(ptr2);
if(SelectedFolderChanged != null)
{
SelectedFolderChanged(this,null);
}
Marshal.FreeHGlobal(ptr2);
Win32API.SendMessage2(new HandleRef(null, hwnd),
0x465, 0, flag1 ? 1 : 0);
}
break;
}
}
return 0;
}
Every time a selection is changed, we will be notified by the BFFM_SELCHANGED
message. Once we receive a message, we will fire an event so our parent class, ExtendedFolderBrowser
, will be notified too.
The last thing we should do is register the event of the inner class and disable the button.
When the selection changes, we will execute a CheckState
method:
private void CheckState()
{
if(m_ShowNewButtonHandler != null)
{
if(m_ShowNewButtonHandler(SelectedPath))
{
UpdateButtonState(GetButtonHandle(IntPtr.Zero), false);
}
else
{
UpdateButtonState(GetButtonHandle(IntPtr.Zero),true);
}
}
}
m_ShowNewButtonHandler
is a delegate to the user function that will check if we should disable the button according to the current selection. When a client creates a dialog, it will pass a delegate to its function to make a decision about the state of the button.
To change the state of the button, we simply use the EnableWindow
API:
private void UpdateButtonState(IntPtr handle, bool state)
{
if(handle != IntPtr.Zero)
{
Win32API.EnableWindow(handle,state);
}
}
The last tricky thing left to do is to get the handle for the button. To get the handle, we will search for the class name "#32770", this is the class of the folder browser dialog. The problem is that this class name is not unique, so we have to make another check whether this handle we found is actually from the same thread that we are running. If it is from the same thread, bingo!, we have found the handler for the dialog control, otherwise we will continue searching.
Once we have found the dialog handle, all that is left is to return the first child handle of the Button
type. This will be the handle to the 'Make New Folder" button.
private bool isFromTheSameThread(IntPtr windowHandle)
{
IntPtr activeThreadID = Win32API.GetWindowThreadProcessId(windowHandle,
IntPtr.Zero);
int currentThread = AppDomain.GetCurrentThreadId();
return (currentThread == activeThreadID.ToInt32());
}
private IntPtr GetButtonHandle(IntPtr handle)
{
if(handle == IntPtr.Zero)
{
IntPtr parent = Win32API.FindWindow(BROWSE_FOR_FOLDER_CLASS_NAME,null);
if(!isFromTheSameThread(parent))
{
return GetButtonHandle(parent);
}
else
{
return Win32API.FindWindowEx(parent,IntPtr.Zero,"Button", null);
}
}
else
{
IntPtr parent = Win32API.FindWindowEx(IntPtr.Zero,
handle,BROWSE_FOR_FOLDER_CLASS_NAME, null);
if(!isFromTheSameThread(parent))
{
return GetButtonHandle(parent);
}
else
{
return Win32API.FindWindowEx(parent,IntPtr.Zero,"Button", null);
}
}
}
The last thing I want to show you is how to use this control:
ExtendedFolderBrowser m_ExtendedFolderBrowser =
new ExtendedFolderBrowser();
m_ExtendedFolderBrowser.Description = "Folder Browser";
ShowNewButtonHandler handler = new ShowNewButtonHandler(IsCDDrive);
m_ExtendedFolderBrowser.SetNewFolderButtonCondition = handler;
m_ExtendedFolderBrowser.ShowDialog();