Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 278
GrandmasterB Avatar asked Sep 16 '10 06:09

GrandmasterB


2 Answers

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.

like image 196
Trinidad Avatar answered Sep 23 '22 00:09

Trinidad


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.

like image 24
Alan Clark Avatar answered Sep 21 '22 00:09

Alan Clark