2008. április 30., szerda

How to create nodes and subnodes of a TTreeView at runtime to represent a master - detail relationship


Problem/Question/Abstract:

I would like to present a master - detail relationship within a TTreeView upon opening a query at runtime. Getting the correct info is not a problem but I'm totally stumped by the use of a TTreeView. Are there commands usable at runtime to enable me to create and edit the nodes/ sub nodes of the treeview to present the master records as nodes and the detail records as sub nodes within the treeview?

Answer:

Here's an example of creating / maintaining TreeNodes at runtime. Before I take off, I assume that you use database tables like the following:

A MASTERTABLE, with fields M_ID (unique identifier) and M_NAME (a descriptive name);
A DETAILTABLE, with fields D_ID (unique identifier), D_NAME (a descriptive name), and D_M_ID (a foreign key value that links the detail record to a master record.)

This could be quite different from what you have, but I need to make assumptions in order to write this example.

The first step of the process is to add all master records as parent nodes of detail nodes. It goes without saying that I need to add parent nodes first, since detail nodes are 'dependent' of them.

You can add all master records to the TreeView by looping the query at runtime and do something like this:

{Get all master records in query}
{ ... }
while not qryMasterRecords.EOF do
begin
  {some code will follow here, be patient my friend.}
  TreeView1.Items.Add(nil, qryMasterRecords.FieldByName('M_NAME').AsString);
  qryMasterRecords.Next;
end;
{ ... }

The Add method (of a TTreeNodes object) adds the node to the TreeView; the first parameter specifies the parent node. (in this case, nil means that it is added as a root node.) The second parameter is the node name that is represented in the TreeView. (this is the Text property of a TTreeNode.)

However, I am finished yet with the master records. How the heck am I going to identify the master nodes when I want to insert detail nodes later on? When adding a detail node, I need to know what its parent should be. A bad answer (to me) is to say that one can use the node name as an identifier. Since we have a unique identifier for each master record in the database, why don't we use it?

The solution to this lies in the Data property of the TreeNode object: This is a pointer one can assign to an application-designed object. The intention is to use such an object to store the unique identifier of the master record.

Let's use an record-type object like this:

type
  PMaster = ^TMaster
    TMaster = record
    Identifier: integer;
  end;

Assuming these types are used, I modify the master-node-adding code to this:

{ ... }
var
  MasterPtr: PMaster;
  { ... }
  {Get all master records in query}
  { ... }
  while not qryMasterRecords.EOF do
  begin
    New(MasterPtr);
    Master^.Identifier := qryMasterRecords.FieldByname('M_ID').AsInteger);
  TreeView1.Items.AddObject(nil, qryMasterRecords.FieldByName('M_NAME').AsString,
    MasterPtr);
  qryMasterRecords.Next;
end;
{ ... }

At runtime, I create a record type object for each record that is found in the query. I use a slightly extended version of the Add method. AddObject also links MasterPtr with the Data property of the new node.

For now, I have finished with the master nodes: The next step is to add all detail nodes. I need to write a small function that searches for a TreeNode with a specified M_ID value. I need this while adding detail nodes, because I need to identify a node that is the parent node of the detail node that is to be inserted.

function SearchMasterNode(iM_ID: integer): TTreeNode;
{Searches for a node with a specified M_ID value. Returns the TreeNode that has the
specified M_ID value. When it is not found, nil is returned.}
var
  iCount: integer;
begin
  {Default value to return}
  Result := nil;
  {For your info: iterating like this loops through all nodes in a TreeView, including detail nodes}
  for iCount := 0 to TreeView1.Items.Count - 1 do
  begin
    if Assigned(TreeView1.Items.Item[iCount].Data) then
      if PMaster(TreeView1.Items.Item[iCount].Data)^.Identifier = iM_ID then
        Result := TreeView1.Items.Item[iCount];
    {We got a match !}
  end;
end;

From now on, adding detail nodes is much like adding master nodes, with one extra move: a search for a parent node.

{ ... }
{Insert all master nodes to the TreeView}
{ ... }
var
  MasterNode: TTreeNode;

  {Get all detail records in query}
  { ... }
  while not qryDetailRecords.EOF do
  begin
    MasterNode := SearchMasterNode(qryDetailRecords.FieldByName('D_M_ID').AsInteger);
    {For your info: The Data property of this new node is set to nil.}
    TreeView1.Items.AddChild(MasterNode,
                                                                                                        qryDetailRecords.FieldByName('D_NAME').AsString);
    qryDetailRecords.Next;
  end;

The Add method is used here, since I assume that you don't need to identify detail nodes for something else. When you do need this (for example, clicking on a detail node must result in the representation of detail record data in edit boxes, memo-boxes, whatever input control.) use the approach with master nodes.

Finally, to create an application that uses computer memory efficiently, I should free all memory used for the record-type objects. I did this by iterating through all nodes and freeing the data objects:

{ ... }
var
  iCount: integer;
  { ... }
  for iCount := 0 to TreeView1.Items.Count - 1 do
  begin
    if Assigned(TreeView1.Items.Item[iCount].Data) then
      Dispose(TreeView1.Items.Item[iCount].Data);
  end;
  {Finally, free all nodes constructed at runtime}
  TreeView1.Items.Clear;
  { ... }

2 megjegyzés: