The Dark Side of Application.ProcessMessages in Delphi Applications

Using Application.ProcessMessages? Should You Reconsider?

Application.ProcessMessages Test
Application.ProcessMessages Test.

Article submitted by Marcus Junglas

When programming an event handler in Delphi (like the OnClick event of a TButton), there comes the time when your application needs to be busy for a while, e.g. the code needs to write a big file or compress some data.

If you do that you'll notice that your application seems to be locked. Your form cannot be moved anymore and the buttons are showing no sign of life.

It seems to be crashed.

The reason is that a Delpi application is single threaded. The code you are writing represents just a bunch of procedures which are called by Delphi's main thread whenever an event occured. The rest of the time the main thread is handling system messages and other things like form and component handling functions.

So, if you don't finish your event handling by doing some lengthy work, you will prevent the application to handle those messages.

A common solution for such type of problems is to call "Application.ProcessMessages". "Application" is a global object of the TApplication class.

The Application.Processmessages handles all waiting messages like window movements, button clicks and so on. It is commonly used as a simple solution to keep your application "working".

Unfortunately the mechanism behind "ProcessMessages" has its own characteristics, which might cause big confusion!

What does ProcessMessages?

PprocessMessages handles all waiting system messages in the applications message queue. Windows uses messages to "talk" to all running applications. User interaction is brought to the form via messages and "ProcessMessages" handles them.

If the mouse is going down on a TButton, for example, ProgressMessages does all what should happen on this event like the repaint of the button to a "pressed" state and, of course, a call to the OnClick() handling procedure if you assigned one.

That's the problem: any call to ProcessMessages might contain a recursive call to any event handler again. Here's an example:

Use the following code for a button's OnClick even handler ("work"). The for-statement simulates a long processing job with some calls to ProcessMessages every now and then.

This is simplified for better readability:

 {in MyForm:}
   WorkLevel : integer;
   WorkLevel := 0;
 procedure TForm1.WorkBtnClick(Sender: TObject) ;
   cycle : integer;
   inc(WorkLevel) ;
   for cycle := 1 to 5 do
     Memo1.Lines.Add('- Work ' + IntToStr(WorkLevel) + ', Cycle ' + IntToStr(cycle) ;
     sleep(1000) ; // or some other work
   Memo1.Lines.Add('Work ' + IntToStr(WorkLevel) + ' ended.') ;
   dec(WorkLevel) ;

WITHOUT "ProcessMessages" the following lines are written to the memo, if the Button was pressed TWICE in a short time:

 - Work 1, Cycle 1
 - Work 1, Cycle 2
 - Work 1, Cycle 3
 - Work 1, Cycle 4
 - Work 1, Cycle 5
 Work 1 ended.
 - Work 1, Cycle 1
 - Work 1, Cycle 2
 - Work 1, Cycle 3
 - Work 1, Cycle 4
 - Work 1, Cycle 5
 Work 1 ended.

While the procedure is busy, the the form does not show any reaction, but the second click was put into the message queue by Windows.

Right after the "OnClick" has finished it will be called again.

INCLUDING "ProcessMessages", the output might be very different:

 - Work 1, Cycle 1
 - Work 1, Cycle 2
 - Work 1, Cycle 3
 - Work 2, Cycle 1
 - Work 2, Cycle 2
 - Work 2, Cycle 3
 - Work 2, Cycle 4
 - Work 2, Cycle 5
 Work 2 ended.
 - Work 1, Cycle 4
 - Work 1, Cycle 5
 Work 1 ended.

This time the form seems to be working again and accepts any user interaction. So the button is pressed half way during your first "worker" function AGAIN, which will be handled instantly. All incoming events are handled like any other function call.

In theory, during every call to "ProgressMessages" ANY amount of clicks and user messages might happen "in place".

So be careful with your code!

Different example (in simple pseudo-code!):

 procedure OnClickFileWrite() ;
 var myfile := TFileStream;
   myfile := TFileStream.create('myOutput.txt') ;
     while BytesReady > 0 do
       myfile.Write(DataBlock) ;
       dec(BytesReady,sizeof(DataBlock)) ;
       DataBlock[2] := #13; {test line 1}
       DataBlock[2] := #13; {test line 2}

This function writes a large amount of data and tries to "unlock" the application by using "ProcessMessages" each time a block of data is written.

If the user clicks on the button again, the same code will be executed while the file is still being written to. So the file cannot be opened a 2nd time and the procedure fails.

Maybe your application will do some error recovery like freeing the buffers.

As a possible result "Datablock" will be freed and the first code will "suddenly" raise an "Access Violation" when it accesses it. In this case: test line 1 will work, test line 2 will crash.

The better way:

To make it easy you could set the whole Form "enabled := false", which blocks all user input, but does NOT show this to the user (all Buttons are not grayed).

A better way would be to set all buttons to "disabled", but this might be complex if you want to keep one "Cancel" button for example. Also you need to go through all the components to disable them and when they are enabled again, you need to check if there should be some remaining in the disabled state.

You could disable a container child controls when the Enabled property changes.

As the class name "TNotifyEvent" suggests, it should only be used for short term reactions to the event. For time consuming code the best way is IMHO to put all the "slow" code into an own Thread.

Regarding the problems with "PrecessMessages" and/or the enabling and disabling of components, the usage of a second thread seems to be not too complicated at all.

Remember that even simple and fast lines of code might hang for seconds, e.g. opening a file on a disc drive might have to wait until the drive spin up has finished. It doesn't look very good if your application seem to crash because the drive is too slow.

That's it. The next time you add "Application.ProcessMessages", think twice ;)