Jef*_*eff 2 delphi synchronization virtualtreeview nodes data-structures
我意识到我真的需要重写我的程序数据结构(不是现在,但很快,因为截止日期是星期一),因为我目前正在使用VST(VirtualStringTree)来存储我的数据.
我想要实现的是一个联系人列表结构.根节点是类别,子节点是联系人.共有2个级别.
但事实是,我需要一个联系人才能在一个以上的类别中显示,但它们需要同步.特别是Checkstate.
目前,为了保持同步,我遍历整个树来查找与刚刚更改的ID具有相同ID的节点.但是当存在大量节点时,这样做非常缓慢.
所以,我想:是否可以在多个类别中显示联系对象的一个实例?
注意:老实说,我并不是100%熟悉术语 - 我的意思是实例,是一个对象(或记录),所以我不必通过整个树来查找具有相同ID的联系对象.
这是一个例子:

如您所见,Todd Hirsch出现在测试类别和所有联系人中.但是在幕后,那些是2个PVirtualNodes,所以当我更改某个节点(如CheckState)上的属性,或节点的数据记录/类中的某个属性时,2个节点不同步.目前我可以同步它们的唯一方法是循环访问我的树,找到容纳相同联系人的所有节点,并将更改应用于它们及其数据.
总结一下:我正在寻找的是一种使用一个对象/记录的方法,并在我的树中的几个类别中显示它 - 每当一个节点被检查时,每个其他节点都包含相同的Contact对象.
我在这里有意义吗?
当然可以.您需要在脑海中分离节点和数据.TVirtualStringTree中的节点不需要保存数据,可以简单地用于指向可以找到数据的实例.当然,您可以将两个节点指向同一个对象实例.
假设您有一个TPerson列表,并且您想要在不同节点中显示每个人的树.然后,您将用于节点的记录声明为:
TNodeRecord = record
... // anything else you may need or want
DataObject: TObject;
...
end;
Run Code Online (Sandbox Code Playgroud)
在初始化节点的代码中,您执行以下操作:
PNodeRecord.DataObject := PersonList[SomeIndex];
Run Code Online (Sandbox Code Playgroud)
这是它的要点.如果你想要一个普通的NodeRecord,就像我上面展示的那样,那么你需要将它强制转换回正确的类,以便在各种Get ...方法中使用它.您当然也可以为每个树创建一个特定的记录,您可以将DataObject声明为您在树中显示的特定类型的类.唯一的缺点是您将树限制为显示该类对象的信息.
我应该在某个地方有一个更详细的例子.当我找到它时,我会将它添加到这个答案中.
例
声明树使用的记录:
RTreeData = record
CDO: TCustomDomainObject;
end;
PTreeData = ^RTreeData;
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
请注意,此类设置为能够保存其他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;
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
所有它真正做的是清除表单并为新的CustomDomainObject设置它,在大多数情况下,它将是包含其他CustomDomainObjects的列表.
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;
Run Code Online (Sandbox Code Playgroud)
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;
Run Code Online (Sandbox Code Playgroud)
我们现在主要设置,只需要实现各种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;
Run Code Online (Sandbox Code Playgroud)
它从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;
Run Code Online (Sandbox Code Playgroud)
现在我们告诉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;
Run Code Online (Sandbox Code Playgroud)
由于我们的CustomDomainObject可以包含其他CustomDomainObjects的列表,因此当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;
Run Code Online (Sandbox Code Playgroud)
那是所有人!
正如我已经展示了一个带有简单列表的示例,您仍然需要确定需要链接到哪些节点的数据实例,但是现在您应该知道需要执行此操作的位置:您设置的OnInitNode事件节点记录的CDO成员指向您选择的CDO实例.