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

发布于 2024-10-31 04:18:31 字数 797 浏览 5 评论 0原文

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

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

但问题是,我需要一个联系人显示在多个类别中,但它们需要同步。特别是Checkstate

目前,为了保持同步,我循环遍历整个树以查找与刚刚更改的节点具有相同 ID 的节点。但当节点数量巨大时,这样做会非常慢。

因此,我想:是否可以在多个类别中显示联系人对象的一个实例

注意:老实说,我并不是 100% 熟悉术语 - 什么我所说的“实例”是指一个对象(或记录),因此我不必遍历整个树来查找具有相同 ID 的联系人对象。

下面是一个示例:

Example

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

总结一下:我正在寻找的是一种使用一个对象/记录并将其显示在树中的多个类别中的方法 - 每当一个节点被检查时,包含相同联系人对象的每个其他节点也会被检查。

我在这里有意义吗?

I realize that I really need to rewrite my programs data structure (not now, but soon, as the deadline is monday), as I am currently using VST (VirtualStringTree) to store my data.

What I would like to achieve, is a Contact List structure. The Rootnodes are the Categories, and the children are the Contacts. There is a total of 2 levels.

The thing is though, that I need a contact to display in more than 1 category, but they need to be synchronized. Particularly the Checkstate.

Currently, to maintain sync, I loop thru my whole tree to find nodes that have the same ID as the one that was just changed. But doing so is very slow when there is a huge ammount of nodes.

So, I thought: Would it be possible to display one instance of the Contact Object, in multiple Categories?

Note: Honestly I am not 100% familiar with the terminology - what I mean by Instance, is one Object (or Record), so I will not have to look thru my entire tree to find Contact Objects with the same ID.

Here is an example:

Example

As you see, Todd Hirsch appears in Test Category, and in All Contacts. But behind the scenes, those are 2 PVirtualNodes, so when I change a property on one of the node's (Like CheckState), or something in the node's Data Record/Class, the 2 nodes are not synchronized. And currently the only way I can synchronize them, is to loop thru my tree, find all the nodes that house that same contact, and apply the changes to them and their data.

To summarize: What I am looking for, is a way to use one object/record, and display it in several Categories in my tree - and whenever one node gets checked, so will every other node that houses the same Contact object.

Do I make any sense here?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

陌路终见情 2024-11-07 04:18:31

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

假设您有一个 TPerson 列表,并且有一棵树,您想在其中显示不同节点中的每个人。然后,您将用于节点的记录简单地声明为:

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

在初始化节点的代码中,您执行以下操作:

PNodeRecord.DataObject := PersonList[SomeIndex];

这就是它的要点。如果您想要一个通用的 NodeRecord,就像我上面展示的那样,那么您需要将其转换回正确的类,以便在各种 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 是包含其他 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 获取 NodeData,并使用获得的 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 的 Count 大于零时,我们还将节点的 InitialStates 设置为包含 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 实例。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文