Jump to content


FourWhey

Member Since 07 Apr 2014
Offline Last Active Apr 14 2026 11:37 PM
-----

Topics I've Started

NextGrid6 True Multi-Column Sort -- Added

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

Delphi 13

24 January 2026 - 12:45 AM

I hope you're doing well. I’m writing to ask about the current status and future plans for the NextGrid 5 package. Our code relies on a few of the controls in this suite, and we are now preparing to migrate our codebase to Delphi 13.
 
I understand that this particular component suite is considered legacy, and I don’t want to assume it will receive updates automatically. Could you please let us know whether you intend to release an updated version that is compatible with Delphi 13?
 
If there are no plans to update the component suite, it would be greatly appreciated if you could provide any guidance or recommendations you can provide on what would be required to manually migrate the package from Delphi 12 to Delphi 13. Tips, known issues, build considerations, or any notes about compiler or RTL changes that would help streamline the transition would be extremely helpful.
 
Thank you for your time, and for all the work you’ve put into maintaining this component over the years. I look forward to hearing from you.