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