Search code examples
delphifiremonkey

Error in FMX.Treeview function TTreeViewContent.GetLastVisibleObjectIndex


I am migrating a VCL application to Firemonkey. It features a couple of TTreeview controls which display folder trees. In the main form's OnCreate event handler the paths of the folders are read from a .Ini file and the treeviews are set up. At some point before the main form appears on screen, an exception occurs in the FMX.TreeView TTreeViewContent.GetLastVisibleObjectIndex function, which is:

function TTreeViewContent.GetLastVisibleObjectIndex: Integer;
var
  Item: TTreeViewItem;
begin
  if (FTreeView.FGlobalList.Count > 0) and (FTreeView.FLastVisibleItem < FTreeView.FGlobalList.Count) then
  begin
    Item := FTreeView.FGlobalList[FTreeView.FLastVisibleItem];
    {etc.}
  end
  else
    Result := ControlsCount;
end;

I have inspected the values of the variables and found that FTreeView.FGlobalList.Count =1 and FTreeView.FLastVisibleItem = -1. The error occurs in the statement

Item := FTreeView.FGlobalList[FTreeView.FLastVisibleItem];

since the array index is invalid.

This code seems to be related to determining which treeview items are visible within the scrolling window of the treeview control. Since the error occurs before the form has been displayed, I tried making the treeview invisible during the update of the treview, as in the following code:

procedure TFormMain.UpdateTreeview(var Folder: TFolder; Treeview: TTreeview);
begin
  if Folder= FInputFolder then
    begin
      if not FTreeviewInputFolderValid then
        begin
          Treeview.Visible:= False;
          Treeview.BeginUpdate;
          FolderToTreeView(Folder, Treeview);
          //Treeview.InvalidateRect(Treeview.ContentRect);
          Treeview.ExpandAll;
          Treeview.EndUpdate;
          Treeview.Visible:= True;
          FTreeviewInputFolderValid:= True;
        end;
    end;
  {Ditto for FOutputFolder}
end;

If the program is run without setting up the treeview controls before the form is shown, i.e. by not reading the folder paths from a .ini file and not updating the treeview controls, then the error does not occur.

Any suggestions about how to avoid what seems to be a coding error in function TTreeViewContent.GetLastVisibleObjectIndex?

In answer to Tom, the code of FolderToTreeview is:

procedure TFormMain.FolderToTreeview(Folder: TFolder; Treeview: TTreeview);
var
  TreeviewOwner: TComponent;
  RootNode:  TTreeViewItemFolderCpt;

  procedure AddFolderChildCpts(ParentTreeNode: TTreeViewItemFolderCpt; Folder: TFolder);
  var
    i: integer;
    FolderCpt: TFolderCpt;
    FileCpt: TFileCpt;
    SubFolder: TFolder;
    ChildTreeNode: TTreeViewItemFolderCpt;
    Found: Boolean;
  begin
    {Add all cpts of folder to child nodes of ParentTreeNode}
    for i:= 0 to Folder.CptCount-1 do
      begin
        FolderCpt:= Folder.Cpts[i];
        ChildTreeNode:= TTreeViewItemFolderCpt.Create(ParentTreeNode);
        ParentTreeNode.AddObject(ChildTreeNode);
        ChildTreeNode.Parent:= ParentTreeNode;
        ChildTreeNode.FolderCpt:= FolderCpt;
        ChildTreeNode.OnPaint:= TreeViewItemPaint;
        if FolderCpt is TFileCpt then
          begin
            FileCpt:= FolderCpt as TFileCpt;
            ChildTreeNode.ImageIndex:= 1;
          end
        else if FolderCpt is TFolder then
          begin
            SubFolder:= FolderCpt as TFolder;
            ChildTreeNode.ImageIndex:= 0;
            {Recursively add subfolder:}
            AddFolderChildCpts(ChildTreeNode, SubFolder);
          end;
      end;
  end;

begin
  if not Folder.IsSorted then
    Folder.Sort(True);
  {Delete all existing nodes in tree:}
  Treeview.Clear;
  {Create a new root node and add to tree:}
  RootNode:= TTreeviewItemFolderCpt.Create(Treeview);
  Treeview.AddObject(RootNode);
  RootNode.Parent:=  Treeview;
  {Link folder object to root tree node:}
  RootNode.FolderCpt:= Folder;
  RootNode.ImageIndex:= 0;
  RootNode.OnPaint:= TreeViewItemPaint;
  {Now install child folder cpts:}
  AddFolderChildCpts(RootNode, Folder);
end;

TFolder, TFolderCpt, TFileCpt are elements of a separate class hierarchy to store in a memory tree structure the names and metadata of all files and folders under the root folder. The root object of class TFolder has a method Read(Path: string) which generates all its nodes by visiting the files and directories under the root directory using FindFirst and FindNext procedures. Folder.Read is called before FolderToTreeview is called. Because of this extra complication I don't think that you be able to run the FolderToTreeview method.


Solution

  • I have found a solution to the problem I posted. The method FolderToTreeview has been amended as follows:

    procedure TFormMain.FolderToTreeview(Folder: TFolder; Treeview: TTreeview;
                                               PaintEventHandler: TOnPaintEvent);
    const
      COptionA= True;   {True if TreeviewItem.Clear used to destroy existing tree nodes}
      COptionB= False;  {True if PaintEventHandler assigned after tree nodes have been created}
    var
      RootNode:  TTreeViewItemFolderCpt;
      i: integer;
    
      procedure AddFolderChildCpts(ParentTreeNode: TTreeViewItemFolderCpt;
                         Folder: TFolder; PaintEventHandler: TOnPaintEvent);
      var
        i: integer;
        FolderCpt: TFolderCpt;
        FileCpt: TFileCpt;
        SubFolder: TFolder;
        ChildTreeNode: TTreeViewItemFolderCpt;
      begin
        {Add all cpts of folder to child nodes of ParentTreeNode}
        for i:= 0 to Folder.CptCount-1 do
          begin
            FolderCpt:= Folder.Cpts[i];
            ChildTreeNode:= TTreeViewItemFolderCpt.Create(ParentTreeNode, FolderCpt);
            ParentTreeNode.AddObject(ChildTreeNode);
            ChildTreeNode.Parent:= ParentTreeNode;
            ChildTreeNode.OnPaint:= PaintEventHandler;
            if FolderCpt is TFileCpt then
              begin
                FileCpt:= FolderCpt as TFileCpt;
                ChildTreeNode.ImageIndex:= 1;
              end
            else if FolderCpt is TFolder then
              begin
                SubFolder:= FolderCpt as TFolder;
                ChildTreeNode.ImageIndex:= 0;
                {Recursively add subfolder:}
                AddFolderChildCpts(ChildTreeNode, SubFolder, PaintEventHandler);
              end;
          end;
      end;
    
      procedure SetSubNodesPaintEventHandler(ParentTreeNode: TTreeViewItem;
           PaintEventHandler: TOnPaintEvent);
      var
        i, j: integer;
        ChildTreeNodeI: TTreeViewItem;
      begin
        ParentTreeNode.OnPaint:= PaintEventHandler;
        {Assign PaintEventHandler to all  child nodes of TreeNode}
        for i:= 0 to ParentTreeNode.Count-1 do
          begin
            ChildTreeNodeI:= ParentTreeNode.Items[i];
            SetSubNodesPaintEventHandler(ChildTreeNodeI, PaintEventHandler);
          end;
      end;
    
    begin
      if not Folder.IsSorted then
        Folder.Sort(True);
      Treeview.BeginUpdate;
      try
        {Delete all existing nodes in tree:}
        if COptionA then
          Treeview.Clear
        else
          for i:= Treeview.Count-1 downto 0 do
            Treeview.Items[i].Release;
        {Create a new root node and add to tree:}
        RootNode:= TTreeviewItemFolderCpt.Create(Treeview, Folder);
        Treeview.AddObject(RootNode);
        RootNode.Parent:=  Treeview;
        {Assign properties to root tree node:}
        RootNode.ImageIndex:= 0;
        {Now install child folder cpts:}
        if COptionB then
          {For testing purposes}
          AddFolderChildCpts(RootNode, Folder, nil)
        else
          AddFolderChildCpts(RootNode, Folder, PaintEventHandler);
        {Assign OnPaint event handler:}
        RootNode.OnPaint:= PaintEventHandler;
        if COptionB then
          {For testing purposes}
          SetSubNodesPaintEventHandler(RootNode, PaintEventHandler);
      finally
        Treeview.EndUpdate;
      end;
    end;
    

    The most important change was to enclose the Treeview nodes update code in BeginUpdate ... EndUpdate brackets.

    The FolderCpt linked to the TreeviewItem is now assigned in the TTreeviewItemFolderCpt constructor:

    TTreeViewItemFolderCpt = class(TTreeViewItem)
      protected
        FFolderCpt: TFolderCpt;
        procedure SetFolderCpt(const Value: TFolderCpt);
        function GetName: string;
        procedure SetName(Value: string);
      public
        constructor Create(Owner: TComponent; FolderCpt: TFolderCpt);
        property FolderCpt: TFolderCpt read FFolderCpt write SetFolderCpt;
        property Name: string read GetName write SetName;
      end;
    
    constructor TTreeViewItemFolderCpt.Create(Owner: TComponent;
      FolderCpt: TFolderCpt);
    begin
      inherited Create(Owner);
      FFolderCpt:= FolderCpt;
    end;