News Archive

Shell Controls

Forums / Groups

Mozilla-Delphi Project

Pascal Newsletter




Vote For Us:
Irongut's Delphi Pages
Pascal Newsletter

Contact Details

Legal Stuff

eBook Download
Using Delphi's Shell Controls
By Dave Murray <irongut at vodafone dot net>

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.

This article first appeared in the Pascal Newsletter Issue 48.

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.

Figure 1 - TconExplorerFrame

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 target folder 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 addedOnStartDrag 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 please contact me. The source for my frame and test program can be downloaded, feel free to use it in your own programs.

Demo Program
Figure 2 - Demo Program

The Code

unit ExplorerFrame;
{Frame that mimics basic Windows Explorer functionality using Borland's Shell Controls TShellTreeView and TShellListView. }
{(c) 2003 Conspiracy Software. Written by Dave Murray, May/June 2003.}

{To enable files to be dropped from Explorer, OnCreate event of form that contains this frame must call:
 DragAcceptFiles(TconExplorerFrame1.Handle, true); }


  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ImgList, ToolWin, ComCtrls, ShellCtrls, ExtCtrls, Menus, StdCtrls;

  TconExplorerFrame = class(TFrame)
    pnlLeft: TPanel;
      pnlButtons: TPanel;
        tlbrButtons: TToolBar;
          tlbtnUp: TToolButton;
          tlbtnRefresh: TToolButton;
          tlbtnSplit1: TToolButton;
          tlbtnCreateFolder: TToolButton;
          tlbtnDelete: TToolButton;
          tlbtnSplit2: TToolButton;
          tlbtnProperties: TToolButton;
          tlbtnViews: TToolButton;
      pnlFoldersHeader: TPanel;
      shlltrvwFolders: TShellTreeView;
    splttrMiddle: TSplitter;
    pnlRight: TPanel;
      shlllstvwFiles: TShellListView;
    pmnuViews: TPopupMenu;
      pmLargeIcons: TMenuItem;
      pmSmallIcons: TMenuItem;
      pmList: TMenuItem;
      pmDetails: TMenuItem;
    imglstButtonsHot: TImageList;
    imglstButtonsNorm: TImageList;
    imglstButtonsDisabled: TImageList;
    imglstViews: TImageList;
    {### tlbrButtons BUTTON METHODS ###}
    procedure tlbtnUpClick(Sender: TObject);
    procedure tlbtnRefreshClick(Sender: TObject);
    procedure tlbtnCreateFolderClick(Sender: TObject);
    procedure tlbtnDeleteClick(Sender: TObject);
    procedure tlbtnPropertiesClick(Sender: TObject);
    {### pmnuViews POPUP MENU METHODS ###}
    procedure pmLargeIconsClick(Sender: TObject);
    procedure pmSmallIconsClick(Sender: TObject);
    procedure pmListClick(Sender: TObject);
    procedure pmDetailsClick(Sender: TObject);
    {### DRAG + DROP METHODS ###}
    procedure shlllstvwFilesStartDrag(Sender: TObject; var DragObject: TDragObject);
    procedure shlllstvwFilesEndDrag(Sender, Target: TObject; X, Y: Integer);
    procedure shlltrvwFoldersStartDrag(Sender: TObject; var DragObject: TDragObject);
    procedure shlltrvwFoldersEndDrag(Sender, Target: TObject; X, Y: Integer);
    procedure shlltrvwFoldersDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
    procedure shlltrvwFoldersDragDrop(Sender, Source: TObject; X, Y: Integer);
    {### Other METHODS ###}
    procedure shlllstvwFilesDblClick(Sender: TObject);
    { Private declarations }
    function FileOperation(const source, dest : string; op, flags : Integer) : boolean;
    function GetNewFolderName(const Path, FolderName : string): string;
    procedure DoContextMenuVerb(AFolder: TShellFolder; Verb: PChar);
    procedure WMDROPFILES(var Message: TWMDROPFILES);message WM_DROPFILES;
    { Public declarations }
    procedure Refresh;


{$R *.dfm}

  ShellAPI, ShlObj, JclSysInfo;

  cmvProperties : PChar = 'properties';

  DragSourceFile : string;

{### tlbrButtons BUTTON METHODS ###}

procedure TconExplorerFrame.tlbtnUpClick(Sender: TObject);
{move up a dir}
end; {procedure TfrmExplorerFrame.tlbtnUpClick}

procedure TconExplorerFrame.tlbtnRefreshClick(Sender: TObject);
{refresh shell displays}
end; {procedure TfrmExplorerFrame.tlbtnRefreshClick}

procedure TconExplorerFrame.tlbtnCreateFolderClick(Sender: TObject);
{creates a new folder in current folder}
  CurrentFolder, NewFolder : string;
  CurrentFolder := shlltrvwFolders.SelectedFolder.PathName + '\';
  {ensure valid current folder}
  if (CurrentFolder = 'Control Panel') or (CurrentFolder = 'Recycle Bin')
    or (Length(CurrentFolder) = 0) or (Pos('nethood', CurrentFolder) > 0) then Exit;
  {get unique version of 'New Folder'}
  NewFolder := GetNewFolderName(CurrentFolder, 'New Folder');
  {create dir}
  if not(CreateDir(NewFolder)) then begin
    MessageDlg('ERROR: Unable to create folder!', mtError, [mbOk], 0);
    end; {if not(CreateDir..}
end; {procedure TfrmExplorerFrame.tlbtnCreateFolderClick}

procedure TconExplorerFrame.tlbtnDeleteClick(Sender: TObject);
{deletes file or folder selected in shlltrvwFolders or shlllstvwFiles}
  FileOrFolder : string;
  OpSuccess : boolean;
  FileOrFolder := '';
  {get selected file or folder}
  if (shlltrvwFolders.Focused) then begin
    FileOrFolder := shlltrvwFolders.SelectedFolder.PathName;
    end {if shlltrvwFolders.Focussed..}
  else if (shlllstvwFiles.Focused) then FileOrFolder := shlllstvwFiles.SelectedFolder.PathName;
  {ensure valid file or folder}
  if (FileOrFolder = 'Control Panel') or (FileOrFolder = 'Recycle Bin')
    or (Length(FileOrFolder) = 0) or (Pos('nethood', FileOrFolder) > 0) then Exit;
  {delete selected file or folder}
  OpSuccess := FileOperation(FileOrFolder, '', FO_DELETE, FOF_ALLOWUNDO);
  if (OpSuccess) then Refresh;
end; {procedure TfrmExplorerFrame.tlbtnDeleteClick}

procedure TconExplorerFrame.tlbtnPropertiesClick(Sender: TObject);
{shows properties dialog for selected file/folder}
  if (shlltrvwFolders.Focused) then
     DoContextMenuVerb(shlltrvwFolders.SelectedFolder, cmvProperties)
  else if (shlllstvwFiles.Focused) then
     DoContextMenuVerb(shlllstvwFiles.SelectedFolder, cmvProperties);
end; {procedure TconExplorerFrame.tlbtnPropertiesClick}

{### pmnuViews POPUP MENU METHODS ###}

procedure TconExplorerFrame.pmLargeIconsClick(Sender: TObject);
{popup menu item, selects Large Icon view}
  shlllstvwFiles.ViewStyle := vsIcon;
end; {procedure TfrmExplorerFrame.pmLargeIconsClick}

procedure TconExplorerFrame.pmSmallIconsClick(Sender: TObject);
{popup menu item, selects Small Icon view}
  shlllstvwFiles.ViewStyle := vsSmallIcon;
end; {procedure TfrmExplorerFrame.pmSmallIconsClick}

procedure TconExplorerFrame.pmListClick(Sender: TObject);
{popup menu item, selects List view}
  shlllstvwFiles.ViewStyle := vsList;
end; {procedure TfrmExplorerFrame.pmListClick}

procedure TconExplorerFrame.pmDetailsClick(Sender: TObject);
{popup menu item, selects Details view}
  shlllstvwFiles.ViewStyle := vsReport;
end; {procedure TfrmExplorerFrame.pmDetailsClick}


procedure TconExplorerFrame.shlllstvwFilesStartDrag(Sender: TObject; var DragObject: TDragObject);
{store name of file/folder being dragged in DragSourceFile var global to frame}
  DragSourceFile := (Sender as TShellListView).SelectedFolder.PathName;
end; {procedure TconExplorerFrame.shlllstvwFilesStartDrag}

procedure TconExplorerFrame.shlllstvwFilesEndDrag(Sender, Target: TObject; X, Y: Integer);
{clear DragSourceFile now nothing being dragged}
  DragSourceFile := '';
end; {procedure TconExplorerFrame.shlllstvwFilesEndDrag}

procedure TconExplorerFrame.shlltrvwFoldersStartDrag(Sender: TObject; var DragObject: TDragObject);
{store name of folder being dragged in DragSourceFile var global to frame}
  DragSourceFile := (Sender as TShellTreeView).SelectedFolder.PathName;
end; {procedure TconExplorerFrame.shlltrvwFoldersStartDrag}

procedure TconExplorerFrame.shlltrvwFoldersEndDrag(Sender, Target: TObject; X, Y: Integer);
{clear DragSourceFile now nothing being dragged}
  DragSourceFile := '';
end; {procedure TconExplorerFrame.shlltrvwFoldersEndDrag}

procedure TconExplorerFrame.shlltrvwFoldersDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
{decides what dropped items shlltrvwFolders accepts}
  Accept := ((Source is TShellListView) or (Source is TShellTreeView))
    and not(shlltrvwFolders.DropTarget = nil);
end; {procedure TconExplorerFrame.shlltrvwFoldersDragOver}

procedure TconExplorerFrame.shlltrvwFoldersDragDrop(Sender, Source: TObject; X, Y: Integer);
{accepts file/folder dropped on shlltrvwFolders, copy/move it to target folder}
  MyListView : TCustomShellListView;
  CurrentFolder : TTreeNode;
  TargetFolder : string;
  OpSuccess : boolean;
  DragOp : integer;
  OpSuccess := false;
  {determine copy or move}
  if (GetKeyState(VK_CONTROL)) then DragOp := FO_COPY
  else DragOp := FO_MOVE;
  if ((Source is TShellListView) or (Source is TShellTreeView)) then begin
    {copy/move file DragSourceFile to DropTarget folder}
    {ensure source valid}
    if (DragSourceFile = 'Control Panel') or (DragSourceFile = 'Recycle Bin')
      or (Pos('nethood', DragSourceFile) > 0) or (Length(DragSourceFile) = 0) then Exit;
    {identify target folder using Select to get a TShellFolder}
    {TShellListView has flickers when TShellTreeView changes so detatch first}
    MyListView := shlltrvwFolders.ShellListView;
    shlltrvwFolders.ShellListView := nil;
    {select target folder}
    CurrentFolder := shlltrvwFolders.Selected;
    {get full path of target folder}
    TargetFolder := shlltrvwFolders.SelectedFolder.PathName;
    {re-select previous folder}
    {re-atatch TShellListView}
    shlltrvwFolders.ShellListView := MyListView;
    {ensure target folder valid}
    if (TargetFolder = 'Control Panel') or (TargetFolder = 'Recycle Bin')
      or (Pos('nethood', TargetFolder) > 0) or (Length(TargetFolder) = 0) then Exit;
    {copy/move source file/folder to target folder}
    OpSuccess := FileOperation(DragSourceFile, TargetFolder, DragOp, FOF_ALLOWUNDO);
    end; {if (Source is TShellListView..}
  if (OpSuccess) then begin
    {refresh controls if successfult}
    if ((Source is TShellTreeView) and (DragOp = FO_MOVE)) then
      shlllstvwFiles.Back; {folder displayed has been moved so go up}
    end; {if OpSuccess..}
end; {procedure TconExplorerFrame.shlltrvwFoldersDragDrop}

{### Other METHODS ###}

procedure TconExplorerFrame.shlllstvwFilesDblClick(Sender: TObject);
{Do default action when file double-clicked,
 because TCustomShellListView.DblClick doesn't do this correctly}
  {ensure something selected}
  if (shlllstvwFiles.SelectedFolder = nil) then Exit;
  {ignore if a folder}
  if (shlllstvwFiles.SelectedFolder.IsFolder) then Exit;
  {do default action}
end; {procedure TconExplorerFrame.shlllstvwFilesDblClick}


function TconExplorerFrame.FileOperation(const source, dest : string; op, flags : Integer) : boolean;
{perform Copy, Move, Delete, Rename on files + folders via WinAPI}
  Structure : TSHFileOpStruct;
  src, dst : string;
  OpResult : integer;
  {setup file op structure}
  FillChar(Structure, SizeOf (Structure), #0);
  src := source + #0#0;
  dst := dest + #0#0;
  Structure.Wnd := 0;
  Structure.wFunc := op;
  Structure.pFrom := PChar(src);
  Structure.pTo := PChar(dst);
  Structure.fFlags := flags;
  case op of
    {set title for simple progress dialog}
    FO_COPY : Structure.lpszProgressTitle := 'Copying...';
    FO_DELETE : Structure.lpszProgressTitle := 'Deleting...';
    FO_MOVE : Structure.lpszProgressTitle := 'Moving...';
    FO_RENAME : Structure.lpszProgressTitle := 'Renaming...';
    end; {case op of..}
  OpResult := 1;
    {perform operation}
    OpResult := SHFileOperation(Structure);
    {report success / failure}
    result := (OpResult = 0);
    end; {try..finally..}
end; {function TconExplorerFrame.FileOperation}

function TconExplorerFrame.GetNewFolderName(const Path, FolderName : string): string;
{returns a unique name for a new folder with a number if necessary
 parameter Path must end with \}
  i : integer;
  result := Path + FolderName;
  i := 0;
  {check if dir exists and if so add a number and try again}
  while DirectoryExists(result) do begin
    result := Path + FolderName + ' ' + IntToStr(i);
    end; {while..}
end; {function TconExplorerFrame.GetNewFolderName}

procedure TconExplorerFrame.DoContextMenuVerb(AFolder: TShellFolder; Verb: PChar);
{executes a command from an item's context menu, currently used for Properties button}
  ICI: TCMInvokeCommandInfo;
  CM: IContextMenu;
  PIDL: PItemIDList;
  if AFolder = nil then Exit;
  FillChar(ICI, SizeOf(ICI), #0);
  with ICI do begin
    cbSize := SizeOf(ICI);
    hWND := 0;
    lpVerb := Verb;
    nShow := SW_SHOWNORMAL;
    end; {with ICI..}
  PIDL := AFolder.RelativeID;
  AFolder.ParentShellFolder.GetUIObjectOf(0, 1, PIDL, IID_IContextMenu, nil, CM);
end; {procedure TconExplorerFrame.DoContextMenuVerb}

procedure TconExplorerFrame.WMDROPFILES(var Message: TWMDROPFILES);
{accepts files dropped from shell (eg. Explorer), copies to shlllstFiles}
  i, NoOfItems, size: integer;
  DragFile : PChar;
  TargetFolder : string;
  OpSuccess : boolean;
  OpSuccess := false;
  DragFile := StrAlloc(255);
  {get + validate target folder}
  TargetFolder := shlllstvwFiles.RootFolder.PathName;
  if (TargetFolder = 'Control Panel') or (TargetFolder = 'Recycle Bin')
    or (Pos('nethood', TargetFolder) > 0) or (Length(TargetFolder) = 0) then Exit;
  {get no of source items}
  NoOfItems := DragQueryFile(Message.Drop, $FFFFFFFF, DragFile, 255);
  {iterate thru items}
  for i := 0 to (NoOfItems - 1) do begin
    {get size of item's full path and allocate PChar}
    size := DragQueryFile(Message.Drop, i , nil, 0) + 1;
    DragFile := StrAlloc(size);
    {get full path of item}
    DragQueryFile(Message.Drop,i , DragFile, size);
    {copy/move source file/folder to target folder}
    OpSuccess := FileOperation(DragFile, TargetFolder, FO_COPY, FOF_ALLOWUNDO);
    end; {for i..}
  if (OpSuccess) then Refresh;
end; {procedure TconExplorerFrame.WMDROPFILES}


procedure TconExplorerFrame.Refresh;
{refresh both shell views}
  MyListView : TCustomShellListView;
  {TShellListView has flickers when TShellTreeView changes so detach first}
  MyListView := shlltrvwFolders.ShellListView;
  shlltrvwFolders.ShellListView := nil;
  {Re-attach TShellListView}
  shlltrvwFolders.ShellListView := MyListView;
end; {procedure TconExplorerFrame.Refresh}

  {initialise vars global to frame}
  DragSourceFile := '';


eBook Download this article in eBook format.
You can download a free eBook reader for Android, Blackberry, iPhone, Palm OS, Windows Mobile and more.
Zip Download the code with a Demo application.

Refer this page to a friend!

Copyright © 2003 - 2010 Conspiracy Software. All Rights Reserved.