How can I sort a TList in Delphi on an arbitrary property of the objects it contains?
Asked Answered
H

2

7

I have a TList. It contains a collection of objects of the same type. These objects are descended from a TPersistent, and have about 50 different published properties.

In my application, the user can issue a search of these objects, and the results of the search are displayed in a TDrawGrid, with the specific columns displayed being based on the properties being searched. For example, if the user searches on 'invoice', an 'invoice' column is displayed in the results' grid. I would like to be able to let the user sort this grid. The kicker, of course, is that I wont know up front what columns are in the grid.

Normally to sort a TList, I'd just make a function, such as SortOnName( p1, p2), and call the TList's sort() method. I'd like to go one step further and find a way to pass a property name to the sort method and use RTTI to make the comparison.

I could, of course, make 50 different sort methods and just use that. Or, set a variable globally or as part of the class doing all this work to indicate to the sorting method what to sort on. But I was curious if any of the Delphi pro's out there had other ideas on how to implement this.

Horatio answered 16/9, 2010 at 6:56 Comment(0)
C
7

Delphi 7 version Here's an example of how to achieve that. I used Delphi2010 to implement it but it should work in Delphi7 at least as I used TypInfo unit directly.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Edit1: TEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    FList: TList;
    procedure DoSort(PropName: String);
    procedure DoDisplay(PropName: String);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses
  TypInfo;

var
  PropertyName: String;

type
  TPerson = class
  private
    FName: String;
    FAge: Integer;
  published
  public
    constructor Create(Name: String; Age: Integer);
  published
    property Name: String read FName;
    property Age: Integer read FAge;
  end;

{ TPerson }

constructor TPerson.Create(Name: String; Age: Integer);
begin
  FName := Name;
  FAge := Age;
end;

function ComparePersonByPropertyName(P1, P2: Pointer): Integer;
var
  propValueP1, propValueP2: Variant;
begin
  propValueP1 := GetPropValue(P1, PropertyName, False);
  propValueP2 := GetPropValue(P2, PropertyName, False);

  if VarCompareValue(propValueP1, propValueP2) = vrEqual then begin
    Result :=  0;
  end else if VarCompareValue(propValueP1, propValueP2) = vrGreaterThan then begin
    Result :=  1;
  end else begin
    Result := -1;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FList := TList.Create;
  FList.Add(TPerson.Create('Zed', 10));
  FList.Add(TPerson.Create('John', 20));
  FList.Add(TPerson.Create('Mike', 30));
  FList.Add(TPerson.Create('Paul', 40));
  FList.Add(TPerson.Create('Albert', 50));
  FList.Add(TPerson.Create('Barbara', 60));
  FList.Add(TPerson.Create('Christian', 70));

  Edit1.Text := 'Age';

  DoSort('Age'); // Sort by age
  DoDisplay('Age');
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  DoSort(Edit1.Text);
  DoDisplay(Edit1.Text);
end;

procedure TForm1.DoSort(PropName: String);
begin
  PropertyName := PropName;
  FList.Sort(ComparePersonByPropertyName);
end;

procedure TForm1.DoDisplay(PropName: String);
var
  i: Integer;
  strPropValue: String;
begin
  ListBox1.Items.Clear;

  for i := 0 to FList.Count - 1 do begin
    strPropValue := GetPropValue(FList[i], PropName, False);
    ListBox1.Items.Add(strPropValue);
  end;
end;

end.

BTW, I used a simple form with a listbox, an edit and a button. The listbox shows the contents of the list (FList) sorted. The button is used to sort the list according to what the user has typed in the editbox.

Delphi 2010 version (uses references to methods)

unit Unit2;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm2 = class(TForm)
    ListBox1: TListBox;
    Edit1: TEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    FList: TList;
    FPropertyName: String; { << }
    procedure DoSort(PropName: String);
    procedure DoDisplay(PropName: String);
    function CompareObjectByPropertyName(P1, P2: Pointer): Integer; { << }
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

uses
  TypInfo;

type
  TPerson = class
  private
    FName: String;
    FAge: Integer;
  published
  public
    constructor Create(Name: String; Age: Integer);
  published
    property Name: String read FName;
    property Age: Integer read FAge;
  end;

{ TPerson }

constructor TPerson.Create(Name: String; Age: Integer);
begin
  FName := Name;
  FAge := Age;
end;

/// This version uses a method to do the sorting and therefore can use a field of the form,
/// no more ugly global variable.
/// See below (DoSort) if you want to get rid of the field also ;)
function TForm2.CompareObjectByPropertyName(P1, P2: Pointer): Integer; { << }
var
  propValueP1, propValueP2: Variant;
begin
  propValueP1 := GetPropValue(P1, FPropertyName, False);
  propValueP2 := GetPropValue(P2, FPropertyName, False);

  if VarCompareValue(propValueP1, propValueP2) = vrEqual then begin
    Result :=  0;
  end else if VarCompareValue(propValueP1, propValueP2) = vrGreaterThan then begin
    Result :=  1;
  end else begin
    Result := -1;
  end;
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
  FList := TList.Create;
  FList.Add(TPerson.Create('Zed', 10));
  FList.Add(TPerson.Create('John', 20));
  FList.Add(TPerson.Create('Mike', 30));
  FList.Add(TPerson.Create('Paul', 40));
  FList.Add(TPerson.Create('Albert', 50));
  FList.Add(TPerson.Create('Barbara', 60));
  FList.Add(TPerson.Create('Christian', 70));

  Edit1.Text := 'Age';

  DoSort('Age'); // Sort by age
  DoDisplay('Age');
end;

procedure TForm2.Button1Click(Sender: TObject);
begin
  DoSort(Edit1.Text);
  DoDisplay(Edit1.Text);
end;

procedure TForm2.DoSort(PropName: String);
begin
  FPropertyName := PropName; { << }
  FList.SortList(CompareObjectByPropertyName); { << }

  /// The code above could be written with a lambda, and without CompareObjectByPropertyName
  /// using FPropertyName, and by using a closure thus referring to PropName directly.

  /// Below is the equivalent code that doesn't make use of FPropertyName. The code below
  /// could be commented out completely and just is there to show an alternative approach.
  FList.SortList(
    function (P1, P2: Pointer): Integer
    var
      propValueP1, propValueP2: Variant;
    begin
      propValueP1 := GetPropValue(P1, PropName, False);
      propValueP2 := GetPropValue(P2, PropName, False);

      if VarCompareValue(propValueP1, propValueP2) = vrEqual then begin
        Result :=  0;
      end else if VarCompareValue(propValueP1, propValueP2) = vrGreaterThan then begin
        Result :=  1;
      end else begin
        Result := -1; /// This is a catch anything else, even if the values cannot be compared
      end;
    end);
  /// Inline anonymous functions (lambdas) make the code less readable but
  /// have the advantage of "capturing" local variables (creating a closure)
end;

procedure TForm2.DoDisplay(PropName: String);
var
  i: Integer;
  strPropValue: String;
begin
  ListBox1.Items.Clear;

  for i := 0 to FList.Count - 1 do begin
    strPropValue := GetPropValue(FList[i], PropName, False);
    ListBox1.Items.Add(strPropValue);
  end;
end;

end.

I marked with { << } the main changes.

Crushing answered 16/9, 2010 at 13:3 Comment(2)
Oh, if you have Delphi2010 then you can use a lambda to get rid of the global variable by turning ComparePersonByPropertyName into a method and instead of Sort() you have to use SortList()Crushing
Thanks Trinidad. Thats probably the approach I'll end up using (the code above). I think the only other option is to create a descendent TList and implement my own quick sort that would accept a property name parameter, or at least accept a method (procedure of object) as a parameter to the sort method, rather than just a procedure.Horatio
B
3

Upgrade to Delphi >= 2009, and then you can use anonymous methods to pass a function declaration directly into TList.Sort.

An example can be found at http://delphi.about.com/od/delphitips2009/qt/sort-generic.htm

I don't know of any other way, other than the methods you describe in your question.

Benitobenjamen answered 16/9, 2010 at 7:37 Comment(5)
Thanks, but I'm not sure that would do the trick since the sorting function still needs a way of knowing what property to sort on, which is only known at runtime. (I do have D2010, btw)Horatio
But you would pass that known-at-runtime property as a parameter. Write code that sorts by something, and then tell it which one that something is at runtime.Gibe
I see the problem yes, you might have to do a case statement to know what to compare in the anonymous method, e.g. MyList.Sort(TComparer<TMyObj>.Construct( function (const L, R: TMyObj): integer begin if Edit1 = 'Age' then result := CompareValue(L.Age, R.Age) end )) ;Benitobenjamen
Not a case statement actually.Benitobenjamen
Thanks for the assist Alan. Its definately a little 'gotcha' with sorting. I only need it in one place, otherwise I'd create a new TList descendent that implemented its own quicksort. I mainly curious if there was a more elegant way to do what I wanted to do.Horatio

© 2022 - 2024 — McMap. All rights reserved.