Enumerating files with Explorer-like components

Suppose that you need a familiar interface to select files for a backup program. This article describes two components that execute this task and clone the Windows Explorer but associate check boxes with each node in order to select the files for the backup program. With these components, the user can select files either by groups (directories) or individually.

This article replaces the article entitled "Tailoring the TShellTreeView component to enumerate files" whose content has now become obsolete.

While in the process of developing a file backup program,  I needed a user interface that would allow users of that program to be able to select a set of files to be backed up from a user interface that would look familiar. The first idea that came to my mind was to develop a clone of the Windows Explorer and use it to select files by groups (directories) or individually.

The immediate answer was a treeview-like component linked to the Windows shell and exhibiting check boxes next to each of its nodes and allowing the user to click on these check boxes in order to enumerate the files contained in the associated directories of the file system and use these files as inputs to a backup program. Since it appeared useful to save these selections to a file for an eventual re-use, this capability was also implemented.

Description of the components

When the components are used together, it presents an Explorer-like user interface as shown below. The left-hand side of the figure is the TGtroCheckShellTreeview component with three nodes checked and the content of its directories and sub-directories enumerated. The right-hand side of the figure is a representation of the TGtroCheckShellListView component with the Root set to rfMyComputer (the default when the component is exesuted).

Explorer-like interface
A clone of the Windows Explorer

What these components do is simple: it selects and enumerates files by groups (content of directories) or individually putting this list of file in a "selection of files" which is the result of a single or of a sequence of clicks that have each added or deleted files to or from the selection. This an enumeration of files that can be used as input to a backup program.

Some rules apply to these clicks:

Filling the selection with list of files resulting from clicks of the check boxes is what the component does.

The TGtroCheckShellTreeview component

Component view
The gtroCheckShellTreeView
component

This component is at the core of the user interface and it can be used stand-alone if it is sufficient for the user to select files by groups, i.e., by directories.

I chose the TShellTreeview as the base class of the component (in fact, I used its ancestor, the TCustomShellTreeview as the base class). Because of this choice, the component behaves exactly like the TShellTreeview component save for the display of check boxes associated with each node and for the generation of a list of files when one clicks on any of the check boxes. This process is the essence of this component: when a check box is clicked, a list of the files contained in the directories associated with this node is produced through an enumeration process and put in the selection.

When several nodes are checked in sequence, each such action has a cumulative effect: files are added to the selection if the node was checked and files are deleted from the selection when a node is unchecked.

In order to meet this objective, the following capabilities needed to be added:

How does the component work?

When you drop the component on a form, it won't appear as shown on the picture shown next to this text. In order to appear as shown, you must set its Root and ObjectType properties to rfMyComputer and otFolder (the default value) respectively. The reason for these choices is to display only nodes associated with real directories. It ia also recommended to set the HideSelection property to "false". Once this setting is performed, you are ready to use the component as part of the software that you develop. Additionally, if you have dropped its companion ListView component on the same form, you need to set its Listview property to the name you gave to the companion component.

This base class of this component, called TGtroCustomCheckShellTreeView, was derived from the TCustomShellTreeView component whereas the component itself was derived from this base class. This approach is the same that is used for the components in the VCL as it allows the designer to control the visibility of the methods and properties of the resulting class.

One last job is performed under the hood. Since .zip and .cab files are recognized as folders by Windows and the TShellTreeview component, these folders are filtered out by adding an internal OnAddFolder() event handler that removes them from the display. The methodology was derived from the article entitled "Using TFilterComboBox with TShellTreeView".

Next, the component needs supplementary capabilities: 

Several methods dealing with the internal mechanics of the component are described in Annex A.

Showing the check boxes

Showing the check boxes is fairly straightforward: simply override the CreateWnd() method of the ancestor as follows:

procedure TGtroCustomCheckShellTreeViewEx.CreateWnd;
var
  Style: Integer;
begin
  inherited CreateWnd;
  Style:= GetWindowLong(Handle, GWL_STYLE);
  Style:= Style or TVS_CHECKBOXES;
  SetWindowLong(Handle, GWL_STYLE, Style);
end;

Detecting where the click occurred

What happens when the user clicks on the component? The WMLButtonDown() method handles this situation by capturing the WML_BUTTONDOWN Windows message and reacting to it. Several cases are distinguished by the method and the following actions are taken:

procedure TGtroCustomCheckShellTreeView.WMLButtonDown(var Msg: TWMLButtonDown);
// Handles the TWMLButtonDown message
var
  HitTests: THitTests;
  Node: TTreeNode;
  OldState: Boolean;
  RemCursor: TCursor;
begin
  LockWindowUpdate(Handle); // prevents flicker
  try
    Node:= GetNodeAt(Msg.XPos, Msg.YPos); // reference of the node that was clicked
    if Node <> nil then // prevents AV if click occurs outside the node area
    begin
      OldState:= IsNodeChecked(Node); // Check the status of the node
      HitTests:= GetHitTestInfoAt(Msg.XPos, Msg.YPos); // where was the node clicked
      if htOnStateIcon in HitTests then // Click on the checkbox
      begin
        Cursor:= crHourGlass;
        LoadingFromFile:= False;
        CheckNode(Node, not Node.Expanded, not OldState);
        if not CanContinue then // if the enumeration was aborted
        begin
          RevertToPrevState(Node, LastNode, not OldState); // undo the enumeration process
          Exit; // exit the method executing finally clause
        end;
      end;
      if htOnLabel in HitTests then // node is selected
      begin
        Select(Node); // Click on the label
        Node.Focused:= True;
        // used only when a ListView is associated w/ component
        if ListView <> nil then 
        begin
          FShortList.Clear;
          RemCursor:= Cursor;
          Cursor:= crHourGlass;
          try
            EnumFiles(Node, True, FShortList);
            if Assigned(FNodeSelected) then FNodeSelected(Self);
            ListView.FillListView(ShortList);
          finally
            Cursor:= RemCursor;
          end;
        end;
      end;
      if htOnButton in HitTests then // Click on the [+] or [-] button
        if Node.Expanded then Node.Collapse(False)
        else Node.Expand(False);
    end;
  finally
    if ListView <> nil then
      ListView.FillListView(ShortList);
    LockWindowUpdate(0);
    Cursor:= crDefault;
  end; // try...finally
end;

The TGtroCheckShellListView component

Contrary to what was expected, the TGtroCheckShellListView conpanion component is derived from the TCustomListView class. I tried to derive it from the TCustomShellListView but it was impossible to display any checkbox even though the Checkboxes property was set to "true".

It is a very simple component with only one public procedure called FillListView() which populates it when a user selects a node in the treeview. This method also checks for the presence of each file in the selection and puts a check mark next to it if they are part of the selection.

When a check box is clicked, the file is immediately added or deleted to or from the selection and a pattern is produced.

The enumeration process

When the user clicks on a check box associated with a node of the left pane or with an item of the right pane of the user interface, he places a command that is executed by the component: enumerate all the files contained in the directory associated with the node (and its sub-directories) and place them in the list of files or place the individual file in the list of files. At the same time, the component updates the user interface to show the result of the action.

If the node was expanded when its check box was clicked, the component adds a pattern to the list of patterns and passes the list of files that have been enumerated to the host application. If the node was collapsed when its check box was clicked, a much more complex process is initiated. The user interface is frozen to avoid flicker and, under the hood, the node is expanded: its immediate sub-nodes are created and made available to the GetFirstChild() and GetNext() methods of the base component. All the sub-nodes are then created, expanded and scanned while the nodes which are in a state different from the value of the State property are handled once again by the SetNodeChecked() and the EnumFiles() methods. The scan is terminated

At the end of the process, the OnCheckChanged() event handler is triggered. It passes the Node, the value of the State property and the list of files to the host application. This process is repeated each time a node is activated but the list of files presented to the user is updated each time by adding or removing files in the list.

For the sake of clarity, the listing of CheckNode() that follows has been reduced to the essential:

procedure TGtroCustomCheckShellTreeViewEx.CheckNode(const Node: TTreeNode;
  IncludeSubs: Boolean; State: Boolean);
begin
  SetNodeChecked(Node, State); // add/remove chech mark on Node
  EnumFiles(Node, State, FList); // enumerate the files in Node
  Pattern:= GetNodePath(Node) + '\'; // Get the path of the node (pattern)
  if IncludeSubs and Node.HasChildren then
  begin // the node was collapsed and its sub-nodes are included in the enumeration
    Level:= Node.Level; // Get the level of Node in the tree
    Node.Expand(False); // expand the node and create new sub-nodes as necessary
    ANode:= Node.getFirstChild; // Deal with the first child of Node
    while (ANode <> nil) and (Level < ANode.Level) do
    begin // for all the nodes <> nil whose Level is smaller than Level
      if State <> IsNodeChecked(ANode) then
      begin
        ANode.Expand(False); // expand ANode and create new sub-nodes as necessary
        SetNodeChecked(ANode, State); // expand Anode and create new sub-nodes as necessary
        EnumFiles(ANode, State, FList); // enumerate the files in ANode
      end; // if State ...
      ANode:= ANode.GetNext; // Get the next node
    end; // while ...
    AddToPatternList(Pattern + '/s'); // Put the pattern in the FPatterns list
    Node.Collapse(True); // collapse all the nodes that were expanded temporarily
    if Assigned(FOnCheckChanged) // pass the list to the host application
      then FOnCheckChanged(Self, Node, State, FList);
  end // if IncludeSubs ...
  else
  begin
    AddToPatternList(Pattern); // Put the pattern in the FPatterns list
    if Assigned(FOnCheckChanged) // pass the list to the host application
      then FOnCheckChanged(Self, Node, State, FList);
  end;
end;

When a user clicks on several check boxes in sequence, files are added or deleted from the list of files. Each time a check box is clicked, the list of files is modified through additions or deletion. The content of the selection of files constitutes the state of the component.

The enumeration itself

The EnumFiles() method is at the core of the component, It enumerates the files contained in the selected directorie. The process is straightforward as shown below:

procedure TGtroCustomCheckShellTreeViewEx.EnumFiles(ANode: TTreeNode;
  State: Boolean; var List: TStringList);

var
  Path: string; Data: TSearchRec; ds: longint; L, M: Integer;
begin
  begin
    Path:= CheckSlash(GetNodePath(ANode)); // remove the ending slash if present
    ds:= FindFirst(Path + '\*.*', faAnyFile, data); // Fetch the first file
    while ds = 0 do // continue as long as the result is successfull
    begin
      if (Data.Attr and faDirectory <> faDirectory) then // don't bother with directories
        if State then
          List.Add(Path + '\' + Data.Name) // add to the list
        else
          List.Delete(List.IndexOf(Path + '\' + Data.Name)); // delete from the list
      ds:= FindNext(Data);
    end;
    FindClose(Data);
  end;
end;

The EnumFiles() method takes the Node, the state of the node (checked or unchecked) as input arguments and produces the list of files by using successive calls to the FindFirst(), FindNext() and FindClose()  functions from SysUtils.pas. FindFirst() uses the path to the directory in order to define the pattern that is used to search for the first instance of a file name with a given set of attributes in a specified directory. The second argument is set to faAnyFile that includes hidden files as well as the sub-directories "." and "..".  If the search is successful, the file will be added or removed from the list depending on the value of the State property. If the file was a sub-directory, no action is taken. The search process is then repeated as long as FindNext() returns success. FindClose() is called to terminate the search process and free resources when the search has exhausted all the content.

Note that if the root of the component is changed to rfNetwork, the enumeration is performed on network files and a selection is obtained after a sequence of clicks either in the treeview or in the Listview. A list of patterns is also produced and the paths are "network paths" as follows "\\<computer name>\<shared directory>\<Path> with or without the ending "3s"..

Aborting the enumeration process

Suppose that a user activated a node by mistake and that this node contained a huge number of sub-nodes. He is in for a fairly long enumeration process that he would surely like to abort. Now, the component has been modified to let him abort the enumeration and bring the component back to the state where it was before the inadvertant activation. This means that, after the interruption, the same nodes as before will be checked, the sames files will appear in the list of files and the list of patterns that results will be the same.

The interruption is controlled by the public variable CanContinue that is normally "true" for the process to continue but can be turned to "false" by the host application. The next section provides another mean to interrupt an inadvertent enumeration.

Achieving this objective is rather complex because of the consideration of some unlikely but extreme scenarios. It has required some modifications in the CheckNode() method, in particular, the filling of a TList component called OddNodesList. This list contains the nodes that were visited and were already in the state called for by the user. I call them "odd" nodes.

When the enumeration process is completed, this list is cleared. When the enumeration process is aborted, the program exits CheckNode() and executes the RevertToPrevState() method where it completes filling the OddNodesList, deactivates the nodes that had been checked and uses the OddNodesList to revert the component to its previous state.

procedure TGtroCustomCheckShellTreeViewEx.RevertToPrevState(Node, LastNode: TTreeNode; State: Boolean);
// if a Node has been checked, State is True, otherwise False
// OddNodesList has been partially filled in CheckNode()
var
  ANode: TTreeNode;
  Level: Integer;
  OldOnCheckChanged: TTVCheckChangedEvent;
  i: Integer;
  NodeLabel: string;
begin
  OldOnCheckChanged:= OnCheckChanged;
  OnCheckChanged:= nil;
  try
    Level:= Node.Level;
    ANode:= LastNode; // will be nil if interruption before CountFolder() is completed.

    // Complete filling the OddNodesList
    if ANode <> nil then // if LastNode = hil - Added 10 May 22010
    begin
      while Level < ANode.Level do
      begin
        NodeLabel:= ANode.Text;
        if State = IsNodeChecked(ANode) then
          OddNodesList.Add(ANode);
        ANode:= ANode.GetNext;
      end;
    end;
    ANode:= Node;

    // Undo the enumeration process
    repeat
      if State = IsNodeChecked(ANode) then
      begin
        SetNodeChecked(ANode, not State);
        EnumFiles(ANode, not State, FList);
      end;
     ANode:= ANode.GetNext;
    until (ANode = nil) or (Level >= ANode.Level);

    for i := 0 to OddNodesList.Count - 1 do
    begin
      ANode:= TTreeNode(OddNodesList[i]);
      SetNodeChecked(ANode, State);
      EnumFiles(ANode, State, FList);
    end;
    OddNodesList.Clear;
  finally
    OnCheckChanged:= OldOnCheckChanged;
  end;
  if Assigned(FOnCheckChanged) then FOnCheckChanged(Self, Node, False, FList);
end;

This capability is enabled by default but it can be disabled by setting the CanAbort published property to "false" in the Object Inspector at design time.

Displaying progress

For the sake of speed, when the TShellTreeview base component is displayed, it creates only the nodes that are visible. When the user checks a node containing a large number of directories, what takes time is the creation of all the nodes corresponding to these directories as well as the time it takes to transfer the list of enumerated files to the host application.

The time taken by this process may appear to drag. The user may loose patience and abort the process thinking that the program had entered an endless loop. It was felt necessary to alleviate the situation by showing that the process was indeed in progress. As a result of this, an optional progress form has been added to the component. It looks like this:

Progress form
Progress form

Note that this form is optional. When you drop the component onto a form, the default value of the public property ProcessFormOn is set to "true" by default and, therefore, the capability is enabled. If it is changed to "false" in the Object Inspector at design time, the form won't appears during the enumeration.

Saving and reloading patterns

Suppose that the user made a complex sequence of node activations. Rather that having to repeat the sequence manually each time he wants to reuse it, he can save it to a file in order to use it again. This essentially the role of the list of patterns.

A "Pattern" is simply the path to a directory associated with a node of the component, say "C:\Users\User\Documents\". It can take two aspects: with or without an appended "/s" which means, when it is part of a pattern, that that sub-directories of the node have to be included in the enumeration. As a result of a click on a check box, a pattern is added to the list of patterns by the private AddToPatternList() method once the list of files has been produced and added to the selection.

Saving/Loading the activations to/from a file

The list of patterns that is produced after a sequence of node activations can be saved to a file by the user of the host application (he chooses whichever extension he likes) using the component's public SaveToFile() method. Later on, he can reload this list of patterns from the host application using the public LoadFromFile() method.

When a list of patterns is reloaded, each pattern of the list replicated automatically the process of clicking on a check box and reproduce a selection. First, the node of the list coresponding to the pattern is identified by the private GetNodeFromPath() method that recognizes standard paths (see Annex B) or the GetNodeFromNetworkPath() method which recognizes the "network paths". Once the node is identified, the rest of the method is straightforward.

procedure TGtroCustomCheckShellTreeView.LoadFromFile(FileName: string);
// Reads patterns from a file, checks the nodes of the component and produces the selection.
// Cannot handle a list of mixed patterns (standard and network paths)
var
  i: Integer;
  Pattern: string;
  ANode: TTreeNode;
  OldState: Boolean;
  IncludeSubs: Boolean;
  FPatterns: TStringList;
  NetworkPattern: Boolean;
begin
  ProgressFormOn:= False; // No progress Form
  FPatterns:= TStringlist.Create; // Create a local list of patterns
  LockWindowUpdate(Handle); // prevents flicker
  LoadingFromFile:= True;
  try
    Initialize;
    FPatterns.LoadFromFile(FileName); // Read patterns from file
    NetworkPattern:= Pos('\\', FPatterns[0]) <> 0; // true if network pah
    if NetworkPattern then
      Root:= 'rfNetwork'  // change Root to rfNetwork
    else
      Root:= 'rfMyComputer'; // change Root to rfMyComputer
    if Assigned(FOnRootChanged) then FOnRootChanged(Self, Root); // notifies host application
    for i:= 0 to FPatterns.Count - 1 do // scan patterns
    begin
      Pattern:= FPatterns[i];
      // Check for inclusion of subdirectories (Patern terminaison = "/s")
      IncludeSubs := Pos('/s', Pattern) > 0;
      if IncludeSubs then
        Pattern:= Copy(Pattern, 0, Length(Pattern) - 2); // remove terminaison "/s"
      // Check for Node/File patterns
      if 0 <> Posex('\', Pattern, Length(Pattern) -1) then
      begin
        if NetworkPattern then
          ANode:= GetNodeFromNetworkPath(Pattern) // returns the node or nil
        else
          ANode:= GetNodeFromPath(Pattern); // returns the node or nil
        if ANode <> nil then  // Node has been found
        begin
          if not IncludeSubs then
            ANode.Collapse(False);
          OldState:= IsNodeChecked(ANode); // Check status of node
          // Produce the list of files
          CheckNode(ANode, IncludeSubs, not OldState);
        end;
      end
      else
        if FileExists(Pattern) then
        begin
          OldState:= FList.IndexOf(Pattern) <> -1;
          CheckFile(Pattern, not OldState);
        end;
    end;
  finally
    FPatterns.Free;
    LockWindowUpdate(0);
    if ListView <> nil then
      ListView.Clear;
    ProgressFormOn:= True;
  end; // try...finally
end;

Note that the progress form does not show when the patterns are loaded from a file.

The LoadFromFile() and SaveToFile() methods should be used in conjunction with TOpenDialog and TSaveDialog components in the host application. These methods do not locate the file containing the list of patterns. Using these methods directly will cause access violations.

Optimizing the list of patterns

Can users behave foolishly? I was told that it can happen. For instance, it may happen that a user would effect a sequence of clicks that takes the selection back to what is was before the sequence of clicks. I call that a circular sequence that created a sequence of redundant patterns in the list of patterns.

The goal of the optimization is to remove these redundant patterns from the list of patterns. This has required that a means to map the selection produced at each step of the sequence to some unique "signature" that can be easily recognize by the component.

The mapping that has been selected is known as the "CRC" or the cyclic redundancy check. It is a non-secure a hash function designed to detect accidental changes to raw computer data, and is commonly used in digital networks and storage devices such as hard disk drives. The InitCRC32() and GetCRC32() function [obtained from Koders.com] performs a byte by byte scan of the list of files contained in the stream and produces a unique signature for the list of files resulting from each step of the sequence.

With this method, each time a user clicks on a check box next to a node, the selection is updated, its CRC determined and, if this same CRC already exists in the objects associated with each pattern of the list, all the patterns since this early state are deleted from the list.

procedure TGtroCustomCheckShellTreeView.AddToPatternList(Pattern: string);
// CRC is considered a valid signature of the list of files
var
  TempStream: TStream;
  Indx, i: Integer;
begin
  if Optimization then // if Optimization property is false, no need to compute CRC
  begin
    TempStream:= TMemoryStream.Create; // create a memory stream
    try
      FList.SaveToStream(TempStream); // put the list of file in a memory stream
      CRC:= GetCRC32(TempStream, 0); // get the CRC/signature of this stream
    finally
      TempStream.Free; // Destroy the stream
    end;
  end; // if Optimization

  if (FPatterns.Count = 0) or (FPatterns.Strings[FPatterns.Count - 1] <> Pattern) then
  begin
    // Add the CRC to the object associated with the pattern
    FPatterns.AddObject(Pattern, TObject(CRC));
    if Optimization then // no need to search for previous identical state
    begin
      Indx:= FPatterns.IndexOfObject(TObject(CRC)); // Indx <> -1 => same CRC exists
      if Indx <> -1 then // delete redundant patterns
        for i:= FPatterns.Count - 1 downto Indx + 1 do
          FPatterns.Delete(i);
    end;
  end
  else
      FPatterns.Delete(FPatterns.Count - 1);

  if FList.Count = 0 then
    FPatterns.Clear; // Clear if FileList is empty
end;

This capability is enabled by default but it can be turned off by setting the published Optimization property to "false" in the Object Inspector at design time.

Accessing the results

Obviously, there need to be a results to this node clicking. It is a list of files that can be used for any purpose. How can it be accessed? Simply by using the List parameter generated by the OnCheckCanged event handler of the TGtroCheckShellTreeView component.

Conclusion

The aim of the exercise was to devise a component that would allow a user to select files for a backup program in a user interface that would clone the Windows Explorer. This was done using the TShellTreeView component of the "Sample" page of the Delphi component palette as its base class, adding it the capability of displaying check boxes next to each of its node as well as the internal mechanics to produce a selection of the files contained in the directories associated with all the nodes that the user has checked.

Since users can select any set of nodes to produce a selection of files and that such selections can be fairly complex, the capability of saving the selection to a file and re-enacting it by reloading this file has been implemented. Several new features including the possibility of aborting the enumeration if it takes too long and the display of a form showing the progress of the process. These improvements to the component are the results of interactions that I had with Fabrice Parisot, a user of the component. Indebtedness is hereby acknowledged.

The addition of the TGtroCheckShellListView companion component has added the capability for the user to select files individually. I believed that one way of providing individual file selection would have been to use a TShellListview control (with its public property Checkboxes set to "true" on execution) in association with this component but I found that, in such a situation, the TShellListView component is plagued by a bug: space for the check boxes is made available but the check boxes are not displayed. As a result, I derived the component from TListView and provided the TGtroCheckShellTreeView with the capability of populating the companion component when a node is selected.

Note that the component has another capability: the user can change the root from rfMyComputer to rfNetwork and access the network. Everything else is cosmetic. In addition, the TGtroCheckShellTreeview component can be used as a stand-alone component. In this situation, files cannot be selected individually.

The code of the components and the demo are now available on this site for download. Put it in a package, compile it, install it (the components will appear in the GTRO pane of the component palette), use it and enjoy it. Some trimming of the component may be required when you drop them on forms. If you wish to test the component with the demo program that I used, click here. Change the .ex_ extension for .exe, put it in any directory and execute it. It does not require any installation.

This code was developed and tested with Delphi 2009. It was also compiled and used with Delphi 7. Porting in backward to Delphi 6 presents a problem: the components uses the Posex() function fairly extensively and this function was introduced in Delphi 7. I am looking for a way to make it backward compatible with Delphi 6.

Annexes

Annex A - Internal mechanics

Some cosmetic operations of the component had to be implemented to execute in the background in order to enhance the appearance of the component. For instance, when a check box is clicked, the check mark that appears or disappears when it is clicked does not do so automaticlly. The SetNodeChecked() method performs this task as follows:

procedure SetNodeChecked(Node :TTreeNode; Checked :Boolean);
const
  TVIS_CHECKED = $2000;
var
  TvItem :TTVItem; // specifies the attributes of the tree view item
  begin
    FillChar(TvItem, SizeOf(TvItem), 0);
    with TvItem do 
    begin
      hItem := Node.ItemId; // handle that uniquely identifies this node in a tree view
      Mask := TVIF_STATE; // state and stateMask members are valid
      StateMask := TVIS_STATEIMAGEMASK; // The item's state image is included when the item is drawn
      if Checked then
        TvItem.State := TVIS_CHECKED // check mark in the check box
      else
        TvItem.State := TVIS_CHECKED shr 1; // check box without the checkmark
      TreeView_SetItem(Node.TreeView.Handle, TvItem);
   end;
end;

The same is true for the check mark that follows the mouse when it hovers the check box:


procedure TGtroCustomCheckShellTreeViewEx.WMMouseMove(var MsG: TWMMouseMove);
var HitTests: THitTests; begin HitTests:= GetHitTestInfoAt(Msg.XPos, Msg.YPos); if htOnStateIcon in HitTests then Screen.Cursor:= crChecked else Screen.Cursor:= crDefault; end;

Finally, the verification of the current checked/unchecked status of a node is tested in the following procedure:


function IsNodeChecked(Node :TTreeNode) :Boolean;
const TVIS_CHECKED = $2000; var TvItem: TTVItem; // specifies the attributes of the tree view item begin TvItem.Mask:= TVIF_STATE; TvItem.hItem:= Node.ItemId; TreeView_GetItem(Node.TreeView.Handle, TvItem); Result:= (TvItem.State and TVIS_CHECKED) = TVIS_CHECKED;
end;

Annex B - The GetNodeFromPath() method

This method is used only by the LoadFromFile() public method. It takes a path to a directory as its argument and returns the node of the gtroCheckShellTreeViewEx component that is associated with the directory. It looks simple but it is somewhat convoluted since it has to search in a lot of nodes before it finds its position.


function TGtroCustomCheckShellTreeViewEx.GetNodeFromPath(const Path: string): TTreeNode;
var FoundNode: TTreeNode; function AppendSlash(ANode: TTreeNode): string;
begin Result:= TShellFolder(ANode.Data).PathName; if '\' <> Copy(Result, Length(Result), 1) then Result:= Result + '\';
end;

procedure ScanNodes(Path: string; Segment: string; ANode: TTreeNode);
var P: Integer; Slash: string;
begin P:= Pos('\', Path);
if P > 0 then begin // Split the path into segments (segments are separated by "\") Segment:= Segment + Copy(Path, 0, P); // First segment of the path Path:= Copy(Path, P+1, Length(Path)); // Remaining segments of the path ANode.Expand(False); // Expand the node ANode:= ANode.getFirstChild; // Move to the first child node
Slash:= AppendSlash(ANode); // Append "\" if not already there while Segment <> Slash do // Iterate on each child nodes begin ANode:= ANode.getNextSibling; if ANode <> nil then
Slash:= AppendSlash(ANode)
else // Iterated beyond last child node Break; // No correspondence => out! end;
if Path <> '' then // Path is not exhausted begin if ANode <> nil then begin ANode.Expand(False); ScanNodes(Path, Segment, ANode); // called recursively end;
end else // Path is exhausted FoundNode:= ANode; // Node found end;
end;

begin ScanNodes(Path, '', Items[0]);
Result:= FoundNode; //Reference to the node if found; nil otherwise end;

The function takes a path (to a node) as its argument and it finds the node of the treeview that is associated with this path. Such path normally looks like C:\Dir1\Dir2\Dir3\Dir4\  and the first node of the component that is searched is the root node (Item[0]) in the call to ScanNodes(). The initial value of Segment is a blank string.

First, this procedure locates the first  occurrence of the character "\" in the Path and given that one if found, it uses the first segment of the path (here C:\) by copying all the characters preceding the "\", redefines the path to be the sequence of all subsequent segments (Here Dir2\Dir3\Dir4\), expands the immediate subnodes of the node, fetches the first child node and scans these sub-nodes for a match. If no match is found (ANode is nil) the procedure terminates. If a match is found and the segments are not exhausted, the procedure is called recursively until the segments are exhausted and the node is found.

The GetNodeFromNetworkPath() performs a similar process on patterns that are network paths.

Annex C - Extreme scenarios

The code needed for the abortion of the enumeration process could have been relatively simple if it was not for some unlikely but still possible extreme scenarios like the following: suppose that you have expanded the "Documents" node and you have checked the collapsed nodes "Folder1", "Folder6" and Folder27". At this point "Folder1", "Folder6" and "Folder27" and all their sub-directories have been created, are checked and all the files that they contain are in the list of files.

Then, you collapse the "Documents" node and inadvertantly check it. The enumeration process starts and, recognizing your mistake, you abort the enumeration process at a point in time where "Folder1" and "Folder6" have been visited but not enumerated (they were already) and put in the OddNodesList because they were "odd". At the time of the interruption, the last Node visited was put in the LastNode variable and through the Exit procedure, the calling method, WMLButtonDown() was given control and RevertToPreState() executed.

Since "Folder27" is located past LastNode, it had not yet been visited and, obviously, not been put in the OddNodesList. Since it had already been enumerated prior to the enumeration, its node and that of all its subfolders had been created. As such, during the execution of RevertToPrevState(), they will be put in the OddNodesList during the scan of the remaining nodes, the enumeration will be undone and the "odd" nodes contained in the OddNodesList will be recreated and enumerated, leaving the component exactly in the same state as it was before the inadvertant check.

I have tested similar cases and the component reverted in its previous state. All the directories contained in "Documents" were unchecked except for "Folder1", "Folder6", "Folder27" and all their sub-directories.

Warning!
This code was developed for the pleasure of it. Anyone who decides to use it does so at its own risk and agrees not to hold the author responsible for its failure.


Questions or comments?
E-Mail
Last modified: September 3rd 2014 13:34:29. []