Jump to content
Renate Schaaf

FMX-TTreeview: Intercept click on Expand-button

Recommended Posts

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 by Renate Schaaf

Share this post


Link to post

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 by Renate Schaaf

Share this post


Link to post
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

image.png.3fb98b3df844f663d703e202446c8b7d.png

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 by Renate Schaaf

Share this post


Link to post
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:

EmbarcaderoTree.png.f8263b76d17b9724130617fc45d07b61.png

I also really want to avoid reading in all directories in the root.

Edited by Renate Schaaf

Share this post


Link to post
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

@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.

 

 

bds_21Y118wjgt.gif

Edited by programmerdelphi2k

Share this post


Link to post

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
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
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 by Remy Lebeau
  • Like 1

Share this post


Link to post

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
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

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×