Search code examples
listviewdelphiwinapidelphi-11-alexandria

Problem with sort-arrow handling in TListView


In a 32-bit VCL Application in Windows 10 in Delphi 11 Alexandria, I have a TListView with two columns, client-aligned to the form. I am using this code to handle the sort-arrows in the ListView Header:

procedure TformMain.ListView1ColumnClick(Sender: TObject; Column: TListColumn);
begin
  FSortedColumn := Column.Index; 
  case FSortedColumn of
    0: FColumn0SortedUp := not FColumn0SortedUp;
    1: FColumn1SortedUp := not FColumn1SortedUp;
  end;
  SetListHeaderSortArrow(FSortedColumn);
end;

procedure TformMain.SetListHeaderSortArrow(const aColumnIndex: Integer);
begin
  var Header: HWND;
  var Item: Winapi.CommCtrl.THDItem;
  case aColumnIndex of
    0:
      begin
        Header := Winapi.CommCtrl.ListView_GetHeader(ListView1.Handle);
        Winapi.Windows.ZeroMemory(@Item, SizeOf(Item));
        Item.Mask := Winapi.CommCtrl.HDI_FORMAT;
        Winapi.CommCtrl.Header_GetItem(Header, 0, Item);
        Item.fmt := Item.fmt and not (HDF_SORTUP or HDF_SORTDOWN); // remove both flags

        if FColumn0SortedUp then
          Item.fmt := Item.fmt or HDF_SORTUP // include the sort ascending flag
        else
          Item.fmt := Item.fmt or HDF_SORTDOWN; // include the sort descending flag

        Header_SetItem(Header, 0, Item);
      end;
    1:
      begin
        Header := Winapi.CommCtrl.ListView_GetHeader(ListView1.Handle);
        Winapi.Windows.ZeroMemory(@Item, SizeOf(Item));
        Item.Mask := Winapi.CommCtrl.HDI_FORMAT;
        Winapi.CommCtrl.Header_GetItem(Header, 1, Item);
        Item.fmt := Item.fmt and not (HDF_SORTUP or HDF_SORTDOWN); // remove both flags

        if FColumn1SortedUp then
          Item.fmt := Item.fmt or HDF_SORTUP // include the sort ascending flag
        else
          Item.fmt := Item.fmt or HDF_SORTDOWN; // include the sort descending flag

        Header_SetItem(Header, 1, Item);
      end;
  end;
end;

procedure TformMain.ListView1Resize(Sender: TObject);
begin
  // This gets inexplicably automatically executed 3 times at program start!!
  // This must be in OnResize, otherwise the sort-arrows get hidden when resizing the ListView:
  SetListHeaderSortArrow(FSortedColumn);
end;

When I click the column header of the SECOND column, the sort arrow on the second column appears, but the sort arrow on the first column does not disappear! Only when I resize the ListView (by resizing the form), the sort arrow on the first column disappears. So how can I make the sort arrow on the first column immediately disappear when clicking the second column header?


Solution

  • When changing the flags, you are not removing the flags from the previous selected column before adding the flags to the new column.

    Try something more like this instead:

    private:
      FColumnSortedUp: array[0..1] of Boolean;
      FSortedColumn: Integer;
    
    ...
    
    procedure TformMain.FormCreate(Sender: TObject);
    begin
      FSortedColumn := -1;
    end;
    
    procedure TformMain.ListView1ColumnClick(Sender: TObject; Column: TListColumn);
    begin
      if FSortedColumn <> -1 then
        SetListHeaderSortArrow(FSortedColumn, False);
    
      if FSortedColumn = Column.Index then
        FColumnSortedUp[FSortedColumn] := not FColumnSortedUp[FSortedColumn];
      else
        FSortedColumn := Column.Index;
    
      SetListHeaderSortArrow(FSortedColumn, True);
    
      // sort ListView items as needed...
    end;
    
    procedure TformMain.SetListHeaderSortArrow(const aColumnIndex: Integer;
      const aEnabled: Boolean);
    var
      Header: HWND;
      Item: THDItem;
    begin
      Header := ListView_GetHeader(ListView1.Handle);
      ZeroMemory(@Item, SizeOf(Item));
      Item.Mask := HDI_FORMAT;
      Header_GetItem(Header, aColumnIndex, Item);
      Item.fmt := Item.fmt and not (HDF_SORTUP or HDF_SORTDOWN); // remove both flags
    
      if aEnabled then
      begin
        if FColumnSortedUp[aColumnIndex] then
          Item.fmt := Item.fmt or HDF_SORTUP // include the sort ascending flag
        else
          Item.fmt := Item.fmt or HDF_SORTDOWN; // include the sort descending flag
      end;
    
      Header_SetItem(Header, aColumnIndex, Item);
    end;
    

    Also note that the sort arrow does not disappear when the ListView is resized, but when the column is resized. So you will have to hook the ListView to handle HDN_ENDTRACK notifications to detect when each column is resized, eg:

    private
      ...
      OldWndProc: TWndMethod;
      procedure ListViewWndProc(var Message: TMessage);
    
    ...
    
    uses
      ..., Winapi.Messages, Winapi.CommCtrl;
    
    procedure TformMain.FormCreate(Sender: TObject);
    begin
      ...
      OldWndProc := ListView1.WindowProc;
      ListView1.WindowProc := ListViewWndProc;
    end;
    
    procedure TformMain.ListViewWndProc(var Message: TMessage);
    begin
      OldWndProc(Message);
      if Message.Msg = WM_NOTIFY then
      begin
        if TWMNotify(Message).NMHdr.code = HDN_ENDTRACK then
        begin
          if PHDNotify(TWMNotify(Message).NMHdr).Item = FSortedColumn then
            SetListHeaderSortArrow(FSortedColumn, True);
        end;
      end;
    end;