2008. június 15., vasárnap

Build a TTreeView from a file


Problem/Question/Abstract:

I would like to populate a TTreeView from a simple file with the following structure

Key: Integer (unique)
Name: String (description)
Parent: Integer (key of parent in treeview)

I assume that the key and parent fields are all I need to build the treeview (parent = 0 would be a root node)

Answer:

I would break this down into two steps:

1) Read the file into memory
2) Populate the treeview using a recursive function


1) One method of doing this would be by building a TCollection/ TCollectionItem pair of classes. The TCollectionItems just need three fields:

TInputItem = class(TCollectionItem)
private
  fKey: integer;
  fName: string;
  fParent: integer;
public
  property Key: integer read fKey write fKey;
  property Name: string read fName write fName;
  property Parent: integer read fParent write fParent;
end;

Note: using properties is not strictly necessary, but is good style as it allows easier subsequent amendment.

Now we could use a standard TCollection to hold our TInputItems but it is neater to have a descendent of this too:

TInputCollection = class(TCollection)
public
  function AddItem(const AName: string; AKey, AParent: integer): TInputItem;
  property InputItem[index: integer]: TInputItem read GetInputItem; default;
end;

Creating a default property like InputItem above makes coding very tidy. It allows us to do the following:

var
  InputCollection: InputCollection;
  ix: integer;

InputCollection := TInputCollection.Create(TInputItem);
InputCollection.AddItem('First', 1, 0);
InputCollection.AddItem('Second', 2, 0);
InputCollection.AddItem('FirstChild', 3, 1);

for ix := 0 to InputCollection.Count - 1 do
  if InputCollection[ix].Parent = 0 then
    {DoSomething};

The last line, because of the index property being declared default, is the same as:

if InputCollection.InputItem[ix].Parent = 0 then
  {DoSomething;}

Without the property at all, you would code:

if TInputItem(InputCollection.Items[ix]).Parent = 0 thenDoSomething;
{DoSomething;}

In order to support the above, the implementation of the two methods:

function TInputCollection.AddItem(const AName: string; AKey, AParent: integer):
  TInputItem;
begin
  Result := Add as TInputItem;
  Result.Key := AKey;
  Result.Name := AName;
  Result.Parent := AParent;
end;

function TInputCollection.GetInputItem(index: integer): TInputItem;
begin
  Result := Items[ix] as TInputItem;
end;

We can now design an overall structure of a PopulateTree procedure:

procedure PopulateTree(tv: TTreeView);
var
  ic: TInputCollection;
begin
  ic := TInputCollection.Create(TInputItem);
  try
    LoadTreeItems(ic);
    PopulateTreeItems(tv, nil, ic, 0);
  finally
    ic.Free;
  end;
end;

LoadTreeItems can be tested via code similar to:

procedure LoadTreeItems(ic: TInputCollection);
begin
  ic.AddItem('First', 1, 0);
  ic.AddItem('Second', 2, 0);
  ic.AddItem('FirstChild', 3, 1);
end;

before replacing with your own loop through your input file. PopulateTreeItems is passed the treeview, the parent node and the parent id and it is a recursive routine.


2) Having done all the above, this part is now very easy. PopulateTreeItems iterates through the collection looking for items that match the passed parent id. For each item that matches, it adds a treenode and then calls PopulateTreeItems passing itself as the parent:

procedure PopulateTreeItems(tv: TTreeView; pnode: TTreeNode; ic: TInputCollection;
  parent: integer);
var
  node: TTreeNode;
  ix: integer;
begin
  for ix := 0 to ic.Count - 1 do
  begin
    if ic[ix].Parent = parent then
    begin
      node := tv.Items.Add(pnode, ic[ix].Name);
      PopulateTreeItems(tv, node, ic, ic[ix].Key); {recursive call}
    end;
  end;
end;

I apologise in advance if there are problems with the above code. It is completely untested. In practice, I don't do things quite like that, but populate treenodes on demand via the OnExpand event handler.

Nincsenek megjegyzések:

Megjegyzés küldése