Mastering Delphi Programming:A Complete Reference Guide
上QQ阅读APP看书,第一时间看更新

Bulk updates

Another aspect of the same problem of overabundance of messages occurs when you want to add or modify multiple lines in a TListBox or TMemo. The demonstration program, BeginUpdate, demonstrates the problem and shows possible solutions.

Let's say we have a listbox and we want to populate it with lots of lines. The demo program displays 10,000 lines, which is enough to show the problem.

A naive program would solve the problem in two lines:

for i := 1 to CNumLines do
ListBox1.Items.Add('Line ' + IntToStr(i));

This, of course, works. It is also unexpectedly slow. 10,000 lines is really not much for modern computers so we would expect this code to execute very quickly. In reality, it takes 1.4 seconds on my test machine!

We could do the same with a TMemo. The result, however, is a lot more terrible:

for i := 1 to CNumLines do
Memo1.Lines.Add('Line ' + IntToStr(i));

With the memo we can see lines appear one by one. The total execution time on my computer is a whopping 12 seconds!

This would make both listbox and memo unusable for displaying a large amount of information. Luckily, this is something that the original VCL designers anticipated, so they provided a solution.

If you look into the code, you'll see that both TListBox.Items and TMemo.Lines are of the same type, TStrings. This class is used as a base class for TStringList and also for all graphical controls that display multiple lines of text.

The TStrings class also provides a solution. It implements methods BeginUpdate and EndUpdate, which turn visual updating of a control on and off. While we are in update mode (after BeginUpdate was called), visual control is not updated. Only when we call EndUpdate will Windows redraw the control to the new state. 

BeginUpdate and EndUpdate calls can be nested. Control will only be updated when every BeginUpdate is paired with an EndUpdate:

ListBox1.Items.Add('1');  // immediate redraw
ListBox1.BeginUpdate; // disables updating visual control
ListBox1.Items.Add('2'); // display is not updated
ListBox1.BeginUpdate; // does nothing, we are already in *update* mode
ListBox1.Items.Add('3;); // display is not updated
ListBox1.EndUpdate; // does nothing, BeginUpdate was called twice
ListBox1.Items.Add('4'); // display is not updated
ListBox1.EndUpdate; // exits update mode, changes to ListBox1 are
// displayed on the screen

Adding BeginUpdate/EndUpdate to existing code is very simple. We just have to wrap them around existing operations:

ListBox1.Items.BeginUpdate;
for i := 1 to CNumLines do
ListBox1.Items.Add('Line ' + IntToStr(i));
ListBox1.Items.EndUpdate;

Memo1.Lines.BeginUpdate;
for i := 1 to CNumLines do
Memo1.Lines.Add('Line ' + IntToStr(i));
Memo1.Lines.EndUpdate;

If you click on the second button in the demo program you'll see that the program reacts much faster. Execution times on my computer were 48 ms for TListBox and 671 ms for TMemo. This second number still seems suspicious. Why does TMemo need 0.7 seconds to add 10,000 lines if changes are not painted on the screen?

To find an answer to that we have to dig into VCL code, into the TStrings.Add method:

function TStrings.Add(const S: string): Integer;
begin
Result := GetCount;
Insert(Result, S);
end;

Firstly, this method calls GetCount so that it can return the proper index of appended elements. Concrete implementation in TMemoStrings.GetCount sends two Windows messages even when the control is in the updating mode: 

Result := SendMessage(Memo.Handle, EM_GETLINECOUNT, 0, 0);
if SendMessage(Memo.Handle, EM_LINELENGTH, SendMessage(Memo.Handle,
EM_LINEINDEX, Result - 1, 0), 0) = 0 then Dec(Result);

After that, TMemoStrings.Insert sends three messages to update the current selection:

if Index >= 0 then
begin
SelStart := SendMessage(Memo.Handle, EM_LINEINDEX, Index, 0);
// some code skipped ... it is not executed in our case
SendMessage(Memo.Handle, EM_SETSEL, SelStart, SelStart);
SendTextMessage(Memo.Handle, EM_REPLACESEL, 0, Line);
end;

All that causes five Windows messages to be sent for each appended line and that slows the program down. Can we do it better? Sure!

To speed up TMemo, you have to collect all updates in some secondary storage, for example in a TStringList. At the end, just assign the new memo state to its Text property and it will be updated in one massive operation.

The third button in the demo program does just that:

sl := TStringList.Create;
for i := 1 to CNumLines do
sl.Add('Line ' + IntToStr(i));
Memo1.Text := sl.Text;
FreeAndNil(sl);

This change brings execution speed closer to the listbox. My computer needed only 75 ms to display 10,000 lines in a memo with this code.

An interesting comparison can be made by executing the same code in the FireMonkey framework. Graphical controls in FireMonkey are not based directly on Windows controls, so effects of BeginUpdate/EndUpdate may be different.

The program BeginUpdateFMX in the code archive does just that. I will not go through the whole process again, but just present the measurements. All times are in milliseconds:

 

We can see that BeginUpdate/EndUpdate are also useful in the FireMonkey framework.

If a class implements BeginUpdate/ EndUpdate, use it when doing bulk updates.

In the next part, we'll see how we can replace a TListBox with an even faster solution if we are programming with the VCL framework.