Link to home
Start Free TrialLog in
Avatar of Marius0188
Marius0188

asked on

TListView Column Click Order By

Dear Experts,

I need a elegant way to order a TListViews items according to the column header that has been clicked.

I have a function to order only string types but need a way to order:
1. Integer/Float
2. String
3. Date
values as well.

Please help!

Thanks in advance!
Avatar of Russell Libby
Russell Libby
Flag of United States of America image


One way of doing it:

type
  TLVSortType    =  (stStr, stInt, stFloat, stDate);

function LVBaseSort(lParam1, lParam2: TListItem; Column: Integer; SortType: TLVSortType): Integer;
var  szItem1:       String;
     szItem2:       String;
     dblResult:     Double;
begin

  // Get values to sort on
  if (Column = 0) then
  begin
     // Get values from caption
     szItem1:=lParam1.Caption;
     szItem2:=lParam2.Caption
  end
  else
  begin
     // Get value from subitems
     szItem1:=lParam1.SubItems[Pred(Column)];
     szItem2:=lParam2.SubItems[Pred(Column)];
  end;

  // Sort the items
  case SortType of
     stStr    :  result:=CompareStr(szItem1, szItem2);
     stInt    :  result:=StrToInt(szItem1) - StrToInt(szItem2);
     stFloat  :
     begin
        dblResult:=StrToFloat(szItem1) - StrToFloat(szItem2);
        if (dblResult > 0) then
           result:=1
        else if (dblResult < 0) then
           result:=(-1)
        else
           result:=0;
     end;
     stDate   :
     begin
        dblResult:=StrToDate(szItem1) - StrToDate(szItem2);
        if (dblResult > 0) then
           result:=1
        else if (dblResult < 0) then
           result:=(-1)
        else
           result:=0;
     end;
  else
     // Sort by string
     result:=CompareStr(szItem1, szItem2);
  end;

end;

function LVStrSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, lParamSort, stStr);
end;

function LVIntSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, lParamSort, stInt);
end;

function LVFloatSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, lParamSort, stFloat);
end;

function LVDateSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, lParamSort, stDate);
end;

// Then decide how you want to sort each column.
procedure TForm1.ListView1ColumnClick(Sender: TObject; Column: TListColumn);
begin

  // Apply whatever sorting is desired based on the column index; eg date, str, etc...
  case Column.Index of
     0  :  ListView1.CustomSort(@LVDateSort, Column.Index);
     1  :  ListView1.CustomSort(@LVStrSort, Column.Index);
     2  :  ListView1.CustomSort(@LVIntSort, Column.Index);
  end;

end;

----

Regards,
Russell


Avatar of Marius0188
Marius0188

ASKER

Thanks for the help.

Why are there 2 TListItem params in the function:
>>function LVBaseSort(lParam1, lParam2: TListItem ...........



Because the CustomSort (of the listview) calls the passed function which needs to be written to take 2 list items and a user defined integer. The 2 items being passed are the items that are currently being compared in the internal quick sort routine.

Russell
Hi,

See the delphi example for OnCompare event of listview.
Unable to find that demo for Delphi 7.
Can you please provide the project folder?



I like the first example by rllibby.
Would love to see more so will wait a day or two.

Thanks all!
From Delphi help file.

This example shows how to use the OnColumnClick and OnCompare events of a list view to let users sort the columns in a report-style list view by clicking on the column headers. This requires a global variable to keep track of the column that was clicked:

var ColumnToSort: Integer;



The OnColumnClick event handler sets the global variable to indicate the column to sort and calls AlphaSort:

procedure TForm1.ListView1ColumnClick(Sender: TObject; Column: TListColumn);

begin
  ColumnToSort := Column.Index;
  (Sender as TCustomListView).AlphaSort;
end;


The OnCompare event handler causes the list view to sort on the selected column:


procedure TForm1.ListView1Compare(Sender: TObject; Item1, Item2: TListItem; Data: Integer; var Compare: Integer);
var
  ix: Integer;
begin
  if ColumnToSort = 0 then
    Compare := CompareText(Item1.Caption,Item2.Caption)
  else begin
   ix := ColumnToSort - 1;
   Compare := CompareText(Item1.SubItems[ix],Item2.SubItems[ix]);
  end;

end;

Note:      This OnCompare event handler uses the global CompareText function. An application may want to use AnsiCompareText, CompareStr, or AnsiCompareStr instead, depending on whether the comparison should be case-sensitive and whether the locale should be considered.

Will this OnCompare example allow to order by any value type?
For example: TDate?
Yes, all you have to do is to set the Compare variable.

The OnCompare event handler compares the list items passed as the Item1 and Item2 parameters. If Item1 is the same as Item2 in the sort order, set the Compare parameter to 0. If Item1 is less than Item2, set the Compare parameter to a value less than 0. If Item1 is greater than Item2, set the Compare parameter to a value greater than 0.

e.g.
  if Date1 = Date2 then
    Compare := 0
  else if Date1 < Date2 then
   Compare := -1
  else
   Compare := 1;
similarly reverse the assignment if you want to sort in descending

e.g.
  if Date1 = Date2 then
    Compare := 0
  else if Date1 < Date2 then
   Compare := 1
  else
   Compare := -1;
Here is a better Integer sort for Listviews.

{***********************************************************
  Listview Integer column sort courtesy of "Grinder"
  http://216.101.185.148/scripts/isapi.dll/execute?SearchFor=LVSort+Grinder&RecordLimit=250
  Originally written to handle any type data, modified to work with Integer data only.
  Don't forget stdcall!!!

  Instead of using the tag for the direction, I just use the global variable FAscending.

***********************************************************}

var
 FSortColumn: Integer;
 FAscending : Boolean;

function LVIntegerSort
 ( lParam1
 , lParam2:                   Integer
 ; lParamSort:                Integer
 ): Integer; stdcall;

 function LVItemValue(const Item: TListItem; Col: Integer): Integer;
 begin
   if Item = nil then
     Result := -1
   else
     Result := StrToInt(Item.SubItems[Col - 1]);
 end;

var
 vData1: Integer;
 vData2: Integer;
begin
 try
   vData1 := LVItemValue(TListItem(lParam1), 3);
   vData2 := LVItemValue(TListItem(lParam2), 3);

   if vData1 > vData2 then
     Result := 1
   else if vData1 < vData2 then
     Result := -1
   else begin
     Result := 0;
   end;
   if not FAscending then
     Result := -Result;
 except
   Result := 0;
 end;
end;

To simplify things further, here is a solution that requires ONE line of code to create a sorting hander for any list view. This will provide ascending/descending sorting for your columns, and allows you to specify the sort type for each column:

Sort Types
-------------------------------------
stString - string
stInteger - integer
stFloat - floating point number
stDate - date value
stTime - time value
stDateTime - date and time value

Just save the unit code (provided below) as ListViewSort.pas and make sure it is accessible (..\lib is a good place, or the application's project path). Add ListViewSort to your uses clause, then in the FormCreate just add:

  // Example only; you need to specify the desired sort type for your columns
  TListViewSort.CreateOwned(ListView1, [stString, stInteger, stDate]);

You should change the "[stString, stInteger, stDate]" to apply whatever sort you desire against each column. Note: if you have (for example) 3 columns, and provide less than 3 sort types, eg [stString, stInteger], then string sorting will be applied to the remaining columns.

Let me know if you have any problems,
Russell


---- ListViewSort.Pas ----
unit ListViewSort;
////////////////////////////////////////////////////////////////////////////////
//
//   Unit        :  ListViewSort
//   Author      :  rllibby
//   Date        :  06.13.2006
//   Description :  Sorting class for list view control. Allows sorting to be
//                  handled for multiple columns (with different sorting), using
//                  a minimal amount of code.
//
////////////////////////////////////////////////////////////////////////////////
interface

////////////////////////////////////////////////////////////////////////////////
//   Include units
////////////////////////////////////////////////////////////////////////////////
uses
  Windows, SysUtils, Classes, ComCtrls;

////////////////////////////////////////////////////////////////////////////////
//   Sorting types
////////////////////////////////////////////////////////////////////////////////
type
  TSortType         =  (stString, stInteger, stFloat, stDate, stTime, stDateTime);

////////////////////////////////////////////////////////////////////////////////
//   TListViewSort
////////////////////////////////////////////////////////////////////////////////
type
  TListViewSort     =  class(TComponent)
  private
     // Private declarations
     FListView:     TListView;
     FSortTypes:    TList;
     FLastSort:     Integer;
     FAscending:    Boolean;
  protected
     // Protected declarations
     function       GetSortType(Column: Integer): TSortType;
     procedure      OnColumnClick(Sender: TObject; Column: TListColumn);
  public
     // Public declarations
     constructor    CreateOwned(ListView: TListView; ColumnSorting: Array of TSortType);
     destructor     Destroy; override;
     procedure      ApplyLastSort;
  end;

////////////////////////////////////////////////////////////////////////////////
//   Utility functions
////////////////////////////////////////////////////////////////////////////////
function   MakeColumnSort(Column: Integer; Ascending: Boolean): Integer;
function   GetColumn(Value: Integer): Integer;
function   GetAscending(Value: Integer): Boolean;

implementation


//// Base sorting function /////////////////////////////////////////////////////
function LVBaseSort(lParam1, lParam2: TListItem; Column: Integer; Ascending: Boolean; SortType: TSortType): Integer;
var  szItem1:       String;
     szItem2:       String;
     dwSubItem:     Integer;
     dblResult:     Double;
begin

  // Check items
  if (lParam1 = nil) then
     result:=(-1)
  else if (lParam2 = nil) then
     result:=1
  else
  begin
     // Clear strings
     SetLength(szItem1, 0);
     SetLength(szItem2, 0);
     // Resource protection
     try
        // Get values to sort on
        if (Column = 0) then
        begin
           // Get values from caption
           szItem1:=lParam1.Caption;
           szItem2:=lParam2.Caption
        end
        else
        begin
           // Get subitem index
           dwSubItem:=Pred(Column);
           // Get subitems
           if (dwSubItem >= 0) then
           begin
              if (dwSubItem < lParam1.SubItems.Count) then szItem1:=lParam1.SubItems[dwSubItem];
              if (dwSubItem < lParam2.SubItems.Count) then szItem2:=lParam2.SubItems[dwSubItem];
           end;
        end;
        // Perform comparison
        case SortType of
           stString    :  result:=CompareStr(szItem1, szItem2);
           stInteger   :  result:=StrToInt(szItem1) - StrToInt(szItem2);
           stFloat     :
           begin
              dblResult:=StrToFloat(szItem1) - StrToFloat(szItem2);
              if (dblResult > 0) then
                 result:=1
              else if (dblResult < 0) then
                 result:=(-1)
              else
                 result:=0;
           end;
           stDate      :
           begin
              dblResult:=StrToDate(szItem1) - StrToDate(szItem2);
              if (dblResult > 0) then
                 result:=1
              else if (dblResult < 0) then
                 result:=(-1)
              else
                 result:=0;
           end;
           stTime      :
           begin
              dblResult:=StrToTime(szItem1) - StrToTime(szItem2);
              if (dblResult > 0) then
                 result:=1
              else if (dblResult < 0) then
                 result:=(-1)
              else
                 result:=0;
           end;
           stDateTime  :
           begin
              dblResult:=StrToDateTime(szItem1) - StrToDateTime(szItem2);
              if (dblResult > 0) then
                 result:=1
              else if (dblResult < 0) then
                 result:=(-1)
              else
                 result:=0;
           end;
        else
           // Sort by string
           result:=CompareStr(szItem1, szItem2);
        end;
     except
        // Conversion error, compare by string values
        result:=CompareStr(szItem1, szItem2);
     end;
  end;

  // Reverse the sort if not ascending
  if not(Ascending) then result:=result * (-1);

end;

//// Specialized sorting functions /////////////////////////////////////////////
function LVStringSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, GetColumn(lParamSort), GetAscending(lParamSort), stString);
end;

function LVIntegerSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, GetColumn(lParamSort), GetAscending(lParamSort), stInteger);
end;

function LVFloatSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, GetColumn(lParamSort), GetAscending(lParamSort), stFloat);
end;

function LVDateSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, GetColumn(lParamSort), GetAscending(lParamSort), stDate);
end;

function LVTimeSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, GetColumn(lParamSort), GetAscending(lParamSort), stTime);
end;

function LVDateTimeSort(lParam1, lParam2: TListItem; lParamSort: Integer): Integer stdcall;
begin
  result:=LVBaseSort(lParam1, lParam2, GetColumn(lParamSort), GetAscending(lParamSort), stDateTime);
end;

//// TListViewSort /////////////////////////////////////////////////////////////
procedure TListViewSort.OnColumnClick(Sender: TObject; Column: TListColumn);
var  stColumn:      TSortType;
     lParam:        Integer;
begin

  // Check list view
  if Assigned(FListView) and Assigned(Column) then
  begin
     // Get sort type for the column
     stColumn:=GetSortType(Column.Index);
     // Resource protection
     try
        // Update the sorting direction
        if (Column.Index <> FLastSort) then
           // Ascending sort
           FAscending:=True
        else
           // Reverse the current sorting
           FAscending:=not(FAscending);
        // Encode into column and sort direction into an lParam
        lParam:=MakeColumnSort(Column.Index, FAscending);
        // Perform the sort
        case stColumn of
           stString    :  FListView.CustomSort(@LVStringSort, lParam);
           stInteger   :  FListView.CustomSort(@LVIntegerSort, lParam);
           stFloat     :  FListView.CustomSort(@LVFloatSort, lParam);
           stDate      :  FListView.CustomSort(@LVDateSort, lParam);
           stTime      :  FListView.CustomSort(@LVTimeSort, lParam);
           stDateTime  :  FListView.CustomSort(@LVDateTimeSort, lParam);
        else
           // Use string sorting
           FListView.CustomSort(@LVStringSort, lParam);
        end;
     finally
        // Save last sorted column index
        FLastSort:=Column.Index;
     end;
  end;

end;

function TListViewSort.GetSortType(Column: Integer): TSortType;
begin

  // Check column against the list count
  if (Column >= FSortTypes.Count) then
     // Default sorting will be by string
     result:=stString
  else
     // Return the stored sort type
     result:=TSortType(FSortTypes[Column]);

end;

procedure TListViewSort.ApplyLastSort;
var  stColumn:      TSortType;
     lParam:        Integer;
begin

  // Check list view and last sort column
  if Assigned(FListView) and (FLastSort >= 0) and (FLastSort < FListView.Columns.Count) then
  begin
     // Apply the last used sort against the list view; this is useful if the
     // list view data is reloaded (or newly loaded) and you wish to apply the previous
     // sort against the new data.
     stColumn:=GetSortType(FLastSort);
     // Encode into column and sort direction into an lParam
     lParam:=MakeColumnSort(FLastSort, FAscending);
     // Perform the sort
     case stColumn of
        stString    :  FListView.CustomSort(@LVStringSort, lParam);
        stInteger   :  FListView.CustomSort(@LVIntegerSort, lParam);
        stFloat     :  FListView.CustomSort(@LVFloatSort, lParam);
        stDate      :  FListView.CustomSort(@LVDateSort, lParam);
        stTime      :  FListView.CustomSort(@LVTimeSort, lParam);
        stDateTime  :  FListView.CustomSort(@LVDateTimeSort, lParam);
     else
        // Use string sorting
        FListView.CustomSort(@LVStringSort, lParam);
     end;
  end;

end;

constructor TListViewSort.CreateOwned(ListView: TListView; ColumnSorting: Array of TSortType);
var  dwIndex:       Integer;
begin

  // Perform inherited (list view becomes our owner)
  inherited Create(ListView);

  // Set defaults
  FSortTypes:=TList.Create;
  FLastSort:=(-1);
  FAscending:=True;

  // Add sorting types to the list
  for dwIndex:=0 to High(ColumnSorting) do FSortTypes.Add(Pointer(ColumnSorting[dwIndex]));

  // Save instance of list view
  FListView:=ListView;

  // Bind the OnColumnClick
  if Assigned(FListView) then FListView.OnColumnClick:=OnColumnClick;

end;

destructor TListViewSort.Destroy;
begin

  // Resource protection
  try
     // Unbind the list views OnColumnClick method
     if Assigned(FListView) then FListView.OnColumnClick:=nil;
     // Free the sorting list
     FSortTypes.Free;
  finally
     // Perform inherited
     inherited Destroy;
  end;

end;

//// Utility functions /////////////////////////////////////////////////////////
function MakeColumnSort(Column: Integer; Ascending: Boolean): Integer;
begin

  // Encode into integer
  if Ascending then
     result:=MakeLong(Word(Column), 1)
  else
     result:=MakeLong(Word(Column), 0);

end;

function GetColumn(Value: Integer): Integer;
begin

  // Decode from value
  result:=LoWord(Value);

end;

function GetAscending(Value: Integer): Boolean;
begin

  // Decode from value
  result:=(HiWord(Value) <> 0);

end;

end.



I like the one by rllibby.
Generic and effortless.
I got it to work quick.

Though one question.
How can I have case to be ignored in String?
For example:
When clicking an column with string items the sort
places "Troskie" before "andre".

This should not be.

Regarding my last post.
Ignore it please cause I got it to work with AnsiCompareStr();


Thanks for everyone!!!
ASKER CERTIFIED SOLUTION
Avatar of Russell Libby
Russell Libby
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial