TListView列排序-续。 (升序和降序)

3

最近我得到了根据列数据类型对TListView的列进行排序的帮助。

以下是代码:

procedure TfrmFind.lvwTagsColumnClick(Sender: TObject; Column: TListColumn);
begin
 ColumnToSort := Column.Index;
 (Sender as TCustomListView).AlphaSort;
end;

procedure TfrmFind.lvwTagsCompare(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
 if ColumnToSort = 1 then
  Compare := CompareTextAsInteger(Item1.subitems[0],Item2.subitems[0])
 else
if ColumnToSort = 2 then
  Compare := CompareTextAsDateTime(Item1.subitems[1],Item2.subitems[1])
 else
 begin
 ix := ColumnToSort - 1;
 Compare := CompareText(Item1.SubItems[ix],Item2.SubItems[ix]);
 end;
end;

我希望能够添加按升序和降序排序的功能,如果可能的话?
用户点击一次以进行升序排序,然后再次点击以进行降序排序。
我可以从当前的代码中完成这个吗?
左列添加一个图标来显示排序类型(升序 vs 降序),这个怎么样?
******************************************************************************

基于专家答案的修改:2013年03月25日

procedure TfrmFind.lvwTagsColumnClick(Sender: TObject; Column: TListColumn);
begin
 ColumnToSort := Column.Index;
 Column.Tag:= Column.Tag * -1;
 if Column.Tag = 0 then Column.Tag:=1;
 (Sender as TCustomListView).AlphaSort;
end;

procedure TfrmFind.lvwTagsCompare(Sender: TObject; Item1, Item2: TListItem;
  Data: Integer; var Compare: Integer);
begin
Case ColumnToSort of
    0:  Compare := TRzListView(Sender).Tag * CompareText(Item1.Caption, Item2.Caption);
    1:  Compare := TRzListView(Sender).Tag * CompareTextAsInteger(Item1.subitems[0],Item2.subitems[0]);
    2:  Compare := TRzListView(Sender).Tag * CompareTextAsDateTime(Item1.subitems[1],Item2.subitems[1]);
    else
    Compare := TRzListView(Sender).Tag * CompareText(Item1.Caption, Item2.Caption);
  End;
end;

3
仅提供提示,使用case ColumnToSort of代替那个冗长的if else语句。回答您的问题:创建一个变量来存储顺序,它将具有值1或-1,并将“比较”值乘以该值。 - TLama
@David,看了你(目前已删除的)答案。整体解决方案将变得难以阅读(不是说仅限于一个控件实例的限制),我建议使用列表视图派生类代替这一系列过程。 - TLama
@TLama,我不同意。将代码放在派生类中并不会改变你拥有的代码量。在可行的情况下,我总是更喜欢组合而不是继承。这些辅助函数是独立的,可以一次理解一个。它们提供明确且简单的目的。UI 和排序已经分离。不同的方面被保持分开,因此未来的增强对设计影响很小。 - David Heffernan
@David,我不喜欢只能与单个类一起使用的单一目的独立过程。当它们应该成为方法时,这显然是情况。但这只是个人口味问题... - TLama
@TLama 在真实的代码中,它们不会像这样。它们将存在于共享代码中以供重用。我会为 TListColumn 实现一个类助手,并将 GetListHeaderSortStateSetListHeaderSortStateListViewFromColumn 放入其中。大部分其他代码都是特定于此特定表单的,因此属于该表单。而那些不属于该表单的代码,我想是 TfrmFind.SortTfrmFind.ListViewCompare。它们可以有用地存在于列表视图派生类中。我不想在这个特定的答案中这样做,因为它会使它变得更加复杂,正如我们从之前的问题中所知道的,这可能对 JH 没有帮助。 - David Heffernan
我希望TRzListViewTListView非常接近,以至于它们之间的差异并不重要。 - David Heffernan
3个回答

8
你现在要做的事情相对较为复杂。为了能够掌握这个问题,我建议你构建一组良好因素的低级帮助程序例程。然后你可以用简短、清晰的方法组合高级UI代码。
首先,让我们编写一些获取和设置列表头排序状态的例程。这是列表视图头控件中的上下排序图标。
function ListViewFromColumn(Column: TListColumn): TListView;
begin
  Result := (Column.Collection as TListColumns).Owner as TListView;
end;

type
  THeaderSortState = (hssNone, hssAscending, hssDescending);

function GetListHeaderSortState(Column: TListColumn): THeaderSortState;
var
  Header: HWND;
  Item: THDItem;
begin
  Header := ListView_GetHeader(ListViewFromColumn(Column).Handle);
  ZeroMemory(@Item, SizeOf(Item));
  Item.Mask := HDI_FORMAT;
  Header_GetItem(Header, Column.Index, Item);
  if Item.fmt and HDF_SORTUP<>0 then
    Result := hssAscending
  else if Item.fmt and HDF_SORTDOWN<>0 then
    Result := hssDescending
  else
    Result := hssNone;
end;

procedure SetListHeaderSortState(Column: TListColumn; Value: THeaderSortState);
var
  Header: HWND;
  Item: THDItem;
begin
  Header := ListView_GetHeader(ListViewFromColumn(Column).Handle);
  ZeroMemory(@Item, SizeOf(Item));
  Item.Mask := HDI_FORMAT;
  Header_GetItem(Header, Column.Index, Item);
  Item.fmt := Item.fmt and not (HDF_SORTUP or HDF_SORTDOWN);//remove both flags
  case Value of
  hssAscending:
    Item.fmt := Item.fmt or HDF_SORTUP;
  hssDescending:
    Item.fmt := Item.fmt or HDF_SORTDOWN;
  end;
  Header_SetItem(Header, Column.Index, Item);
end;

我从以下回答中获取了此代码:如何在TListView列上显示排序箭头? 接下来我会创建一个记录来保存排序规范。理想情况下,这应该在其Data参数中传递到排序比较函数中。但遗憾的是VCL框架错过了使用该参数的机会。因此,我们需要将活动排序的规范存储在拥有列表视图的表单中。
type
  TSortSpecification = record
    Column: TListColumn;
    Ascending: Boolean;
    CompareItems: function(const s1, s2: string): Integer;
  end;

然后在表单本身中,您将声明一个字段来保存其中之一:

type
  TfrmFind = class(...)
  private
    ....
    FSortSpecification: TSortSpecification;
    ....
  end;

比较函数使用规范,很简单:
procedure TfrmFind.ListViewCompare(Sender: TObject; Item1, Item2: TListItem;
  Data: Integer; var Compare: Integer);
var
  Index: Integer;
  s1, s2: string;
begin
  Index := FSortSpecification.Column.Index;
  if Index=0 then
  begin
    s1 := Item1.Caption;
    s2 := Item2.Caption;
  end else
  begin
    s1 := Item1.SubItems[Index-1];
    s2 := Item2.SubItems[Index-1];
  end;
  Compare := FSortSpecification.CompareItems(s1, s2);
  if not FSortSpecification.Ascending then
    Compare := -Compare;
end;

接下来,我们将实现一个排序函数。
procedure TfrmFind.Sort(Column: TListColumn; Ascending: Boolean);
var
  ListView: TListView;
begin
  FSortSpecification.Column := Column;
  FSortSpecification.Ascending := Ascending;
  case Column.Index of
  1:
    FSortSpecification.CompareItems := CompareTextAsInteger;
  2:
    FSortSpecification.CompareItems := CompareTextAsDateTime;
  else 
    FSortSpecification.CompareItems := CompareText;
  end;

  ListView := ListViewFromColumn(Column);
  ListView.OnCompare := ListViewCompare;
  ListView.AlphaSort;
end;

这个Sort函数与OnClick处理程序解耦。这将允许您独立于用户的UI操作对列进行排序。例如,当您首次显示表单时,可能希望对特定列上的控件进行排序。

最后,OnClick处理程序可以调用排序函数:

procedure TfrmFind.lvwTagsColumnClick(Sender: TObject; Column: TListColumn);
var
  i: Integer;
  Ascending: Boolean;
  State: THeaderSortState;
begin
  Ascending := GetListHeaderSortState(Column)<>hssAscending;
  Sort(Column, Ascending);
  for i := 0 to ListView.Columns.Count-1 do
  begin
    if ListView.Column[i]=Column then
      if Ascending then
        State := hssAscending
      else
        State := hssDescending
    else
      State := hssNone;
    SetListHeaderSortState(ListView.Column[i], State);
  end;
end;

为了完整起见,这里有一个完整的单元,实现了这些想法:

unit uFind;

interface

uses
  Windows, Messages, SysUtils, Classes, Math, DateUtils, Controls, Forms, Dialogs, ComCtrls, CommCtrl;

type
  TSortSpecification = record
    Column: TListColumn;
    Ascending: Boolean;
    CompareItems: function(const s1, s2: string): Integer;
  end;

  TfrmFind = class(TForm)
    ListView: TListView;
    procedure lvwTagsColumnClick(Sender: TObject; Column: TListColumn);
  private
    FSortSpecification: TSortSpecification;
    procedure ListViewCompare(Sender: TObject; Item1, Item2: TListItem;
      Data: Integer; var Compare: Integer);
    procedure Sort(Column: TListColumn; Ascending: Boolean);
  end;

var
  frmFind: TfrmFind;

implementation

{$R *.dfm}

function CompareTextAsInteger(const s1, s2: string): Integer;
begin
  Result := CompareValue(StrToInt(s1), StrToInt(s2));
end;

function CompareTextAsDateTime(const s1, s2: string): Integer;
begin
  Result := CompareDateTime(StrToDateTime(s1), StrToDateTime(s2));
end;

function ListViewFromColumn(Column: TListColumn): TListView;
begin
  Result := (Column.Collection as TListColumns).Owner as TListView;
end;

type
  THeaderSortState = (hssNone, hssAscending, hssDescending);

function GetListHeaderSortState(Column: TListColumn): THeaderSortState;
var
  Header: HWND;
  Item: THDItem;
begin
  Header := ListView_GetHeader(ListViewFromColumn(Column).Handle);
  ZeroMemory(@Item, SizeOf(Item));
  Item.Mask := HDI_FORMAT;
  Header_GetItem(Header, Column.Index, Item);
  if Item.fmt and HDF_SORTUP<>0 then
    Result := hssAscending
  else if Item.fmt and HDF_SORTDOWN<>0 then
    Result := hssDescending
  else
    Result := hssNone;
end;

procedure SetListHeaderSortState(Column: TListColumn; Value: THeaderSortState);
var
  Header: HWND;
  Item: THDItem;
begin
  Header := ListView_GetHeader(ListViewFromColumn(Column).Handle);
  ZeroMemory(@Item, SizeOf(Item));
  Item.Mask := HDI_FORMAT;
  Header_GetItem(Header, Column.Index, Item);
  Item.fmt := Item.fmt and not (HDF_SORTUP or HDF_SORTDOWN);//remove both flags
  case Value of
  hssAscending:
    Item.fmt := Item.fmt or HDF_SORTUP;
  hssDescending:
    Item.fmt := Item.fmt or HDF_SORTDOWN;
  end;
  Header_SetItem(Header, Column.Index, Item);
end;

procedure TfrmFind.ListViewCompare(Sender: TObject; Item1, Item2: TListItem;
  Data: Integer; var Compare: Integer);
var
  Index: Integer;
  s1, s2: string;
begin
  Index := FSortSpecification.Column.Index;
  if Index=0 then
  begin
    s1 := Item1.Caption;
    s2 := Item2.Caption;
  end else
  begin
    s1 := Item1.SubItems[Index-1];
    s2 := Item2.SubItems[Index-1];
  end;
  Compare := FSortSpecification.CompareItems(s1, s2);
  if not FSortSpecification.Ascending then
    Compare := -Compare;
end;

procedure TfrmFind.Sort(Column: TListColumn; Ascending: Boolean);
var
  ListView: TListView;
begin
  FSortSpecification.Column := Column;
  FSortSpecification.Ascending := Ascending;
  case Column.Index of
  1:
    FSortSpecification.CompareItems := CompareTextAsInteger;
  2:
    FSortSpecification.CompareItems := CompareTextAsDateTime;
  else
    FSortSpecification.CompareItems := CompareText;
  end;

  ListView := ListViewFromColumn(Column);
  ListView.OnCompare := ListViewCompare;
  ListView.AlphaSort;
end;

procedure TfrmFind.lvwTagsColumnClick(Sender: TObject; Column: TListColumn);
var
  i: Integer;
  Ascending: Boolean;
  State: THeaderSortState;
begin
  Ascending := GetListHeaderSortState(Column)<>hssAscending;
  Sort(Column, Ascending);
  for i := 0 to ListView.Columns.Count-1 do
  begin
    if ListView.Column[i]=Column then
      if Ascending then
        State := hssAscending
      else
        State := hssDescending
    else
      State := hssNone;
    SetListHeaderSortState(ListView.Column[i], State);
  end;
end;

end.

不,我刚刚检查过了,它已经设置好了。它可以排序。如果我点击一次,它会按升序排序。如果我再点击一次,它就不会有任何反应。对于所有的列都是如此。 - JakeSays
如果你能多留一会儿聊聊天就好了,每次评论之间等待一个小时真的很烦人。当你调试代码时,你看到了什么?在后续的点击中会发生什么? - David Heffernan
我打算尝试运行你的代码,但你的pastebin网站不允许我粘贴代码。我也得到了行号。建议你调试代码。 - David Heffernan
抱歉,我同意,你已经做得足够好了,感谢你的帮助。我会解决问题或者回到我的代码。再次感谢! - JakeSays
你也应该投票支持@bummi的答案。这是一个好答案。他还修复了原始版本中的微小错误。并且在你的代码上使用调试器。我相信它会很简单解决的。 - David Heffernan
显示剩余13条评论

5
您可以使用您的代码。只需使用标签来切换排序。
procedure TfrmFind.lvwTagsColumnClick(Sender: TObject; Column: TListColumn);
begin
 ColumnToSort := Column.Index;
 if Column.Tag = 0 then Column.Tag := 1 else Column.Tag := 0; 
 (Sender as TCustomListView).AlphaSort;
end;

在进行比较时,请注意:
  Case ColumnToSort of
    0:begin
        if TListView(Sender).Column[ColumnToSort].Tag = 0 then
          Compare := CompareText(Item1.Caption, Item2.Caption)
        else
          Compare := CompareText(Item2.Caption, Item1.Caption);
      end;
    1:begin
      ........................
    end;
  End;

或者像TLama建议的那样更好。
procedure TfrmFind.lvwTagsColumnClick(Sender: TObject; Column: TListColumn);
begin
 ColumnToSort := Column.Index;
 Column.Tag := Column.Tag * -1;
 if Column.Tag = 0 then Column.Tag := 1; 
 (Sender as TCustomListView).AlphaSort;
end;

使用比较功能

  Case ColumnToSort of
    0:  Compare := TListView(Sender).Column[ColumnToSort].Tag * CompareText(Item1.Caption, Item2.Caption);
    1: ........................

  End;

4
您可以将Tag变量简单地赋值为1或-1,并乘以CompareText(Item1.Caption, Item2.Caption)的值来赋给Compare,这将会切换(switch)比较结果。 - TLama
1
+1. 正如 @TLama 所说,将 lvwTagsColumnClick 代码更改为:if Column.Tag = -1 then Column.Tag := 1 else Column.Tag := -1; TCustomListView(Sender).AlphaSort;。(对于 Tag 的块可以正确地切换它,(Sender as TCustomListView) 是不需要的,因为 lvwTagsColumnClick 事件的 Sender 应该总是持有列的 ListView)。 - Ken White
根据专家的答案进行了更改,但仍然无法正常工作。事实上,现在我完全没有任何排序。请查看我的编辑后的帖子。- 谢谢 - JakeSays
这段代码的错误在于它写入了 Column.Tag 但是从 TListView(Sender).Tag 读取。这就是使用 Tag 时会发生的事情之一。编译器无法定位这种愚蠢的错误。 - David Heffernan

0

我认为有一个简单的方法。我已经在C++Builder中测试过它,它可以正常工作。

注意:初始化FColSorted = -1。

1.创建以下辅助方法。

void 
TFormFind::SetSortCol(int ASortCol)
{
  FColToSort = ASortCol;
  //If new column: ascending sort. Else: toggle sort order.
  FSortToggle = (FColSorted != FColToSort) ? +1 : -1*FSortToggle;
  ListView->AlphaSort();
  FColSorted = FColToSort;
}

2. 使用 OnColumnClick 事件的辅助方法。

void __fastcall
TFormFind::ListViewColumnClick(TObject* Sender, TListColumn* Column)
{
  SetSortCol(Column->Index);
}

3. 使用 FSortToggle 与您的比较逻辑。

void __fastcall
TFormFind::ListViewCompare(TObject* Sender, 
  TListItem* Item1, TListItem* Item2, int Data, int& Compare)
{
  //Your Compare logic here.
  //...
  Compare = FSortToggle * Compare; 
}

最好的祝福,

马塞洛。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接