1

I have heard a lot of praise about the VirtualTreeView component and looked at using it in a rewrite we are doing. Currently we use a StringGrid.

I can't find a way to sort multiple columns, though single column sorting works great. Is there any way to do something similar to click column 1>sort, Ctrl+click column 2>sort column 2 after column 1, etc?

Specifically, I want to sort at least three columns: PO Number, Line Item, Release.

Thanks in advance for your help!

Here is the code (slightly simplified) I am testing the theory with (not from the same project referenced above):

Note: After your update I edited my code, also, to show what it currently is. Below I posted the results of the sort:

type
  PBatchDetails = ^TBatchDetails;
  TBatchDetails = record
    TheBatchKey
    OperationKey,
    PO,
    Line,
    Release,
    Temp,
    Notes : String;
    TransDate : TDateTime;
end;

....
Sorting_Columns: array of TColumnIndex;
....
procedure TForm1.TreeHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
var
  I: Integer;
begin
  if not CtrlDown then //function I have to test Ctrl state.
  begin
    setlength(Sorting_Columns,0);
  end;
  SetLength(Sorting_Columns,length(Sorting_Columns)+1);
  Sorting_Columns[Length(Sorting_Columns)-1] := HitInfo.Column;
  tree.SortTree(HitInfo.Column,Sender.SortDirection,True);
  if Sender.SortDirection=sdAscending then
    Sender.SortDirection:=sdDescending
  else
    Sender.SortDirection:=sdAscending
end;


procedure TForm1.TreeCompareNodes(Sender: TBaseVirtualTree; Node1,
  Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var
  BatchRec1 : PBatchDetails;
  BatchRec2: PBatchDetails;
  I: Integer;
begin
  if length(Sorting_Columns) > 0 then
  begin
    BatchRec1 := Tree.GetNodeData(Node1);
    BatchRec2 := Tree.GetNodeData(Node2);
    if (not Assigned(BatchRec1)) or (not Assigned(BatchRec2)) then
      Result:=0
    else
    begin
      for I := High(Sorting_Columns) downto 0 do
      begin
        case Sorting_Columns[i] of
          0,1: Result := Result + CompareDate(BatchRec1.TransDate,BatchRec2.TransDate); // col 0 is Date and col 1 is Time.
          2: Result := Result + CompareText(BatchRec1.OperationKey,BatchRec2.OperationKey);
          3: Result := Result + CompareText(BatchRec1.PO,BatchRec2.PO);
          4: Result := Result + CompareText(BatchRec1.Line,BatchRec2.Line);
          5: Result := Result + CompareText(BatchRec1.Release,BatchRec2.Release);
          6: Result := Result + CompareText(BatchRec1.Temp, BatchRec2.Temp);
          7: Result := Result + CompareText(BatchRec1.Notes,BatchRec2.Notes);
        end; //end case;
        if Result <> 0 then
          Break;
      end;
    end;
  end;
end;

This produced the following results (I am only showing the three columns I am trying to sort here):

When originally loaded:
PO Line Release
153 7 2
153 7 1
153 1 1
153 1 2
153 4 1
153 6 2
153 6 1
120 3 2
120 3 1
153 2 1
153 4 2
120 2 1
153 4 1
120 1 1
153 3 1
153 2 1
111 2 1
111 1 5
111 1 1
111 4 2
111 3 1
111 4 1
111 1 3
111 1 2
111 1 4

After first click
PO Line Release
111 2 1
111 1 5
111 1 1
111 4 2
111 3 1
111 4 1
111 1 3
111 1 2
111 1 4
120 3 2
120 3 1
120 2 1
120 1 1
153 7 2
153 7 1
153 1 1
153 1 2
153 4 1
153 6 2
153 6 1
153 2 1
153 4 2
153 4 1
153 3 1
153 2 1

After second click
PO Line Release
153 7 2
153 7 1
153 6 2
153 6 1
153 4 1
153 4 2
153 4 1
111 4 2
111 4 1
153 3 1
120 3 2
120 3 1
111 3 1
153 2 1
153 2 1
120 2 1
111 2 1
153 1 1
153 1 2
120 1 1
111 1 5
111 1 1
111 1 3
111 1 2
111 1 4

After Third Click
PO Line Release
111 1 1
120 1 1
153 1 1
111 2 1
120 2 1
153 2 1
153 2 1
111 3 1
120 3 1
153 3 1
111 4 1
153 4 1
153 4 1
153 6 1
153 7 1
111 1 2
153 1 2
120 3 2
111 4 2
153 4 2
153 6 2
153 7 2
111 1 3
111 1 4
111 1 5

Thanks for your time!

Alan
  • 11
  • 1
  • 3
  • 1
    Are you using OnCompareNodes event ? –  Dec 15 '10 at 15:40
  • OnCompareNodes is the answer, + handling Ctrl+clicks in code. – Ondrej Kelle Dec 15 '10 at 15:45
  • About the CTRL check. It's wrapped in the HitInfo parameter. if not ssCtrl in HitInfo.Shift ... –  Dec 16 '10 at 17:48
  • But I thought you developed your own solution. This one works strange. I'll try to write the better one ... –  Dec 16 '10 at 17:56
  • Sorry, I just tried implementing yours. I couldn't figure out how to do my own so that is why I finally tried posting a question for the first time :) – Alan Dec 16 '10 at 20:03
  • Hi there, check my code below. I've made a mistake at OnCompareNodes in the logic itself. You need to iterate columns from the last selected one to the first one, so correct "0 to high(Sorting_Columns)" to "high(Sorting_Columns) downto 0". And of course if there is such difference between two rows in certain column, stop the iteration. So add "if Result <> 0 then Break;" after case statement. Hope this will help :) –  Dec 17 '10 at 00:20
  • And another thing. Be careful in the TreeHeaderClick event, you may now add the same column (to the Sorting_Columns array) multiple times. Before you add the column to that array, check whether it's not already in. Cheers –  Dec 17 '10 at 00:37
  • Your welcome. Please confirm my answer as accepted solution - if it's possible somehow. Anyway it's a little bit confusing (especially for angry users like me :) that when you click first time, you sort ASC, the second click DESC, third ASC etc. It would be fine to add if clause like "if Length(Sorting_Columns) then ChangeSortDirection". ChangeSortDirection means if Sender.SortDirection=sdAscending then ... –  Dec 17 '10 at 07:58

2 Answers2

2

Disable every auto sorting options in general. Then you need to implement OnCompareNodes along with OnHeaderClick events.

Here is I hope working code (I've made just quick test :)

The aim is to store sorting columns in some variable (Sorting_Columns). This variable you can feed in OnHeaderClick event.
In the OnCompareNodes event, which will be triggered after SortTree function call, iterate through the variable from the last added column to the first added one and to the Result parameter pass the first nonzero comparision result. Now humanly - you should go through the columns backwards as they were "selected" and check if they are same, if yes go to the previously selected, if not break the loop and pass the result. Note that you are comparing two nodes (rows) in one event hit, what's the reason for the iteration and subsequent comparision of sorting columns.

type
  PRecord = ^TRecord;
  TRecord = record
    ID: integer;
    Text_1: string;
    Text_2: string;
    Text_3: string;
    Date: TDateTime;
  end;

...

var Sorting_Columns: array of TColumnIndex;

...

procedure TForm1.VirtualStringTree1CompareNodes(Sender: TBaseVirtualTree;
  Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var Actual_Index: integer;
    Data_1: PRecord;
    Data_2: PRecord;

begin
  if Length(Sorting_Columns) > 0 then
    begin
      Data_1 := VirtualStringTree1.GetNodeData(Node1);
      Data_2 := VirtualStringTree1.GetNodeData(Node2);

      if Assigned(Data_1) and Assigned(Data_2) then
        for Actual_Index := High(Sorting_Columns) downto 0 do
          case Sorting_Columns[Actual_Index] of
            0: Result := Result + Data_1^.ID - Data_2^.ID;
            1: Result := Result + CompareStr(Data_1^.Text_1, Data_2^.Text_1);
            2: Result := Result + CompareStr(Data_1^.Text_2, Data_2^.Text_2);
            3: Result := Result + CompareStr(Data_1^.Text_3, Data_2^.Text_3);
            4: Result := Result + CompareDateTime(Data_1^.Date, Data_2^.Date);
          end;

      if Result <> 0 then
        Break;
    end;
end;
  • Is it possible you can give me a little sample code to get me started? Sorry, I am NOT very experienced. In the TreeHeaderClick I call SortTree and then reverse the Header's SortDirection. – Alan Dec 15 '10 at 20:28
  • Oops, I didn't finish :) Then in OnCompareNodes I used a CompareDate or CompareText function in a case statement based on the column that was sent to OnCompareNodes. So it sounds like I am on the right path, I just don't know how to do the Ctrl+Clicks, etc. :) – Alan Dec 15 '10 at 20:35
  • @Alan: I think you must simply check if the Ctrl or Shift key was pressed in the OnClick event. eg: if GetKeyState(VK_SHIFT... see http://msdn.microsoft.com/en-us/library/ms646301(VS.85).aspx – Remko Dec 15 '10 at 20:54
  • @Remko - nonsense. he can use directly OnHeaderClick to set up the sorting style. There is either fairly Shift or HitInfo.Shift member (depends on your Delphi version) for the CTRL handling. –  Dec 16 '10 at 09:28
  • @Alan - yes, you're on the right way. I haven't tried this yet, so I can't offer you some piece of code. But this question is quite interesting (+1 for you :), so I'll try to write some sample, but I have no time for this now, so this may take some time for me. –  Dec 16 '10 at 09:32
  • @Alan - you need to consider at least these things. OnCompareNodes is triggered when you call SortTree, but only for one column (the one you pass in the SortTree function). This means, that when you wish to sort by more columns, you need to store them (their indexes) in some "global" variable (array, set ...) In the OnCompareNodes event you should go through this variable, for each column compare given two nodes (Node1, Node2) and to the Result pass the overall result of iterated comparision. See my updated answer ... I have no Delphi now, so forgive me syntax mistakes –  Dec 16 '10 at 09:54
  • @Remko - I guess you meant TVTHeaderClickEvent = procedure(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo) of object; This one is thiggered on header plate click. –  Dec 16 '10 at 12:04
  • @daemon_x: yes indeed (I removed my comment since it was incorrect) – Remko Dec 16 '10 at 12:07
  • @daemon_x, I tried to implement your suggestion but it didn't quite work. In OnHeaderClick I test to see if Ctrl is not down, and if it isn't I clear the Sorting_Columns array (set length to 0), otherwise I add the column to the array and then call SortTree(HitInfo.Column,Sender.SortDirection,True). Then in OnCompareNodes I implemented into my code the differences based on your suggestion above and it definitely sorted different when there were multiple columns in the Sorting_Columns array, but didn't achieve the desired effect. If it would help I can post my actual code. – Alan Dec 16 '10 at 16:02
  • @Alan - yes, post your code, please. As I mentioned, the aim in OnCompareNodes is to compare two nodes for more than one column, so I would say that my suggestion is wrong in Result := Result + "ColumnComparisionResult". If you imagine how this peace of code works, you will find out, that when you have e.g. 4 columns to sort, then when 2 of them are alphabetically higher and 2 of them lower, then the result is 0, which means no re-sort and that's a bug. –  Dec 16 '10 at 16:20
0

Slightly modified code from @user532231 to get a working solution

type
  PRecord = ^TRecord;
  TRecord = record
    ID: integer;
    Text_1: string;
    Text_2: string;
    Text_3: string;
    Date: TDateTime;
  end;

...

var Sorting_Columns: array of TColumnIndex;

...

procedure TForm1.VirtualStringTree1CompareNodes(Sender: TBaseVirtualTree;
  Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var Actual_Index: integer;
    Data_1: PRecord;
    Data_2: PRecord;
    Matrix : array of integer;
    I: Integer;
begin
  if Length(Sorting_Columns) > 0 then
    begin
      Data_1 := VirtualStringTree1.GetNodeData(Node1);
      Data_2 := VirtualStringTree1.GetNodeData(Node2);

      if Assigned(Data_1) and Assigned(Data_2) then
        begin
          SetLength(Matrix,Length(Sorting_Columns));
          for Actual_Index := 0 to High(Sorting_Columns) do
            begin
              case Sorting_Columns[Actual_Index] of
                0: Matrix[Actual_Index] := Data_1^.ID - Data_2^.ID;
                1: Matrix[Actual_Index] := CompareStr(Data_1^.Text_1, Data_2^.Text_1);
                2: Matrix[Actual_Index] := CompareStr(Data_1^.Text_2, Data_2^.Text_2);
                3: Matrix[Actual_Index] := CompareStr(Data_1^.Text_3, Data_2^.Text_3);
                4: Matrix[Actual_Index] := CompareDateTime(Data_1^.Date, Data_2^.Date);
              end;
            end;
          for I := 0 to Length(Matrix) - 1 do
            if (Matrix[i] <> 0) then
              begin
                Result:=Matrix[i];
                break;
              end;
          SetLength(Matrix,0);
        end;      
    end;
end;

The difference is, you need to remember the result of each column comparison and then return the first most significant non-zero value (most significant being the column that was added to sorting first). You don't need to loop from highest to lowest column. This code needs the OP's TreeHeaderClick procedure to add/remove columns to Sorting_Columns.

Here, the sort direction is always the same for all columns. It should be fairly easy to implement sorting direction, by reversing the comparison result of each column according to its sort direction, ascending or descending. I didn't try this.

misterti
  • 615
  • 6
  • 15