1

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.

user11935527
  • 101
  • 1
  • 7
  • Which Delphi version do you have and how are you adding the items (in `FolderToTreeView()`) to the tree view? When I try adding items to a tree view in the forms `OnCreate` I have no problems. – Tom Brunberg Aug 16 '19 at 11:55
  • I have Delphi Community Edition. Items are added using ChildTreeNode:= TTreeViewItemFolderCpt.Create(ParentTreeNode); ParentTreeNode.AddObject(ChildTreeNode); ChildTreeNode.Parent:= ParentTreeNode; – user11935527 Aug 16 '19 at 12:58
  • So, Delphi 10.3.2 Community Edition? Please add the `FolderToTreeView´ code, so I can trace it in the debugger. Ah, you already edited your comment. Good, but maybe you could add it to your question post. – Tom Brunberg Aug 16 '19 at 13:01
  • The few lines of code you shown from `FolderToTreeView()` is not enough to evaluate. Too many uncertainties. There's no error in the `GetLastVisibleObjectIndex`, so the error must be in your code which you havent shown to a sufficient degree in order to analyze. Edit your question and add the complete code from `FolderToTreeView()` – Tom Brunberg Aug 16 '19 at 13:38
  • There's an error in the statement "if (FTreeView.FGlobalList.Count > 0) and (FTreeView.FLastVisibleItem < FTreeView.FGlobalList.Count) then", which is presumably intended to trap invalid values of FTreeView.FLastVisibleItem. It fails to check that this value is >-1. This is a programming error to my mind. – user11935527 Aug 16 '19 at 14:02
  • Sorry, I omitted to say that I am using 10.3 Community Edition. – user11935527 Aug 16 '19 at 14:48
  • *...Because of this extra complication I don't think...* you are absolutely right. Sorry I wasted both your and my time, I should have requested a [reprex] from the start. – Tom Brunberg Aug 16 '19 at 16:31

1 Answers1

1

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;
user11935527
  • 101
  • 1
  • 7