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).
|
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:
- An expanded node of the treeview is clicked - the content of the companion directory is added to or deleted from the selection;
- A collapsed node of the treeview is clicked - the content of the companion directory and that of all its sub-directories is added to or deleted from the selection; and
- An item of the listview is clicked - its file is added to or deleted from the selection.
Filling the selection with list of files resulting from clicks of the check boxes is what the component does.
The TGtroCheckShellTreeview component
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:
- displaying check boxes associated with each node;
- updating the selection by clicking on these check boxes; and
- generating list of patterns, a by-product of the component that is used when saving a sequence of clicks to a file in order to replay it later.
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:
- displaying check boxes associated with each node;
- detecting where the user clicks on the component;
- updating the user interface once a user has clicked a check box;
- producing a list of files through enumeration and adding it to the selection;
- interrupting the enumeration process if a node was checked by mistake; and
- viewing the progress of the enumeration (optional).
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:
- If the user clicks outside of the node area of the component, nothing happens.
- If the user clicks on a node, it detects where on the node the click was performed
- The click was on the label: it selects the node;
- The click was on the + or - button: it expands or collapses the node;
- The click was on the check box: the enumeration is performed, the list of files added to the selection and delivered to the host application.
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
- when the list of nodes is exhausted;
- when the process meets a node at the same level as the starting node; or
- when the user aborts the process.
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:
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.