VB高精度計時器編程的討論

 VB高精度計時器編程的討論  
   
  VB記時器編程的討論   在很多場合下編程(例如工業控制、遊戲)中需要比較精確的記時器,本文討論的是在VB下實現記時器的若干方法以及它們的精度控制問題。   在VB中最常用的是Timer控件,它的設置和使用都非常方便,理論上它的記時精度可以達到1ms(毫秒)。但是衆所周知的,實際上Timer在記時間隔小於50ms之下是精度是十分差的。它只適用於對於精度要求不太高的場合。   這裏作者要介紹的是兩中利用Windows   API函數實現精確記時的方法。第一中方法是利用高性能頻率記數(作者本人的稱呼)法。利用這種方法要使用兩個API函數QueryPerformanceFrequency和QueryPerformanceCounter。QueryPerformanceFrequency函數獲得高性能頻率記數器的震盪頻率,該函數的定義如下:    
   
  Private   Declare   Function   QueryPerformanceFrequency   Lib   "kernel32"   _  
                              (lpFrequency   As   LARGE_INTEGER)   As   Long  
  函數中的數據結構LARGE_INTEGER定義如下:  
  Type   LARGE_INTEGER  
          lowpart   As   Long  
          highpart   As   Long  
  End   Type  
   
  調用該函數後,函數會將系統頻率記數器的震盪頻率保存到lpPerformanceCount中,其中低位保存到lowpart中,高位保存到highpart中。但是現在的Windows沒有使用到hightpart(系統頻率記數器的震盪頻率與計算機的主頻無關,我在幾臺機上做過驗證,都是lowpart爲1193180,highpart爲0)。  
  QueryPerformanceCounter函數獲得系統頻率記數器的震盪次數,函數的定義如下  
   
  Private   Declare   Function   QueryPerformanceCounter   Lib   "kernel32"   _  
                  (lpPerformanceCount   As   LARGE_INTEGER)   As   Long  
   
  獲得記時器震盪次數保存在lpPerformanceCount中。  
  顯然,如果首先獲得利用QueryPerformanceFrequency函數獲得頻率記數器的震盪頻率,然後在執行某個程序段之前調用QueryPerformanceCounter函數獲得頻率記數器的震盪次數,在程序段結束再調用QueryPerformanceCounter函數獲得頻率記數器的震盪次數,將兩次獲得的震盪次數相減後再除以震盪頻率就獲得的了兩次間隔之間的時間(以秒爲單位)。如果在程序中建立一個循環,在循環中不停的調用QueryPerformanceCounter獲得頻率記數器的震盪次數並同先前的頻率記數器的震盪次數相減,將結果除以頻率記數器的震盪頻率,如果達到一定的時間就執行某個任務,這樣就實現了一個比較精確的記時器的功能。  
   
  另外的一種精確記時器的功能是利用多媒體記時器函數(這也是作者的定義,因爲這個系列的函數是在Winmm.dll中定義並且是爲媒體播放服務的)。  
  實現多媒體記時器首先要定義timeSetEvent函數,該函數的定義如下:  
   
  Public   Declare   Function   timeSetEvent   Lib   "winmm.dll"   (ByVal   uDelay   As   Long,   ByVal   _  
                  uResolution   As   Long,   ByVal   lpFunction   As   Long,   ByVal   dwUser   As   Long,   _  
                  ByVal   uFlags   As   Long)   As   Long  
   
  函數定義中參數uDelay定義延遲時間,以毫秒爲單位,該參數相當於Timer控件的Interval屬性。參數uResolution定義記時精度,如果要求儘可能高的精度,要將該參數設置爲0;參數lpFunction定義了timeSetEvent函數的回調函數的地址。參數dwUser定義用戶自定義的回調值,該值將傳遞給回調函數。參數uFlags定義定時類型,如果定義爲Time_OneShot,則只會在當達到uDelay定義的時間後調用回調函數一次,如果定義爲TIME_PERIODIC,則在每次達到定時時間後調用回調函數。  
  如果函數調用成功,在系統中建立了一個多媒體記時器對象,每當經過一個uDelay時間後lpFunction指定的函數都會被調用。同時函數返回一個對象標識,如果不再需要記時器則必須要使用timeKillEvent函數刪除記時器對象。  
  由於Windows是一個多任務的操作系統,因此基於API調用的記時器的精度都會受到其它很多因素的干擾。到底這兩中記時器的精度如何,我們來使用以下的程序進行驗證:  
  設置三種記時器(Timer控件、高性能頻率記數、多媒體記時器)。將它們的定時間隔設置爲10毫秒,讓它們不停工作直到達到一個比較長的時間(比如60秒),這樣記時器的誤差會被累計下來,然後同實際經過的時間相比較,就可以得到它們的精度。  
  下面是具體的檢測程序。  
  首先建立一個工程文件,在Form1中加入一個Timer控件,兩個CommandButton控件和三個TextBox控件,然後在Form1的代碼窗口中加入以下代碼  
   
   
  Option   Explicit  
   
  Private   Sub   Command1_Click()  
          Dim   lagTick1   As   LARGE_INTEGER  
          Dim   lagTick2   As   LARGE_INTEGER  
          Dim   lTen   As   Long  
           
          Command2.Enabled   =   True  
          Command1.Enabled   =   False  
          iCountStart   =   60  
          lmmCount   =   60  
          TimerCount   =   60  
          actTime1   =   GetTickCount  
          lTimeID   =   timeSetEvent(10,   0,   AddressOf   TimeProc,   1,   1)  
          Timer1.Enabled   =   True  
           
          lTen   =   10   *   lMSFreq  
          Call   QueryPerformanceCounter(lagTick1)  
          lagTick2   =   lagTick1  
          While   iCountStart   >   0  
                  Call   QueryPerformanceCounter(lagTick2)  
                  '如果時鐘震動次數超過10毫秒的次數則刷新Text1的顯示  
                  If   lagTick2.lowpart   -   lagTick1.lowpart   >   lTen   Then  
                          lagTick1   =   lagTick2  
                          iCountStart   =   iCountStart   -   0.01  
                          Text1.Text   =   Format$(iCountStart,   "00.00")  
                  End   If  
                  DoEvents  
          Wend  
  End   Sub  
   
  Private   Sub   Command2_Click()  
          EndCount  
  End   Sub  
   
  Private   Sub   Form_Load()  
          Dim   lim   As   LARGE_INTEGER  
           
          Text1.Text   =   "60.00"  
          Text2.Text   =   "60.00"  
          Text3.Text   =   "60.00"  
          Command1.Caption   =   "開始倒記時"  
          Command2.Caption   =   "停止記時"  
          Command2.Enabled   =   False  
           
          '獲得系統板上時鐘頻率  
          QueryPerformanceFrequency   lim  
           
          '將頻率除以1000就的出時鐘1毫秒震動的次數  
          lMSFreq   =   (lim.highpart   *   2   ^   16)   /   1000   +   lim.lowpart   /   1000  
          Timer1.Interval   =   10  
          Timer1.Enabled   =   False  
  End   Sub  
   
  Private   Sub   Timer1_Timer()  
          TimerCount   =   TimerCount   -   0.01  
          Text3.Text   =   Format$(TimerCount,   "00.00")  
          If   TimerCount   <=   0   Then  
                  Timer1.Enabled   =   False  
          End   If  
  End   Sub  
  在Project中加入一個Module,然後在其中加入以下代碼:  
  Option   Explicit  
   
  Type   LARGE_INTEGER  
          lowpart   As   Long  
          highpart   As   Long  
  End   Type  
   
  Public   Declare   Function   QueryPerformanceCounter   Lib   "kernel32"   _  
                  (lpPerformanceCount   As   LARGE_INTEGER)   As   Long  
  Public   Declare   Function   QueryPerformanceFrequency   Lib   "kernel32"   _  
                  (lpFrequency   As   LARGE_INTEGER)   As   Long  
  Public   Declare   Function   timeSetEvent   Lib   "winmm.dll"   (ByVal   uDelay   As   Long,   ByVal   _  
                  uResolution   As   Long,   ByVal   lpFunction   As   Long,   ByVal   dwUser   As   Long,   _  
                  ByVal   uFlags   As   Long)   As   Long  
  Public   Declare   Function   timeKillEvent   Lib   "winmm.dll"   (ByVal   uID   As   Long)   As   Long  
  Public   Declare   Function   GetTickCount   Lib   "kernel32"   ()   As   Long  
   
  Public   lMSFreq   As   Long  
  Public   TimerCount   As   Single  
  Public   lmmCount   As   Single  
  Public   lTimeID   As   Long  
  Public   actTime1   As   Long  
  Public   actTime2   As   Long  
  Public   iCountStart   As   Single  
   
  Dim   iCount   As   Single  
   
  'timeSetEvent的回調函數  
  Sub   TimeProc(ByVal   uID   As   Long,   ByVal   uMsg   As   Long,   ByVal   dwUser   As   Long,   _  
          ByVal   dw1   As   Long,   ByVal   dw2   As   Long)  
           
          Form1.Text2.Text   =   Format$(lmmCount,   "00.00")  
          lmmCount   =   lmmCount   -   0.01  
          If   lmmCount   <=   0   Then  
                  iCountStart   =   60  
                  lmmCount   =   60  
                  TimerCount   =   60  
                  EndCount  
          End   If  
  End   Sub  
  Sub   EndCount()  
          iCount   =   iCountStart  
          iCountStart   =   0  
          timeKillEvent   lTimeID  
          actTime2   =   GetTickCount   -   actTime1  
          With   Form1  
                  .Command1.Enabled   =   True  
                  .Command2.Enabled   =   False  
                  .Timer1.Enabled   =   False  
                   
                  .Text1   =   "計數器記時"   +   Format$((60   -   iCount),   "00.00")   +   "     "   _  
                                  +   "實際經過時間"   +   Format$((actTime2   /   1000),   "00.00")  
                  .Text2   =   "計數器記時"   +   Format$((60   -   lmmCount),   "00.00")   +   "     "   _  
                                  +   "實際經過時間"   +   Format$((actTime2   /   1000),   "00.00")  
                  .Text3   =   "計數器記時"   +   Format$((60   -   TimerCount),   "00.00")   +   "     "   _  
                                  +   "實際經過時間"   +   Format$((actTime2   /   1000),   "00.00")  
          End   With  
  End   Sub  
   
   
  運行程序,點擊“開始倒記時”按鈕開始倒記時,可以看到兩種API記時器工作基本正常,文本框中的倒記時顯示流暢,而Timer控件的時間顯示相比之下卻不堪重負,十分緩慢。按“停止記時”按鈕就可以停止倒記時,由圖1可以看到,兩種API記時器的累計誤差在2‰以下,考慮到系統原因和處理記時顯示的時間,這個誤差基本是可以接受的,而且經過作者的多次檢測,誤差都在3‰以下。而Timer控件的誤差簡直是無法接受的。  
   
  圖   三種不同的記時器記時結果  
   
   
  在運行程序時作者還發現一個問題,如果在倒記時時拖動窗口,文本框中的顯示都會停止,而當停止窗口拖放後,多媒體記時器顯示會跳過這段時間記時,而其它兩種記時器顯示倒記時卻還是從原來的時間倒數。這說明多媒體記時器是在獨立的線程中運行的,不會受到程序的影響。  
  綜合上面的介紹和範例,我們可以看到,如果要建立高精度的記時器,使用多媒體記時器是比較好的選擇。而高性能頻率記數法比較適合計算某個耗時十分短的過程所消耗的時
發佈了46 篇原創文章 · 獲贊 16 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章