JD,
the behaviour you are seeing is perfectly normal.
a windows Timer just posts WM_TIMER messages to itself to the interval that you have set.
The reason I don't like Application.Processmessages is that people don't understand the windows message pump and use it in the wrong context like you are doing now.
I'll try to explain it here in detail.
For this I created a new project in Delphi (just select new->VCL forms application) here is the source code (.pas + dfm)
Code:
.pas ->
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls;
type
TForm1 = class(TForm)
Btn_good: TButton;
Btn_bad: TButton;
Lbl_timer: TLabel;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Btn_goodClick(Sender: TObject);
procedure Btn_badClick(Sender: TObject);
private
{ Private declarations }
MyTimer : TTimer;
Counter : Integer;
procedure GoodTimerEvent(Sender: TObject);
procedure BadTimerEvent(Sender: TObject);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function LongCalculationLoop : double;
var i : integer;
begin
for i :=0 to 1000000 do
Result := Sqrt(i);
end;
procedure TForm1.BadTimerEvent(Sender: TObject);
begin
Inc(Counter);
Lbl_timer.Caption := Format('Counter is now: %d', [Counter]);
LongCalculationLoop;
Application.ProcessMessages;
Counter := 0;
end;
procedure TForm1.GoodTimerEvent(Sender: TObject);
begin
Inc(Counter);
Lbl_timer.Caption := Format('Counter is now: %d', [Counter]);
LongCalculationLoop;
Counter := 0;
end;
procedure TForm1.Btn_badClick(Sender: TObject);
begin
Counter := 0;
MyTimer.Enabled := False;
MyTimer.OnTimer := BadTimerEvent;
MyTimer.Enabled := True;
end;
procedure TForm1.Btn_goodClick(Sender: TObject);
begin
Counter := 0;
MyTimer.Enabled := False;
MyTimer.OnTimer := GoodTimerEvent;
MyTimer.Enabled := True;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
MyTimer := TTimer.Create(nil);
MyTimer.Interval := 10;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FreeAndNil(MyTimer);
end;
end.
.dfm ->
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 79
ClientWidth = 208
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
TextHeight = 13
object Lbl_timer: TLabel
Left = 20
Top = 12
Width = 6
Height = 13
Caption = '0'
end
object Btn_good: TButton
Left = 20
Top = 44
Width = 75
Height = 25
Caption = 'Good'
TabOrder = 0
OnClick = Btn_goodClick
end
object Btn_bad: TButton
Left = 101
Top = 44
Width = 75
Height = 25
Caption = 'Bad'
TabOrder = 1
OnClick = Btn_badClick
end
end
now click on the 'good' button and you will see the text "the counter is now: 1"
clicking on the 'bad' button and you will see the counter value fluctuating.
huh what is going here??
Please remember that a VCL forms application is single threaded (and a TTimer component does not change that fact).
The understand what is going on here, we need to take a look under the delphi hood.
we start with the .dpr file source code:
Code:
program Project1;
uses
Forms,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
not much to see here:
- some initializing
- our main form is autocreated
- aplication is run
but what does Application.Run really do? let's find out:
small note: this is the source code for D2006, it may vary from version to version.
Code:
procedure TApplication.Run;
begin
FRunning := True;
try
AddExitProc(DoneApplication);
if FMainForm <> nil then
begin
case CmdShow of
SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized;
SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;
end;
if FShowMainForm then
if FMainForm.FWindowState = wsMinimized then
Minimize else
FMainForm.Visible := True;
repeat
try
HandleMessage;
except
HandleException(Self);
end;
until Terminated;
end;
finally
FRunning := False;
end;
end;
we see a repeat loop:
Handle a windows message until the application is terminated.
That's it, that's all that is needed to run a windows application.
What the HandleMessage procedure in reality does is read the Application's message queue.
Code:
function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Unicode: Boolean;
Handled: Boolean;
MsgExists: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) then
begin
Unicode := (Msg.hwnd <> 0) and IsWindowUnicode(Msg.hwnd);
if Unicode then
MsgExists := PeekMessageW(Msg, 0, 0, 0, PM_REMOVE)
else
MsgExists := PeekMessage(Msg, 0, 0, 0, PM_REMOVE);
if not MsgExists then Exit;
Result := True;
if Msg.Message <> WM_QUIT then
begin
Handled := False;
if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
if not IsPreProcessMessage(Msg) and not IsHintMsg(Msg) and
not Handled and not IsMDIMsg(Msg) and
not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
begin
TranslateMessage(Msg);
if Unicode then
DispatchMessageW(Msg)
else
DispatchMessage(Msg);
end;
end
else
FTerminate := True;
end;
end;
procedure TApplication.HandleMessage;
var
Msg: TMsg;
begin
if not ProcessMessage(Msg) then Idle(Msg);
end;
procedure TApplication.ProcessMessages;
var
Msg: TMsg;
begin
while ProcessMessage(Msg) do {loop};
end;
Handlemessage calls Processmessage.
Processmessage reads the message queue (PeekMessage) and if there is a message in the queue it will dispatch the message (DispatchMessage).
DispatchMessage will in fact forward the message to the destined control (form, button, timer, ...)
I included Application.ProcessMessages and as you can see it is a bit different than HandleMessage.
HandleMessage will take 1 message out of the queue (if there is any).
ProcessMessages will take messages out of the queue UNTIL it is empty.
Now what does this mean for our little application?
When creating and activating a Timer in windows, windows will send WM_TIMER messages at the specified interval.
Delphi will get the WM_TIMER message in it's queue and will dispatch the message to the TTimer object.
Our TTimer object will fire the onTimer event when that happens. That's all, nothing more, nothing less...
when we activate the 'good' timer the application will behave like this:
application.run ->
handlemessage (WM_TIMER in queue) ->
processmessage -> TForm1.GoodTimerEvent
-> increase counter
-> set label
-> longcalculation
-> reset counter
handlemessage (WM_TIMER in queue) ->
processmessage -> TForm1.GoodTimerEvent
-> increase counter
-> set label
-> longcalculation
-> reset counter
...
and so on
when we activate the 'bad' timer we will see this
application.run ->
handlemessage (WM_TIMER in queue) ->
processmessage -> TForm1.BadTimerEvent
-> increase counter
-> set label
-> longcalculation
-> ProcessMessages
handlemessage (WM_TIMER in queue) ->
processmessage -> TForm1.BadTimerEvent
-> increase counter
-> set label
-> longcalculation
-> ProcessMessages
handlemessage (WM_TIMER in queue) ->
processmessage -> TForm1.BadTimerEvent
...
-> reset counter
-> reset counter
See what's happening here?
the key is that if you have a long calculation in your timer event (longer than the specified Timer interval),
WM_timer messages will start filling the up the application's message queue and in extreme conditions (like JD's code) the
Application's stack will be filled with calls to the timerevent until there is no more room in the stack and our nice little app will crash...
There I hope this clears things up for everyone.
As you can see Application.ProcessMessages can easily fuck up things if used in the wrong context...
Cheers,
Daddy
-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!