Is it possible to display one object multiple times in a VirtualStringTree?

倾然丶 夕夏残阳落幕 提交于 2019-12-03 20:58:21

Of course you can. You need to separate nodes and data in your mind. Nodes in TVirtualStringTree do not need to hold the data, the can simply be used to point to an instance where the data can be found. And of course you can point two nodes to the same object instance.

Say you have a list of TPerson's and you haev a tree where you want to show each person in different nodes. Then you declare the record you use for your nodes simply as something like:

TNodeRecord = record
  ... // anything else you may need  or want
  DataObject: TObject;
  ...
end;

In the code where the nodes are initialized, you do something like:

PNodeRecord.DataObject := PersonList[SomeIndex];

That's the gist of it. If you want a general NodeRecord, like I showed above, then you would need to cast it back to the proper class in order to use it in the various Get... methods. You can of course also make a specific record per tree, where you declare DataObject to be of the specific type of class that you display in the tree. The only drawback is that you then limit the tree to showing information for that class of objects.

I should have a more elaborate example lying around somewhere. When I find it, I'll add it to this answer.


Example

Declare a record to be used by the tree:

RTreeData = record
  CDO: TCustomDomainObject;
end;
PTreeData = ^RTreeData;

TCustomDomainObject is my base class for all domain information. It is declared as:

TCustomDomainObject = class(TObject)
private
  FList: TObjectList;
protected
  function GetDisplayString: string; virtual;
  function GetCount: Cardinal;
  function GetCDO(aIdx: Cardinal): TCustomDomainObject;
public
  constructor Create; overload;
  destructor Destroy; override;

  function Add(aCDO: TCustomDomainObject): TCustomDomainObject;

  property DisplayString: string read GetDisplayString;
  property Count: Cardinal read GetCount;
  property CDO[aIdx: Cardinal]: TCustomDomainObject read GetCDO;
end;

Please note that this class is set up to be able to hold a list of other TCustomDomainObject instances. On the form which shows your tree you add:

TForm1 = class(TForm)
  ...
private
  FIsLoading: Boolean;
  FCDO: TCustomDomainObject;
protected
  procedure ShowColumnHeaders;
  procedure ShowDomainObject(aCDO, aParent: TCustomDomainObject);
  procedure ShowDomainObjects(aCDO, aParent: TCustomDomainObject);

  procedure AddColumnHeaders(aColumns: TVirtualTreeColumns); virtual;
  function GetColumnText(aCDO: TCustomDomainObject; aColumn: TColumnIndex;
    var aCellText: string): Boolean;
protected
  property CDO: TCustomDomainObject read FCDO write FCDO;
public
  procedure Load(aCDO: TCustomDomainObject);
  ...
end;  

The Load method is where it all starts:

procedure TForm1.Load(aCDO: TCustomDomainObject);
begin
  FIsLoading := True;
  VirtualStringTree1.BeginUpdate;
  try
    if Assigned(CDO) then begin
      VirtualStringTree1.Header.Columns.Clear;
      VirtualStringTree1.Clear;
    end;
    CDO := aCDO;
    if Assigned(CDO) then begin
      ShowColumnHeaders;
      ShowDomainObjects(CDO, nil);
    end;
  finally
    VirtualStringTree1.EndUpdate;
    FIsLoading := False;
  end;
end;

All it really does is clear the form and set it up for a new CustomDomainObject which in most cases would be a list containing other CustomDomainObjects.

The ShowColumnHeaders method sets up the column headers for the string tree and adjusts the header options according to the number of columns:

procedure TForm1.ShowColumnHeaders;
begin
  AddColumnHeaders(VirtualStringTree1.Header.Columns);
  if VirtualStringTree1.Header.Columns.Count > 0 then begin
    VirtualStringTree1.Header.Options := VirtualStringTree1.Header.Options
      + [hoVisible];
  end;
end;

procedure TForm1.AddColumnHeaders(aColumns: TVirtualTreeColumns);
var
  Col: TVirtualTreeColumn;
begin
  Col := aColumns.Add;
  Col.Text := 'Breed(Group)';
  Col.Width := 200;

  Col := aColumns.Add;
  Col.Text := 'Average Age';
  Col.Width := 100;
  Col.Alignment := taRightJustify;

  Col := aColumns.Add;
  Col.Text := 'CDO.Count';
  Col.Width := 100;
  Col.Alignment := taRightJustify;
end;

AddColumnHeaders was separated out to allow this form to be used as a base for other forms showing information in a tree.

The ShowDomainObjects looks like the method where the whole tree will be loaded. It isn't. We are dealing with a virtual tree after all. So all we need to do is tell the virtual tree how many nodes we have:

procedure TForm1.ShowDomainObjects(aCDO, aParent: TCustomDomainObject);
begin
  if Assigned(aCDO) then begin
    VirtualStringTree1.RootNodeCount := aCDO.Count;
  end else begin
    VirtualStringTree1.RootNodeCount := 0;
  end;
end;

We are now mostly set up and only need to implement the various VirtualStringTree events to get everything going. The first event to implement is the OnGetText event:

procedure TForm1.VirtualStringTree1GetText(Sender: TBaseVirtualTree; Node:
    PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText:
    string);
var
  NodeData: ^RTreeData;
begin
  NodeData := Sender.GetNodeData(Node);
  if GetColumnText(NodeData.CDO, Column, {var}CellText) then
  else begin
    if Assigned(NodeData.CDO) then begin
      case Column of
        -1, 0: CellText := NodeData.CDO.DisplayString;
      end;
    end;
  end;
end;

It gets the NodeData from the VirtualStringTree and used the obtained CustomDomainObject instance to get its text. It uses the GetColumnText function for this and that was done, again, to allow for using this form as a base for other forms showing trees. When you go that route, you would declare this method virtual and override it in any descendant forms. In this example it is simply implemented as:

function TForm1.GetColumnText(aCDO: TCustomDomainObject; aColumn: TColumnIndex;
  var aCellText: string): Boolean;
begin
  if Assigned(aCDO) then begin
    case aColumn of
      -1, 0: begin
        aCellText := aCDO.DisplayString;
      end;
      1: begin
        if aCDO.InheritsFrom(TDogBreed) then begin
          aCellText := IntToStr(TDogBreed(aCDO).AverageAge);
        end;
      end;
      2: begin
        aCellText := IntToStr(aCDO.Count);
      end;
    else
//      aCellText := '';
    end;
    Result := True;
  end else begin
    Result := False;
  end;
end;

Now that we have told the VirtualStringTree how to use the CustomDomainObject instance from its node record, we of course still need to link the instances in the main CDO to the nodes in the tree. That is done in the OnInitNode event:

procedure TForm1.VirtualStringTree1InitNode(Sender: TBaseVirtualTree;
    ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
  ParentNodeData: ^RTreeData;
  ParentNodeCDO: TCustomDomainObject;
  NodeData: ^RTreeData;
begin
  if Assigned(ParentNode) then begin
    ParentNodeData := VirtualStringTree1.GetNodeData(ParentNode);
    ParentNodeCDO := ParentNodeData.CDO;
  end else begin
    ParentNodeCDO := CDO;
  end;

  NodeData := VirtualStringTree1.GetNodeData(Node);
  if Assigned(NodeData.CDO) then begin
    // CDO was already set, for example when added through AddDomainObject.
  end else begin
    if Assigned(ParentNodeCDO) then begin
      if ParentNodeCDO.Count > Node.Index then begin
        NodeData.CDO := ParentNodeCDO.CDO[Node.Index];
        if NodeData.CDO.Count > 0 then begin
          InitialStates := InitialStates + [ivsHasChildren];
        end;
      end;
    end;
  end;
  Sender.CheckState[Node] := csUncheckedNormal;
end;

As our CustomDomainObject can have a list of other CustomDomainObjects, we also set the InitialStates of the node to include HasChildren when the Count of the lsit is greater than zero. This means that we also need to implement the OnInitChildren event, which is called when the user clicks on a plus sign in the tree. Again, all we need to do there is tell the tree for how many nodes it needs to prepare:

procedure TForm1.VirtualStringTree1InitChildren(Sender: TBaseVirtualTree; Node:
    PVirtualNode; var ChildCount: Cardinal);
var
  NodeData: ^RTreeData;
begin
  ChildCount := 0;

  NodeData := Sender.GetNodeData(Node);
  if Assigned(NodeData.CDO) then begin
    ChildCount := NodeData.CDO.Count;
  end;
end;

That's all folks!!!

As I have shown an example with a simple list, you still need to figure out which data instances you need to link to which nodes, but you should have a fair idea now of where you need to do that: the OnInitNode event where you set the CDO member of the node record to point to the CDO instance of your choice.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!