listviewdelphitlistview

TListView OnSelectItem: why does accessing TopItem changes procedure input Item?


Reproduction: click the button to initialize the ListView, then select any item (except the first). The output in the Form caption will show that, having accessed TopItem, the value of Item has now been changed.

Can anyone explain this?

procedure TForm1.Button1Click(Sender: TObject);
begin
  ListView1.OwnerData := True;
  ListView1.Items.Count := 5;
end;

procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
begin
  Item.Caption := '...';
end;

procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
    Selected: Boolean);
var
  one, two: string;
begin
  if Item <> nil then
    one := IntToStr(Item.Index)
  else
    one := 'nil';
  if ListView1.TopItem <> nil then;
  if Item <> nil then
    two := IntToStr(Item.Index)
  else
    two := 'nil';
  Form1.Caption := one + '/' + two;
end;

Solution

  • In non-virtual mode (OwnerData=False), the ListView holds physical items, where every item is represented by a unique TListItem object in memory. But, in virtual mode (OwnerData=True), the ListView has no physical items at all, so no TListItem object is created for each item.

    However, since TListView exposes public interfaces that use TListItem, and those interfaces have to continue working even in virtual mode, TListView holds 1 internal TListItem object in virtual mode, which gets reused for any operation that involves a TListItem.

    So, in your example, when you select any list item, the OnSelectItem event is fired with a TListItem parameter, so TListView has to first fire its OnData event to fill the internal TListItem with data for the selected item. Then, when you access the TListView.TopItem property, TListView has to fire its OnData event again to fill that same internal TListItem object with new data for the top visible item.

    That is why you are seeing the Item.Index property change value.

    When using virtual mode, don't expect a given TListItem to remain intact after you have accessed a different TListItem. In fact, you should really avoid using any TListItem operations as much as possible. If you need to access information about a particular list item, and that information is not already available in your own data source, then you should query the underlying ListView control using the Win32 API directly, so as not to affect the TListView's sole TListItem object, eg:

    procedure TForm1.ListView1SelectItem(Sender: TObject;
      Item: TListItem; Selected: Boolean);
    var
      one, two: string;
    begin
      if Item <> nil then
        one := IntToStr(Item.Index)
      else
        one := 'nil';
    
      //if ListView1.TopItem <> nil then;     // Item is affected!
      ListView_GetTopIndex(ListView1.Handle); // Item is not affected!
    
      if Item <> nil then
        two := IntToStr(Item.Index)
      else
        two := 'nil';
    
      Caption := one + '/' + two;
    end;