2
votes

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?

1
It should be ListView1 and not TListView1. Also use BeginUpdate and EndUpdate procedures when you add items. - Alberto Miola
Note that calling (Begin|End)Update() only affects the ListView's painting (to avoid repaints while you are making changes to items), not its events - Remy Lebeau
@RemyLebeau yes sure! I suggested to use them because I always do that when I deal with items to be edited (in particular if they are a lot). I guess that they should be used in any case - Alberto Miola
You can't add items in another thread. VCL access needs to be in main thread. Whatever you are trying to do, GUI control events are the wrong solution. - David Heffernan
Correct. Utter bunk. I'm afraid that an overwhelming majority of what Zarko writes is flawed. I would trust nothing that you read of his. Anyway, I feel very strongly that the entire premise of the question is wrong. You are only wishing to respond to an event because you are adding the items in the wrong place. If you moved the code that worked with the VCL into the main thread (you have to do this), you wouldn't need to respond to an event. You could perform the action at the same time as you add the item. - David Heffernan

1 Answers

7
votes

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.