Jump to content
JohnLM

TListView - manually adding items faster at runtime

Recommended Posts

Specs:  Delphi XE7, VCL, windows 7, Dell laptop (i3 core, 2.40GHz) 

 

I have been playing around with the Listview control, (via ViewStyle=vsReport) learning how to add items at runtime through code (not using a database/binder), for 100 to a few thousand items.  Again, just playing around with throwing together a quick listview of data, and seeing if there is use for it versus going the database dataset route. 

 

Note, because I was self-learning about listview from scratch, I did have a lot of trouble figuring out how to populate the listview.  And now that I know how to, I am also sharing it here for reference for others struggling to do the same.  Below, is the code snippet of how I created the initial fields and then populate them.  The ViewStyle should be set to vsReport in the properties section in order to show a table column layout at run time.  It works and that's all that matters at this point in this indeviour. 

 

procedure TForm1.btnAddClick(Sender: TObject);
var
  itm : TListItem;
  Col : TListColumn;
  s   : string;
  et  : int64;
  SW  : TStopwatch;
begin
  Col := lv1.Columns.Add;
  Col.Caption := 'LN';
  Col.Alignment := taLeftJustify;
  Col.Width := 30;

  Col := lv1.Columns.Add;
  Col.Caption := 'ItemNo';
  Col.Alignment := taLeftJustify;
  Col.Width := 60;

  Col := lv1.Columns.Add;
  Col.Caption := 'Desc';
  Col.Alignment := taLeftJustify;
  Col.Width := 160;


  setlength(ary,10);                           // create the array length (1,2,3,4,5,6,7,8,9,0) chars
  beep;
  SW := TSTopWatch.StartNew;                   // start timing it
  lv1.Items.BeginUpdate;
  for i := 0 to 25000 do begin                 //
    ary := genRandValues;                      // generate random numbers ie (2,9,8,4,7,5,6,0,1,3)
    s:=listtostr(ary);                         // convert array list into a string var ie ('2984756013')
    itm:=lv1.Items.Add;                        // create a row
    itm.Caption := i.ToString();               // add the major field, ie 'LN', thus the populate with variable i as line numbers
    itm.SubItems.Add(''); itm.SubItems[0]:= s; // itemno ie '2984756013', and so on. 
    itm.SubItems.Add(''); itm.SubItems[1]:= s; //   desc ie same, ...
  end;
  lv1.Items.EndUpdate;
  SW.Stop;                                     // finish timing
	eb1.text:=(intToStr(SW.ElapsedTicks)+' ticks / '+intToStr(SW.ElapsedMilliseconds)+' ms');
  beep;
end;

But the problem I am having is that even when using the .BeginUpdate/.EndUpdate the speed is still a bit slow. 

 

For example, to fill a listview with 10,000 elements, it costs me about aprox 1.6 seconds run time, or for 25,000 elements, 7.6 seconds. 

 

I had a look at the earlier part of the for/loop, where I am generating the random numbers and then converting them to strings.  I REM'ed them out and the time was still the same or ever-so-slightly less, maybe 1.5 seconds for instance.  So that does not seem to be a major issue in slowing the listview down. 

 

Is this the maximum throughput I can expect from listview? or Is there anything else I can do to speed this up a lot more? 

 

 

Share this post


Link to post

As well a lot of controls with lists have a BeginUpdate and EndUpdate so you can stop visually updating the control for every change to an item. Wrap things in an Try/Finally to make sure EndUpdate is always called.

lv1.items.BeginUpdate;
try
  // add multiple items
finally
  lv1.items.EndUpdate;
end;

From the help for Vcl.ComCtrls.TListItems.BeginUpdate :

 

Prevents updating of the list view until the EndUpdate method is called.

Call BeginUpdate before making multiple changes to the list of items. When all changes are complete, call EndUpdate so that the changes can be reflected on screen. BeginUpdate and EndUpdate prevent excessive redraws and speed processing time when new items are added, deleted, or inserted.

Edited by Brian Evans

Share this post


Link to post

For a non-trivial number of items, it is best to use the TListView in virtual mode. Set the OwnerData property to true, assign an OnData event handler, and then set the TListView.Items.Count property instead of using the TListView.Items.Add() method, eg:

type
  TItemData = record
    LN: Integer;
    ItemNo: string;
    Desc: string;
  end;
  
private
  lvItems: array of TItemData;

procedure TForm1.btnAddClick(Sender: TObject);
var
  Col : TListColumn;
  SW  : TStopwatch;
  s   : string;
  i   : Integer;
begin
  Col := lv1.Columns.Add;
  Col.Caption := 'LN';
  Col.Alignment := taLeftJustify;
  Col.Width := 30;

  Col := lv1.Columns.Add;
  Col.Caption := 'ItemNo';
  Col.Alignment := taLeftJustify;
  Col.Width := 60;

  Col := lv1.Columns.Add;
  Col.Caption := 'Desc';
  Col.Alignment := taLeftJustify;
  Col.Width := 160;

  SetLength(lvItems, 25000);
  SetLength(ary,10);                           // create the array length (1,2,3,4,5,6,7,8,9,0) chars
  beep;
  SW := TStopWatch.StartNew;                   // start timing it
  for i := Low(lvItems) to High(lvItems) do begin
    ary := genRandValues;                      // generate random numbers ie (2,9,8,4,7,5,6,0,1,3)
    s := ListToStr(ary);                       // convert array list into a string var ie ('2984756013')
    lvItems[i].LN := i;
    lvItems[i].ItemNo := s;
    lvItems[i].Desc := s;
  end;
  lv1.Items.Count := Length(lvItems);
  SW.Stop;                                     // finish timing
  eb1.Text := IntToStr(SW.ElapsedTicks) + ' ticks / ' + IntToStr(SW.ElapsedMilliseconds) + ' ms';
  beep;
end;

procedure TForm1.lv1Data(Sender: TObject; Item: TListItem);
begin
  Item.Caption := lvItems[Item.Index].LN.ToString();  // add the major field, ie 'LN', thus the populate with the index as line numbers
  Item.SubItems.Add(lvItems[Item.Index].ItemNo);      // itemno ie '2984756013', and so on.
  Item.SubItems.Add(lvItems[Item.Index].Desc);        // desc ie same, ...
end;

 

Edited by Remy Lebeau
  • Thanks 1

Share this post


Link to post
6 hours ago, Brian Evans said:

As well a lot of controls with lists have a BeginUpdate and EndUpdate so you can stop visually updating the control for every change to an item. Wrap things in an Try/Finally to make sure EndUpdate is always called.


lv1.items.BeginUpdate;
try
  // add multiple items
finally
  lv1.items.EndUpdate;
end;

From the help for Vcl.ComCtrls.TListItems.BeginUpdate :

 

Prevents updating of the list view until the EndUpdate method is called.

Call BeginUpdate before making multiple changes to the list of items. When all changes are complete, call EndUpdate so that the changes can be reflected on screen. BeginUpdate and EndUpdate prevent excessive redraws and speed processing time when new items are added, deleted, or inserted.

As stated in the OP this is already being used. Virtual mode is the only answer. 

Share this post


Link to post

Update on this endeavour. . .  a solution has been found and is working, thanks to Alexander S. and Remy for their help. 

 

After some time, I managed to get the suggested virtual method posted by Remy working successfully, thank you.  But my listview is showing some artifacts in the drawing of the rows.  I will have to research that later.  I will study this method further in order to understand how it works.   Oh, and the run time on my old Dell laptop is now 38ms for the 25k, and 72ms for 100k.  A huge difference! 

 

Thank you @Alexander Sviridenkov, for first suggesting it to me.  I did do some research into it right after your post and was trying but failing to figure out how to put it all together.   And also to @Remy Lebeau, for the posted code snippet that showed me the way.  Much appreciated!!

 

Edited by JohnLM
updated opening reply.

Share this post


Link to post

If it's a TreeView or a ListView, make sure you don't have an OnChange handler wired up -- or if you do, you want to put a flag in it that says:

 

if ignore_this then Exit;

 

And have a var at the top of the implementation section

 

var ignore_this : Boolean;

 

Set it to true before you start your loading up, and false when you finish.

 

Or you could un-hook the OnChange event before you start and replace it when you're finished.

Edited by David Schwartz

Share this post


Link to post
8 minutes ago, David Schwartz said:

If it's a TreeView or a ListView, make sure you don't have an OnChange handler wired up -- or if you do, you want to put a flag in it that says:

 

if ignore_this then Exit;

 

And have a var at the top of the implementation section

 

var ignore_this : Boolean;

 

Set it to true before you start your loading up, and false when you finish.

 

Or you could un-hook the OnChange event before you start and replace it when you're finished.

yeah, global variables in your GUI, great idea!

Share this post


Link to post

Am I right in understanding that we can't switch to such a virtual mode with the standard TTreeView? Is this why Virtual TreeView exists?

Share this post


Link to post
3 hours ago, zed said:

Am I right in understanding that we can't switch to such a virtual mode with the standard TTreeView? Is this why Virtual TreeView exists?

That is correct. 

Share this post


Link to post
5 hours ago, JohnLM said:

But my listview is showing some artifacts in the drawing of the rows. 

Are you custom drawing the TListView? Virtual mode affects only data storage, so it should not cause drawing issues if you are otherwise using default settings. 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×