Watch, Follow, &
Connect with Us
Public Report
Report From: Delphi-BCB/VCL/Win 32 Controls/TTreeView    [ Add a report in this area ]  
Report #:  3455   Status: Closed
TTreeNode loses HasChildren property when TreeView handle is recreated.
Project:  Delphi Build #:  4.453
Version:    7.0 Submitted By:   Constantine Yannakopoulos
Report Type:  Basic functionality failure Date Reported:  1/30/2003 9:54:10 AM
Severity:    Serious / Highly visible problem Last Updated: 3/20/2012 2:24:39 AM
Platform:    All versions Internal Tracking #:   193516
Resolution: Fixed (Resolution Comments) Resolved in Build: : 9.0.1761.24408
Duplicate of:  None
Voting and Rating
Overall Rating: (8 Total Ratings)
4.75 out of 5
Total Votes: 2
Description
In several cases it is necessary to perform incremental fetching of a treeview's nodes, either because initializing the whole tree structure is costly or because there are circular references in the structure that would make full initialization enter infinite recursion. To do this one would set the property TTreeNode.HasChildren to True at the time the node is added and use the event handler OnExpanding to load the child nodes when the node is being expanded.

This technique works but if for some reason the treeview's window handle is recreated then all the nodes that have ther HasChildren property set to True but have not been expanded yet by the user (therefore do not have any child nodes) lose their [+] sign, The reson for this is that the value of the TTreeNode's property HasChildren is not stored in the internal stream when the control's handle is recreated.
Steps to Reproduce:
Start a new project, place a TTreeView on the main form and name it "Tree".
In the form's OnCreate event handler add the following code:

procedure TForm1.FormCreate(Sender: TObject);
var
  Node: TTreeNode;
begin
  Node := Tree.Items.Add(nil, 'Current User');
  Node.HasChildren := True;
end;

that adds the tree's root node. Then in the treeview's OnExpanding event handler add the code:

procedure TForm1.TreeExpanding(Sender: TObject; Node: TTreeNode;
  var AllowExpansion: Boolean);
var
  SubKeys: TStringList;
  I: Integer;
  ChildNode: TTreeNode;
begin
  if Node.HasChildren and (Node.Count = 0) then
  begin
    with TRegistry.Create do
    try
      if OpenKey(string(PChar(Node.Data)), False) then
      begin
        SubKeys := TStringList.Create;
        try
          GetKeyNames(SubKeys);
          for I := 0 to SubKeys.Count - 1 do
          begin
            ChildNode := Tree.Items.AddChild(Node, SubKeys[I]);
            ChildNode.Data := StrNew(PChar(string(PChar(Node.Data)) + PathDelim + SubKeys[I]));
            ChildNode.HasChildren := True;
          end;
          Node.HasChildren := SubKeys.Count <> 0;
        finally
          SubKeys.Free;
        end;
      end;
    finally
      Free;
    end;
  end;
end;

As you can see the code reads the subkeys of the selected registry key node and adds the corresponding child nodes. Notice that every new node has its HasChildren property set to True.

The treeview works well (although it has a memory leak) and displays the registry keys under HKEY_CURRENT_USER.

Then add a button to the form and in its OnClick handler write the code:

procedure TForm1.Button1Click(Sender: TObject);
begin
  Tree.Perform(CM_RECREATEWND, 0, 0);
end;

that will force the treview to recreate its handle. Press the button and notice that any nodes that were not expanded have lost their [+] sign, so now you cannot expand them, even if the underlying registry key has subkeys.
Workarounds
I am afraid this can only be fixed in ComCtrls.pas itself. To fix modify TTreeNode.WriteData like this:

procedure TTreeNode.WriteData(Stream: TStream; Info: PNodeInfo);
var
  I, Size, L, ItemCount: Integer;
begin
  L := Length(Text);
  if L > 255 then L := 255;
  Size := SizeOf(TNodeInfo) + L - 255;
  Info^.Text := Text;
  Info^.ImageIndex := ImageIndex;
  Info^.SelectedIndex := SelectedIndex;
  Info^.OverlayIndex := OverlayIndex;
  Info^.StateIndex := StateIndex;
  Info^.Data := Data;
  ItemCount := Count;
// ------------------------------------------------------------------
  if (ItemCount = 0) and HasChildren then
    Info^.Count := -1
  else
// ------------------------------------------------------------------
  Info^.Count := ItemCount;
  Stream.WriteBuffer(Size, SizeOf(Size));
  Stream.WriteBuffer(Info^, Size);
  for I := 0 to ItemCount - 1 do
    Item[I].WriteData(Stream, Info);
end;

and TTreeNode.ReadData like this:

procedure TTreeNode.ReadData(Stream: TStream; Info: PNodeInfo);
var
  I, Size, ItemCount: Integer;
  LNode: TTreeNode;
begin
  Owner.ClearCache;
  Stream.ReadBuffer(Size, SizeOf(Size));
  Stream.ReadBuffer(Info^, Size);
  Text := Info^.Text;
  ImageIndex := Info^.ImageIndex;
  SelectedIndex := Info^.SelectedIndex;
  StateIndex := Info^.StateIndex;
  OverlayIndex := Info^.OverlayIndex;
  Data := Info^.Data;
  ItemCount := Info^.Count;
//---------------------------------------------------------
  HasChildren := ItemCount <> 0;
  if ItemCount > 0 then
//---------------------------------------------------------
  for I := 0 to ItemCount - 1 do
  begin
    LNode := Owner.AddChild(Self, '');
    LNode.ReadData(Stream, Info);
    Owner.Owner.Added(LNode);
  end;
end;

Notice that the change is in the implementation section only.

While we are at it, i think that the streaming of TreeNodes should be made protected-virtual, so that if somebody decides to override TTreeView.CreateNode and return a TTreeNode descendant with additional data-members has also the chance to store these data-members in the internal stream.

---------

I am afraid this can only be fixed in ComCtrls.pas itself. To fix modify TTreeNode.WriteData like this:

procedure TTreeNode.WriteData(Stream: TStream; Info: PNodeInfo);
var
  I, Size, L, ItemCount: Integer;
begin
  L := Length(Text);
  if L > 255 then L := 255;
  Size := SizeOf(TNodeInfo) + L - 255;
  Info^.Text := Text;
  Info^.ImageIndex := ImageIndex;
  Info^.SelectedIndex := SelectedIndex;
  Info^.OverlayIndex := OverlayIndex;
  Info^.StateIndex := StateIndex;
  Info^.Data := Data;
  ItemCount := Count;
// ------------------------------------------------------------------
  if (ItemCount = 0) and HasChildren then
    Info^.Count := -1
  else
// ------------------------------------------------------------------
  Info^.Count := ItemCount;
  Stream.WriteBuffer(Size, SizeOf(Size));
  Stream.WriteBuffer(Info^, Size);
  for I := 0 to ItemCount - 1 do
    Item[I].WriteData(Stream, Info);
end;

and TTreeNode.ReadData like this:

procedure TTreeNode.ReadData(Stream: TStream; Info: PNodeInfo);
var
  I, Size, ItemCount: Integer;
  LNode: TTreeNode;
begin
  Owner.ClearCache;
  Stream.ReadBuffer(Size, SizeOf(Size));
  Stream.ReadBuffer(Info^, Size);
  Text := Info^.Text;
  ImageIndex := Info^.ImageIndex;
  SelectedIndex := Info^.SelectedIndex;
  StateIndex := Info^.StateIndex;
  OverlayIndex := Info^.OverlayIndex;
  Data := Info^.Data;
  ItemCount := Info^.Count;
//---------------------------------------------------------
  HasChildren := ItemCount <> 0;
  if ItemCount > 0 then
//---------------------------------------------------------
  for I := 0 to ItemCount - 1 do
  begin
    LNode := Owner.AddChild(Self, '');
    LNode.ReadData(Stream, Info);
    Owner.Owner.Added(LNode);
  end;
end;

Notice that the change is in the implementation section only.

While we are at it, i think that the streaming of TreeNodes should be made protected-virtual, so that if somebody decides to override TTreeView.CreateNode and return a TTreeNode descendant with additional data-members has also the chance to store these data-members in the internal stream.
Attachment
None
Comments

Sebastian Modersohn at 2/5/2003 3:55:44 PM -
Very good report! You may add a reference to the most prominent TreeView where this technique (setting HasChildren) is used: the one in Windows Explorer!

ACTUALLY: The same is true (loosing state after window recreation) for "Expanded" as well! If you recreate a TreeView all nodes are collapsed...

Server Response from: ETNACODE01