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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With