Why we can’t close a form that runs a tight FOR loop, even if we are using Application.ProcessMessages?

A stackOverflow user wanted to know why clicking on the form’s close button while some FOR loop is running does nothing:

procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
begin
  for i := 0 to 9999999 do
  begin
    Memo1.Lines.Add('hi');
    Application.ProcessMessages;
  end;
end;

The issue comes from the lack of understanding of how the forms work. Here is a complete analysis (and solution) of the problem.

Why it happens?

Most user interactions with the OS happens through messages: every time the user moves the mouse over our program, every time a key is pressed or a timer times up, a message is sent by the OS to our program.

In Delphi VCL, almost everything happens in a single thread: the main thread (which is also the GUI thread).

When you click the X button to close a form, a special message (named WM_QUIT) is sent by the OS, to our program. Our program has a queue called the message queue. All messages received are placed into that queue, but WM_QUIT is special and is always at the top of the queue. When the main thread of our program has a chance, it processed the queue, it finds there the WM_QUIT message and close the form.

But you are keeping the main thread (the GUI) really busy because you are calling Memo1.Lines.Add in a tight loop. Memo1.Lines.Add could be very slow if you add thousands of lines, so the main thread does not have a chance to process that message. The signal has arrived and it is waiting in the queue. The program won’t know about it until it finishes that “for” loop.

This is in almost all cases the reason a program freezes temporarily: the main thread is not processing the messages from Windows because it is too busy doing something else. Because of the message queue, everything is sequential (until Application.ProcessMessages breaks the sequence).

You are (accidentally) making it worse!

Using Application.ProcessMessages in your loop to “unfreeze” the program is a “good” idea. But you are calling it too often. Application.ProcessMessages is slow in itself. It will have a huge impact over program’s performance. Call it, but not that often.

Two possible fixes

1 The hard (and good) way

It always is recommended to run any code that might take more than a few seconds in a thread. This will solve the problem in the above code because the “for” loop runs now in its own thread, leaving the main thread free to check for new messages.

begin 
  TTask.Run(procedure
  begin
    for i := 0 to 9999999 do
    begin
      TThread.Synchronize(nil, procedure
        begin
          Memo1.Lines.Add('hi');
        end);
      if TThread.CheckTerminated then Exit; // Optional early exit
    end;
end);
end;

2 The easy/cheap (but not recommended) way

If you really really want to keep the code as it is, Application.ProcessMessages will make your loop terribly slow. So, you need to run Application.ProcessMessages not so often:

procedure TForm1.Btn1Click(Sender: TObject);
var i, Counter: Integer;
begin
  Counter:= 0;
  Memo1.Lines.BeginUpdate; //  be a good citizen and do use try/finally here!
  try
    for i := 0 to 250000 do
    begin
     Memo1.Lines.Add(IntToStr(i));

     { Prevent freeze }
     inc(Counter);
     if counter > 10000 then // A smaller number will make the app more responsive but will waste more CPU.
      begin
        Counter:= 0;
        Application.ProcessMessages; // Necessary to process the messages
        if Application.Terminated then Exit;  // Necessary if you want to "quit early"
       end;
    end;
  finally
    Memo1.Lines.EndUpdate;
  end;
end;

How does it work?

In the Application.ProcessMessages, your program processes the message queue to and finds there the special WM_QUIT message. Because of that, it sets the Terminate flag to true. Pretty much nothing else (relevant to this discussion) happens there.

Now that the “Terminate” flag has been set, you need to manually check for it, therefore the line:

 if Application.Terminated then Exit;

The “I told you so”

Multi-threading is hard but it is strongly recommended. Application.ProcessMessages has serious repercussions (like code re-entrance) especially when used with timers.
I have a big chapter in my Delphi in all its glory (part 1) book dedicated to the (hidden evils) of Application.ProcessMessages.

If you use Application.ProcessMessages in your app, you better lock the GUI so the user won’t be able to press the same button twice. This won’t save you from timers, though….

Fixing it with the right architecture

The fact that you “waste” soooo much CPU to update the GUI (Memo1) shows that you could probably invest in some new program architecture.

Don’t write the data to the GUI line by line. Write it to memory and when you are done, show the data, all at once (Memo1.Text:= SomeMemoryLog).

EVEN BETTER: write the data to memory and show on the scree not all 9999999 lines but only the top 10 (most recent) or whatever number fits on the screen. It is much faster to show 10 lines instead of 9999999. The rest of the lines (9999989) are irrelevant to the user since the user won’t see them anyway! For the rest of 9999989 lines add a scrollbar – discard the previous 10 lines you shown and show the next 10 lines, as the user scrolls down.

I don’t know why you show 9999999 lines on the screen, but if you use the Memo1 for logging, forget about it. Check my GitHub repository: I have a class called TRamLog that can be connected to a displaying component like VCL TLogViewer (also available on FMX). Together, they implement exactly what I described above. This way, you have on the screen ONLY 10 (or whatever fits on the screen) lines of data. No more memory and CPU wasted.

You have full blown demos on how to use those components.

Leave a Comment

Scroll to Top