Delphi 是否可以在VirtualStringTree中多次显示一个对象?

Delphi 是否可以在VirtualStringTree中多次显示一个对象?,delphi,data-structures,synchronization,nodes,virtualtreeview,Delphi,Data Structures,Synchronization,Nodes,Virtualtreeview,我意识到我真的需要重写我的程序数据结构(不是现在,而是很快,因为截止日期是星期一),因为我目前正在使用VST(VirtualStringTree)来存储我的数据 我想实现的是一个联系人列表结构。根节点是类别,子节点是联系人。总共有两个级别 问题是,我需要一个联系人显示在多个类别中,但它们需要同步。尤其是检查状态 目前,为了保持同步,我在整个树中循环查找与刚刚更改的节点ID相同的节点。但当有大量节点时,这样做非常缓慢 所以,我想:有可能在多个类别中显示联系人对象的一个实例吗 注意:老实说,我对这个

我意识到我真的需要重写我的程序数据结构(不是现在,而是很快,因为截止日期是星期一),因为我目前正在使用VST(VirtualStringTree)来存储我的数据

我想实现的是一个联系人列表结构。根节点是类别,子节点是联系人。总共有两个级别

问题是,我需要一个联系人显示在多个类别中,但它们需要同步。尤其是检查状态

目前,为了保持同步,我在整个树中循环查找与刚刚更改的节点ID相同的节点。但当有大量节点时,这样做非常缓慢

所以,我想:有可能在多个类别中显示联系人对象的一个实例吗

注意:老实说,我对这个术语不是100%熟悉——我所说的实例是指一个对象(或记录),所以我不必在整个树中查找具有相同ID的联系人对象

以下是一个例子:

如您所见,Todd Hirsch出现在测试类别和所有联系人中。但在幕后,它们是2个PVirtualNodes,因此当我更改其中一个节点的属性(如CheckState)或节点的数据记录/类中的某些内容时,这2个节点不会同步。目前我唯一能同步它们的方法是在我的树中循环,找到所有容纳同一联系人的节点,并将更改应用到它们和它们的数据

总而言之:我要寻找的是一种使用一个对象/记录的方法,并在我的树中的多个类别中显示它-无论何时选中一个节点,包含相同联系人对象的每个其他节点都会这样做


我说得通吗?

当然可以。你需要在头脑中分离节点和数据。TVirtualStringTree中的节点不需要保存数据,可以简单地使用来指向可以找到数据的实例。当然,您可以将两个节点指向同一个对象实例

假设你有一个TPerson的列表,你有一个树,你想在其中显示不同节点中的每个人。然后声明用于节点的记录,如下所示:

TNodeRecord = record
  ... // anything else you may need  or want
  DataObject: TObject;
  ...
end;
PNodeRecord.DataObject := PersonList[SomeIndex];
在初始化节点的代码中,执行如下操作:

TNodeRecord = record
  ... // anything else you may need  or want
  DataObject: TObject;
  ...
end;
PNodeRecord.DataObject := PersonList[SomeIndex];
这就是要点。如果你想要一个通用的节点记录,就像我上面展示的那样,那么你需要将它转换回适当的类,以便在各种Get中使用它。。。方法。当然,您也可以为每棵树创建一个特定的记录,其中您将DataObject声明为您在树中显示的特定类型的类。唯一的缺点是,然后将树限制为显示该类对象的信息

我应该有一个更详细的例子。当我找到它时,我会把它添加到这个答案中


示例

声明树要使用的记录:

RTreeData = record
  CDO: TCustomDomainObject;
end;
PTreeData = ^RTreeData;
TCustomDomainObject是我所有域信息的基类。声明如下:

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;
请注意,该类被设置为能够保存其他TCustomDomainObject实例的列表。在显示树的表单上,您添加:

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;  
Load方法是一切开始的地方:

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;
它真正做的只是清除表单并为一个新的CustomDomainObject设置它,在大多数情况下,它是一个包含其他CustomDomainObject的列表

ShowColumnHeaders方法为字符串树设置列标题,并根据列数调整标题选项:

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被分离出来,以允许此表单用作在树中显示信息的其他表单的基础

ShowDomainObjects看起来像是加载整个树的方法。事实并非如此。我们毕竟是在处理一棵虚拟树。所以我们需要做的就是告诉虚拟树我们有多少节点:

procedure TForm1.ShowDomainObjects(aCDO, aParent: TCustomDomainObject);
begin
  if Assigned(aCDO) then begin
    VirtualStringTree1.RootNodeCount := aCDO.Count;
  end else begin
    VirtualStringTree1.RootNodeCount := 0;
  end;
end;
我们现在大部分都已经设置好了,只需要实现各种VirtualStringTree事件就可以让一切顺利进行。要实现的第一个事件是OnGetText事件:

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;
它从VirtualStringTree获取节点数据,并使用获得的CustomDomainObject实例获取其文本。它使用GetColumnText函数来实现这一点,这也是为了允许使用此表单作为显示树的其他表单的基础。当您使用该方法时,您会将该方法声明为虚拟的,并在任何子代形式中重写它。在本例中,它简单地实现为:

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;
现在我们已经告诉VirtualStringTree如何从其节点记录中使用CustomDomainObject实例,当然我们仍然需要将主CDO中的实例链接到树中的节点。这在OnInitNode事件中完成:

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;
因为我们的CustomDomainObject可以有一个其他CustomDomainObject的列表,所以我们还将节点的初始状态设置为在lsit的计数大于零时包含HasChildren。这意味着我们还需要实现OnInitChildren事件,当用户单击树中的加号时调用该事件。同样,我们需要做的就是告诉树需要准备多少节点:

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;
这就是所有的人


正如我用一个简单列表展示的示例所示,您仍然需要确定哪些数据实例需要链接到哪些节点,但是您现在应该对需要在何处执行该操作有了一个合理的想法:在OnInitNode事件中,您将节点记录的CDO成员设置为指向您选择的CDO实例。

说这句话时,我真的觉得自己像个傻瓜:我不确定我是否理解这个概念。我已经检查了代码,试图实现它,但我遇到了一个心理障碍:P@Jeff:虚拟树的概念是不将值从数据结构复制到树中。当树需要绘制某些内容时,它会要求您输入值。记住,它会问f