2

我的树有 2 个级别的节点 - 它是一个联系人列表样式树。

我的问题是,我想检查所有“联系人类别”中的每个联系人。这是我现在看起来的联系人列表的屏幕截图(是的,我有权发布它)

联系人列表

如您所见,Todd Hirsch在 Category Test Category中被选中,但不是在All Contacts中。我想要实现的是让联系人在每个类别中都具有相同的检查状态。

示例:我在测试类别中检查 Todd Hirsch - Todd Hirsch 在所有联系人(以及所有其他类别)中自动检查。如果我在所有联系人中检查 Todd Hirsch,他也会在测试类别中检查。如果我在所有联系人中取消选中 Todd Hirsch,他也将在测试类别中取消选中。

我尝试通过 VirtualStringtree 的 OnChecking 事件来完成它,通过循环遍历树中每个节点的整个树,但是当联系人列表很大(2000 +)时,它非常慢,当有 5000 + 时,它甚至可能使我的程序崩溃(应用程序已停止工作)

你有什么建议?

这是我用来确保只检查一次联系人的代码。(这不是我现在想要的,但这是我现在正在使用的。)

////////////////////////////////////////////////////////////////////////////////
/// HasDuplicateChecked
////////////////////////////////////////////////////////////////////////////////
Function HasDuplicateChecked(Node: PVirtualNode): PVirtualNode;
Var
  ParentNode, ChildNode: PVirtualNode;
  I, J: Integer;
Begin

  // IHCW
  Result := Nil;

  // Get the first node of the tree..
  ParentNode := VT.GetFirst;

  // Loop thru the parent nodes.
  for I := 0 to VT.RootNodeCount - 1 do
  begin
    // Get the first child node.
    ChildNode := ParentNode.FirstChild;
    // Loop thru the children..
    for J := 0 to ParentNode.ChildCount - 1 do
    begin
      // If the ChildNode is checked...
      if NodeIsChecked(ChildNode) then
        // And it is NOT the passed node..
        if ChildNode <> Node then
          // but the data matches..
          if GetData(ChildNode).SkypeID = GetData(Node).SkypeID then
          begin
            // Then pass the Childnode as a result, and EXIT!
            Result := ChildNode;
            Exit;
          end;
      // Next child..
      ChildNode := ChildNode.NextSibling;
    end;
    // Next parent...
    ParentNode := ParentNode.NextSibling;
  end;

End;


////////////////////////////////////////////////////////////////////////////////
/// vtSkypeChecking
////////////////////////////////////////////////////////////////////////////////
procedure TSkypeListEventHandler.vtSkypeChecking(Sender: TBaseVirtualTree;
  Node: PVirtualNode; var NewState: TCheckState; var Allowed: Boolean);
Var
  Level: Integer;
  I: Integer;
  Child: PVirtualNode;
begin
  // Allow the checking..
  Allowed := True;
  // Get the Level..
  Level := Sender.GetNodeLevel(Node);

  // If the level is 0 (Category Level)
  if Level = 0 then
  begin
    // And if the Node's Childcount is more than 0
    if Node.ChildCount > 0 then
    Begin
      // Get the first child..
      Child := Node.FirstChild;
      // Loop thru the children..
      for I := 0 to Node.ChildCount - 1 do
      begin
        // Set the checkstate, and go next..
        Child.CheckState := NewState;
        Child := Child.NextSibling;
      end;
    End;
  end;


  // If the level is 1 (User Level)
  if Level = 1 then
  begin
    // and if the Node's parent is not Nil..
    if Node.Parent <> nil then
    begin
      // aaand, if the new state is Unchecked...
      if (NewState = csUncheckedNormal) or (NewState = csUncheckedPressed) then
      begin
        // .. and if the node checkstate is checked..
        if NodeIsChecked(Node) then
        Begin
          // Set the PARENT node's checkstate to Unchecked!
          Node.Parent.CheckState := csUncheckedNormal;
        End;

      end;
      // BUT, if there is a DUPLICATE of the node, screw the above, and
      // forbid the checking!
      if HasDuplicateChecked(Node) <> nil then
        Allowed := False;

    end;
  end;

  // Uncheck all the duplicates.
  UncheckDuplicates;

  // Refresh the Tree
  Sender.Refresh;

end;
4

3 回答 3

5

首先,OnChecking是处理错误的事件。你想要OnCheckedOnChecking真的只是问,“这个节点的检查状态是否允许改变?” 这并不意味着要关闭并检查其他节点。为此使用OnChecked

其次,您不需要处理类别节点的检查状态。打开该toAutoTristateTracking选项,控件将自动调整所有相关子节点和父节点的状态。(改变一个父母,所有的孩子都改变。改变一个孩子,父母改变为“不确定”。)

不过,您的代码似乎在正确的轨道上。当子节点发生变化时,您需要在树的其余部分中找到该节点的所有其他副本,并更改​​它们的检查状态以匹配刚刚更改的节点的新状态。执行该操作所花费的时间应该与树中的节点数呈线性关系——节点数的两倍,并且找到所有重复项大约需要两倍的时间。但即使有几千个节点,它也应该在眨眼之间完成。如果需要更长的时间,还有其他一些您未在此处显示的耗时操作。尝试使用分析器来发现瓶颈。

下面的代码遍历树中的所有节点一次。它暂时禁用OnChecked事件处理程序,因为否则,每次它更改副本之一的状态时,事件都会再次运行。如果新的检查状态与当前状态相同,则该事件不会运行,因此不存在无限递归的危险,但禁用该事件确实可以防止它对树进行大量冗余遍历。

procedure PropagateCheckState(Tree: TVirtualStringTree; Node: PVirtualNode);
var
  Data: PNodeData;
  TargetID: string;
  Parent: PVirtualNode;
  FoundOne: Boolean;
begin
  Data := Tree.GetNodeData(Node);
  TargetID := Data.SkypeID;

  Parent := Tree.GetFirst;
  while Assigned(Parent) do begin
    // Assume no user appears twice in the same category
    if Parent <> Tree.NodeParent[Node] then begin
      FoundOne := False;
      Child := Tree.GetFirstChild(Parent);
      while Assigned(Child) and not FoundOne do begin
        Data := Tree.GetNodeData(Child);
        if Data.SkypeID = TargetID then begin
          // Found a duplicate. Sync it with Node.
          Tree.CheckState[Child] := Tree.CheckState[Node];
          FoundOne := True;
        end;
        Child := Tree.GetNextSibling(Child);
      end;
    end;
    Parent := Tree.GetNextSibling(Parent);
  end;
end;

procedure TSkypeListEventHandler.vtSkypeChecked(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
  CheckedEvent: TVTChangeEvent;
begin
  if Sender.GetNodeLevel(Node) = 0 then
    exit; // The tree cascades changes automatically

  Assert(Sender.GetNodeLevel(Node) = 1, 'Unexpected node level');
  // We'll be accessing members that are protected in TBaseVirtualTree, but
  // they're public in TVirtualStringTree, so make sure we're still operating
  // on the same tree.
  Assert(Sender = vtSkype);

  CheckedEvent := vtSkype.OnChecked;
  vtSkype.OnChecked := nil;
  try
    PropagateCheckState(vtSkype, Node);
  finally
    vtSkype.OnChecked := CheckedEvent;
  end;
end;

如果您的数据结构具有与给定用户 ID 关联的所有节点的列表,那么它会更简单:

procedure PropagateCheckState(Tree: TVirtualStringTree; Node: PVirtualNode);
var
  Data: PNodeData;
  i: Integer;
begin
  Data := Tree.GetNodeData(Node);

  for i := 0 to Pred(Data.User.Nodes.Count) do
    Tree.CheckState[Data.User.Nodes[i]] := Tree.CheckState[Node];
end;

即使您继续将所有数据存储在树控件本身中(您多次被告知这是一个坏主意),您仍然可以使用辅助数据结构作为树节点的索引,锁定用户ID。如果您有足够新的 Delphi 版本,则可以使用TDictionary<string, TList<PVirtualNode>>. 然后PropagateCheckState可能看起来像这样:

uses Generics.Collections;

var
  UserNodes: TDictionary<string, TList<PVirtualNode>>;

procedure PropagateCheckState(Tree: TVirtualStringTree; Node: PVirtualNode);
var
  Data: PNodeData;
  Nodes: TList<PVirtualNode>;
  i: Integer;
begin
  Data := Tree.GetNodeData(Node);

  if not UserNodes.TryGetValue(Data.SkypeID, Nodes) then
    exit; // Weird. The node's ID isn't in the index at all.

  for i := 0 to Pred(Nodes.Count) do
    Tree.CheckState[Nodes[i]] := Tree.CheckState[Node];
end;

确保在UserNodes添加或删除类别中的用户时更新索引。

于 2011-04-08T18:05:56.847 回答
0

对于遍历所有节点可能有用的函数 GetNext(PVirtualNode, bool childs) 喜欢(C++ 代码的借口)

TVirtualNode* parent=NULL;
TVirtualNode* node=VST->GetFirst();
 while (node)
    {
    data=static_cast<TreeItemData*>(VST->GetNodeData(node));
    //doo something with node, data
    node=VST->GetNext(node, true);
    } 
于 2013-07-04T09:19:39.797 回答
0

为简单起见,我假设“Todd”包含在 TreeView 用于创建条目的类中。只要您的 TreeView 从该类请求它的信息,您就可以通过在类本身中添加布尔检查并使树视图无效来逃脱。
当树重新绘制自身时,它将使用您的类来相应地设置复选框。

我知道 VirtualTreeView 足够快,可以在一瞬间对数千个条目执行此操作。

于 2011-04-08T14:38:48.497 回答