TListView SelCount reporting the wrong number of items in a virtual list
Asked Answered
A

1

7

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
Additory answered 7/3, 2014 at 2:42 Comment(12)
fyi - I can not reproduce the error with Delphi 7.Wicketkeeper
-1 The code in the question works as expected, and not as you describe, in XE5.Djambi
@DavidHeffernan: What operating system did you run it on? I am using Windows 7 64bit and XE5 with the latest updates. You can download the project from here: dl.dropboxusercontent.com/u/35370420/TestListView2.zip. I used the sysinternals DebugView to view the output. When I select multiple items in the list only one SelectItem event is fired and SelCount is 0. If I select only one item two events are fired. I tested the project on another computer with Windows 7 64 bit and the behavior is the same.Additory
Win7 x64 here. The button is enabled if and only if > 0 items selected.Djambi
Maybe I wasn't clear in my explanations. I also posted a screenshot: dl.dropboxusercontent.com/u/35370420/app_screenshot.png. I selected Item1, then I SHIFT+Clicked on Item3 to select the 3 items. As you can see in the screenshot the button is disabled. Also see the output.Additory
I would comment that the right way to control properties like Enabled is through actions and action lists/managers.Djambi
I'll look at this later. If I can reproduce I'm sure I can solve!Djambi
I can repro this, simply copy/pasting the code and DFM content into XE5 on Win7 64 (given the additional details provided in the "Maybe I wasn't clear" comment, which you should edit into your question itself). The missing step of "I selected Item1, then I SHIFT+Clicked on Item3" is important to the question.Mineralize
@KenWhite: I updated the question with all the relevant information.Additory
@KenWhite: And one more thing to point out, this happens only when the list is in virtual mode.Additory
@costa: Ctrl+Click works because it is updating individual items one at a time, and thus triggering individual OnSelectItem events. Shift+Click, on the other hand, can cause a different flow of events to be triggered (specifically, OnDataStateChange), which you are not taking into account in virtual mode. This is not a bug in the underlying ListView control, it is by design to help optimize virtual mode usage by reporting state changes when a consecutive range of items change to the same state at the same time.Whiff
I turned my -1 into +1. Thanks for your update. It did need further elaboration, and thanks for doing it. Remy's answer is good. I will repeat what he said, and what I commented before, use actions to manage enabled and visible properties in your UI.Djambi
W
5

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.

Whiff answered 7/3, 2014 at 20:5 Comment(3)
Although I agree that in this case, an action list/manager certainly is the right approach (so that the actual LV issue becomes irrelevant in this case), both the Q and this A are extremely valuable in some other cases. For instance, you often want to display something like "%d item(s) selected" in the status bar.Crankshaft
@AndreasRejbrand I would still use the TAction.OnUpdate event for that kind of display, eg: procedure TForm3.MyActionUpdate(Sender: TObject); begin StatusBar1.Panels[0].Text := Format('%d item(s) selected', [ListView1.SelCount]); end;Whiff
I usually don't do it like that, but I agree there are clear benefits to it.Crankshaft

© 2022 - 2024 — McMap. All rights reserved.