Search code examples
listviewdelphidelphi-7tlistview

TListView: on item added?


How can I catch the event when an item is added to TListView?

I thought the OnInsert event would do the job, according to the documentation. It even passes the actual TListItem object to the handler:

OnInsert Occurs immediately after a new item is inserted into the list view.

Write an OnInsert event handler to respond when an item has just been added to the list. The Item parameter is the TListItem object that was added to the Items property

Here is my code:

procedure TForm1.Button1Click(Sender: TObject);
begin
  with ListView1.Items.Add do
  begin
     Caption := 'foo';
     SubItems.Add('bar');
  end;
end;

procedure TForm1.TListView1Insert(Sender: TObject; Item: TListItem);
begin
   //Item is empty
   ShowMessage(Item.Caption);
end;

But surprisingly, the Item.Caption is always empty. Seems nonsense to me.

EDIT:

Switching to Items.AddItem(), as suggested, leads to another weird issue. The OnInsert event handler now works as expected, however TListView does not display the TListItem.Caption.

procedure TForm1.Button1Click(Sender: TObject);
begin
  with ListView1.Items.Add do
  begin
     Caption := 'foo1';
     SubItems.Add('bar1');
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  item: TListItem;
begin
  item := TListItem.Create(ListView1.Items);
  item.Caption := 'foo2';
  item.Subitems.Add('bar2');
  ListView1.Items.AddItem(item);
end; 

procedure TForm1.ListView1Insert(Sender: TObject; Item: TListItem);
begin
  //this now works as expected
  ShowMessage(Item.Caption);
end;

image

Why is this?


Solution

  • The TListView.OnInsert event is indeed triggered when a new item is added to the ListView. However, the Item is added to the ListView when TListView.Items.Add() is called, not when Button1Click() exits. The OnInsert event handler is called (in response to a LVN_INSERTITEM notification) while Add() is still running. So, of course the Item in the OnInsert event handler will always be empty, as you haven't assigned any values to it yet.


    Update: When a TListItem is added to the ListView, the LVIF_TEXT flag of the underlying LVITEM is not enabled. To display the TListItem.Caption and TListItem.SubItems text, TListView is designed to rely on ListView_SetItemText() with the LPSTR_TEXTCALLBACK flag instead:

    This parameter can be LPSTR_TEXTCALLBACK to indicate a callback item for which the parent window stores the text. In this case, the list-view control sends the parent an LVN_GETDISPINFO notification code when it needs the text.

    If you assign the TListItem.Caption or TListItem.SubItems property while the TListItem is not actually in the ListView yet, the LPSTR_TEXTCALLBACK flag will not get applied to those fields. LVN_GETDISPINFO will not query the TListView for the text of the 1st column without LPSTR_TEXTCALLBACK (as column 0 has special meaning at the OS layer), but it does query for the text of the 2nd column (even if LPSTR_TEXTCALLBACK is not applied to it). That is why your second example is missing the 'foo2' caption text in the UI, but not the 'bar2' text.

    The actual 'foo2' caption string is stored in the TListItem object, which is why your ShowMessage() is able to work.

    So, if you create a new TListItem and modify its Caption before the item has been added to the ListView, you will have to call ListView_SetItemText() manually to enable the LPSTR_TEXTCALLBACK flag for the caption, eg:

    uses
      Commctrl;
    
    procedure TForm1.Button2Click(Sender: TObject);
    var
      item: TListItem;
    begin
      item := TListItem.Create(ListView1.Items);
      item.Caption := 'foo2';
      item.Subitems.Add('bar2');
      ListView1.Items.AddItem(item);
      ListView_SetItemText(ListView1.Handle, item.Index, 0, LPSTR_TEXTCALLBACK);
    end; 
    

    Or, reset the Caption property value temporarily (the property setter checks for a duplicate string before calling ListView_SetItemText()):

    procedure TForm1.Button2Click(Sender: TObject);
    var
      item: TListItem;
    begin
      item := TListItem.Create(ListView1.Items);
      item.Caption := 'foo2';
      item.Subitems.Add('bar2');
      ListView1.Items.AddItem(item);
      item.Caption := '';
      item.Caption := 'foo2';
    end; 
    

    Just note that either way, the TListItem.Caption text will not appear in the UI until after the OnInsert event is called first, since it is triggered while AddItem() is running.

    I reproduced this in XE2. If the problem still happens in 10.2 Tokyo, I would suggest filing a bug report with Embarcadero. AddItem() should probably be forcing LPSTR_TEXTCALLBACK after insertion for any already-assigned string fields, or at least the Caption, anyway.