用Delphi建立精確計數器
在Windows中的很多場合下編程(例如工業控制、遊戲)中需要比較精確的記時器,本文討論的是在Delphi下實現記時器的若干方法以及它們的精度控制問題。
在Delphi中最常用的是Timer控制項,它的設定和使用都非常方便,理論上它的記時精度可以達到1ms(毫秒)。但是眾所周知的,實際上Timer在記時間隔小於50ms之下是精度是十分差的。它只適用於對於精度要求不太高的場合。
這裡作者要介紹的是兩種利用Windows API函數實現精確記時的方法。第一中方法是利用高效能頻率記數(作者本人的稱呼)法。利用這種方法要使用兩個API函數QueryPerformanceFrequency和QueryPerformanceCounter。QueryPerformanceFrequency函數獲得高效能頻率記數器的震蕩頻率。
調用該函數後,函數會將系統頻率記數器的震蕩頻率(每毫秒)儲存到一個LargeInteger中。不過利用該函數在幾台機器上做過實驗,結果都是1193180。讀者朋友可以在自己的機器上試一下
QueryPerformanceCounter函數獲得系統頻率記數器的震蕩次數,結果也儲存到一個Largenteger中。
很顯然,如果在計時中首先使用QueryPerformanceFrequency獲得高效能頻率記數器每毫秒的震蕩次數,然後在計時開始時使用QueryPerformanceCounter函數獲得當前系統頻率記數器的震蕩次數。在計時結束時再調用QueryPerformanceCounter函數獲得系統頻率記數器的震蕩次數。將兩者相減,再將結果除以頻率記數器每毫秒的震蕩次數,就可以獲得某一事件經過的準確時間。(次數除以頻率等於時間)
另外的一種精確記時器的功能是利用多媒體記時器函數(這也是作者的定義,因為這個系列的函數是在Winmm.dll中定義並且是為媒體播放服務的)。
實現多媒體記時器首先要使用timeSetEvent函數建立計時事件。該函數在Delphi中的mmsystem.pas中有定義,定義如下:
function timeSetEvent(uDelay, uResolution: UINT;
lpFunction: TFNTimeCallBack; dwUser: DWord; uFlags: UINT): MMRESULT; stdcall
函數定義中參數uDelay定義延遲時間,以毫秒為單位,該參數相當於Timer控制項的Interval屬性。參數uResolution定義記時精度,如果要求儘可能高的精度,要將該參數設定為0;參數lpFunction定義了timeSetEvent函數的回呼函數。該函數相當於一個定時中斷處理函數,每當經過一個uDelay長度的時間間隔,該函數就會被調用,編程者可以在該函數中加入相應的處理語句。參數dwUser定義使用者自訂的回調值,該值將傳遞給回呼函數。參數uFlags定義定時類型,如果要不間斷的記時,該值應設定為1。
如果函數調用成功,在系統中建立了一個多媒體記時器對象,每當經過一個uDelay時間後lpFunction指定的函數都會被調用。同時函數返回一個對象標識,如果不再需要記時器則必須要使用timeKillEvent函數刪除記時器對象。
XML:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
由於Windows是一個多任務的作業系統,因此基於API調用的記時器的精度都會受到其它很多因素的幹擾。到底這兩中記時器的精度如何,我們來使用以下的程式進行驗證:
設定三種記時器(Timer控制項、高效能頻率記數、多媒體記時器)。將它們的定時間隔設定為10毫秒,讓它們不停工作直到達到一個比較長的時間(比如60秒),這樣記時器的誤差會被累計下來,然後同實際經過的時間相比較,就可以得到它們的精度。
下面是具體的檢測程式。
unit Unit1;interfaceuses Windows, Messages, SysUtils, Classes, GraphiCS, Controls, Forms, Dialogs, StdCtrls, ExtCtrls,mmSystem;type TForm1 = class(TForm) Edit1: TEdit; Edit2: TEdit; Edit3: TEdit; Button1: TButton; Button2: TButton; Timer1: TTimer; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Timer1Timer(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } end;var Form1: TForm1; actTime1,actTime2:Cardinal; smmCount,sTimerCount,sPCount:Single; hTimeID:Integer; iTen:Integer; proTimeCallBack:TFNTimeCallBack;procedure TimeProc(uTimerID, uMessage: UINT; dwUser, dw1, dw2: DWord) stdcall;procedure proEndCount;implementation{$R *.DFM}//timeSetEvent的回呼函數procedure proEndCount;begin actTime2:=GetTickCount-actTime1; Form1.Button2.Enabled :=False; Form1.Button1.Enabled :=TRue; Form1.Timer1.Enabled :=False; smmCount:=60; sTimerCount:=60; spCount:=-1; timeKillEvent(hTimeID);end;procedure TimeProc(uTimerID, uMessage: UINT; dwUser, dw1, dw2: DWord) stdcall;begin Form1.Edit2.Text:=FloatToStr(smmCount); smmCount:=smmCount-0.01;end;procedure TForm1.FormCreate(Sender: TObject);begin Button1.Caption :='開始倒計時'; Button2.Caption :='結束倒計時'; Button2.Enabled :=False; Button1.Enabled :=True; Timer1.Enabled :=False; smmCount:=60; sTimerCount:=60; sPCount:=60;end;procedure TForm1.Button1Click(Sender: TObject);var lgTick1,lgTick2,lgPer:TLargeInteger; fTemp:Single;begin Button2.Enabled :=True; Button1.Enabled :=False; Timer1.Enabled :=True; Timer1.Interval :=10; proTimeCallback:=TimeProc; //timeSetEvent使用http://blog.csdn.net/xieyunc/archive/2009/04/29/4136125.aspx hTimeID:=timeSetEvent(10,0,proTimeCallback,1,1); actTime1:=GetTickCount; //獲得系統的高效能頻率計數器在一秒內的震動次數,參考http://www.cnblogs.com/del/archive/2008/02/16/1070610.html QueryPerformanceFrequency(lgPer); fTemp:=lgPer/1000; iTen:=Trunc(fTemp*10); QueryPerformanceCounter(lgTick1); lgTick2:=lgTick1; sPCount:=60; while sPCount>0 do begin QueryPerformanceCounter(lgTick2); //如果時鐘震動次數超過10毫秒的次數則重新整理Edit3的顯示 If lgTick2 - lgTick1 > iTen Then begin lgTick1 := lgTick2; sPCount := sPCount - 0.01; Edit3.Text := FloatToStr(sPCount); Application.ProcessMessages; end; end;end;procedure TForm1.Timer1Timer(Sender: TObject);begin Edit1.Text := FloatToStr(sTimerCount); sTimerCount:=sTimerCount-0.01;end;procedure TForm1.Button2Click(Sender: TObject);begin proEndCount; //顯示從開始記數到記數實際經過的時間 ShowMessage('實際經過時間'+IntToStr(actTime2)+'毫秒');end;end.
運行程式,點擊“開始倒記時”按鈕,程式開始60秒倒記時,由於上面的程式只涉及了記時器程式的原理而沒有將錯誤處理加入其中,所以不要等60秒倒記時結束。點擊“結束倒記時”按鈕可以結束倒記時。這時在彈出對話方塊中會顯示實際經過的時間(單位為毫秒),將三個文字框內的時間乘以1000再加上實際經過的時間,越接近60000,則記時精度越高。
下面是在我的機器上的執行結果。
XML:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" />ASPectratio="t">2000/temp/msoclip1/01/clip_image001.jpg" o:title="精確記時器">
從上面的結果看,由Delphi的Timer控制項建立的記時器的精度十分差,無法在實際中使用,而利用高效能頻率記數法和多媒體計數器法的誤差都在1%以下。考慮到程式中在文字框中顯示時間對程式所造成的影響,這個誤差在應用中是完全可以忽略的。
另外在運行程式時作者還發現一個問題,如果在倒記時時拖動視窗,文字框中的顯示都會停止,而當停止視窗拖放後,多媒體記時器顯示會跳過這段時間記時,而其它兩種記時器顯示倒記時卻還是從原來的時間倒數。這說明多媒體記時器是在獨立的線程中啟動並執行,不會受到程式的影響。
綜合上面的介紹和範例,我們可以看到,如果要建立高精度的記時器,使用多媒體記時器是比較好的選擇。而高效能頻率記數法比較適合計算某個耗時十分短的過程所消耗的時間(例如剖析器中某個被多次調用的程式段執行時間以最佳化程式),因為畢竟高效能頻率記數的理論可以達到微秒層級。Timer控制項雖然精度比上面兩者差很多,但是它使用方便,在要求不高的場合它還是最佳選擇。