Click here to Skip to main content
16,006,001 members
Articles / Programming Languages / C#
Article

C# File Browser

Rate me:
Please Sign up or sign in to vote.
4.93/5 (173 votes)
21 Aug 200628 min read 2.4M   82K   589   370
A file browser written in C#, very much like Windows Explorer.

Contents

Image 1

Introduction

This project introduces a Windows Explorer clone in an early state. It contains browsing through all files and folders on your computer, including virtual folders. It uses the same ContextMenus as Windows Explorer and includes drag and drop support.

I created this project with Visual Studio 2005 (.NET 2.0) and haven't tried it with .NET 1.1. I'm pretty sure it will work with .NET 1.1, but some changes need to be made. For example, I used the ToolBarMenuStrip which isn't available in 1.1. If anyone wants a 1.1 version and isn't able to convert it, I'm willing to convert it myself and provide the code.

Update V1.3

Quite a lot of different new things in this update. The most important new feature is plug-ins. You can now add your own plugins to this application, which will be used to add columns to the details view and to add special views. More on this in the "Plug-ins" section. Another important update is an addition to this article which explains how to use this Control in your own application and how to use it's functions, see the "Using the Control" section for this update.

Furthermore there are some small updates, additions and bug fixes. See the "History" section for these updates.

Update V1.2

A bit of a small update really, but quite a handy one. I added the "New" menu to the standard ContextMenu of the ListView. So now it's possible to add new folders and files from within the program.

Also a few bug fixes and another change in the update thread has been made, which also comes with some speed improvement.

Update V1.1

This is quite a large improvement since the first version. It doesn't include a lot of new features, but has a lot of fixes and speed improvement. The most important fix is the memory leak fix, which was caused by the update thread. When I was solving this problem I also added another update method. This method uses the SHChangeNotifyRegister function to retrieve Shell notify messages. These messages are used to make some more updates, like changing icons, inserting media and renaming. So now when you insert a disc into your disc-drive the icon and text of the drive will change to the ones from the disc.

One new feature which needs some attention is the rename function. You can now rename items by selecting the rename item from their ContextMenu or by pressing F2. Be aware though that this will also change the extension of the file, but it will warn you if you do this. When renaming multiple items it will take the name you entered and add a number to it, different for any item, somewhat like Windows Explorer. I recommend trying the rename function on some test files to see exactly what is does, before using it on other files.

For any other changes made, see the "History" section.

Background

I was looking for something nice to program, when I got the idea of making my own Windows Explorer. I started this project with the idea of making an enhanced version of Windows Explorer with plug-in support. But before being able to enhance the Windows Explorer, you got to have a program which works like Windows Explorer. So I started searching the Internet for solutions.

While searching the Internet I found a lot of programming around Windows Explorer and Shell extensions. But none really had everything I needed and most programs where written in C++ while I really wanted one in C#. Finally I found the article I needed to start my program: An All VB.NET Explorer Tree Control with ImageList Management. Although it was written in VB I could get a really great deal of information from it and the largest part of this project relies on that article. So for more information or for a VB version see that article.

The only problem now was that I had never worked with the Windows Shell before. So first things first, I searched for articles explaining about how the Shell works and what you can do with it. Well, you can do a lot with it, too much to explain here. If you never worked with the Shell before or don't really know how the Shell works, I recommend this article: C# does Shell. This article also provides you with some links to MSDN articles. It takes some time to read them, but it definitely helped me a lot in making this program. You can also find a lot of info about all the Shell methods, structures and enumerations used in this program on MSDN.

After the base of the program was created I started implementing things like the Shell ContextMenu, drag/drop support and a Windows Explorer like ComboBox. I didn't really find a nice article on CodeProject for this, but a whole bunch where available on the Internet. I programmed everything with a wrapper around the Shell functions from the Windows API and was surprised how well it worked.

Using the Control

To use this control in your own program, add a reference to the dll to your project. After that you can add the Browser control to the toolbox and add it to your own project. I've created some properties which allow you to alter the behaviour and look of the control (at design time):

ShowNavigationBarShows or hides the navigation bar
ShowFoldersShows or hides the folder TreeView
ShowFoldersButtonShows or hides the button for the folder TreeView
StartUpDirectoryEnumeration indicating which directory will be opened at startup
StartUpDirectoryOtherString indicating which directory will be opened at startup (StartUpDirectory must be "Other")
ShellBrowserSets the ShellBrowser used for retrieving ShellItems, if set to null the Browser will create it's own
PluginWrapperSets the PluginWrapper used for retrieving plug-ins, if set to null the Browser will create it's own
SplitterDistanceSets the distance of the Splitter between the TreeView and the ListView>/TD>

StartUpDirectory is an enumeration of special folders which are used to determine the startup location of the Browser. If you want to provide your own location, you must set this value to "Other" and provide your own location in the StartUpDirectoryOther property.

The ShellBrowser and PluginWrapper properties are used when you want to add more than one Browser control to your program. You can link those Browser controls by setting the ShellBrowser and PluginWrapper to the same object. This will make the program run a lot faster and more efficient than using a different ShellBrowser and PluginWrapper for the controls. In the demo project you'll see an example of how to add two Browser controls to a project.

There are also a few properties which you can only use at run-time:

ListViewModeSets the initial view of the ListView (can't be "Small Icons")
SelectedItemSets the current directory (ShellItem)
SelectedNodeSets the current directory (TreeNode)
ShowFoldersButtonShows or hides the button for the folder TreeView

Lastly, there are methods to programmatically do some actions for the Browser:

SelectPathSets the current directory
BrowserBackSame as clicking the Back button of the Browser
BrowserForwardSame as clicking the Forward button of the Browser
BrowserUpSame as clicking the Up button of the Browser
CreateNewFolderCreates a new directory in the current directory, if possible

SelectPath takes 3 different Objects to set the current directory. Either the ShellItem of the folder to select, a string of the path to the directory (this can also be like "My Documents\My Music") or a value of the SpecialFolders enumeration.

Class Overview

The main classes

These are the classes which provide the actual control.

BrowserThe actual FileBrowser control
BrowserTreeView, BrowserListView and BrowserComboBoxThe controls used in the Browser
BrowserTreeSorter and BrowserListSorterThe classes for sorting the TreeNodes and ListViewItems
BrowserComboItemProvides the items for the BrowserComboBox

These classes are quite simple and don't need a lot of explanation. To use my control in your project, you actually only need to use the Browser class. Just add this control to a form and all should be working. For more info, take a look at the comment in my code. Unfortunately at the moment my code doesn't have many comments, but I will try to add more shortly.

The Shell classes

These classes provide easy access to Shell functions.

ShellAPIIncludes Windows API imports, constants, structures, and enumerations
ShellBrowserUsed to retrieve the ShellItems which represent the file system
ShellItemRepresents a file system object, folder or file (this can be a virtual folder)
ShellImageListRetrieves the Shell ImageList and makes them available for the Browser
PIDLBuild around a pointer to a PIDL-structure, which is used to identify a file system object

ShellAPI and ShellImageList are very much like the classes in Jim Parsells' project I mentioned earlier. They are similar to ShellDll and SystemImageListManager respectively. For more info about these classes first try his article. ShellItem comes from his CShItem class, but I've completely rewritten it, to match my needs. I'm not going to go through the detail of this class, but if many people really need more info about it, I might write an article about it.

The wrapper classes

These classes provide a wrapper around the drag/drop operations and the ContextMenus for the control.

BrowserTVContextMenuWrapper and BrowserLVContextMenuWrapper

Provide ContextMenus to the TreeView and ListView

ContextMenuHelperTakes care of executing Shell ContextMenu commands
BrowserTVDropWrapper and BrowserLVDropWrapperProvide drop operations to the TreeView and ListView
BrowserTVDragWrapper and BrowserLVDragWrapperProvide drag operations to the TreeView and ListView

These classes are the most important and they are the ones I will explain in the rest of this article.

The Shell ContextMenu

Image 2

Retrieving it

The first thing you'll notice when trying to find a nice article about getting the Shell ContextMenu in your program, is that almost all articles are about making extensions to the menu and not about retrieving it for your own program. Luckily I found one blog that did explain this very thoroughly: How to host an IContextMenu. It was all in C++, so I had to translate it to C#. As this is an article existing of 11 parts, I'll try to explain everything here from a C# point of view. I'm going to assume you are familiar with the Shell namespace and pidls as it would take a lot of time to explain this here and this article is meant to cover the ContextMenu. So if you are not familiar with these terms, look for an article on those things first. The one I mentioned in the start of this article was all I needed (C# does Shell).

Before I explain the procedure for showing the ContextMenu, I'll give a short description for the interfaces we are going to use:

IShellFolderIs used to manage folders, it is exposed by all Shell namespace folder objects
IContextMenuIs called by the Shell to either create or merge a shortcut menu associated with a Shell object
IContextMenu2Is used to either create or merge a shortcut menu associated with a certain object when the menu involves owner-drawn menu items
IContextMenu3Is used to create or merge a shortcut menu associated with a certain object when the menu implementation needs to process the WM_MENUCHAR message

So know let's get started with the ContextMenu stuff. To retrieve the menu you want, you'll need a few things:

  • The IShellFolder interface from the parent directory
  • The pidls (relative to the parent) from the items you want to get the ContextMenu for
  • The IContextMenu from the same items

Not much has to be done to obtain the IShellFolder interface. The ShellItem class provides the IShellFolder for each directory, so you just have to get the ShellItem class for the parent directory and then you'll have the IShellFolder interface. The pidls can also be retrieved from the ShellItem class. In my control each TreeNode and ListViewItem has their own ShellItem in their Tag property, so it is also quite easy to get the pidls you need. After this has been done you have everything you to get the IContextMenu interface. The IShellFolder interface has a method which will provide a lot of different interfaces for its children, these interfaces include the IContextMenu. We need to make a call to the GetUIObjectOf method from the IShellFolder, like in the following example:

C#
public static bool GetIContextMenu(
          IShellFolder parent,
          IntPtr[] pidls,
          out IntPtr icontextMenuPtr,
          out IContextMenu iContextMenu)
{
    if (parent.GetUIObjectOf(
                IntPtr.Zero,
                (uint)pidls.Length,
                pidls,
                ref ShellAPI.IID_IContextMenu,
                IntPtr.Zero,
                out icontextMenuPtr) == ShellAPI.S_OK)
    {
        iContextMenu =
            (IContextMenu)Marshal.GetTypedObjectForIUnknown(
                icontextMenuPtr, typeof(IContextMenu));

        return true;
    }
    else
    {
        icontextMenuPtr = IntPtr.Zero;
        iContextMenu = null;

        return false;
    }
}

As you can see, you need an array of IntPtr. This array includes the pidls of the items for which to retrieve the IContextMenu. This can be any number, in our program this number depends on how many items are selected. With GetUIObjectOf you'll get a pointer to the IContextMenu and to obtain the real interface you need to use the Marshal class.

Now we need a ContextMenu and because we are calling only Windows API methods, all we need is a Handle to a ContextMenu. To make a new ContextMenu the Windows API way, we just need to call ShellAPI.CreatePopupMenu(), which will return a pointer to the new ContextMenu. You can now add all the menu items from the Shell ContextMenu by calling the QueryContextMenu method from the IContextMenu interface.

C#
contextMenu = ShellAPI.CreatePopupMenu();
  
iContextMenu.QueryContextMenu(
  contextMenu,
  0,
  ShellAPI.CMD_FIRST,
  ShellAPI.CMD_LAST,
  ShellAPI.CMF.EXPLORE |
  ShellAPI.CMF.CANRENAME |
  ((Control.ModifierKeys & Keys.Shift) != 0 ? 
    ShellAPI.CMF.EXTENDEDVERBS : 0));

Invoking the selected command

Now the contextMenu pointer points to the ContextMenu we need. After this call you can change the menu in any way you want. To change the menu you can use the API functions AppendMenu and InsertMenu from the ShellAPI class. After that it's time to show our menu to the user. We do this by calling ShellAPI.TrackPopupMenuEx. This method will wait for the user to select an item and will return the id of the selected item. This id is not just the index of the item in the list, but it's a special id. To execute the command that goes with the selected item we need a CMINVOKECOMMANDINFOEX structure. We can use this with the InvokeCommand method from the IContextMenu to execute the selected command. For more info about this structure see MSDN.

C#
ShellAPI.CMINVOKECOMMANDINFOEX invoke = 
        new ShellAPI.CMINVOKECOMMANDINFOEX();
invoke.cbSize = ShellAPI.cbInvokeCommand;
invoke.lpVerb = (IntPtr)cmd;
invoke.lpDirectory = parentDir;
invoke.lpVerbW = (IntPtr)cmd;
invoke.lpDirectoryW = parentDir;
invoke.fMask = ShellAPI.CMIC.UNICODE | ShellAPI.CMIC.PTINVOKE |
    ((Control.ModifierKeys & Keys.Control) != 0 ? ShellAPI.CMIC.CONTROL_DOWN : 0) |
    ((Control.ModifierKeys & Keys.Shift) != 0 ? ShellAPI.CMIC.SHIFT_DOWN : 0);
invoke.ptInvoke = new ShellAPI.POINT(ptInvoke.X, ptInvoke.Y);
invoke.nShow = ShellAPI.SW.SHOWNORMAL;

iContextMenu.InvokeCommand(ref invoke);

In the previous example the cmd variable is the selected index. All we need to do is cast this to a pointer and the Shell functions know what to do with it. As you can see I also included some code for ModfierKeys. As you might know, when you delete a file using Windows Explorer, there are two ways to do it: moving it to the recycle bin, or deleting it permanently. When you just press delete, the selected file will be moved into the recycle bin, but when you hold shift and press delete, the file will be deleted permanently. That is why you have to add the ModifierKeys to the structure.

Another thing to notice is that we add a POINT to the structure. This POINT represents the place on the screen where you pressed the right mouse button. Have you ever noticed that when you clicked Properties on the ContextMenu of Windows Explorer, that the Properties window will be shown on the point where you right clicked your mouse button? Well it does and to have the same effect in your program you will have to set this POINT.

The "Open With" and "Send To" submenus

When all of this worked I was really happy, but soon I found something strange. When you select the "Open With" or the "Send To" submenus, you don't see other menu items in it. As we also want this menus to work, we need to get some more interfaces. The IContextMenu has two child classes which are needed to get the menu's to work: IContextMenu2 and IContextMenu3. To get these interfaces we simply use the Marshal class like this:

C#
Marshal.QueryInterface(
    icontextMenuPtr, ref ShellAPI.IContextMenu2_IID, out context2Ptr);
    
Marshal.QueryInterface(
    icontextMenuPtr, ref ShellAPI.IContextMenu3_IID, out context3Ptr);
    
iContextMenu2 =
    (IContextMenu2)
        Marshal.GetTypedObjectForIUnknown(context2Ptr, typeof(IContextMenu2));

iContextMenu3 =
    (IContextMenu3)
        Marshal.GetTypedObjectForIUnknown(context3Ptr, 
        typeof(IContextMenu3));

These interfaces will draw the menus for us, but they need to know when to do this. For this we need to override the WndProc method and check the messages that are being send to it. When these messages are about creating, measuring or drawing the ContextMenu items we will call the HandleMenuMsg and HandleMenuMsg2 methods from the IContextMenu2 and IContextMenu3 interfaces respectively, these methods will do the rest of the necessary work.

As you can read on MSDN, the IContextMenu2 interface will process the WM_INITMENUPOPUP, WM_MEASUREITEM and WM_DRAWITEM messages and the IContextMenu3 interface will process the WM_MENUCHAR message. So if you encounter one of these messages while showing the ContextMenu call the HandleMenuMsg and HandleMenuMsg2 methods to handle the specific messages.

C#
protected override void WndProc(ref Message m)
{
    if (iContextMenu2 != null &&
        (m.Msg == (int)ShellAPI.WM.INITMENUPOPUP ||
         m.Msg == (int)ShellAPI.WM.MEASUREITEM ||
         m.Msg == (int)ShellAPI.WM.DRAWITEM))
    {
        if (iContextMenu2.HandleMenuMsg(
            (uint)m.Msg, m.WParam, m.LParam) == ShellAPI.S_OK)
            return;
    }
    
    if (iContextMenu3 != null &&
        m.Msg == (int)ShellAPI.WM.MENUCHAR)
    {
        if (iContextMenu3.HandleMenuMsg2(
            (uint)m.Msg, m.WParam, m.LParam, IntPtr.Zero) == ShellAPI.S_OK)
            return;
    }
    
    base.WmdProc(ref Message m);
}

Once you implemented this, you will see that now the submenu's will also work the way they are supposed to.

This is the main idea to get the ContextMenus to work. My program also adds the Collapse and Expand MenuItems on a TreeNode ContextMenu, like Windows Explorer does. It will also raise an event for showing the ContextMenuItems help String when the item is being hovered over. Just check my code if you need to know how to do this.

Drag and drop support

Image 3

Before I started implementing the drag/drop support to my program I read the following article by Jim Parsells: Adding Drag and Drop to an Explorer Tree Control and another one by Michael Dunn: How to Implement Drag and Drop Between Your Program and Explorer. These let me in the right way and also made it a bit more challenging for me. At the end of Jim Parsells article he mentions some problems when implementing it the way he did. I think I solved these problems, by implementing it without using the .Net drag/drop methods. So forget all the nice implementations of .Net, we are going to use the Windows API to get this to work.

Three new interfaces are needed for drag and drop support:

IDropTargetContains methods used in any application that can be a target for data during a drag/drop operation
IDropSourceContains the methods for generating visual feedback to the end user and for canceling or completing the drag/drop operation
IDataObjectIs used by the Clipboard class and in drag/drop operations to store data from the dragged object

Fortunately we have the Shell namespace

The nice thing of the Shell namespace is that it will do all the dirty work for you, the only problem is to figure out how to let the Shell do his job. Once you are working with the Shell and get more familiar with it however, this will get a lot easier and you will find solutions to your problem quite fast. Once I got the hang of the ContextMenu stuff, it was actually quite an easy job for me to implement drag/drop.

Dropping items onto your control

To register your program for drop operations you need a class which implements the IDropTarget interface. In my program the BrowserTVDropWrapper and the BrowserLVDropWrapper are both IDropTargets. Before we get the necessary events raised in our own classes we need to register them. You can do this by calling the ShellAPI.RegisterDragDrop method, this method takes two arguments. One argument being the handle of the control to register the drag operation for and the other being the IDropTarget to receive messages about the drag. You also need to revoke your registration once you program finishes using the ShellAPI.RevokeDragDrop method. Once you registered your IDropTarget, your class will receive 4 different messages which need some more attention.

The first message being DragEnter, which will be called when someone drags an object and enters you control. You will receive a pointer to the IDataObject being dragged, the current state of the modifier keys and mouse buttons, the location of the mouse pointer and a reference to an instance of the DragDropEffects enumeration. This is quite a lot of info, but we don't really need to use it all. The Shell provides us with an IDropTarget from specific Shell objects which will do all the work for us. The only thing we need to do is check which item is being dragged over, obtain the IDropTarget for that item and pass all the info to that interface. To get the IDropTarget from an item, we have to call the GetUIObjectOf method from the parent IShellFolder interface again (as with the IContextMenu interface). So the basic idea in code form looks like this:

C#
private ShellDll.IDropTarget GetIDropTarget(ShellItem item, 
                             out IntPtr dropTargetPtr)
{
    ShellItem parent = item.ParentItem != null ? item.ParentItem : item;

    if (parent.ShellFolder.GetUIObjectOf(
            IntPtr.Zero,
            1,
            new IntPtr[] { item.PIDLRel.Ptr },
            ref ShellAPI.IID_IDropTarget,
            IntPtr.Zero,
            out dropTargetPtr) == ShellAPI.S_OK)
    {
        ShellDll.IDropTarget target =
            (ShellDll.IDropTarget)Marshal.GetTypedObjectForIUnknown(
                dropTargetPtr, typeof(ShellDll.IDropTarget));

        return target;
    }
    else
    {
        dropTargetPtr = IntPtr.Zero;
        return null;
    }
}

public int DragEnter(
    IntPtr pDataObj, 
    ShellAPI.MK grfKeyState, 
    ShellAPI.POINT pt, 
    ref DragDropEffects pdwEffect)
{
    Point point = br.FolderView.PointToClient(new Point(pt.x, pt.y));
    TreeViewHitTestInfo hitTest = br.FolderView.HitTest(point);

    dropNode = hitTest.Node;

    if (dropNode != null)
    {
        ShellItem item = (ShellItem)dropNode.Tag;
        parentDropItem = item;
    
        dropTarget = GetIDropTarget(item, out dropTargetPtr);
    
        if (dropTarget != null)
        {
            dropTarget.DragEnter(pDataObj, grfKeyState, pt, ref pdwEffect);
        }
    }
    
    return ShellAPI.S_OK;
}

When the DragEnter method has been called, the DragOver method will be called many times while the dragged item is over your control. This way you can give specific information on where the dragged item can be dropped and where it can't be. We can once again let the Shell do all the dirty work, just like with the DragEnter method.

Now there are two methods left, either the drag operation on your control will be canceled, or it will succeed and the item is dropped on your control. For when the operation is cancelled there is the DragLeave method. There is no additional info given, just a notice that the drag has ended on your control. Nothing much has to be done now, except for preparing your class to receive another drag operation.

If the drop succeeds the DragDrop method will be called providing you with pretty much the same info as the DragEnter and DragOver methods. The only difference being that some action has to be taken. Once again this action will be performed by the Windows Shell. When we call the DragDrop method from the IDropTarget which we retrieved earlier, the Shell does all the work for us. All the same notifications and process windows are shown like when you are using Windows Explorer.

Well that's pretty much it for the drop operations. In my classes a bit more has been done to give it all a nice look. This includes selecting the node over which you are dragging an object, and showing the nice ghost image from the content you are dragging which Windows Explorer shows. But these things aren't really necessary to get it all working.

Dragging items from your control

Once the drop part is out of the way, it's time to implement the drag operations. This is the part where it's getting a bit different from the VB explorer. In Jim Parsell's explorer, he uses a special class to create IDataObjects for the items being dragged. He's doing it the .NET way by making use of .NET's IDataObject interface, but actually you do not really need to do anything with the IDataObject interface other than passing it on. That is, if you are doing it the Shell way, which is in my opinion a lot easier than the .Net way.

Because we are going to drag items using API methods, we are going to need an IDropSource interface. This interface will take care of any drawing or canceling while dragging your item. The BrowserTVDragWrapper and BrowserLVDragWrapper classes implement this interface, and they will make sure dragging will be supported.

The first thing we need is to get a notification when an item is being dragged. Both the TreeView and ListView have an event for this (ItemDrag), so we just register to it. Once a drag has been initialized we need to make a call to an API method, to register the wrapper as an IDropSource and to trigger the drag. The method to call is ShellAPI.DoDragDrop, it has two input arguments and one output. The two input arguments are the IDataObject from the item being dragged and an instance of the DragDropEffects enumeration telling the method which drag/drop effects are allowed. The output argument is also an instance of the DragDropEffects enumeration specifying which effect has been executed.

The DragDropEffects are easy to provide, but the IDataObject needs a bit more work. Fortunately we've already seen the procedure to get this interface twice. We can once again use the GetUIObjectOf method (this method is really very useful). Notice that when you are dragging multiple items the ItemDrag event will only be raised once, so you'll have to check which items are selected to get the right IDataObject.

C#
public ShellDll.IDataObject GetIDataObject(ShellItem[] items, 
                            out IntPtr dataObjectPtr)
{
    ShellItem parent = 
        items[0].ParentItem != null ? items[0].ParentItem : items[0];

    IntPtr[] pidls = new IntPtr[items.Length];
    for (int i = 0; i < items.Length; i++)
        pidls[i] = items[i].PIDLRel.Ptr;

    if (parent.ShellFolder.GetUIObjectOf(
            IntPtr.Zero,
            (uint)pidls.Length,
            pidls,
            ref ShellAPI.IID_IDataObject,
            IntPtr.Zero,
            out dataObjectPtr) == ShellAPI.S_OK)
    {
        ShellDll.IDataObject dataObj =
            (ShellDll.IDataObject)
                Marshal.GetTypedObjectForIUnknown(
                    dataObjectPtr, typeof(ShellDll.IDataObject));

        return dataObj;
    }
    else
    {
        dataObjectPtr = IntPtr.Zero;
        return null;
    }
}

Once the drag has been initialized, your IDropSource interface will receive two messages concerning the drag. The first one is QueryContinueDrag, which asks what to do with a certain situation, either perform the drop, cancel it, or continue dragging. You get some information to determine what to do. You'll get a bool whether the escape key has been pressed, if so the operation has to be cancelled. You also get the state of the modifier keys and the mouse buttons. This is where you have to check whether to continue the drag or perform the drop. If the mouse button which initialized the drag is still pressed, continue the drag, otherwise perform the drop. Then this is what the method is going to look like.

C#
public int QueryContinueDrag(bool fEscapePressed, ShellAPI.MK grfKeyState)
{
    if (fEscapePressed)
        return ShellAPI.DRAGDROP_S_CANCEL;
    else
    {
        if ((startButton & MouseButtons.Left) != 0 && 
            (grfKeyState & ShellAPI.MK.LBUTTON) == 0)
            return ShellAPI.DRAGDROP_S_DROP;
        else if ((startButton & MouseButtons.Right) != 0 && 
                 (grfKeyState & ShellAPI.MK.RBUTTON) == 0)
            return ShellAPI.DRAGDROP_S_DROP;
        else
            return ShellAPI.S_OK;
    }
}

The other message your interface will receive is GiveFeedback. You will get the current DragDropEffect which applies to the dragged object. This message will allow you to change the Cursor to match this specific DragDropEffect. Because the normal Cursors are the ones we need, this method will only have one line in it. The Shell provides us with an option to just use the standard Cursors for drag/drop operations, which is exactly what we want. So the method will look like this.

C#
public int GiveFeedback(DragDropEffects dwEffect)
{
    return ShellAPI.DRAGDROP_S_USEDEFAULTCURSORS;
}

Well, we're done. That was all you have to do for the drag operations. I haven't said anything about the browsing part of my control just because it would take too much time and make this article too long. If anyone really wants to know, I might write another article about this.

Plug-ins

What kind of plug-ins

With the 1.3 update I added the option to add plug-ins to the program. These plug-ins are to gain extra information about files and folders. At this moment I have two different plug-ins and one is in the making. The first of the two is a plug-ins is a plug-in to retrieve extra columns for the details view of the ListView. Without plug-ins you only have the "Name" column which is just to little for a details view. With the demo project I have added a plug-in of this kind which addes the "size", "date created" and "date modified" columns. The second plug-in is a bit more advanced, it is a special view for the ListView. In the demo project I have added a demo plug-in of this kind which will add the "Image View" to the ListViews view options. If you select this view, you'll get to see a preview of images once you select them. See the following picture to get a better idea of what I mean.

Sample image

How to make them

To make your own plug-ins, you'll have to make a project with a reference to the FileBrowser.dll. Once you've done this there are two interface for the plug-ins I mentioned. One is the IColumnPlugin, the other is the IViewPlugin. You have to make a public class which implement one or even both of these interfaces. After you've done that build your project as a Class Library which will create a DLL-file. Add this dll-file to a folder named "plugins" in the folder where you start your program and start your program. Now your plug-in should be loaded and you can use it. For a demo project see the plug-in demo project you can download above.

What do all the methods do

Before you can build your own plug-in however, you obviously need to know what every method of both interfaces do and when they are called. So I'll give a short explanation of what they do. Both interfaces implement the basic IBrowserPlugin interface which I will explain first.

  • IBrowserPlugin

    NameThe name of the plug-in.
    InfoA short description of the plug-in

    These properties aren't used at the moment, but I will use them later to list the loaded plug-ins and to allow the user to select which plug-in to use.

  • IColumnPlugin

    ColumnNamesAn array with the names of all the columns this plug-in provides
    GetAlignmentReturns the HorizontalAlignment of a specific column
    GetFolderInfoReturns the information for a specific column for a folder
    GetFileInfoReturns the information for a specific column for a file

    The GetFolderInfo and GetFileInfo methods are called when the current directory changes, they return the info which will be put in the columns for the plug-in. The plug-in will get two arguments when this method is called. Either an IDirInfoProvider if the item is a directory or IFileInfoProvider when the item is a file. These interface will provide a structure with info about the file or folder and for files it will also provide a Stream to that file. The second argument is the ShellItem of the specific item to provide the info for. With these two arguments the plug-in should retrieve the needed info and provide a string for the column. To get a better idea of the possibilities see the demo project.

  • IViewPlugin

    ViewNameThe name to show when selecting the ListView view options
    ViewControlThe Control which will be showed when the view is selected
    FolderSelectedWill be called when a folder is selected
    FileSelectedWill be called when a file is selected
    ResetWill be called when a new directory is opened

    The ViewControl can be about anything you like, so you can make quite a variety of view plug-ins. Just make sure the Control is initialized when the plug-ins constructor is called or you'll get cross-thread problems. The FolderSelected and FileSelected methods will be called when an item is selected and have the same arguments as the GetFolderInfo and GetFileInfo methods. In my demo plug-in I use the Stream I got from the IFileInfoProvider to read a picture and show it on the Control.

If you need any more info on the plug-ins please post a message below and I will try to answer as soon as possible. In the next update I hope to include a third plug-in with which you can add commands to the ContextMenu. For now you can experiment with these two.

Credits

While I was writing my program I used a lot of sources on the Internet, because there is just so much written for this subject, so I can't give you all the sites which contributed to my work, but I'll give you the main articles which are the ones I couldn't have done without.

Improvements

There is definitely room for improvement in my program. The first thing I need to do is add much more comment to my code, because it almost hasn't got any, after that a few main things need to be polished:

  • Make a nice drag image when dragging from my control
  • Improve speed when browsing folders with many objects
  • Add more menu items to the standard ContextMenu of the ListView
  • Make a undo and redo option
  • And probably a lot more which I can't think of right now, any other ideas from readers would obviously be welcome as well

History

08/23/2006: V1.3.3

  • Added entry-point to SHNotifyRegister and SHNotifyDeregister in order to prevent any problems when calling them

08/22/2006: V1.3.2

  • Updated PIDL Class, Added IL-functions
  • Added demo project with only a TreeView to browse folders

08/21/2006: V1.3

  • Added plug-in feature to the Browser
  • Updated the demo project with a dual-pane browser
  • Added demo project for plug-ins
  • Added Back and Forward buttons to the navigation bar
  • Added new properties and public methods to the Browser
  • Added Tooltips for ListViewItems (only files for now)
  • Added shortcut for creating a new folder (Ctrl + N)
  • Bug fix for navigation bar when entering an address
  • Bug fix for drop operation
  • Created ShellHelper class with methods which are used often
  • Improved Browser startup
  • Other small bug fixes
  • Updated article with "Using the Control" and "Plug-ins" sections

08/14/2006: V1.2

  • Added "New" menu to the ListView's ContextMenu
  • Improvement in the update thread
  • Small bug fixes

08/11/2006: V1.1

  • Memory leak fixed in the update thread
  • Added ShellNotify functions
  • Large speed improvement when browsing folders with many items
  • Added dialogs when media is not inserted
  • Navigationbar bug fixes
  • Added rename function
  • Rewritten PIDL class to use Shell32 imported methods
  • Added "Paste" and "Paste Shortcut" menus to ListView ContextMenu
  • Added "Name" column to details view
  • Other small bug fixes

08/05/2006: V1.0

  • Initial version of this article
  • Initial version of the program

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Netherlands Netherlands
I'm a student in Amsterdam (The Netherlands). I study Artificial Intelligence at the University of Amsterdam and I'm very fond of programming.

I discovered .Net programming a few years ago and immediately liked the Visual Studio environment. Since then I experimented a lot with .Net.

Comments and Discussions

 
AnswerRe: Implementing drag&drop with ShellDll Pin
Steven Roebert6-Sep-06 21:33
Steven Roebert6-Sep-06 21:33 
GeneralRe: Implementing drag&drop with ShellDll Pin
Lichtalberich7-Sep-06 23:06
Lichtalberich7-Sep-06 23:06 
GeneralRe: Implementing drag&drop with ShellDll Pin
Lichtalberich11-Sep-06 6:15
Lichtalberich11-Sep-06 6:15 
GeneralRe: Implementing drag&drop with ShellDll Pin
Steven Roebert11-Sep-06 7:14
Steven Roebert11-Sep-06 7:14 
GeneralRe: Implementing drag&drop with ShellDll Pin
Lichtalberich11-Sep-06 10:05
Lichtalberich11-Sep-06 10:05 
Generalerror with network file system Pin
adamsmin4-Sep-06 17:03
adamsmin4-Sep-06 17:03 
GeneralRe: error with network file system Pin
Steven Roebert5-Sep-06 6:40
Steven Roebert5-Sep-06 6:40 
GeneralRe: error with network file system Pin
adamsmin5-Sep-06 16:44
adamsmin5-Sep-06 16:44 
I remember there is a ExplorerTree.cs in project UtilityLibrary. It's show more fast.
I found it in my computer. Maybe it's helpful

ExplorerTree.cs
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Text;

using UtilityLibrary.Win32;
using UtilityLibrary.General;

namespace UtilityLibrary.WinControls
{
#region Delegates
public delegate void ExplorerNodeChangedHandler(ExplorerTree et, TreeNodeData tnd);
#endregion

#region Enumerations
[Flags]
public enum TreeNodeFlags
{
ShowFiles = 0x00001,
ShowHiddenFiles = 0x00003,
ShowName = 0x00004,
ShowPath = 0x00008
}
#endregion

#region Helper Classes
public class ShellIDList : ShellHandle
{
#region Class Variables
IntPtr hWnd = IntPtr.Zero;
#endregion

#region Constructors
public ShellIDList(IntPtr idList, IntPtr hWnd): base(idList)
{
Debug.Assert(Handle != IntPtr.Zero);
this.hWnd = hWnd;
}

public ShellIDList(IntPtr idList) : base(idList)
{
Debug.Assert(idList != IntPtr.Zero);
}
#endregion

#region Overrides
#endregion

#region Operators
public static implicit operator IntPtr(ShellIDList shellIDList)
{
return shellIDList.Handle;
}
#endregion

#region Properties
public int Length
{
get
{
// We have a valid handle find its length
int count = 0;
int length = 0;

// Iterate through the list to find every item length
// and add it to the count
int pBase = (int)Handle;
do
{
// Make sure we are not trying to ready zero out memory
if ( !IsValidPointer((IntPtr)pBase))
break;

ITEMIDLIST idl = (ITEMIDLIST)Marshal.PtrToStructure((IntPtr)pBase,
typeof(ITEMIDLIST));
count = idl.mkid.cb;
pBase = pBase + count;
length += count;
}
while ( count != 0 );

return length;
}
}

#endregion

#region Methods
public string GetPath()
{
StringBuilder path = new StringBuilder(1024);
int result = WindowsAPI.SHGetPathFromIDList(Handle, path);
if ( result == 0 )
return string.Empty;
return path.ToString();
}

public IntPtr GetFirstChild()
{
return GetNextChild(this.Handle);
}

public IntPtr GetLastChild()
{
return GetLastChild(this.Handle);
}

public int GetIconIndex(ShellFileInfoFlags flags)
{
SHFILEINFO shfi = new SHFILEINFO();

// Get small icon index
ShellFileInfoFlags fileFlags = ShellFileInfoFlags.SHGFI_SYSICONINDEX | ShellFileInfoFlags.SHGFI_PIDL;
fileFlags |= flags;
WindowsAPI.SHGetFileInfo(Handle, 0, out shfi, Marshal.SizeOf(typeof(SHFILEINFO)), fileFlags);
return shfi.iIcon;
}

public int GetCount()
{
// Make sure we are not trying to ready zero out memory
int pBase = (int)Handle;
if ( !IsValidPointer((IntPtr)pBase))
return 0;

// Bring the pointer into a managed type
ITEMIDLIST idList = (ITEMIDLIST)Marshal.PtrToStructure(Handle, typeof(ITEMIDLIST));
return idList.mkid.cb;
}
static public IntPtr GetNextChild(IntPtr parent)
{
// Make sure we are not trying to ready zero out memory
if ( !IsValidPointer((IntPtr)parent))
return IntPtr.Zero;

// Bring the pointer into a managed type
ITEMIDLIST idList = (ITEMIDLIST)Marshal.PtrToStructure(parent, typeof(ITEMIDLIST));
// Get the size of the idList
int cb = idList.mkid.cb;

// If the size is zero, it is the end of the list.
if ( cb == 0)
return IntPtr.Zero;

// Add cb to original list to obtain the next child list
IntPtr pBase = parent;
IntPtr nextList = (IntPtr)((int)pBase + cb);

// Make sure we are not trying to ready zero out memory
if ( IsValidPointer(nextList) )
{
// Bring the pointer into a managed type
idList = (ITEMIDLIST)Marshal.PtrToStructure(nextList, typeof(ITEMIDLIST));
return nextList;
}
else
{
// Return IntPtr.Zero if we reached the terminator, or a pidl otherwise.
return IntPtr.Zero;
}
}

static public IntPtr GetLastChild(IntPtr parent)
{
IntPtr current = GetNextChild(parent);
IntPtr last = IntPtr.Zero;
while ( current != IntPtr.Zero)
{
last = current;
current = GetNextChild(current);
}

return last;
}
static public ShellIDList Combine(IMalloc iMalloc, ShellIDList idList1, ShellIDList idList2)
{
// Combine PUIDL to form a new one
IntPtr list1 = IntPtr.Zero;
if ( idList1 != null )
list1 = idList1.Handle;
IntPtr list2 = IntPtr.Zero;
if ( idList2 != null )
list2 = idList2.Handle;

// Get PUIDL lengths
int length1 = 0;
if ( idList1 != null )
{
length1 = idList1.Length;
}
int length2 = 0;
if ( idList2 != null )
{
length2 = idList2.Length;
}

// Allocate memory for the combined handle
IntPtr newHandle = IntPtr.Zero;
newHandle = (IntPtr)iMalloc.Alloc(length1 + length2 + Marshal.SizeOf(typeof(ushort)));
IntPtr pBase = newHandle;

// If memory was successfully allocated
if ( newHandle != IntPtr.Zero )
{
if ( length1 > 0 )
{
for ( int i = 0; i < length1; i++ )
{
byte currentByte = Marshal.ReadByte(list1, i);
Marshal.WriteByte(pBase, currentByte);
pBase = (IntPtr)((int)pBase + 1);
}
}

if ( length2 > 0 )
{
for ( int i = 0; i < length2; i++ )
{
byte currentByte = Marshal.ReadByte(list2, i);
Marshal.WriteByte(pBase, currentByte);
pBase = (IntPtr)((int)pBase + 1);
}
}

// Append terminating zero
Marshal.WriteInt16(pBase, 0);
return new ShellIDList(newHandle);
}

return null;
}

public bool IsRootList()
{
return Length == 0;
}

static bool IsValidPointer(IntPtr pBase)
{
// Make sure we are not trying to read into null memory
if ( pBase == IntPtr.Zero )
return false;

short readValue = Marshal.ReadInt16(pBase);
return readValue != 0;
}
#endregion

#region Implementation
#endregion

}

public class ShellIFolder : COMInterface
{
#region Class Variables
IShellFolder iShellFolder = null;
#endregion

#region Constructors
public ShellIFolder(IShellFolder iShellFolder): base((IUnknown)iShellFolder)
{
this.iShellFolder = iShellFolder;
}

public ShellIFolder(ShellIFolder shellIFolder) : base((IUnknown)shellIFolder.Interface)
{
IShellFolder iShellFolder = shellIFolder.Interface;
this.iShellFolder = iShellFolder;
}

public static ShellIFolder CreateShellIFolder(ShellIFolder shellIFolder, ShellIDList idList)
{
IShellFolder iShellFolder = shellIFolder.BindToObject(idList);
Debug.Assert(iShellFolder != null);
return new ShellIFolder(iShellFolder);
}

~ShellIFolder()
{
Dispose(false);
}
#endregion

#region Properties
public IShellFolder Interface
{
get { return iShellFolder; }
}
#endregion

#region Methods
public string GetDisplayNameOf(IntPtr idList, ShellGetDisplayNameOfFlags flags)
{
// Use the native handle this class is wrapping to obtain the Display Name of the node
STRRET strRet = new STRRET();

iShellFolder.GetDisplayNameOf(idList, flags, ref strRet);
if ( strRet.uType == STRRETFlags.STRRET_WSTR )
{
// Get the OLE string into a managed string
int pointer = (((int)strRet.cStr[3] & 0x000000FF) << 24);
pointer |= (((int)strRet.cStr[2] & 0x000000FF) << 16);
pointer |= (((int)strRet.cStr[1] & 0x000000FF) << 8);
pointer |= ((int)strRet.cStr[0] & 0x000000FF);
IntPtr pOLEString = (IntPtr)pointer;
string displayName = Marshal.PtrToStringAuto(pOLEString);

// Release memory
WindowsAPI.SHFreeMalloc(pOLEString);
return displayName;
}
else if ( strRet.uType == STRRETFlags.STRRET_CSTR )
{
return GetFolderText(strRet.cStr);
}

return string.Empty;
}

public void GetAttributesOf(int count, ref IntPtr idList, out GetAttributeOfFlags attributes)
{
iShellFolder.GetAttributesOf(count, ref idList, out attributes);
}

public IShellFolder BindToObject(IntPtr idList)
{
IShellFolder iFolder = null;
REFIID refiid = new REFIID("000214E6-0000-0000-c000-000000000046");
int result = iShellFolder.BindToObject(idList, IntPtr.Zero, ref refiid, ref iFolder);
Debug.Assert(iFolder !=null);
return iFolder;
}

public IEnumIDList EnumObjects(IntPtr hWnd, ShellEnumFlags flags)
{
IEnumIDList iEnumIDList = null;
iShellFolder.EnumObjects(hWnd, flags, ref iEnumIDList);
// iEnumIDList could be null if we are enumerating a removable media drive
return iEnumIDList;
}

public int CompareIDs(int lparam, ShellIDList shellIDList1, ShellIDList shellIDList2)
{
IntPtr list1 = shellIDList1.GetLastChild();
IntPtr list2 = shellIDList2.GetLastChild();
int result = iShellFolder.CompareIDs(lparam, list1, list2);

if ( WindowsAPI.FAILED(result) )
return 0;

short ret = WindowsAPI.HRESULT_CODE(result);
if ( ret < 0 ) return -1;
else if ( ret > 0) return 1;

return 0;
}

public int GetUIObjectOf(IntPtr hWnd, int count, ref IntPtr idList, REFIID riid, ref IUnknown iUnknown)
{
int arrayInOut = 0;
return iShellFolder.GetUIObjectOf(hWnd, count, ref idList, ref riid, out arrayInOut, ref iUnknown);
}

#endregion

#region Implementation
string GetFolderText(byte[] buffer)
{
int i = 0;
string builder = string.Empty;
while ( buffer[i] != '\0' )
{
builder += Convert.ToChar(buffer[i++]);
}
return builder;
}
#endregion

}

public class ShellIEnumIDList : COMInterface
{
#region Class Variables
IEnumIDList iEnumIDList = null;
#endregion

#region Constructors

public ShellIEnumIDList(IEnumIDList iEnumIDList) : base((IUnknown)iEnumIDList)
{
this.iEnumIDList = iEnumIDList;
}

public static ShellIEnumIDList CreateShellIEnumIDList(IntPtr hWnd, ShellIFolder shellIFolder, ShellEnumFlags flags)
{
// Interface could be null if we are trying to
// enumerate a removable media
IEnumIDList iEnumIDList = shellIFolder.EnumObjects(hWnd, flags);
// Don't let the object to succed if we could get the interface
if ( iEnumIDList == null )
throw new Exception("Failed to obtain IEnumIDList interface.");
return new ShellIEnumIDList(iEnumIDList);
}
#endregion

#region Properties
public IEnumIDList Interface
{
get { return iEnumIDList; }
}
#endregion

#region Methods
public int Next(int count, ref IntPtr idListPtr, out int fetched)
{
return iEnumIDList.Next(count, ref idListPtr, out fetched);

}
#endregion

}

[ToolboxItem(false)]
public class ShellPopupMenu : ContextMenu
{
#region Class Variables
IContextMenu iContextMenu = null;
const int FIRST_COMMAND = 20000;
const int LAST_COMMAND = FIRST_COMMAND + 1000;
int lastCommand = 0;
IntPtr hOwner = IntPtr.Zero;
IntPtr hOldWinProc = IntPtr.Zero;
#endregion

#region Constructors
public ShellPopupMenu()
{

}
public void Initialize(ShellIFolder folder, ShellIDList idList)
{
// Dispose of any resources we have before
Dispose();

// Clear any previous menu items
MenuItems.Clear();

IntPtr childIDList = idList.GetLastChild();
// If there is not child, use the parent
if ( childIDList == IntPtr.Zero )
{
childIDList = idList.Handle;
Debug.Assert(childIDList!=IntPtr.Zero);
}
REFIID riid = new REFIID("000214e4-0000-0000-c000-000000000046");
IUnknown iUnknown = null;
folder.GetUIObjectOf(IntPtr.Zero, 1, ref childIDList, riid, ref iUnknown);
Debug.Assert(iUnknown!=null);
iContextMenu = (IContextMenu)iUnknown;

// Get the menu items of the context menu
QueryContextMenu();
}

~ShellPopupMenu()
{
Dispose(false);;
}

#endregion

#region Overrides
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
// Relese previous reference to interface
Release();
}
#endregion

#region Properties
#endregion

#region Methods

public void InvokeCommand(int commandID)
{

if ( iContextMenu != null )
{
CMINVOKECOMMANDINFO ici = new CMINVOKECOMMANDINFO();
ici.cbSize = Marshal.SizeOf(typeof(CMINVOKECOMMANDINFO));
ici.hwnd = hOwner;
ici.nShow = (int)ShowWindowStyles.SW_SHOWNORMAL;
ici.lpVerb = (IntPtr)WindowsAPI.MAKEINTRESOURCE(commandID - FIRST_COMMAND);
WinErrors error = (WinErrors)iContextMenu.InvokeCommand(ref ici);
}
}

public new void Dispose()
{
// Let the Garbage Collector know that it does
// not need to call finalize for this class
GC.SuppressFinalize(this);

// Do the disposing
Dispose(true);
}
#endregion

#region Implementation
void QueryContextMenu()
{
Debug.Assert(iContextMenu!=null);
QueryContextMenuFlags flags = QueryContextMenuFlags.CMF_NODEFAULT| QueryContextMenuFlags.CMF_EXPLORE;
int hr = iContextMenu.QueryContextMenu(Handle, 0, FIRST_COMMAND, LAST_COMMAND, flags);
Debug.Assert(WindowsAPI.SUCCEEDED(hr));

// Get last command index that the shell assigned us
lastCommand = FIRST_COMMAND + WindowsAPI.HRESULT_CODE(hr) -1;

}

void Release()
{
// Release interface
if ( iContextMenu!=null )
{
IUnknown iUnknown = (IUnknown)iContextMenu;
Debug.Assert(iUnknown!=null);
iUnknown.Release();
iContextMenu = null;
}
}

#endregion

}

public class TreeNodeData
{
#region Class Variables
ShellIDList idList = null;
ShellIFolder folder = null;
TreeNodeFlags flags = TreeNodeFlags.ShowName;
IntPtr handle = IntPtr.Zero;
bool expanded = false;
#endregion

#region Constructors
#endregion

#region Properties
public ShellIDList IDList
{
set { idList = value; }
get { return idList; }
}

public ShellIFolder Folder
{
set { folder = value; }
get { return folder; }
}

public TreeNodeFlags Flags
{
set { flags = value; }
get { return flags; }
}

public IntPtr Handle
{
set { handle = value; }
get { return handle; }
}

#endregion

#region Methods
public bool IsValid()
{
return idList != null && folder != null;
}
#endregion

#region Implementation

// Properties
internal bool Expanded
{
set { expanded = value; }
get { return expanded; }
}


// Functions

#endregion

}
#endregion


///
/// Explorer like implementation of a Tree Control that show the resources in the Computer
///

[ToolboxItem(false)]
public class ExplorerTree : UtilityLibrary.WinControls.TreeViewEx
{
#region Class Variables

// Property backers
IntPtr hSystemImageList = IntPtr.Zero;
bool optimizeMemoryUsage = false;
TreeNodeFlags treeNodeFlags = TreeNodeFlags.ShowName;

// Keep an instance of the shell memory allocator since we are going
// to use it a lot
IMalloc iMalloc = null;

// Other helpers
ShellIFolder desktopFolder = null;
Hashtable hashTable = new Hashtable();
TreeNode rootTreeNode = null;
ShellPopupMenu contextMenu = new ShellPopupMenu();

#endregion

#region Constructors
public ExplorerTree()
{
Initialize();
}

public ExplorerTree(bool optimizeMemoryUsage)
{
Initialize();
this.optimizeMemoryUsage = optimizeMemoryUsage;
}

void Initialize()
{
// Obtain IShellFolder interface for shell root folder
IShellFolder folder;
WindowsAPI.SHGetDesktopFolder(out folder);
Debug.Assert(folder != null);
desktopFolder = new ShellIFolder(folder);
// Get a pointer to the Shell Memory allocator
WindowsAPI.SHGetMalloc(out iMalloc);
Debug.Assert(iMalloc!=null);
}

void InitializeImageList()
{
// Get System Image List
SHFILEINFO shfi = new SHFILEINFO();

// Initialie idList by obtaining it from the shell
IntPtr idHandle = IntPtr.Zero;
WindowsAPI.SHGetSpecialFolderLocation(Handle, ShellSpecialFolder.CSIDL_DESKTOP, out idHandle);

ShellIDList idList = new ShellIDList(idHandle, Handle);
hSystemImageList = WindowsAPI.SHGetFileInfo(idList, 0, out shfi, Marshal.SizeOf(typeof(SHFILEINFO)),
ShellFileInfoFlags.SHGFI_SYSICONINDEX | ShellFileInfoFlags.SHGFI_SMALLICON );
Debug.Assert(hSystemImageList != IntPtr.Zero);

// Don't let the Garbage collector to dispose of these resource
// when calling the Finalize methods. The garbage collector runs on a different
// thread and calling the SHGetMalloc does seem to like this
idList.Dispose();

}

~ExplorerTree()
{
// Make sure we relese the shell memory allocator
if ( iMalloc!=null )
{
IUnknown iUnknown = (IUnknown)iMalloc;
iUnknown.Release();
}

}
#endregion

#region Overrides
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
// Tree control has been created
InitializeImageList();

// Associate the System image list with the tree control
WindowsAPI.SendMessage(Handle, (int)TreeViewMessages.TVM_SETIMAGELIST,
(int)TreeViewImageListFlags.TVSIL_NORMAL, (int)hSystemImageList);

}

protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
// Check for right click
if (e.Button == MouseButtons.Right)
{
OnContextMenu(e);
}
}

protected override void OnTreeViewItemExpanding(ref Message m)
{
Cursor = Cursors.WaitCursor;
NM_TREEVIEW nmtv = (NM_TREEVIEW) m.GetLParam(typeof(NM_TREEVIEW));
IntPtr currentHandle = nmtv.itemNew.hItem;

// Get corresponding TreeNode object
TreeNode tn = (TreeNode)hashTable[currentHandle];

// Process any valid node except the root node
if ( tn != null && tn.Parent != null )
{
TreeNodeData tnd = (TreeNodeData)tn.Tag;
if ( optimizeMemoryUsage || tnd.Expanded == false || IsRemovableMedia(tnd.IDList) )
{
Debug.Assert(tnd!=null);
if ( nmtv.action == (int)TreeViewItemExpansion.TVE_EXPAND )
{
EnumerateChildrenNodes(tn, tnd.IDList, tnd.Flags);
tnd.Expanded = true;
}
}
}

// So that expansion takes place
m.Result = IntPtr.Zero;
Cursor = Cursors.Default;
}

protected override void OnTreeViewItemExpanded(ref Message m)
{
Cursor = Cursors.WaitCursor;
NM_TREEVIEW nmtv = (NM_TREEVIEW) m.GetLParam(typeof(NM_TREEVIEW));
IntPtr currentHandle = nmtv.itemNew.hItem;

// Get corresponding TreeNode object
TreeNode tn = (TreeNode)hashTable[currentHandle];
TreeNodeData tnd = null;
if ( tn != null )
{
tnd = (TreeNodeData)tn.Tag;
Debug.Assert(tnd!=null);
}

// Process any valid node except the root node
if ( (tn != null && tn.Parent != null && optimizeMemoryUsage) || IsRemovableMedia(tnd.IDList) )
{
if ( nmtv.action == (int)TreeViewItemExpansion.TVE_COLLAPSE )
{
DeleteChildren(tn);
}
}

// So that expansion takes place
m.Result = IntPtr.Zero;
Cursor = Cursors.Default;

}
protected override void OnTreeViewSelectionChanging(ref Message m)
{
// Fire event that contains the TreeNodeData class
NM_TREEVIEW nmtv = (NM_TREEVIEW) m.GetLParam(typeof(NM_TREEVIEW));
IntPtr currentHandle = nmtv.itemNew.hItem;

// Get associated TreeNodeData object
TreeNode treeNode = (TreeNode)hashTable[currentHandle];
// We should always get a valid object
TreeNodeData tnd = (TreeNodeData)treeNode.Tag;
Debug.Assert(tnd!=null);

// Fire event containing a copy of the object
FireExplorerNodeChanged(tnd);

}

protected override void OnBeforeExpand(TreeViewCancelEventArgs e)
{
// Set paintBackground flag to true
paintBackground = true;
base.OnBeforeExpand(e);
}

protected override void OnAfterExpand(TreeViewEventArgs e)
{

base.OnAfterExpand(e);
// Reset flag
paintBackground = false;
}

protected override void OnBeforeCollapse(TreeViewCancelEventArgs e)
{
// Set paintBackground flag to true
paintBackground = true;
base.OnBeforeCollapse(e);
}

protected override void OnAfterCollapse(TreeViewEventArgs e)
{

base.OnAfterCollapse(e);
// Reset flag
paintBackground = false;
}

protected override void WndProc(ref Message message)
{

base.WndProc(ref message);

switch (message.Msg)
{
case (int)Msg.WM_COMMAND:
InvokeCommand(message.WParam.ToInt32());
break;
case (int)Msg.WM_DESTROY:
OnDestroyTree();
break;
default:
break;
}
}
#endregion

#region Properties
public bool OptimizeMemoryUsage
{
get { return optimizeMemoryUsage; }
}

public IntPtr SystemImageList
{
get { return hSystemImageList; }
}

public TreeNodeFlags TreeNodeFlags
{
set { treeNodeFlags = value; }
get { return treeNodeFlags; }
}
#endregion

#region Methods
public void InsertRootFolder(bool expand)
{
// If there is already a tree node delete it
RemoveRootFolder();

// Get Desktop IDList
IntPtr idHandle = IntPtr.Zero;
WindowsAPI.SHGetSpecialFolderLocation(Handle, ShellSpecialFolder.CSIDL_DESKTOP, out idHandle);
ShellIDList idList = new ShellIDList(idHandle, Handle);

// Insert Root Node
TreeNode tn = InsertNode(null, desktopFolder, null, idList, treeNodeFlags);

// Enumerate Root Node children
if ( expand )
{
// Insert children
EnumerateChildrenNodes(tn, idList, treeNodeFlags);
// Expand the tree
tn.Expand();
}

// Release in this thread not the Garbage collector thread
idList.Dispose();

// Save the root node for later use
rootTreeNode = tn;

}
public void RemoveRootFolder()
{
// Remove the root
if ( rootTreeNode != null )
{
DeleteNode(rootTreeNode);
rootTreeNode = null;
}
}
#endregion

#region Events
public event ExplorerNodeChangedHandler ExplorerNodeChanged;
#endregion

#region Implementation
TreeNode InsertNode( TreeNode parentNode, ShellIFolder parentFolder,
ShellIDList parentList, ShellIDList idList, TreeNodeFlags flags)
{
// Tree Node item to insert
TVITEM item = new TVITEM();

// Prepare helper object for this node
TreeNodeData tnd = new TreeNodeData();
tnd.Folder = parentFolder;
tnd.Flags = flags;
Debug.Assert(iMalloc!=null);
tnd.IDList = ShellIDList.Combine(iMalloc, parentList, idList);
Debug.Assert(tnd.IsValid());

// Setup which fields we are going to use
TreeViewItemFlags itemFlags = TreeViewItemFlags.TVIF_TEXT | TreeViewItemFlags.TVIF_IMAGE
| TreeViewItemFlags.TVIF_SELECTEDIMAGE | TreeViewItemFlags.TVIF_CHILDREN | TreeViewItemFlags.TVIF_PARAM;
item.mask = (int)(itemFlags);
string nodeText = string.Empty;
SetupTreeViewItem(ref item, tnd, out nodeText);

// Insert item using the Nodes collection
TreeNode treeNode = null;
if ( parentNode == null )
{
// ROOT node
treeNode = Nodes.Add(nodeText);
}
else
{
// This is a child node, add it to the parent
treeNode = parentNode.Nodes.Add(nodeText);
}

IntPtr handle = treeNode.Handle;
tnd.Handle = handle;
// Save item data in the Tag bucket
treeNode.Tag = tnd;

// Set some flags for the node we are inserting
item.hItem = handle;
// We are going to use the lParam when sorting
item.lParam = handle;
SetItem(ref item);

// Release previously allocated memory if necessary
if ( item.pszText != IntPtr.Zero )
Marshal.FreeHGlobal(item.pszText);

// Add it to the hash table for easy mapping and retrieval
hashTable.Add(handle, treeNode);

return treeNode;
}

void SetupTreeViewItem(ref TVITEM item, TreeNodeData tnd, out string path)
{
// Initialize out parameter
path = string.Empty;

// Get TreeNodeData
Debug.Assert(tnd!=null && tnd.IsValid());
// Relative pIDList
IntPtr idRel = tnd.IDList.GetLastChild();
if ( idRel == IntPtr.Zero )
{
// If the parent ID List has not children
// set the relative ID List to the parent list
idRel = tnd.IDList.Handle;
}

// If we need to show the node name
if ( (item.mask & (uint)TreeViewItemFlags.TVIF_TEXT) != 0)
{
if ( (tnd.Flags & TreeNodeFlags.ShowPath) != 0 )
{
// Get full path
path = tnd.IDList.GetPath();
if ( path != string.Empty && (tnd.Flags & TreeNodeFlags.ShowName) != 0 )
{
// Get just the name of the node
int slashIndex = path.LastIndexOf('\\');
path = path.Substring(slashIndex+1);
}
}

// If we could not get a path, try getting a global name
if ( path == string.Empty )
{
ShellGetDisplayNameOfFlags flags = ShellGetDisplayNameOfFlags.SHGDN_INFOLDER;
if ( (tnd.Flags & TreeNodeFlags.ShowName) != 0 )
flags = ShellGetDisplayNameOfFlags.SHGDN_NORMAL;
flags |= ShellGetDisplayNameOfFlags.SHGDN_INCLUDE_NONFILESYS;
path = tnd.Folder.GetDisplayNameOf(idRel, flags);
}

// Set the Node text
if ( path != string.Empty )
item.pszText = Marshal.StringToHGlobalAuto(path);
}

// If we need to show node image
if ( (item.mask & (uint)(TreeViewItemFlags.TVIF_IMAGE |
TreeViewItemFlags.TVIF_SELECTEDIMAGE)) != 0)
{
GetAttributeOfFlags attributes = GetAttributeOfFlags.SFGAO_FOLDER | GetAttributeOfFlags.SFGAO_LINK
| GetAttributeOfFlags.SFGAO_SHARE | GetAttributeOfFlags.SFGAO_GHOSTED;
tnd.Folder.GetAttributesOf(1, ref idRel, out attributes);

// set correct icon
if ( (attributes & GetAttributeOfFlags.SFGAO_GHOSTED) != 0)
{
item.mask |= (int)ListViewItemFlags.LVIF_STATE;
item.stateMask |= (int)ListViewItemState.LVIS_CUT;
item.state |= (int)ListViewItemState.LVIS_CUT;
}

if ( (attributes & GetAttributeOfFlags.SFGAO_SHARE) != 0 )
{
item.mask |= (int)ListViewItemFlags.LVIF_STATE;
item.state &= ~(int)ListViewItemState.LVIS_OVERLAYMASK;
item.state |= (int)WindowsAPI.INDEXTOOVERLAYMASK(1);
item.stateMask |= (int)ListViewItemState.LVIS_OVERLAYMASK;
}
else if ( (attributes & GetAttributeOfFlags.SFGAO_LINK) != 0)
{
item.mask |= (int)ListViewItemFlags.LVIF_STATE;
item.state &= ~(int)ListViewItemState.LVIS_OVERLAYMASK;
item.state |= (int)WindowsAPI.INDEXTOOVERLAYMASK(2);
item.stateMask |= (int)ListViewItemState.LVIS_OVERLAYMASK;
}

if ( (item.mask & (int)(TreeViewItemFlags.TVIF_IMAGE)) != 0 )
{
item.iImage = tnd.IDList.GetIconIndex(ShellFileInfoFlags.SHGFI_SMALLICON);
item.iSelectedImage = item.iImage;
}

if ( ((item.mask & (int)TreeViewItemFlags.TVIF_SELECTEDIMAGE) != 0)
&& ((attributes & GetAttributeOfFlags.SFGAO_FOLDER) != 0) )
{
item.iSelectedImage = tnd.IDList.GetIconIndex(ShellFileInfoFlags.SHGFI_SMALLICON
| ShellFileInfoFlags.SHGFI_OPENICON);
}
}

// If the node has children and is a folder
if ( (item.mask & (uint)TreeViewItemFlags.TVIF_CHILDREN) != 0 )
{
GetAttributeOfFlags attributes = GetAttributeOfFlags.SFGAO_FOLDER;
tnd.Folder.GetAttributesOf(1, ref idRel, out attributes);

// Get children
item.cChildren = 0;
if ( (attributes & GetAttributeOfFlags.SFGAO_FOLDER) != 0 )
{
if ( (tnd.Flags & TreeNodeFlags.ShowFiles) != 0 )
item.cChildren = 1;
else if ( (attributes & GetAttributeOfFlags.SFGAO_REMOVABLE) != 0 )
item.cChildren = 1;
else
{
attributes = GetAttributeOfFlags.SFGAO_HASSUBFOLDER;
tnd.Folder.GetAttributesOf(1, ref idRel, out attributes);
item.cChildren = ((attributes & GetAttributeOfFlags.SFGAO_HASSUBFOLDER) != 0) ? 1 : 0;
}
}
}
}

void SetItem(ref TVITEM item)
{
// Only Windows NT based systems support for the moment
WindowsAPI.SendMessage(Handle, TreeViewMessages.TVM_SETITEMW, 0, ref item);
}

void GetItem(ref TVITEM item)
{
// Only Windows NT based systems support for the moment
WindowsAPI.SendMessage(Handle, TreeViewMessages.TVM_GETITEMW, 0, ref item);
}

void EnumerateChildrenNodes(TreeNode parentNode, ShellIDList idListParent, TreeNodeFlags flags)
{
ShellIFolder shellFolder = null;
if ( idListParent.IsRootList() )
{
// Root object, just increase the reference count for the interface
shellFolder = new ShellIFolder(desktopFolder);
}
else
{
// Not a root item, get a new IShellFolder interface bound to the idList
shellFolder = ShellIFolder.CreateShellIFolder(desktopFolder, idListParent);
}

Debug.Assert(shellFolder!=null);

// Get enumeration interface
ShellEnumFlags enumFlags = ShellEnumFlags.SHCONTF_FOLDERS |
( ((flags & TreeNodeFlags.ShowFiles) != 0) ? ShellEnumFlags.SHCONTF_NONFOLDERS : 0) |
( ((flags & TreeNodeFlags.ShowHiddenFiles) != 0) ? ShellEnumFlags.SHCONTF_INCLUDEHIDDEN : 0);

ShellIEnumIDList shellEnumIDList = null;
try
{
shellEnumIDList = ShellIEnumIDList.CreateShellIEnumIDList(Handle, shellFolder, enumFlags);
}
catch(Exception e)
{
// We are going to get an exception if we tried to enumerate a removable
// media and the user did not insert a CD or other valid media
Debug.WriteLine(e.Message);
return;
}

// Do the insertion
IntPtr idListPtr = IntPtr.Zero;
int fetched = 0;
while ( (int)WinErrors.NOERROR == shellEnumIDList.Next(1, ref idListPtr, out fetched) )
{
ShellIDList idList = new ShellIDList(idListPtr);
InsertNode(parentNode, shellFolder, idListParent, idList, flags);
idList.Dispose();
}

// Sort the recently inserted children
SortChildren(parentNode);

}
void DeleteChildren(TreeNode parentNode)
{
int count = parentNode.Nodes.Count;
while ( count != 0 )
{
// Remove children nodes
// Remove from hash table first
TreeNode childNode = (TreeNode)parentNode.Nodes[0];

// Actively release the shell Memory allocated here
// -- Letting the garbage collector do it from its thread
// causes an exception --
TreeNodeData tnd = (TreeNodeData)childNode.Tag;
Debug.Assert(tnd!=null);
tnd.IDList.Dispose();

// Recursively delete the children of
// the child node
DeleteChildren(childNode);
hashTable.Remove(childNode.Handle);

// Remove from the collection
parentNode.Nodes.RemoveAt(0);
count = parentNode.Nodes.Count;

}

ResetChildrenCountFlag(parentNode.Handle);
}

void DeleteNode(TreeNode treeNode)
{
bool hasChildren = false;
IntPtr handle = treeNode.Handle;
if ( treeNode.Nodes.Count != 0 )
{
hasChildren = true;
DeleteChildren(treeNode);
}
// Now delete this node from the collection
Nodes.Remove(treeNode);

// Actively release the shell Memory allocated here
// -- Letting the garbage collector do it from its thread
// causes an exception --
TreeNodeData tnd = (TreeNodeData)treeNode.Tag;
Debug.Assert(tnd!=null);
tnd.IDList.Dispose();
hashTable.Remove(handle);

if ( hasChildren )
ResetChildrenCountFlag(handle);

}


void ResetChildrenCountFlag(IntPtr hItem)
{
// Since we are wiping out all children
// we need to reset the style of the node
// so that it shows the plus sign
TVITEM item = new TVITEM();
item.hItem = hItem;
item.mask |= (int)(TreeViewItemFlags.TVIF_CHILDREN | TreeViewItemFlags.TVIF_HANDLE);
item.cChildren = 1;
SetItem(ref item);
}

void SortChildren(TreeNode parentNode)
{
TVSORTCB tvscb = new TVSORTCB();
tvscb.hParent = parentNode.Handle;
tvscb.lpfnCompare = new WindowsAPI.CompareFunc(OnCompare);

// Pin the delegate object so that it can be passed
// to unmanaged code without risk of the garbage collector free its memory
GCHandle compareHandle = GCHandle.Alloc(tvscb.lpfnCompare);

// Now do the sorting
WindowsAPI.SendMessage(Handle, TreeViewMessages.TVM_SORTCHILDRENCB, 0, ref tvscb);

// Free our handle
compareHandle.Free();

}

int OnCompare(IntPtr param1, IntPtr param2, IntPtr sortParam)
{
// Do the comparison
TreeNode tn1 = (TreeNode)hashTable[param1];
TreeNode tn2 = (TreeNode)hashTable[param2];
Debug.Assert(tn1!=null && tn2!=null);

TreeNodeData tnd1 = (TreeNodeData)tn1.Tag;
TreeNodeData tnd2 = (TreeNodeData)tn2.Tag;
Debug.Assert(tnd1!=null && tnd2!=null);

// Sort items by comparing using the parent
// folder to compare their IDs
ShellIFolder folder = tnd1.Folder;
Debug.Assert(folder!=null);

return folder.CompareIDs(0, tnd1.IDList, tnd2.IDList);
}

bool IsRemovableMedia(ShellIDList idList)
{
StringBuilder path = new StringBuilder(1024);
int result = WindowsAPI.SHGetPathFromIDList(idList, path);
if ( result != 0 )
{
string drivePath = path.ToString();
drivePath = drivePath.Substring(0,2);
DriveType driveType = (DriveType)WindowsAPI.GetDriveType(drivePath);
if ( driveType == DriveType.DRIVE_CDROM || driveType == DriveType.DRIVE_REMOVABLE )
return true;
}

return false;

}
void OnContextMenu(MouseEventArgs e)
{
// Display shell context menu for the item that was clicked
TreeViewHitTestFlags htFlags;
IntPtr hItem = HitTest(new Point(e.X, e.Y), out htFlags);

// If we did not click on an item, just return
if ( (htFlags & TreeViewHitTestFlags.TVHT_ONITEM) == 0 )
return;

// Should have a valid item
Debug.Assert(hItem!=IntPtr.Zero);
TreeNode tn = (TreeNode)hashTable[hItem];
Debug.Assert(tn!=null);
// Select this node
SelectedNode = tn;

// Get item shellInformation
TreeNodeData tnd = (TreeNodeData)tn.Tag;
Debug.Assert(tnd!=null);

// Get clicked item context menu
contextMenu.Initialize(tnd.Folder, tnd.IDList);
// Now display the menu
contextMenu.Show(this, new Point(e.X, e.Y));

}

void InvokeCommand(int commandID)
{
contextMenu.InvokeCommand(commandID);
}

void FireExplorerNodeChanged(TreeNodeData tnd)
{
if ( ExplorerNodeChanged != null )
{
ExplorerNodeChanged(this, tnd);
}
}
void OnDestroyTree()
{
// Delete all the nodes here so that
// we dispose of the memory allocated by IMalloc here
// in the main UI thread instead of letting the Garbage collector
// to call the dispose method itself since it seems to be
// a problem with COM thread initialization
if ( rootTreeNode != null )
{
DeleteChildren(rootTreeNode);
}
}
#endregion

}
}
GeneralRe: error with network file system Pin
Steven Roebert5-Sep-06 20:35
Steven Roebert5-Sep-06 20:35 
GeneralA question about retrieving selected file name Pin
jnlin4-Sep-06 4:00
jnlin4-Sep-06 4:00 
GeneralRe: A question about retrieving selected file name Pin
Steven Roebert4-Sep-06 6:48
Steven Roebert4-Sep-06 6:48 
GeneralRe: A question about retrieving selected file name Pin
jnlin4-Sep-06 7:03
jnlin4-Sep-06 7:03 
GeneralRe: A question about retrieving selected file name Pin
Steven Roebert4-Sep-06 8:01
Steven Roebert4-Sep-06 8:01 
GeneralRe: A question about retrieving selected file name Pin
jnlin4-Sep-06 8:44
jnlin4-Sep-06 8:44 
GeneralRe: A question about retrieving selected file name Pin
ThanE|28-Sep-06 1:36
ThanE|28-Sep-06 1:36 
GeneralRe: A question about retrieving selected file name Pin
Steven Roebert28-Sep-06 2:35
Steven Roebert28-Sep-06 2:35 
AnswerRe: A question about retrieving selected file name Pin
Effi b10-Mar-15 1:47
Effi b10-Mar-15 1:47 
GeneralFeature request: display filtering Pin
UnderscoreC1-Sep-06 22:20
UnderscoreC1-Sep-06 22:20 
GeneralRe: Feature request: display filtering Pin
Steven Roebert1-Sep-06 22:49
Steven Roebert1-Sep-06 22:49 
GeneralRe: Feature request: display filtering Pin
UnderscoreC2-Sep-06 18:12
UnderscoreC2-Sep-06 18:12 
GeneralRe: Feature request: display filtering Pin
Marcus Deecke27-Sep-06 4:40
Marcus Deecke27-Sep-06 4:40 
GeneralRe: Feature request: display filtering Pin
UnderscoreC29-Jun-07 21:19
UnderscoreC29-Jun-07 21:19 
GeneralA few suggestions Pin
youngn28-Aug-06 12:53
youngn28-Aug-06 12:53 
GeneralRe: A few suggestions Pin
Steven Roebert28-Aug-06 13:55
Steven Roebert28-Aug-06 13:55 
GeneralRe: A few suggestions Pin
youngn28-Aug-06 14:17
youngn28-Aug-06 14:17 

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

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