How can I keep the check state of multiple Virtual Tree View nodes in sync?

北慕城南 提交于 2019-12-07 21:47:04

问题


My tree has 2 levels of nodes - it's a Contact List style tree.

My problem is, that I would like to have every contact checked, in all the "Contact Categories". Here is a screenshot of my contact list as it looks now (And yes, I have permission to post it)

As you see, Todd Hirsch is checked in the Category Test Category, but not in All Contacts. What I am trying to achieve, is to have a contact have the same checked status in every category.

Example: I check Todd Hirsch in the Test Category - Todd Hirsch is automatically checked in All Contacts (And every other category). If I check Todd Hirsch in the All Contacts, he will also get checked in Test Category. If I Uncheck Todd Hirsch in All Contacts, he will also get unchecked in Test Category.

I tried doing it through the VirtualStringtree's OnChecking events, by looping thru the whole tree for each node in the tree, however when the contact list is big (2000 +), it is very slow, and when there's like 5000+, it might even crash my program (Application has stopped working)

What do you suggest?

Here is the code that I use to make sure that a contact is only checked once. (That is not what I want now, but it's what I am using right now.)

////////////////////////////////////////////////////////////////////////////////
/// 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;

回答1:


First, OnChecking is the wrong event to handle. You want OnChecked. OnChecking really justs ask, "Is this node's check state allowed to change?" It's not meant to go off and check other nodes. Use OnChecked for that.

Second, you shouldn't need to handle the check-state of the category nodes. Turn on the toAutoTristateTracking option and the control will automatically adjust the states of all related child and parent nodes. (Change a parent, and all the children change. Change a child, and the parent changes to "indeterminate.")

Your code seems to be on the right track otherwise, though. When a child node changes, you need to find all the other copies of that node in the rest of the tree and change their check states to match the new state of the just-changed node. The time it takes to perform that operation should be linearly in the number of nodes in the tree — double the number of nodes, and it should take roughly twice the amount of time to find all the duplicates. But even with a few thousand nodes, it should finish in the blink of an eye. If it takes longer, there's some other time-consuming operation that you haven't shown here. Try using a profiler to discover the bottleneck.

The code below traverses once through all the nodes in the tree. It temporarily disables the OnChecked event handler because otherwise, each time it changes the state of one of the duplicates, the event would run again. If the new check state is the same as the current one, the event doesn't run, so there's no danger of infinite recursion, but disabling the event does prevent it from doing lots of redundant traversals through the tree.

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;

If your data structure had a list of all the nodes associated with a given user ID, it would be much more straightforward:

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;

Even if you continue to store all your data in the tree control itself (which you've been advised many times is a bad idea), you can still use a secondary data structure to act as an index for tree nodes, keyed off the user ID. If you have a sufficiently recent Delphi version, you can use TDictionary<string, TList<PVirtualNode>>. Then PropagateCheckState could look like this:

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;

Make sure to update the UserNodes index whenever you add or remove a user in a category.




回答2:


I'll assume for simplicity sake that "Todd" is contained in a class used by the TreeView to create the entries. As long as your TreeView is requesting it's information from that class, you could get away by adding the boolean check in the class itself and invalidate the treeview.
When the tree repaints itself, it will use your class to set the checkboxes accordingly.

I know VirtualTreeView is fast enough to perform this on several thousands of entries in a split second.




回答3:


For traverse over all nodes may be usefull function GetNext(PVirtualNode, bool childs) like (excuse for C++ code)

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);
    } 


来源:https://stackoverflow.com/questions/5596588/how-can-i-keep-the-check-state-of-multiple-virtual-tree-view-nodes-in-sync

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!