Alberto Rossi
Alberto Rossi

Reputation: 1800

Sort TStringGrid by row and its integer value

I have StringGrid with 2 columns: Player and Scores. I have to sort this table looking at the score of a player.

enter image description here

This is the situation. I have tried with a StringGrid3.SortColRow(true, 1); but it sorts only the string value. If I have numbers like [1, 4, 12, 3] the sorted StringGrid becomes [a, 12, 3, 4].

I have to sort the Integer value, not the string one. Also, my problem is that I must move the player's name too with the numbers (as you can see in the picture above).

How could I do it?

Upvotes: 5

Views: 10432

Answers (4)

Bosj
Bosj

Reputation: 1

It seems the Sorted := True should be within the repeat. Now the repeat only stops thanks to (J >= Grid.RowCount + 1000).

  repeat
    Sorted := True;
    Inc(J);
    for I := FirstRow to Grid.RowCount - 2 do
      if Sort(I, I + 1) > 0 then
      begin
        TMoveSG(Grid).MoveRow(i + 1, i);
        Sorted := False;
      end;
  until Sorted or (J >= Grid.RowCount + 1000);

Upvotes: 0

bummi
bummi

Reputation: 27394

You might write a comparator which is trying to sort by Numbers if both compared values are numbers. With casting the Stringgrid to its ancestor TCustomgrid you are able to use the procedure MoveRow to swap rows. An example showing sorting over many columns could look like this:

unit StringGridSortEnh;
interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, Grids, StdCtrls;

type
  TForm1 = class(TForm)
    StringGrid1: TStringGrid;
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}
type
  TMoveSG = class(TCustomGrid);
  TSortInfo = Record
    col: Integer;
    asc: Boolean;
  End;

function CompareNumber(i1, i2: Double): Integer;
// Result: -1 if i1 < i2, 1 if i1 > i2, 0 if i1 = i2
begin
  if i1 < i2 then
    Result := -1
  else if i1 > i2 then
    Result := 1
  else
    Result := 0;
end;

// Compare Strings if possible try to interpret as numbers
function CompareValues(const S1, S2 : String;asc:Boolean): Integer;
var
  V1, V2 : Double;
  C1, C2 : Integer;
begin
  Val(S1, V1, C1);
  Val(S2, V2, C2);
  if (C1 = 0) and (C2 = 0) then  // both as numbers
     Result := CompareNumber(V1, V2)
  else  // not both as nubers
     Result := AnsiCompareStr(S1, S2);
  if not Asc then Result := Result * -1;

end;

procedure SortGridByCols(Grid: TStringGrid; ColOrder: array of TSortInfo; Fixed: Boolean);
var
  I, J, FirstRow: Integer;
  Sorted: Boolean;

  function Sort(Row1, Row2: Integer): Integer;
  var
    C: Integer;
  begin
    C := 0;
    Result := CompareValues(Grid.Cols[ColOrder[C].col][Row1], Grid.Cols[ColOrder[C].col][Row2],ColOrder[C].asc);
    if Result = 0 then
    begin
      Inc(C);
      while (C <= High(ColOrder)) and (Result = 0) do
      begin
        Result := CompareValues(Grid.Cols[ColOrder[C].col][Row1], Grid.Cols[ColOrder[C].col][Row2],ColOrder[C].asc);
        Inc(C);
      end;
    end;
  end;

begin
  for I := 0 to High(ColOrder) do
    if (ColOrder[I].col < 0) or (ColOrder[I].col >= Grid.ColCount) then
      Exit;

  if Fixed then
    FirstRow := 0
  else
    FirstRow := Grid.FixedRows;

  J := FirstRow;
  Sorted := True;
  repeat
    Inc(J);
    for I := FirstRow to Grid.RowCount - 2 do
      if Sort(I, I + 1) > 0 then
      begin
        TMoveSG(Grid).MoveRow(i + 1, i);
        Sorted := False;
      end;
  until Sorted or (J >= Grid.RowCount + 1000);
  Grid.Repaint;
end;

procedure TForm1.Button1Click(Sender: TObject);
const // we want to use only 4 columns
  MyArray: array[0..3] of TSortInfo =
    ((col: 1; asc: true),
     (col: 2; asc: true),
     (col: 3; asc: true),
     (col: 4; asc: false)
     );
begin
  SortGridByCols(StringGrid1,MyArray,true);
end;

procedure TForm1.Button2Click(Sender: TObject);
const  // we want to use only one column
  MyArray: array[0..0] of TSortInfo = ((col: 1; asc: true));
begin
  SortGridByCols(StringGrid1,MyArray,true);
end;

end.

enter image description here enter image description here

Upvotes: 7

Re0sless
Re0sless

Reputation: 10886

In Lazarus you can define how the cells of a TStringGrid sort using the OnCompareCells event, see the following link for and example of how to do this with numbers and more info.

http://wiki.lazarus.freepascal.org/Grids_Reference_Page#Sorting_Columns_or_Rows


If you want to sort the rows in Delphi you would need to re-implement the SortColRow method from Lazarus.

You can do this with a class helper

Note that this has NOT been tested fully!

The AnsiCompareStr in the below procedures can then be swamped out to compare integers.

type
  TStringGridHelper = class helper  for TStringGrid
  public
    procedure SortColRow(IsColumn: Boolean; Index: Integer); overload;
    procedure SortColRow(IsColumn: Boolean; Index: Integer; FromIndex: Integer; ToIndex: Integer); overload;
end;


implementation


procedure TStringGridHelper.SortColRow(IsColumn: Boolean; Index: Integer);
begin
     if (IsColumn) then SortColRow(IsColumn, Index, FixedCols, ColCount - 1)
     else SortColRow(IsColumn, Index, FixedRows, RowCount - 1)
end;

procedure TStringGridHelper.SortColRow(IsColumn: Boolean; Index: Integer; FromIndex: Integer; ToIndex: Integer);
    var i, p, x, c : Integer;
    s1, s2 : String;
begin

    if (IsColumn) then
    begin
        for x := ToIndex downto FromIndex do
            for I := FromIndex to ToIndex - 1 do
            begin
                s1 := Cells[i, index];
                s2 := Cells[i + 1, index];

                c := AnsiCompareStr(s1, s2);
                if (c > 0) then
                begin
                    p := i + 1;
                    p := Max(p, FromIndex);
                    p := Min(p, ToIndex);

                    MoveColumn(i, p);
                end;
            end;
    end
    else
    begin
        for x := ToIndex downto FromIndex do
            for I := FromIndex to ToIndex - 1 do
            begin
                s1 := Cells[index, i];
                s2 := Cells[index, i + 1];
                c := AnsiCompareStr(s1, s2);
                if (c > 0) then
                begin
                    p := i + 1;
                    p := Max(p, FromIndex);
                    p := Min(p, ToIndex);

                    MoveRow(i, p);
                end;
            end;
    end;
end;

Upvotes: 2

Stik
Stik

Reputation: 607

Are you using Lazarus instead of Delphi? I'm not aware of a sorting feature in the standard Delphi StringGrid implementation.

The issue is that the StringGrid is just that, a grid of Strings. It has no understanding that the strings are in fact numbers, so it can only sort them alphabetically.

If you are looking for a more complex sorting ability than the grid provides naturally you have a couple of choices. Either override the TStringGrid class and produce your own specialised version that sorts how you want it to (i'm afraid i don't have the Lazarus source code to hand to give you more specific instructions), or you can simply sort the records before you put them into the grid.

For instance, if you have a TPlayer class that represents your player and their scores then you can use TList.Sort(..) to sort it however you like before you loop over and populate the grid.

// 
// Sort the player scores by their *numeric* value, not by the string representation
//
function SortByPlayerScore(A, B: Pointer) : integer
begin
    Result := TPlayer(A).Score - TPlayer(B).Score;
end;

// ....

list.Sort(SortByPlayerScore);

grid.RowCount := list.Count + 1;
for i := 0 to list.Count - 1 do begin
    grid.Cells[0, i + 1] := list[i].Name;
    grid.Cells[1, i + 1] := IntToStr(list[i].Score);
end;

Upvotes: 1

Related Questions