Saturday, November 23, 2019

The Dark Side of Application.ProcessMessages

The Dark Side of Application.ProcessMessages 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 youll 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 Delphis 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 dont 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. Thats the problem: any call to ProcessMessages might contain a recursive call to any event handler again. Heres an example: Use the following code for a buttons 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; {OnCreate:}   Ã‚  WorkLevel : 0; procedure TForm1.WorkBtnClick(Sender: TObject) ; var   Ã‚  cycle : integer; begin   Ã‚  inc(WorkLevel) ;   Ã‚  for cycle : 1 to 5 do   Ã‚  begin   Ã‚  Ã‚  Ã‚  Memo1.Lines.Add(- Work IntToStr(WorkLevel) , Cycle IntToStr(cycle) ;   Ã‚  Ã‚  Ã‚  Application.ProcessMessages;   Ã‚  Ã‚  Ã‚  sleep(1000) ; // or some other work   Ã‚  end;   Ã‚  Memo1.Lines.Add(Work IntToStr(WorkLevel) ended.) ;   Ã‚  dec(WorkLevel) ; end; 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; begin   Ã‚  myfile : TFileStream.create(myOutput.txt) ;   Ã‚  try   Ã‚  Ã‚  Ã‚  while BytesReady 0 do   Ã‚  Ã‚  Ã‚  begin   Ã‚  Ã‚  Ã‚  Ã‚  Ã‚  myfile.Write(DataBlock) ;   Ã‚  Ã‚  Ã‚  Ã‚  Ã‚  dec(BytesReady,sizeof(DataBlock)) ;   Ã‚  Ã‚  Ã‚  Ã‚  Ã‚  DataBlock[2] : #13; {test line 1}   Ã‚  Ã‚  Ã‚  Ã‚  Ã‚  Application.ProcessMessages;   Ã‚  Ã‚  Ã‚  Ã‚  Ã‚  DataBlock[2] : #13; {test line 2}   Ã‚  Ã‚  Ã‚  end;   Ã‚  finally   Ã‚  Ã‚  Ã‚  myfile.free;   Ã‚  end; end; 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 doesnt look very good if your application seem to crash because the drive is too slow. Thats it. The next time you add Application.ProcessMessages, think twice ;)

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.