【问题标题】:Is it possible to display one object multiple times in a VirtualStringTree?是否可以在 VirtualStringTree 中多次显示一个对象?
【发布时间】:2011-04-09 00:39:36
【问题描述】:

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

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

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

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

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

注意:老实说,我不是 100% 熟悉该术语 - 我的意思是实例,是一个对象(或记录),因此我不必查看整个树来查找具有相同 ID 的联系人对象。

这是一个例子:

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

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

我在这里有意义吗?

【问题讨论】:

    标签: delphi data-structures synchronization nodes virtualtreeview


    【解决方案1】:

    当然可以。您需要在脑海中分离节点和数据。 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,在大多数情况下,它是一个包含其他 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;
    

    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 可以有其他 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;
    

    这就是所有人!!!

    正如我展示的带有简单列表的示例一样,仍然需要弄清楚您需要将哪些数据实例链接到哪些节点,但您现在应该清楚地知道您需要在哪里为此:您将节点记录的 CDO 成员设置为指向您选择的 CDO 实例的 OnInitNode 事件。

    【讨论】:

    • 说这句话的时候我真的觉得自己像个坑:我不确定我是否理解这个概念。我查看了代码,试图实现它,但我遇到了一个心理障碍。 :P
    • @Jeff:虚拟树的概念是您不要将数据结构中的值复制到树中。树在需要绘制某些东西时会询问您的值。请记住,几乎每次鼠标移动都会询问信息,并且告诉树要绘制什么的方法(即 OnGetText)会被非常频繁地调用,并且需要在实现时牢记这一点。如果这不能解除您的障碍,那么 TVirtualStringTree 文档实际上提供了有关该概念的非常好的信息。这是您真正需要在潜入之前阅读手册的控件之一。
    • @Marjan - 我得到的文档是使用 PVirtualNode 将数据存储在 AFAIK 中。我确实明白了这个概念,它只是在实施它是我迷失的地方。 VT 的演示无法理解(对我来说)。
    • @Marjan - 在您的示例中,您不是将 CDO 对象存储在节点的数据中吗?
    • @Jeff:我只在树节点中存储指向 CDO 实例的指针。没有任何内容被复制到树节点中。在 Delphi 中,类实例(对象)是指针。当您编写MyDog := TDog(List[i]); 之类的代码时,您并没有复制狗实例中包含的所有数据,而只是复制了指向该实例的指针。这就是任何虚拟控件与非虚拟控件不同的地方。如果您要使用普通的 TListView,您将复制像狗的名字这样的字符串到 ListItem 及其每​​只狗的子项。从而在内存中复制字符串。当……(续)
    猜你喜欢
    • 2018-01-13
    • 1970-01-01
    • 2014-10-20
    • 1970-01-01
    • 1970-01-01
    • 2010-10-18
    • 1970-01-01
    • 2020-05-30
    • 1970-01-01
    相关资源
    最近更新 更多