在VirtualStringTree中可以多次显示一个对象吗? [英] Is it possible to display one object multiple times in a VirtualStringTree?

查看:176
本文介绍了在VirtualStringTree中可以多次显示一个对象吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



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



事情是,我需要一个联系人来显示超过1个类别,但需要同步。特别是 Checkstate



目前,为了保持同步,我循环遍历整个树,找到与刚刚改变了但是这样做是非常缓慢的,当有一大堆节点。



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



注意:老实说,我不是100%熟悉术语 - 我的意思是Instance,是一个对象(或记录),所以我不必通过我的整个树查找具有相同ID的联系人对象。



这里是一个例子: p>



如你所见,Todd Hirsch出现在测试类别和所有联系人中。但在幕后,那些是2 PVirtualNodes ,所以当我更改一个节点(像CheckState)上的一个属性或节点的数据记录/类中的某个时,两个节点不同步。目前,我可以同步它们的唯一方法是通过我的树进行循环,找到所有连接在同一个联系人上的节点,并对它们和数据进行更改。



总结:我正在寻找什么,是一种使用一个对象/记录的方法,并将其显示在我的树中的几个类别中 - 每当一个节点被检查时,每个其他节点都拥有相同的Contact对象。



我有什么感觉吗?

解决方案

能够。你需要分开你心目中的节点和数据。 TVirtualStringTree中的节点不需要保存数据,可以简单地指向可以找到数据的实例。当然,你可以将两个节点指向同一个对象实例。



假设你有一个TPerson的列表,并且你想要显示每个人在不同的树节点。然后,您声明用于节点的记录,简单如下:

  TNodeRecord = record 
... / /你可能需要或想要的任何东西
DataObject:TObject;
...
end;

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

  PNodeRecord.DataObject:= PersonList [SomeIndex]; 

这是它的要点。如果你想要一个通用的NodeRecord,就像我上面显示的那样,那么你需要将它转换回适当的类,以便在各种Get ...方法中使用它。您当然也可以为每个树创建一个特定的记录,在这里您将DataObject声明为您在树中显示的特定类型的类。唯一的缺点是您然后限制树以显示该类对象的信息。



我应该在某处放置一个更详细的例子。当我找到它,我会把它添加到这个答案。






示例



声明要由树使用的记录:

  RTreeData = record 
CDO:TCustomDomainObject;
结束
PTreeData = ^ RTreeData;

TCustomDomainObject是我所有域信息的基类。它被声明为:

  TCustomDomainObject = class(TObject)
private
FList:TObjectList;
protected
function GetDisplayString:string;虚拟;
函数GetCount:Cardinal;
函数GetCDO(aIdx:Cardinal):TCustomDomainObject;
public
构造函数创建;超载;
析构函数覆盖

函数添加(aCDO:TCustomDomainObject):TCustomDomainObject;

属性DisplayString:string read GetDisplayString;
属性计数:Cardinal读取GetCount;
属性CDO [aIdx:Cardinal]:TCustomDomainObject读取GetCDO;
结束

请注意,此类设置为能够容纳其他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);虚拟;
函数GetColumnText(aCDO:TCustomDomainObject; aColumn:TColumnIndex;
var aCellText:string):Boolean;
protected
属性CDO:TCustomDomainObject读取FCDO写FCDO;
public
procedure加载(aCDO:TCustomDomainObject);
...
end;

Load方法是所有开始的地方:

  procedure TForm1.Load(aCDO:TCustomDomainObject); 
begin
FIsLoading:= True;
VirtualStringTree1.BeginUpdate;
尝试
如果分配(CDO)然后开始
VirtualStringTree1.Header.Columns.Clear;
VirtualStringTree1.Clear;
结束
CDO:= aCDO;
如果分配(CDO)然后开始
ShowColumnHeaders;
ShowDomainObjects(CDO,nil);
结束
finally
VirtualStringTree1.EndUpdate;
FIsLoading:= False;
结束
结束

它真正做的是清除窗体并设置一个新的CustomDomainObject,在大多数情况下作为包含其他CustomDomainObjects的列表。



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

  procedure TForm1.ShowColumnHeaders; 
begin
AddColumnHeaders(VirtualStringTree1.Header.Columns);
如果VirtualStringTree1.Header.Columns.Count> 0然后开始
VirtualStringTree1.Header.Options:= VirtualStringTree1.Header.Options
+ [hoVisible];
结束
结束

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

Col:= aColumns.Add;
Col.Text:='平均年龄';
Col.Width:= 100;
Col.Alignment:= taRightJustify;

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

AddColumnHeaders已被分离出来,允许此表单用作其他表单中的信息的基础树。



ShowDomainObjects看起来像整个树被加载的方法。不是毕竟,我们正在处理一个虚拟树。所以我们需要做的就是告诉虚拟树我们有多少个节点:

  procedure TForm1.ShowDomainObjects(aCDO,aParent: TCustomDomainObject); 
begin
如果分配(aCDO)然后开始
VirtualStringTree1.RootNodeCount:= aCDO.Count;
end else begin
VirtualStringTree1.RootNodeCount:= 0;
结束
结束

我们现在大部分都是设置的,只需要实现各种VirtualStringTree事件即可完成所有操作。第一个要实现的事件是OnGetText事件:

  procedure TForm1.VirtualStringTree1GetText(Sender:TBaseVirtualTree; Node:
PVirtualNode ;列:TColumnIndex; TextType:TVSTTextType; var CellText:
string);
var
NodeData:^ RTreeData;
begin
NodeData:= Sender.GetNodeData(Node);
如果GetColumnText(NodeData.CDO,Column,{var} CellText)然后
else begin
如果已分配(NodeData.CDO)然后开始
案例列
- 1,0:CellText:= NodeData.CDO.DisplayString;
结束
结束
结束
结束

它从VirtualStringTree获取NodeData,并使用获取的CustomDomainObject实例获取其文本。它使用GetColumnText函数为此,并且完成了,再次允许使用此窗体作为显示树的其他窗体的基础。当你去那条路线,你会声明这个方法虚拟,并以任何后代的形式覆盖它。在这个例子中,它简单地实现为:

  function TForm1.GetColumnText(aCDO:TCustomDomainObject; aColumn:TColumnIndex; 
var aCellText:string):Boolean;
begin
如果分配(aCDO)然后开始
case aColumn of
-1,0:begin
aCellText:= aCDO.DisplayString;
结束
1:begin
如果aCDO.InheritsFrom(TDogBreed)然后开始
aCellText:= IntToStr(TDogBreed(aCDO).AverageAge);
结束
结束
2:begin
aCellText:= IntToStr(aCDO.Count);
结束
else
// aCellText:='';
结束
结果:= True;
end else begin
结果:= False;
结束
结束

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

  procedure TForm1.VirtualStringTree1InitNode(Sender:TBaseVirtualTree; 
ParentNode,Node:PVirtualNode ; var InitialStates:TVirtualNodeInitStates);
var
ParentNodeData:^ RTreeData;
ParentNodeCDO:TCustomDomainObject;
NodeData:^ RTreeData;
begin
如果分配(ParentNode)然后开始
ParentNodeData:= VirtualStringTree1.GetNodeData(ParentNode);
ParentNodeCDO:= ParentNodeData.CDO;
end else begin
ParentNodeCDO:= CDO;
结束

NodeData:= VirtualStringTree1.GetNodeData(Node);
如果分配(NodeData.CDO)然后开始
// CDO已经设置,例如通过AddDomainObject添加时。
end else begin
如果分配(ParentNodeCDO)然后开始
如果ParentNodeCDO.Count> Node.Index然后开始
NodeData.CDO:= ParentNodeCDO.CDO [Node.Index];
如果NodeData.CDO.Count> 0然后开始
InitialStates:= InitialStates + [ivsHasChildren];
结束
结束
结束
结束
Sender.CheckState [Node]:= csUncheckedNormal;
结束

由于我们的CustomDomainObject可以有其他CustomDomainObjects的列表,我们还将节点的InitialStates设置为包含HasChildren当lsit的Count大于零时。这意味着我们还需要实现OnInitChildren事件,当用户点击树中的加号时,它将被调用。再次,我们需要做的就是告诉树中需要准备多少个节点:

  procedure TForm1.VirtualStringTree1InitChildren发件人:TBaseVirtualTree;节点:$ b​​ $ b PVirtualNode; var ChildCount:Cardinal); 
var
NodeData:^ RTreeData;
begin
ChildCount:= 0;

NodeData:= Sender.GetNodeData(Node);
如果分配(NodeData.CDO)然后开始
ChildCount:= NodeData.CDO.Count;
结束
结束

这是所有人!!!



正如我以简单的列表显示一个例子,仍然需要弄清楚哪些数据实例需要链接到哪些节点,但是你应该有一个公平的想法,现在你需要做什么:OnInitNode事件,您将设置节点记录的CDO成员指向您选择的CDO实例。


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:

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?

解决方案

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.

这篇关于在VirtualStringTree中可以多次显示一个对象吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆