Renate Schaaf 64 Posted May 9, 2023 (edited) I'm building a Directory-tree for FMX as a descendent of TTreeview. Because reading in the full directory-tree of a root-directory takes way too long, I only read in the first 2 levels. After that I only want to create sub-items as necessary for any item that the user selects or expands. In the VCL-version this can be done by overriding TTreeview.Change and TTreeview.CanExpand. For the FMX-version I can override TTreeview.DoChange, but I can't find an analogue of CanExpand. So I tried to override MouseDown. But the Treeview never gets the MouseDown when the user clicks the expand-button. Is there anything else I could do? Here some of the relevant code: procedure TDirectoryTree.CreateSubNodesToLevel2(aItem: TTreeViewItem); var DirArraySize1, DirArraySize2, i, j: integer; DirArray1, DirArray2: TStringDynArray; TreeItem, TreeItem2: TTreeViewItem; NewName: string; begin DirArray1 := TDirectory.GetDirectories(GetFullFolderName(aItem)); DirArraySize1 := Length(DirArray1); if DirArraySize1 > 0 then begin for i := 0 to DirArraySize1 - 1 do begin NewName := DirArray1[i]; if aItem.Count = i then begin TreeItem := TTreeViewItem.Create(self); fDirectoryDict.Add(NativeUInt(TreeItem), NewName); TreeItem.Text := ExtractFilename(NewName); TreeItem.ImageIndex := 0; TreeItem.Parent := aItem; end else TreeItem := aItem.Items[i]; DirArray2 := TDirectory.GetDirectories(NewName); DirArraySize2 := Length(DirArray2); for j := 0 to DirArraySize2 - 1 do begin if TreeItem.Count = j then begin TreeItem2 := TTreeViewItem.Create(self); fDirectoryDict.Add(NativeUInt(TreeItem2), DirArray2[j]); TreeItem2.Text := ExtractFilename(DirArray2[j]); TreeItem2.ImageIndex := 0; TreeItem2.Parent := TreeItem; end; end; end; end; end; function TDirectoryTree.GetFullFolderName(aItem: TTreeViewItem): string; begin Result := fDirectoryDict.Items[NativeUInt(aItem)]; end; procedure TDirectoryTree.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); begin if (HoveredItem <> nil) then CreateSubNodesToLevel2(HoveredItem); inherited; end; Edited May 9, 2023 by Renate Schaaf Share this post Link to post
Renate Schaaf 64 Posted May 9, 2023 (edited) I got it to "work" by defining an interposer class for TTreeViewItem type TTreeViewItem = class(FMX.TreeView.TTreeViewItem) protected procedure SetIsExpanded(const Value: Boolean); override; end; ... procedure TTreeViewItem.SetIsExpanded(const Value: Boolean); begin inherited; if IsExpanded then if TreeView is TDirectoryTree then TDirectoryTree(TreeView).CreateSubNodesToLevel2(self); end; It doesn't seem to interfere with the inner workings of TTreeView, but it makes me feel a bit uneasy. Also whenever you now use an item of the treeview you have to do a typecast TTreeviewItem(aTreeviewItem). I have attached the a small test project. Tested for Windows platforms only. DirectoryTest.zip Edited May 9, 2023 by Renate Schaaf Share this post Link to post
Remy Lebeau 1397 Posted May 9, 2023 5 hours ago, Renate Schaaf said: In the VCL-version this can be done by overriding TTreeview.Change and TTreeview.CanExpand. Why not Expand() instead? That is the method which calls the OnExpanded event handler after CanExpand() says it is OK to do so. 5 hours ago, Renate Schaaf said: For the FMX-version I can override TTreeview.DoChange, but I can't find an analogue of CanExpand. All I see available is TTreeView.ItemExpanded(), but it is not virtual, so you can't override it. Internally, TTreeViewItem has an FButton member of type TCustomButton which it assigns an OnClick handler to. That handler simply toggles the TTreeViewItem.IsExpanded property, which calls TCustomTreeView.ItemExpanded(), which doesn't fire any events about the state change into user code, AFAICS. Share this post Link to post
Renate Schaaf 64 Posted May 9, 2023 (edited) 40 minutes ago, Remy Lebeau said: Why not Expand() instead? There was a reason I didn't do that, but I can't remember :). What do you think about using the interposer class for TTreeViewItem? Meanwhile I excluded Directories which have faHidden or faSystem, because that was creating errors. Other than that it seems to be working OK, unless you try to expand something like the Windows-folder, that still takes forever. I would have liked to use TTreeView.ItemExpanded, but as you say, I can't. Edited May 9, 2023 by Renate Schaaf Share this post Link to post
programmerdelphi2k 237 Posted May 9, 2023 (edited) @Renate Schaaf maybe some like this; ( for a "BIG" directory , needs patience... of course!) Deleted: sorry! Edited May 9, 2023 by programmerdelphi2k Share this post Link to post
programmerdelphi2k 237 Posted May 9, 2023 (edited) @Renate Schaaf now, it's easer use the NODE to open the folder and show your files into it! Deleted: sorry! Edited May 9, 2023 by programmerdelphi2k Share this post Link to post
Renate Schaaf 64 Posted May 9, 2023 (edited) 2 hours ago, programmerdelphi2k said: maybe some like this; The speed isn't too bad, certainly better than filling the nodes recursively. But it still takes several minutes to display the content of my personal folder, whereas in my version you'd see it almost instantly. There must be something wrong with the logic in your MyFillTreeView. It never displays sibling nodes right. This is for example the output I get when I choose the Embarcadero folder under documents as root: I also really want to avoid reading in all directories in the root. Edited May 9, 2023 by Renate Schaaf Share this post Link to post
programmerdelphi2k 237 Posted May 9, 2023 (edited) @Renate Schaaf I see the "my fault", then, just use this new procedures Deleted: sorry! Edited May 9, 2023 by programmerdelphi2k Share this post Link to post
programmerdelphi2k 237 Posted May 9, 2023 (edited) Deleted: sorry! Edited May 9, 2023 by programmerdelphi2k Share this post Link to post
Renate Schaaf 64 Posted May 9, 2023 1 hour ago, programmerdelphi2k said: programmerdelphi2k Nevermind, I have it working my way, it's fast, and I can also avoid all those errors. But thanks for your time. Renate Share this post Link to post
programmerdelphi2k 237 Posted May 9, 2023 (edited) @Renate Schaaf NOW, all works as expected! it was necessary to forget the use of TDirectory, due to the fact that it delivered the matrix ready, however, with all the absolute paths for each folder found in the indicated path. So, I preferred to go straight to the source and use the functions FindFirst, FindNext, and have access to each path and thus create the TreeView nodes as it should be. much less code and good speed for both functions: finding directories and showing their files implementation {$R *.fmx} uses System.IOUtils, System.Diagnostics; var SW: TStopWatch; function MyFullDirectoryPath(ANode: TTreeViewItem): string; var LDirs: TArray<string>; begin if ANode <> nil then begin while ANode <> nil do begin LDirs := LDirs + [ANode.Text]; ANode := ANode.ParentItem; end; // for var i: integer := high(LDirs) downto 0 do result := result + '\' + LDirs[i]; // if result.StartsWith('\') then result := result.Remove(0, 1); // "\C:..." end; end; function MyCreateSubNode(ATV: TTreeView; ADirRoot: string; ASubDirName: string; ALevel: integer; ANode: TTreeViewItem): TTreeViewItem; var LSrcRec : TSearchRec; LTrue : boolean; LResult : integer; LCurDirName: string; LSubNode : TTreeViewItem; LSlash : string; XNode : TTreeViewItem; begin LCurDirName := ''; LSlash := ''; XNode := nil; result := nil; // if not ASubDirName.IsEmpty then LSlash := '\'; // LResult := FindFirst(ADirRoot + '\' + ASubDirName + LSlash + '*.*', System.SysUtils.faDirectory, LSrcRec); try LTrue := (LResult = 0); // if LTrue and (ALevel = 0) and (ANode = nil) then begin //ATV.Clear; // clear all!! ??? ANode := TTreeViewItem.Create(ATV); ANode.Text := ADirRoot; ATV.AddObject(ANode); end; // // while LTrue do begin LCurDirName := LSrcRec.Name; // if (LCurDirName > '..') and (LSrcRec.Attr = System.SysUtils.faDirectory) then begin if (ANode <> nil) then begin LSubNode := TTreeViewItem.Create(ATV); LSubNode.Text := LCurDirName; ANode.AddObject(LSubNode); XNode := LSubNode; end; // result := MyCreateSubNode(ATV, ADirRoot + '\' + ASubDirName, LCurDirName, 1, XNode); end; // LTrue := FindNext(LSrcRec) = 0; end; // if result = nil then exit; finally if LResult = 0 then FindClose(LSrcRec); end; end; procedure MyShowFilesInDirectory(const ANode: TTreeViewItem; const AMemo: TMemo); var LText: string; begin LText := MyFullDirectoryPath(ANode); // if not LText.IsEmpty then begin try AMemo.Text := 'Directory:' + slinebreak + LText + slinebreak + slinebreak + 'Files:'; // if TDirectory.Exists(LText) then AMemo.Lines.AddStrings(TDirectory.GetFiles(LText)) else AMemo.Lines.Add('this path does not exists anymore, or...'); except on E: Exception do AMemo.Text := 'Error on "MyShowFilesInDirectory"' + slinebreak + E.Message; end; end; end; procedure TForm2.BtnFindDirectoriesClick(Sender: TObject); var LNode: TTreeViewItem; begin // // TreeView1.Clear; // needs reset it!!! // SW.StartNew; SW.Start; // { LNode := TTreeViewItem.Create(TreeView1); LNode.Text := 'Hello World'; TreeView1.AddObject(LNode); MyCreateSubNode(TreeView1, 'C:\Users\Public\Documents\Embarcadero', '', 1, LNode); } // TreeView1.Clear; MyCreateSubNode(TreeView1, 'C:\Users\Public\Documents\Embarcadero', '', 0, nil); // SW.Stop; // ShowMessage('Finding directory: Time: ' + SW.Elapsed.Duration.ToString + slinebreak + '... after this, wait for expand all nodes!'); // TreeView1.ExpandAll; end; procedure TForm2.BtnExpand_Colapse_ALLClick(Sender: TObject); begin if TreeView1.CountExpanded = 0 then TreeView1.ExpandAll else TreeView1.CollapseAll; end; procedure TForm2.TreeView1Click(Sender: TObject); begin SW.StartNew; SW.Start; // MyShowFilesInDirectory(TreeView1.Selected, Memo1); // SW.Stop; // ShowMessage('Finding directory: Time: ' + SW.Elapsed.Duration.ToString); end; initialization ReportMemoryLeaksOnShutdown := true; SW := TStopWatch.Create; end. Edited May 10, 2023 by programmerdelphi2k Share this post Link to post
Fr0sT.Brutal 900 Posted May 10, 2023 Usually the expand button is shown for all folder nodes and when you click it, it either expands or disappears if the folder contains no displayable items. But you can scan one level deeper beforehand to show correct buttons. Share this post Link to post
Renate Schaaf 64 Posted May 10, 2023 9 hours ago, programmerdelphi2k said: NOW, all works as expected! Yes, it works, but it is still too slow to be useable as an explorer tree. Let me make a bold statement: You will never be able to create a functioning explorer tree with the plain TTreeview in FMX, if you insist on reading in the full directory tree of a root directory. 2 hours ago, Fr0sT.Brutal said: But you can scan one level deeper beforehand to show correct buttons. Yes, that's why I want to scan 2 levels at a time, so the correct expand buttons are always shown. But for this I need to also know when the user expands a node without actually selecting it. By defining an interposer class for TTreeViewItem, which overrides TTreeViewItem.SetIsExpanded, I can now intercept this event. I would have been interested in learning, whether anybody sees a problem with this approach. I don't see any in the use case I need it for. Renate Share this post Link to post
Renate Schaaf 64 Posted May 10, 2023 You can see the brandnew explorer-trees in action here: https://github.com/rmesch/Parallel-Bitmap-Resampler. See the demos ThreadsInThreads* Renate Share this post Link to post
Remy Lebeau 1397 Posted May 10, 2023 (edited) 6 hours ago, Renate Schaaf said: Yes, that's why I want to scan 2 levels at a time, so the correct expand buttons are always shown. Too bad FMX's TTreeViewItem doesn't have a ShowButtons property, like VCL's TTreeNode does. So unfortunately, in FMX you have no choice but to populate the immediate child nodes just for TTreeViewItem to decide for itself whether or not to display its expand button (ie, when its VisibleCount is > 0). You don't need to scan the full 2nd level right away, though. When creating a new TTreeViewItem, just scan enough to discover its 1st subfolder and then stop scanning. You can't accomplish that with any of the TDirectory methods, but you can with FindFirst()/FindNext(). Or, you could just use a dummy child node instead, no scanning needed. Either way, when the TTreeViewItem is then expanded for the first time, you can then (remove the dummy and) do a full scan of the 2nd level to finish populating the TTreeViewItem with all of the child nodes as needed. Edited May 10, 2023 by Remy Lebeau 1 Share this post Link to post
programmerdelphi2k 237 Posted May 10, 2023 I have achieved using FindFirst/FindNext in "RELEASE/FMX" mode the time below. Naturally, using OS functions (at a lower level) would be ideal for a task like this. Total dirs found / Nodes created: 852 'C:\Users\Public\Documents\Embarcadero' Finding directory: Time: 00:00:00.0924180 (not expanded) - TreeView1.BeginUpdate; ... MyCreateSubNode(TreeView1, LDirRootForTest, '', 0, nil); ... TreeView1.EndUpdate; the reading of the files found in the clicked directory/node will also be relative... but generally it will be very fast, respecting the quantity, of course. And, to speed things up even more, some kind of "cache" of the previous read could be implemented, as I think is done at the level of the O.S. ... after this, wait for expand all nodes! Expand all directory: Time: 00:00:00.7945893 Collapse all directory: Time: 00:00:00.0065527 MS Explorer shows (by default) only the first level of a directory, and when expanding all subfolders of all directories, the time will also be longer. And, if by necessity, you need to expand folders/sub-folders on the network, this time will be even longer... Share this post Link to post
Renate Schaaf 64 Posted May 11, 2023 14 hours ago, Remy Lebeau said: You don't need to scan the full 2nd level right away, though. Good idea. Going back to FindFirst/FindNext might have been the better design right from the beginning, but right now I'm satisfied with the speed. I just had to fix the VCL-tree, because I forgot that the darn thing recreates its Window-handle on any DPI-change. Arghh! Share this post Link to post