Ide Indy Delphi

Title: Using Delphi's Shell Controls
Question: From Delphi 6 onwards, Borland provides shell controls including TShellTreeView and TShellListView that mimic the functionality of Windows Explorer but they are tucked away on the Samples page of the palette, have no documentation and even their source can be hard to find; its in Delphi\Demos\ShellControls. You could be forgiven for thinking they are an afterthought that you're not expected to use.
Answer:
This article first appeared in the Pascal Newsletter Issue 48. The most up to date version, including screnshots and source code, can be found at Irongut's Delphi Pages.
Recently, I wanted to build my own FTP client because I don't like any of the free ones I've tried and I thought 'I've got Indy' so how hard can it be? I checked out the demo for TIdFTP, the Indy FTP client component, and the networking end looked pretty easy so I started to think about design. I wanted something simple and decided on a view of the local file system above a view of the remote file system with the main toolbar in-between. Each view would contain a TreeView and a ListView with a few buttons for simple, Explorer-like navigation. I also wanted drag and drop between controls and with Explorer. At this point I went looking for components to implement the local side and discovered Borland's shell controls. I decided that this kind of layout is something that I could reuse so I started working on a generic frame.
So how do they work? Well some functionality is easy to implement but in other ways these controls can be awkward and confusing. Most of the methods you'd expect to find either don't exist or return parameters that are of dubious value. Often they are the wrong type for other calls you want to make. What should have been a couple of hours of easy programming rapidly turned into several nights of reading source code, experimentation and hair pulling. At some point during this process I decided to turn it into an article so I could share my pain with you. ;)
Let's start with the easy stuff. I connected my TShellTreeView to a TShellListView and then started on a toolbar. The first button I wanted was one to move up the directory tree and after a bit of looking I realised that the TShellListView.Back method would do this for me. Most of the other buttons I wanted were more difficult so I'll come back to them but a Views button for the TShellListView was easy. I just created a popup menu for my button that sets TShellListView.ViewStyles. At this point I had a very simple file manager that provides the standard Explorer context menu and basic navigation features.
I considered adding a TShellComboBox above the file list. I wanted it to resize with the frame like the other controls but it doesn't have an Align property. I tried using Anchors but could not get the effect I wanted so I scrapped that idea.
Now on to the more complicated stuff. The shell controls don't provide
methods to help us manipulate files so we need to use the Windows API. The SHFileOperation() function can perform Copy, Move, Delete and Rename operations so I wrote the wrapper function FileOperation() to make it easier to use.
This function returns true if the operation is a success and displays a progress dialog if necessary. Look-up SHFILEOPSTRUCT in the WinAPI help to see the possible values of op and flags. I still can't decide if I'll need to access this function from outside my frame so for the moment it is a private method but I may change this in the future.
A Delete button was now simple, all I had to do was determine which file or folder is selected and delete it using FileOperation().
While doing a Refresh button I decided to write a generic function that could be called from other methods and would refresh both controls. TShellTreeView.Refresh takes a node as a parameter, but which node to pass? I tried passing the current folder but that didn't always work (this also seems to be a problem in Explorer). Then I tried passing the root node and this works properly. TShellListView flickers when we refresh a TShellTreeView it is connected to so I detach them first. See the procedure TconExplorerFrame.Refresh in the source.
When creating a new folder we need to give it a unique name. The usual way to do this is to call it 'New Folder' and add a number to the name if that folder already exists. I wrote a GetNewFolderName() function that would return the unique name I needed using the DirectoryExists() function from SysUtils.pas and a while loop. My Create Folder button calls this function and then uses CreateDir() from SysUtils.pas.
I wanted to provide a Properties button but the shell controls don't have any useful methods. Since pressing Alt+Enter in a TShellListView works I dived into ShellCtrls.pas and checked out the source. Initially it looked simple, all you need to do is call DoContextMenuVerb. Or not, because DoContextMenuVerb is not a method of TShellListView but is a procedure private to ShellCtrls.pas. At this stage I decided plagiarism was the easiest course and copied it into my frame unit as a private method.
Double-clicking files in TShellListView doesn't work (in Win2k at least) but choosing Open from the context menu does. Checking the source it does have a DblClick method that calls ShellExecute(). At this point I noticed TShellFolder.ExecuteDefault. Since a TShellFolder can be a file or a folder and we can get the selected item as a TShellFolder by calling TShellListView.SelectedFolder, writing an OnDblClick event method was easy. This also ensures that double-click doesn't just try to open the file but performs the default action from its context menu which is what Explorer does. See TconExplorerFrame.shlllstvwFilesDblClick.
At this point I had everything I wanted except Drag and Drop. I'd never done Drag and Drop before so I had to do some reading before I started. Ideally I would like to be able to change the cursor if the user presses Ctrl, like Explorer does. This means using a TDragControlObject to supply a drag image list so I decided to keep things simple for now and leave that effect out.
I started with dragging from my TShellListView to my TShellTreeView. The methods and properties necessary (with the right return types) don't seem to exist until you realise the SelectedFolder property can return files as well as folders. I wrote an OnDragOver event for the TShellTreeView so that it would accept items from the TShellListView and started on its OnDragDrop event. I quickly found I couldn't access the file being dragged from this event so decided to store it in a variable global to my frame during TShellListView.OnStartDrag and then clear it in TShellListView.OnEndDrag. I had trouble with the targetfolder too, TShellTreeView.GetNodeAt and TShellTreeView.DropTarget return a TTreeNode but to get the path for a file operation I wanted a TShellFolder so I Select the DropTarget to retrieve the SelectedFolder (a TShellFolder) and then Select the previous folder again. This makes the TShellListView flicker horribly (you can actually see it change dir) so I tried using TShellListView.Items.BeginUpdate and EndUpdate but this didn't work so I had to detach it from its TShellTreeView, perform the select operations and then re-attach it. This is nasty and I don't like it but it works. The OnDragDrop event doesn't provide any information about the keyboard and I want a copy operation if the user is pressing Ctrl at the end of the drag. I used the GetKeyState() function from the Jedi Code Library (JCLSysInfo.pas) for this.
Having got drag working from my TShellListView I changed my OnDragOver and OnDragDrop events to also accept a folder dragged from my TShellTreeView and added OnStartDrag and OnEndDrag events to it. These six events provide all the features required for dropping a file or folder on the TShellTreeView from within the frame. To keep it simple I only allow the user to select and drag one item at a time.
Explorer allows you to drag a folder from the tree to the file list and to drag files to folders within the list. But dragging a folder from TShellTreeView selects and displays that folder and in any case TShellListView.DropTarget always returns nil! Because of this I couldn't find any way to implement these features. :(
Having done what I could to provide Drag and Drop within my frame I now wanted to make it work with Explorer. To accept a file dropped from Explorer we use Windows messages and I was unsure if this would effect my Drag and Drop events but I was pleased to find that it didn't. I did have a few problems getting it properly setup though. We need to call DragAcceptFiles() with the handle of the control that will accept files to let Windows know to send it the drop files message but TFrame doesn't have an OnCreate event and we can't refer to its components or itself in an initialization section. I had wanted my frame to be fully encapsulated but I had to settle for calling DragAcceptFiles() in the OnCreate event of the form that contains it. Initially I wanted to pass the handle to my TShellListView so that files could only be dropped there but that would need a WMDROPFILES message for the TShellListView so I ended up accepting files anywhere on the frame by passing its handle. Once I got round these problems the rest was easy.
The procedure TconExplorerFrame.WMDROPFILES handles the drop message. It uses DragQueryFile() from the WinAPI to determine the number of items being dropped and then uses it again to get an item's full path as it iterates through the list. Windows automatically provides us with a Copy cursor and pressing Ctrl or Shift doesn't effect it so I copy the items to the folder currently displayed in my TShellListView.
I had also wanted to be able to drag files to Explorer or other instances of my test program but I've been unable to find any articles or tips that explain how to do it. I had hoped that enabling drag from Explorer might have given me one of these features as a side-effect or helped me work out how to do them but it didn't. I think I need to create my own descendant of TCustomShellListView that is drag enabled with the shell but I'm unsure where to begin.
So that's the end of my exploration of Delphi's shell controls. If anyone knows how to drag from Delphi to other applications or can suggest other improvements to my frame leave a comment. The source for my frame and test program can be downloaded, feel free to use it in your own programs.