Issues in renaming of TShellTreeView nodes
- Lazarus/FPC Version: Lazarus 2.3.0 (rev main-2_3-771-gcf4306ec) FPC 3.2.2 i386-win32-win32/win64
- Operating System: Windows 11
- CPU / Bitness: 32 or 64 bit
What happens
TShellTreeView
is primarily meant to be a component for displaying the directory structure of a file system. However, it is possible to rename nodes at runtime. These changes are not propagated to the file system automatically. But the user can provide a handler for the OnEdited
event in which the file or directory is renamed physically, like this (code by forum user GetMem in https://forum.lazarus.freepascal.org/index.php/topic,58161.msg433237.html#msg433237)
uses LazFileUtils;
procedure TForm1.ShellTreeView1Edited(Sender: TObject; Node: TTreeNode;
var S: string);
var
OldPath: String;
NewPath: String;
begin
OldPath := TShellTreeNode(Node).FullFilename;
NewPath := TShellTreeNode(Node).BasePath + S;
if TShellTreeNode(Node).IsDirectory then
begin
OldPath := AppendPathDelim(OldPath);
NewPath := AppendPathDelim(NewPath);
end;
if not RenameFile(OldPath, NewPath) then
begin
S := Node.Text;
MessageDlg('Cannot rename object! Error message:' + sLineBreak + '"' + SysErrorMessage(GetLastOSError) + '"', mtError, [mbOK], 0);
end
end;
However, this does not work. On Windows the application crashes with a SIGSEGV. On Linux, still the old name is returned when the file/directory name is queried by calling ShellTreeView.GetPathFromNode()
; in the file system, however, the renaming command has been executed - totally confusing...
What did you expect
No crash on Windows. Consistent file names on Linux and others.
Steps to reproduce
Run attached demo (again by forum user GetMem): Edit a node (make sure you edit a dummy folder node, since the changes will be propagated back to the filesystem). An exception should occur in unit win32wsshellctrls
, method TWin32WSCustomShellTreeView.DrawBuiltInIcon()
. The icon is not found since the tree is looking for the old file name, not the new one.
Analysis
When the TShellTreeView
is populated all files and folders are collected by the FindFirst()
/FindNext()
procedures. For each found object a TShellTreeNode
is created which is a descendant of the regular TTreeNode
and provides an extra field FFileInfo
for the TSearchRec
of the FindFirst()
/FindNext()
procedure. It must be emphasized that the TSearchRec
in particular contains in field Name
the filename of the found file system object so that the file name is stored twice in a TShellTreeNode
: as element Name
of FFileInfo
, and as the caption of the node (Node.Text
). This is because the new node is created like this:
NewNode := Items.AddChildObject(ANode, Files[i], nil);
TShellTreeNode(NewNode).FFileInfo := TFileItem(Files.Objects[i]).FileInfo;
And this double storage is the root of the problem.
When a user edits a node text in the treeview, he changes only the element Text
of the node. This change is not propagated to the FFileInfo
record.
On the other hand the FFileInfo
elements are used in the TShellTreeNode
methods GetShortFileName()
and GetLongFileName()
. These methods are public and are used by the ShellTreeView method GetPathFromNode()
and by the win32 widgetset for construction of the shell icon. After renaming in above code, both functions use the name before renaming and must produce a false result (GetPathFromNode()
) or result in a crash (shell icons, because the file is not found any more under its old name).
Fix
A first idea would be to override the setter of the tree node's Text
property, SetText()
, and pass a new value of Text
on to the FFileInfo
. Since changing the node text occurs in the ancestor, TCustomTreeView
, this method must be virtual in order to work in the TShellTreeView
. However, SetText()
is a static method. Of course, it could be declared to be virtual
in the TCustomTreeView
. But I don't like this idea because the solution of a problem in a descendant class has an effect on all treeview classes.
An alternative solution is to handle the double storage itself. Since Node.Text
and TShellTreeNode(Node).FFileInfo.Name
contain the same information, the issue can also be solved by rewriting the shelltreenode's GetShortFileName()
and GetLongFileName()
on the basis of Node.Text
, rather than Node.FFileInfo.Name
.
function TShellTreeNode.ShortFilename: String;
begin
Result := Text;
end;
function TShellTreeNode.FullFilename: String;
begin
if (FBasePath <> '') then
Result := AppendPathDelim(FBasePath) + Text
else
//root nodes
Result := Text;
{$if defined(windows) and not defined(wince)}
if (Length(Result) = 2) and (Result[2] = DriveSeparator) then
Result := Result + PathDelim;
{$endif}
end;
It should be noted that the FFileInfo
record is a private field in TShellTreeNode
and the user has no access to the FFileInfo.Name
element beyond the GetShortFileName()
and GetLongFileName()
methods:
This solves the issue.