1

I'm having a (strange) sorting problem with VirtualTreView (v 6.1.0 / Delphi 10 Seattle). I've looked into more recent versions of VTV and there's no mention of a similar behavior.

I'll post full source code, but let me first explain what I'm trying to accomplish:

vtv sorting nodes issue

  1. There are nodes I'd like to sort
  2. Each node has a kind of stage - where the node stage determines the sort order of the node (in that stage)
  3. I'd like to have nodes sorted when a particular stage is selected by their stage sort value. Those nodes that are not included in the stage are to be set invisible.

Here's the record I'm using:

PRecord = ^TRecord;
TRecord = record
  SortOrder : array [0..4] of integer;
  PositionAdded : integer;
end;

Here's how nodes are added:

procedure TVTVSortForm.FormCreate(Sender: TObject);
var
  vn : PVirtualNode;
  pr : PRecord;
begin
  tree.NodeDataSize := SizeOf(TRecord);

  vn := tree.AddChild(nil);
  pr := PRecord(tree.GetNodeData(vn));
  pr.PositionAdded := vn.Index;
  pr.SortOrder[0] := 0; //first in 0 - removed in later
  pr.SortOrder[1] := -1;
  pr.SortOrder[2] := -1;
  pr.SortOrder[3] := -1;
  pr.SortOrder[4] := 2; //third in 4

  vn := tree.AddChild(nil);
  pr := PRecord(tree.GetNodeData(vn));
  pr.PositionAdded := vn.Index;
  pr.SortOrder[0] := 1; //second in 0
  pr.SortOrder[1] := 0; //first in 1
  pr.SortOrder[2] := 1; //second in 2
  pr.SortOrder[3] := 1; //second in 3
  pr.SortOrder[4] := 0; //first in 4

  vn := tree.AddChild(nil);
  pr := PRecord(tree.GetNodeData(vn));
  pr.PositionAdded := vn.Index;
  pr.SortOrder[0] := 2; // third in 0
  pr.SortOrder[1] := 2; //third in 1
  pr.SortOrder[2] := 0; //first in 2
  pr.SortOrder[3] := -1; //removed in 3
  pr.SortOrder[4] := 1; //second in 4

  vn := tree.AddChild(nil);
  pr := PRecord(tree.GetNodeData(vn));
  pr.PositionAdded := vn.Index;
  pr.SortOrder[0] := -1; //not in 0
  pr.SortOrder[1] := 1;  //second in 1
  pr.SortOrder[2] := 2; //third in 2
  pr.SortOrder[3] := 0; // first in 3
  pr.SortOrder[4] := -1; // not in 4

  tree.ValidateNode(nil, true);
end;

"Stages" are SortOrder indices. SortOrder[x] := y means that the order of the node in stage X should be y. For example, SortOrder[2] = 0 means that in stage 2 the node should be first node. SortOrder[3] = -1 means that the node "does not exist" in stage 3.

There's a radiogroup having 5 elements (0...4) presenting the stage. When the stage is selected the nodes should be sorted according their SortOrder value for the stage selected (I'm also hiding nodes that are not existing in a stage:

procedure TVTVSortForm.rgFilterAndSortClick(Sender: TObject);
begin
  tree.Sort(tree.RootNode, 0, sdAscending);

  tree.IterateSubtree(
  nil,
  procedure (Sender: TBaseVirtualTree; Node: PVirtualNode; Data: Pointer; var Abort: Boolean)
  var
    d : PRecord;
  begin
    d := PRecord(tree.GetNodeData(Node));
    //tree.IsVisible[Node] := -1 <> d.SortOrder[rgFilterAndSort.ItemIndex];
  end,
  nil);
end;

The OnCompare looks like:

procedure TVTVSortForm.treeCompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var
  d1, d2 : PRecord;
begin
  d1 := PRecord(Sender.GetNodeData(Node1));
  d2 := PRecord(Sender.GetNodeData(Node2));

  if (-1 <> d1.SortOrder[rgFilterAndSort.ItemIndex]) AND (-1 <> d2.SortOrder[rgFilterAndSort.ItemIndex]) then
    result := d1.SortOrder[rgFilterAndSort.ItemIndex] - d2.SortOrder[rgFilterAndSort.ItemIndex];
end;

Therefore I'm only comparing those nodes that exist in the selected stage (i.e. SortOrder[stage] <> -1).

The GetText displays the sort order for the selected stage + the original position of the node at the time of adding to the tree.

procedure TVTVSortForm.treeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
  Data: PRecord;
begin
  Data := Sender.GetNodeData(Node);
  CellText := Format('s:%d, p:%d', [Data.SortOrder[rgFilterAndSort.ItemIndex], Data.PositionAdded]);
end;

Now, what happens when you start the program and go (select radio button/click) to stage (3), the order is 1,0 instead of 0,1 (I'm omitting "-1" nodes).

If after selecting stage (3) you select (1) then (3) -> stage 3 is sorted ok. But then go to stage (0),(2),(4) and the order is 1,2,0 instead of 0,1,2.

Any ideas why this is not working (as expected)?

I've noted that when two nodes go into OnCompare: if one of the nodes has -1 for the selected stage, the other node will not any more be compared to the remaining nodes that exist in the selected stage. How to force the other node to still be matched with other same-stage nodes?

Torpedo
  • 185
  • 10
  • When `SortOrder` is –1 for one node and not the other, then `Result` is undefined. The comparison must define a *total ordering*; if you don't define the comparison between *all* possible pairs, then the underlying sorting algorithm will give unexpected results, such as stopping before all nodes have been sorted. – Rob Kennedy Nov 28 '16 at 15:06
  • @Rob, sorry but result is always preset to 0 (Node1 < Node2), never undefined. – Torpedo Nov 28 '16 at 15:10
  • That yields the same problem. A result of zero means the nodes are equal. Suppose you have the nodes, and node 1 is set to –1. Node 2 will compare equal to it, and so will node 3, because you've skipped the comparison. By transitivity, that must mean node 2 is equal to node 3, but if your comparison function returns a different answer, then you don't have a total ordering, and the sort algorithm may fail. – Rob Kennedy Nov 28 '16 at 15:14
  • Yes, makes sense, thanks! – Torpedo Nov 28 '16 at 15:26

1 Answers1

2

Omg, I've been trying to figure out what's wrong here for the last few days and, of course, once I posted my problem here I've figured it out.

The OnCompare needs to compare "all" nodes - so skipping some nodes (when a node is "not in stage") is what was wrong with my code.

I need to make sure that the stage node gets still compared to other same-stage nodes when it does not get compared to the matched different-stage node.

Finally, I rewrote the OnCompare method like this, and for the moment (I have a larger real world application for testing) all seems to be ok:

procedure TVTVSortForm.treeCompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var
  d1, d2 : PRecord;
begin
  d1 := PRecord(Sender.GetNodeData(Node1));
  d2 := PRecord(Sender.GetNodeData(Node2));

  if (-1 = d1.SortOrder[rgFilterAndSort.ItemIndex]) then result := -1
  else if (-1 = d2.SortOrder[rgFilterAndSort.ItemIndex]) then result := 1
  else
    if (-1 <> d1.SortOrder[rgFilterAndSort.ItemIndex]) AND (-1 <> d2.SortOrder[rgFilterAndSort.ItemIndex]) then
      result := d1.SortOrder[rgFilterAndSort.ItemIndex] - d2.SortOrder[rgFilterAndSort.ItemIndex]
  else
  begin
    //just testing if this ever happens 
    if (-1 = d1.SortOrder[rgFilterAndSort.ItemIndex]) then Inc(d1.PositionAdded);
    if (-1 = d2.SortOrder[rgFilterAndSort.ItemIndex]) then Inc(d2.PositionAdded);
  end;
end;

Still, frankly, this is not what I would expect from how comparing nodes works.

Torpedo
  • 185
  • 10
  • Can you explain more detailed what you mean with _"this is not what I would expect from how comparing nodes works."_? Do you see a difference to other sort algorithms? – Joachim Marder Nov 29 '16 at 14:42
  • @JoachimMarder: I guess I used bad wording here. I wanted to say that by the first look I would not expect I still have to compare nodes I'm "not interested" in. Once you know how sorting works - then of course, all makes sense. – Torpedo Dec 02 '16 at 16:03
  • Yea, what you tell as 'stage' is actually `level`. – user30478 Aug 20 '23 at 07:45