Jump to content


Photo

NextGrid6 True Multi-Column Sort -- Added


  • Please log in to reply
4 replies to this topic

#1 FourWhey

FourWhey
  • Members
  • 175 posts

Posted 23 March 2026 - 08:59 PM

The bulk of the plumbing was done for multi-sort. When MultiSort is enabled and multiple columns are sorted (Ctrl+Click on headers), the grid tracks sorted columns in FSortedList and displays sort arrows, but the Sort method only sorted by a single column.

 

I updated the sort algorithm to handle N number of selected columns.

 

NxCells6.pas

procedure TNxCells6.Sort(const Index: Integer; SortKind: TNxSortKind; Resort: Boolean);
var
ALessCompFunc: TNxCompareFunc;
AGreaterCompFunc: TNxCompareFunc;

  function GetChildCell(ACol, ARow: Integer): INxBase;
  begin
    Result := (Self.ChildRow[ARow] as INxCellsRow).Cells[ACol];
  end;

  procedure Exchange(Pos1, Pos2: Integer);
  var
    P: IInterface;
  begin
    P := FChildRowsList[Pos1];
    FChildRowsList[Pos1] := FChildRowsList[Pos2];
    FChildRowsList[Pos2] := P;
  end;

  procedure InverseSort;
  var
    i, Middle, Count: Integer;
  begin
    if RowCount > 0 then
    begin
      Middle := Pred(ChildRowCount div 2);

      { Optimize }
      Count := ChildRowCount;

      for i := 0 to Middle do Exchange(i, Pred(Count) - i);
    end;
  end;

  // New
  function MultiCompare(Row1, Row2: Integer): Integer;
  var
    K: Integer;
    Col: TNxColumn6;
    C1, C2: INxBase;
  begin
    Result := 0;
    for K := 0 to Control.Columns.MultiSortedCount - 1 do
    begin
      Col := Control.Columns.MultiSortedColumn[K];
      C1 := GetChildCell(Col.Index, Row1);
      C2 := GetChildCell(Col.Index, Row2);

      case Col.SortType of
        stAlphabetic:
          if C1.AsString < C2.AsString then Result := -1
          else if C1.AsString > C2.AsString then Result := 1;
        stUnicodeAlphabetic:
          Result := WideCompareStr(C1.AsString, C2.AsString);
        stCaseInsensitive:
          if LowerCase(C1.AsString) < LowerCase(C2.AsString) then Result := -1
          else if LowerCase(C1.AsString) > LowerCase(C2.AsString) then Result := 1;
        stNumeric:
          if C1.AsFloat < C2.AsFloat then Result := -1
          else if C1.AsFloat > C2.AsFloat then Result := 1;
        stDate:
          if C1.AsDateTime < C2.AsDateTime then Result := -1
          else if C1.AsDateTime > C2.AsDateTime then Result := 1;
        stBoolean:
          if C1.AsBoolean < C2.AsBoolean then Result := -1
          else if C1.AsBoolean > C2.AsBoolean then Result := 1;
        stCustom:
          DoCellCompare(C1, C2, Result);
        stIP:
          if IsIPSmaler(C1.AsString, C2.AsString) then Result := -1
          else if IsIPSmaler(C2.AsString, C1.AsString) then Result := 1;
      end;

      if Result <> 0 then
      begin
        if Col.SortKind = skDescending then
          Result := -Result;
        Exit;
      end;
    end;
  end;

  function DivideAsc(l, r: Integer; LessCompFunc,
   GreaterCompFunc: TNxCompareFunc): Integer;
  var
    i, j: Integer;
    pivot: INxBase;
  begin
   pivot := GetChildCell(Index, l + 1 + Random(r - l));

    i := l - 1;
    j := r + 1;

    repeat
      repeat Inc(i) until LessCompFunc(pivot, GetChildCell(Index, i));
      repeat Dec(j) until GreaterCompFunc(pivot, GetChildCell(Index, j));

      if GetChildCell(Index, j) <> GetChildCell(Index, i)
        then Exchange(i, j)
    until j <= i;

    if GetChildCell(Index, j) <> GetChildCell(Index, i)
      then Exchange(i, j);

    Result := i;
  end;

  function DivideDesc(l, r: Integer; LessCompFunc,
   GreaterCompFunc: TNxCompareFunc): Integer;
  var
    i, j: Integer;
    pivot: INxBase;
  begin
   pivot := GetChildCell(Index, l + 1 + Random(r - l));
    i := l - 1;
    j := r + 1;
    repeat
      repeat Inc(i) until GreaterCompFunc(pivot, GetChildCell(Index, i));
      repeat Dec(j) until LessCompFunc(pivot, GetChildCell(Index, j));
      if (GetChildCell(Index, j) <> GetChildCell(Index, i)) then Exchange(i, j)
    until j <= i;
    if (GetChildCell(Index, j) <> GetChildCell(Index, i)) then Exchange(i, j);
    Result := i;
  end;

  procedure Quicksort(Lft, Rgt: Integer);
  var
    Middle: Integer;
  begin
    if Lft < Rgt then
    begin
      if FSortedColumn.SortKind = skAscending 
        then Middle := DivideAsc(Lft, Rgt, ALessCompFunc, AGreaterCompFunc)
        else Middle := DivideDesc(Lft, Rgt, ALessCompFunc, AGreaterCompFunc);

      Quicksort(Lft, Middle - 1);
      Quicksort(Middle, Rgt);
    end;
  end;

  // New
  function DivideMulti(l, r: Integer): Integer;
  var
    i, j, pivotPos: Integer;
  begin
    pivotPos := l + 1 + Random(r - l);
    i := l - 1;
    j := r + 1;
    repeat
      repeat Inc(i) until MultiCompare(pivotPos, i) <= 0;
      repeat Dec(j) until MultiCompare(pivotPos, j) >= 0;
      if i < j then
      begin
        Exchange(i, j);
        if pivotPos = i then pivotPos := j
        else if pivotPos = j then pivotPos := i;
      end;
    until j <= i;
    Result := i;
  end;

  // New
  procedure QuicksortMulti(Lft, Rgt: Integer);
  var
    Middle: Integer;
  begin
    if Lft < Rgt then
    begin
      Middle := DivideMulti(Lft, Rgt);
      QuicksortMulti(Lft, Middle - 1);
      QuicksortMulti(Middle, Rgt);
    end;
  end;
begin
  FSortedColumn := Control.Columns[Index];

  case FSortedColumn.SortType of
    stAlphabetic:
    begin
      ALessCompFunc := AlphabeticLessCompare;
      AGreaterCompFunc := AlphabeticGreaterCompare;
    end;
    stBoolean:
    begin
      ALessCompFunc := BooleanLessCompare;
      AGreaterCompFunc := BooleanGreaterCompare;
    end;
    stCaseInsensitive:
    begin
      ALessCompFunc := CaseInsensitiveLessCompare;
      AGreaterCompFunc := CaseInsensitiveGreaterCompare;
    end;
    stCustom:
    begin
      ALessCompFunc := CustomLessCompare;
      AGreaterCompFunc := CustomGreaterCompare;
    end;
    stNumeric:
    begin
      ALessCompFunc := NumericLessCompare;
      AGreaterCompFunc := NumericGreaterCompare;
    end;
    stDate:
    begin
      ALessCompFunc := DateLessCompare;
      AGreaterCompFunc := DateGreaterCompare;
    end;
    stIP:
    begin
      ALessCompFunc := IPLessCompare;
      AGreaterCompFunc := IPGreaterCompare;
    end;
    stUnicodeAlphabetic:
    begin
      ALessCompFunc := AlphabeticUnicodeLessCompare;
      AGreaterCompFunc := AlphabeticUnicodeGreaterCompare;
    end;
  end;

  if (Index = FSortedCol) and FSorted and (SortKind <> FSortKind)
    and not Resort and (Control.Columns.MultiSortedCount <= 1) then // Guard against MultiSortedCount <= 1
  begin
    FSortKind := SortKind;
    InverseSort;
  end else
  begin
    FSortKind := SortKind;
    if Control.Columns.MultiSortedCount > 1 then // This is multi-sort 
      QuicksortMulti(0, Pred(ChildRowCount))     // Call QuickSortMulti to sort by all selected columns
    else                                         // fall back to single column sort
      Quicksort(0, Pred(ChildRowCount));
  end;

  ResortBranches;

  FSortedCol := Index;
  FSortKind := SortKind;
  FSorted := True;
end;

Usage:

Interactive -- User Clicks Column(s)

-----

  The grid must have MultiSort = True.
  - Click "Name" column header --> sorts by Name ascending
  - Click "Name" again         --> sorts by Name descending
  - Ctrl+Click "City" header   --> adds City as secondary sort (Name desc, City asc)
  - Ctrl+Click "City" again    --> City toggles to descending
  - Ctrl+Click "City" again    --> City removed from sort (Name desc only)
  Result: rows sort by Name first; rows with equal Name sort by City.
 
Programmatic -- Add secondary sort at runtime
-----
  // Sort by Status column first
  Grid.Columns[StatusColIndex].SetMultiSorted(True, False); 
  // Add=False clears existing sorts, makes Status the primary sort
  // Add SubId as secondary tiebreaker (preserves Status sort)
  Grid.Columns[SubIdColIndex].SortKind := skAscending;
  Grid.Columns[SubIdColIndex].SetMultiSorted(True, True);
  // Add=True preserves existing sorts, adds SubId as secondary
  // Rows now sort by Status first, then SubId for equal Status values.
 
Programmatic -- Re-sort after data update
-----
  // After updating grid cell data, re-apply the active multi-sort:
  if Grid.Columns.MultiSortedCount > 0 then
  begin
    SortCol := Grid.Columns.MultiSortedColumn[
      Grid.Columns.MultiSortedCount - 1];
    SortCol.Resort;
  end;
 
  // Resort triggers Sort() which detects MultiSortedCount > 1
  // and uses the multi-column QuicksortMulti path automatically.
 
Programmatic -- Query current sort state
-----
  // How many columns are currently sorted?
  Count := Grid.Columns.MultiSortedCount;
 
  // Get the Nth sorted column (0-based, in priority order):
  Col := Grid.Columns.MultiSortedColumn[0];  // primary sort column
  Col := Grid.Columns.MultiSortedColumn[1];  // secondary sort column
 
  // Check if a specific column is in the sort:
  if MyColumn.Sorted then ...
 
  // Get a column's position in the sort priority:
  Idx := Grid.Columns.SortedIndexOf(MyColumn); // -1 if not sorted


#2 Boki (Berg)

Boki (Berg)

    Boki (Berg)

  • Forum Admin
  • PipPipPipPipPip
  • 8,287 posts
  • Gender:Male

Posted 25 March 2026 - 01:20 AM

Hello,

 

This looks promising. I will test if little bit. I want to understand how you done it.


boki@bergsoft.net | LinkedIn Profile
--
BergSoft Home Page: www.bergsoft.net
Users Section: users.bergsoft.net
Articles and Tutorials: help.bergsoft.net (Developers Network)
--
BergSoft Facebook page
--
Send us applications made with our components and we will submit them on: www.bergsoft.net/apps.htm. Link to this page will be also set on home page too.

#3 Boki (Berg)

Boki (Berg)

    Boki (Berg)

  • Forum Admin
  • PipPipPipPipPip
  • 8,287 posts
  • Gender:Male

Posted 27 March 2026 - 04:44 PM

Thank you,

 

So far it work well. Can I include it in official release?

 

I still think about adding MultiSelect mode (virtual and data) so that it still can be used in DBGrid in similar.

 

Best regards


boki@bergsoft.net | LinkedIn Profile
--
BergSoft Home Page: www.bergsoft.net
Users Section: users.bergsoft.net
Articles and Tutorials: help.bergsoft.net (Developers Network)
--
BergSoft Facebook page
--
Send us applications made with our components and we will submit them on: www.bergsoft.net/apps.htm. Link to this page will be also set on home page too.

#4 FourWhey

FourWhey
  • Members
  • 175 posts

Posted 01 April 2026 - 10:22 PM

Thank you,

 

So far it work well. Can I include it in official release?

 

I still think about adding MultiSelect mode (virtual and data) so that it still can be used in DBGrid in similar.

 

Best regards

Yes please do.



#5 FourWhey

FourWhey
  • Members
  • 175 posts

Posted 01 April 2026 - 11:04 PM

Thank you,

 

So far it work well. Can I include it in official release?

 

I still think about adding MultiSelect mode (virtual and data) so that it still can be used in DBGrid in similar.

 

Best regards

Do you mean sorting Virtual and Data NxDBGrid6 using a common abstraction, or something else? When you said virtual, I think you might mean when NxDBGrid6 with buffer records enabled?

 

I also changed NxVirtualGrid6 to sort similar to NxGrid6. Its sort methods were stubs so I created an index map so it could sort in a similar way as NxGrid6 but was a problem because it doesn't own the cells in the same way.

It goes through TNxVirtualCellSource6 instead of TNxCells6.

 

I never got to test it, so I didn't share it.






1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users