CSDN博客

img smallboy_5

VB高精度计时器编程的讨论

发表于2008/10/2 16:12:00  1377人阅读

分类: 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控件的误差简直是无法接受的。  
   
  图   三种不同的记时器记时结果  
   
   
  在运行程序时作者还发现一个问题,如果在倒记时时拖动窗口,文本框中的显示都会停止,而当停止窗口拖放后,多媒体记时器显示会跳过这段时间记时,而其它两种记时器显示倒记时却还是从原来的时间倒数。这说明多媒体记时器是在独立的线程中运行的,不会受到程序的影响。  
  综合上面的介绍和范例,我们可以看到,如果要建立高精度的记时器,使用多媒体记时器是比较好的选择。而高性能频率记数法比较适合计算某个耗时十分短的过程所消耗的时
阅读全文
0 0

相关文章推荐

img
取 消
img