Question

I need to enable or disable a button depending on whether at least a row is selected in the list or not.

Below is the code to reproduce this issue. The list is populated using the OnData event and it allows multiple rows to be selected.

I thought that I could use OnSelectItem to detect when the user changes the selection and then use the TListView SelCount function to detect the number of selected rows.

The problem is that SelCount returns 0 when the user selects multiple rows. This works fine if the list is populated manually (i.e. not through the OnData event).

Any ideas?

Thanks

Update: using the OnChange event instead seems to do the trick. Still it would be interesting to understand why SelCount returns 0 when multiple rows are selected (from within the SelectItem event).

Another Update: I posted a test project: https://dl.dropboxusercontent.com/u/35370420/TestListView2.zip as well as a screenshot:

enter image description here

To reproduce this issue run the app, select Item1, then SHIFT+Click on Item2. The button is disabled. My intention was to enable the button dynamically as long as there is at least one item selected in the list. If there is no selected item the button is disabled.

PAS file:

unit MainUnit;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.StdCtrls;

type
  TForm3 = class(TForm)
    ListView1: TListView;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure ListView1Data(Sender: TObject; Item: TListItem);
    procedure ListView1SelectItem(Sender: TObject; Item: TListItem; Selected: Boolean);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form3: TForm3;

implementation

{$R *.dfm}

procedure TForm3.FormCreate(Sender: TObject);
begin
 ListView1.Items.Count := 5;
end;

procedure TForm3.ListView1Data(Sender: TObject; Item: TListItem);
begin
  Item.Caption := String.Format('Item%d', [Item.Index]);
end;

procedure TForm3.ListView1SelectItem(Sender: TObject; Item: TListItem; Selected: Boolean);
begin
 Button1.Enabled := ListView1.SelCount > 0;
 OutputDebugString(pchar(String.Format('SelCount = %d', [ListView1.SelCount])));
end;

end.

Form:

object Form3: TForm3
  Left = 0
  Top = 0
  Caption = 'Form3'
  ClientHeight = 600
  ClientWidth = 952
  Color = clBtnFace
  DoubleBuffered = True
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  PixelsPerInch = 96
  TextHeight = 13
  object ListView1: TListView
    Left = 168
    Top = 160
    Width = 250
    Height = 150
    Columns = <
      item
        AutoSize = True
        Caption = 'Test'
      end>
    HideSelection = False
    MultiSelect = True
    OwnerData = True
    TabOrder = 0
    ViewStyle = vsReport
    OnData = ListView1Data
    OnSelectItem = ListView1SelectItem
  end
  object Button1: TButton
    Left = 168
    Top = 120
    Width = 75
    Height = 25
    Caption = 'Some Action'
    Enabled = False
    TabOrder = 1
  end
end
Was it helpful?

Solution

The root issue is that when you SHIFT+Click multiple items, you will NOT get any OnSelectItem events for the items that have become selected. The SHIFT+Click causes all list view items to be unselected first, triggering a single OnSelectItem event with Item=nil and Selected=False, before the new items then become selected. At the time of that event, TListView.SelCount really is 0, so you disable your button, but then there are no further OnSelectItem events to tell you that new items have been selected, so you do not check SelCount again to re-enable the button.

The OnSelectItem event is triggered in reply to the LVN_ITEMCHANGED notification when a single item changes state between selected and unselected, or when ALL items in the entire ListView change to the same selected/unselected state. However, in virtual mode, when multiple consecutive items change to the same state at the same time, Windows can instead send a single LVN_ODSTATECHANGED notification for that range of items. TListLiew does not trigger OnSelectItem when it receives LVN_ODSTATECHANGED, it triggers OnDataStateChange instead, eg:

procedure TForm3.ListView1DataStateChange(Sender: TObject; StartIndex, EndIndex: Integer; OldState, NewState: TItemStates);
begin
  if (NewState * [isSelected]) <> (OldState * [isSelected]) then
    Button1.Enabled := ListView1.SelCount > 0;
end;

So you need to use both OnSelectItem and OnDataStateChange to handle all possible select/unselect state changes.

The best solution is to not enable/disable the TButton manually on individual item state changes. Drop a TActionManager on the Form, create a new TAction and assign it to the TButton.Action property, and then use the TAction.OnUpdate event to enable/disable the TAction based on the current TListView.SelCount, eg:

procedure TForm3.MyActionUpdate(Sender: TObject);
begin
  MyAction.Enabled := ListView1.SelCount > 0;
end;

That will automatically enable/disable the associated TButton every time the main message queue goes idle, including after ListView notification messages have been processed. This way, you can keep the TButton updated no matter what combination of input is used to select/unselect ListView items.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top