Windows編程 第十一回 三問計時器

啥是計數器?

計時器是一種輸入設備,它週期性地在每經過一個指定的時間間隔後就通知應用程序一次。當你的程序將時間間隔告訴Windows,例如“每10秒鐘通知我一聲”,然後Windows給你的程序發送週期性發生的WM_TIMER消息以表示時間到了。

我們可以通過調用SetTimer函數爲的Windows程序分配一個定時器。SetTimer有一個時間間隔範圍爲1毫秒到 4,294,967,295毫秒(將近50天)的整型參數,這個值指示Windows每隔多久時間給程序發送WM_TIMER消息。例如,如果間隔爲 1000毫秒,那麼Windows將每秒給程序發送一個WM_TIMER消息。

當你的程序用完定時器時,它調用KillTimer函數來停止計時器消息。在處理WM_TIMER消息時,你可以通過調用KillTimer函數來 編寫一個“瞬間”的定時器。KillTimer調用除了會銷燬以前調用SetTimer創建的定時器事件,還會清除消息隊列中尚未被處理的WM_TIMER消息,從而使程序在調用KillTimer之後就不會再 接收到WM_TIMER消息。

下面就介紹一下計時器的使用方法吧。

計時器怎麼用?

如果你需要在整個程序執行期間都使用計時器,那麼你將得從WinMain函數中或者在處理WM_CREATE消息時調用SetTimer,並在退出 WinMain或響應WM_DESTROY消息時調用KillTimer。根據調用SetTimer時使用的參數,可以選擇以下兩種方法之一來使用計時 器。

 

方法一

這是最方便的一種方法,它讓Windows把WM_TIMER消息發送到應用程序的正常窗口過程中,SetTimer調用如下所示:

SetTimer (hwnd, 1, uiMsecInterval, NULL) ;

第一個參數是其窗口過程將接收WM_TIMER消息的窗口句柄。第二個參數是定時器ID,它是一個非0數值,在整個例子中假定爲1。第三個參數是一 個32位無符號整數,以毫秒爲單位指定一個時間間隔,一個60,000的值將使Windows每分鐘發送一次WM_TIMER消息。

你可以通過調用

KillTimer (hwnd, 1) ;

在任何時刻停止WM_TIMER消息(即使正在處理WM_TIMER消息)。此函數的第二個參數是SetTimer調用中所用的同一個定時器ID。在終止程序之前,你應該在響應WM_DESTROY消息中停止任何活動的定時器。

當你的窗口過程收到一個WM_TIMER消息①時,wParam參數等於定時器的ID值(上述情形爲1),lParam參數爲0。爲了使程序更具有可讀性,您可以使用#define敘述定義不同的定時器ID:

#define TIMER_SEC 1

#define TIMER_MIN 2

然後你可以使用兩個SetTimer調用來設定兩個定時器:

SetTimer (hwnd, TIMER_SEC, 1000, NULL) ;

SetTimer (hwnd, TIMER_MIN, 60000, NULL) ;

WM_TIMER的處理如下所示:

caseWM_TIMER:

       

   switch (wParam)

       

    {

       

   case TIMER_SEC:

       

           //每秒一次的處理

       

                  break ;

       

   case TIMER_MIN:

       

           //每分鐘一次的處理

       

           break ;

       

    }

       

return 0 ;

       

如果你想將一個已經存在的定時器設定爲不同的時間間隔,您可以簡單地用不同的時間值再次調用SetTimer。

 

方法二

設定計時器的第一種方法是把WM_TIMER消息發送到通常的窗口過程,而第二種方法是讓Windows直接將計時器消息發送給你程序的另一個函數。

接收這些計時器消息的函數被稱爲回調函數,這是一個在你的程序之中但是由Windows調用的函數(在第四回曾提到)。 你先告訴Windows此函數的地址,然後Windows調用此函數。這看起來也很熟悉,因爲程序的窗口過程實際上也是一種回調函數。當註冊窗口類時,要 將函數的地址告訴Windows,當發送消息給程序時,Windows會調用此函數。

像窗口過程一樣,回調函數也必須定義爲CALLBACK,因爲它是由Windows從程序的程序代碼段調用的。 callback函數的參數和callback函數的返回值取決於callback函數的目的。跟計時器有關的callback函數中,輸入參數與窗口過 程的輸入參數一樣。計時器callback函數不向Windows返回值。

我們把以下的callback函數稱爲TimerProc(你能夠選擇與其它一些用語不會發生衝突的任何名稱),它只處理WM_TIMER消息:

VOID CALLBACK TimerProc (  HWND hwnd, UINT message, UINT iTimerID, DWORDdwTime)

       

{

       

           //處理WM_TIMER消息

       

}

       

TimerProc的參數hwnd是在調用SetTimer時指定的窗口句柄。Windows只把WM_TIMER消息 送給TimerProc,因此消息參數message總是等於WM_TIMER。iTimerID值是計時器ID,dwTimer值是與從 GetTickCount函數的返回值相容的值。這是自Windows啓動後所經過的毫秒數。

用第一種方法設定計時器時要求下面格式的SetTimer調用:

SetTimer (hwnd, iTimerID, iMsecInterval, NULL) ;

你使用回調函數處理WM_TIMER消息時,SetTimer的第四個參數由回調函數的地址取代,如下所示:

SetTimer (hwnd, iTimerID, iMsecInterval, TimerProc) ;

看個例子吧。

  1. #include <windows.h>  
  2.           
  3. #define ID_TIMER    1  
  4.           
  5.   
  6. LRESULT     CALLBACK       WndProc   (HWNDUINTWPARAMLPARAM) ;  
  7.           
  8. VOID    CALLBACK   TimerProc (HWNDUINTUINT,   DWORD ) ;  
  9.           
  10.   
  11. int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow)  
  12.           
  13. {  
  14.           
  15.    static char   szAppName[]           = " Timer Demo " ;  
  16.           
  17.            HWND                                 hwnd ;  
  18.           
  19.            MSG                                  msg ;  
  20.           
  21.            WNDCLASS                      wndclass ;  
  22.           
  23.      
  24.           
  25.            wndclass.style                                       = CS_HREDRAW | CS_VREDRAW ;  
  26.           
  27.            wndclass.lpfnWndProc                                 = WndProc ;  
  28.           
  29.           wndclass.cbClsExtra                                  = 0 ;  
  30.           
  31.            wndclass.cbWndExtra                                  = 0 ;  
  32.           
  33.            wndclass.hInstance                                   = hInstance ;  
  34.           
  35.            wndclass.hIcon                                       = LoadIcon (NULL, IDI_APPLICATION) ;  
  36.           
  37.            wndclass.hCursor                                     = LoadCursor (NULL, IDC_ARROW) ;  
  38.           
  39.           wndclass.hbrBackground                       = (HBRUSH) GetStockObject (WHITE_BRUSH) ;  
  40.           
  41.            wndclass.lpszMenuName                        = NULL ;  
  42.           
  43.            wndclass.lpszClassName                       = szAppName ;  
  44.           
  45.      
  46.           
  47.            if (!RegisterClass (&wndclass))  
  48.           
  49.            {  
  50.           
  51.                   MessageBox (  NULL, TEXT ("Program requires Windows NT!"),szAppName, MB_ICONERROR) ;  
  52.           
  53.                   return 0 ;  
  54.           
  55.            }  
  56.           
  57.      
  58.           
  59.            hwnd = CreateWindow ( szAppName, "Timer Demo",  
  60.           
  61.                                                          WS_OVERLAPPEDWINDOW,  
  62.           
  63.                                   CW_USEDEFAULT, CW_USEDEFAULT,  
  64.           
  65.                           CW_USEDEFAULT, CW_USEDEFAULT,  
  66.           
  67.                         NULL, NULL, hInstance, NULL) ;  
  68.           
  69.      
  70.           
  71.            ShowWindow (hwnd, iCmdShow) ;  
  72.           
  73.            UpdateWindow (hwnd) ;  
  74.           
  75.           
  76.           
  77.            while (GetMessage (&msg, NULL, 0, 0))  
  78.           
  79.            {  
  80.           
  81.                   TranslateMessage (&msg) ;  
  82.           
  83.                   DispatchMessage (&msg) ;  
  84.           
  85.            }  
  86.           
  87.            return msg.wParam ;  
  88.           
  89. }  
  90.           
  91.   
  92. LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)  
  93.           
  94. {  
  95.           
  96.    switch (message)  
  97.           
  98.     {  
  99.           
  100.            case   WM_CREATE:  
  101.           
  102.                   SetTimer (hwnd, ID_TIMER, 1000, TimerProc) ;  
  103.           
  104.                  return 0 ;  
  105.           
  106.          
  107.           
  108.            case   WM_DESTROY:  
  109.           
  110.                   KillTimer (hwnd, ID_TIMER) ;  
  111.           
  112.                   PostQuitMessage (0) ;  
  113.           
  114.                   return 0 ;  
  115.           
  116.            }  
  117.           
  118.            return DefWindowProc (hwnd, message, wParam, lParam) ;  
  119.           
  120. }  
  121.           
  122.   
  123. VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime)  
  124.           
  125. {  
  126.           
  127.            static BOOL fFlipFlop = FALSE ;  
  128.           
  129.            HBRUSH                        hBrush ;  
  130.           
  131.            HDC                                  hdc ;  
  132.           
  133.            RECT                                 rc ;  
  134.          
  135.            fFlipFlop = !fFlipFlop ;  
  136.           
  137.      
  138.           
  139.            GetClientRect (hwnd, &rc) ;  
  140.           
  141.           hdc = GetDC (hwnd) ;  
  142.           
  143.     hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ;  
  144.                               //括號裏的這種判斷語句我估計大家都懂,就不解釋了  
  145.      
  146.           
  147.     FillRect (hdc, &rc, hBrush) ;  
  148.           
  149.    ReleaseDC (hwnd, hdc) ;  
  150.           
  151.    DeleteObject (hBrush) ;  
  152.           
  153. }  

這裏計時器的時間間隔設定爲1秒。當它收到WM_TIMER消息時,它將顯示區域的顏色由藍色變爲紅色或由紅色變爲藍色。

程序在窗口過程處理WM_CREATE消息時設定計時器。在處理WM_TIMER消息處理期間,翻轉bFlipFlop的值並使窗口無效以產生 WM_PAINT消息。在處理WM_PAINT消息處理期間,通過調用GetClientRect獲得窗口大小的RECT結構,並通過調用 FillRect改變窗口的顏色。

計時器精確嗎?

很可惜它不精確,原因如下。

原因一:Windows計時器是PC硬件和ROM BIOS構造的計時器邏輯的一種相對簡單的擴展。回到Windows以前的MS-DOS編程,應用程序能夠通過捕獲稱爲timer tick的BIOS中斷來實現時鐘或計時器。這些中斷每54.915毫秒產生一 次,或者大約每秒18.2次。一些爲MS-DOS編寫的程序自己捕獲這個硬件中斷以實現時鐘和計時器。這是原始的IBM PC的微處理器頻率值4.772720 MHz被262144所除而得出的結果。Windows應用程序不攔截BIOS中斷,相反地,Windows本身處理硬件中斷,這樣應用程序就不必進行處 理。在Windows 98中,計時器與其下的PC計時器一樣具有55毫秒的分辨率,在MicrosoftWindows NT中,計時器的分辨率爲10毫秒。即Windows應用程序不能以高於這些分辨率的頻率(在Windows 98下,每秒18.2次,在Windows NT下,每秒大約100次)接收WM_TIMER消息。在SetTimer中指定的時間間隔總是截尾後tick數的整數倍。例如,1000毫秒的間隔除以 54.925毫秒,得到18.207個tick,截尾後是18個tick,它實際上是989毫秒。對每個小於55毫秒的間隔,每個tick都會產生一個 WM_TIMER消息。 

  可見,計時器並不能嚴格按照指定的時間間隔發送WM_TIMER消息,它總要相差那麼幾毫秒。 

 

  即使忽略這幾個毫秒的差別,計時器仍然不精確。請看原因二: 

   WM_TIMER消息放在正常的消息隊列之中,和其他消息排列在一起,因此,如果在SetTimer中指定間隔爲1000毫秒,那麼不能保證程序每 1000毫秒或者989毫秒就會收到一個WM_TIMER消息。如果其他程序的運行時間超過一秒,在此期間內,你的程序將收不到任何WM_TIMER消 息。事實上, Windows對WM_TIMER消息的處理非常類似於對WM_PAINT消息的處理,這兩個消息都是低優先級的,程序只有在消息隊列中沒有其他消息時才 接收它們。 

  WM_TIMER還在另一方面和WM_PAINT相似:Windows不能持續向消息隊列中放入多個 WM_TIMER消息,而是將多餘的WM_TIMER消息組合成一個消息。因此,應用程序不會一次收到多個這樣的消息,儘管可能在短時間內得到兩個 WM_TIMER消息。應用程序不能確定這種處理方式所導致的WM_TIMER消息“丟失”的數目。 

  可見,WM_TIMER消息並不能及時被應用程序所處理,WM_TIMER在消息隊列中的延誤可能就不能用毫秒來計算了。 

 

   由以上兩點,你不能通過在處理WM_TIMER時一秒一秒計數的方法來計時。如果要實現一個時鐘程序,可以使用系統的時間函數如 GetLocalTime ,而在時鐘程序中,計時器的作用是定時調用GetLocalTime獲得新的時間並刷新時鐘畫面,當然這個刷新的間隔要等於或小於1秒。 

①WM_TIMER消息。

    wParam爲計數器的ID:如果需要設定多個計時器,那麼對每個計時器都使用不同的計時器ID。wParam的值將隨傳遞到窗口過程的WM_TIMER消息的不同而不同。

    lParam爲指向TimerProc的指針,如果調用SetTimer時沒有指定TimerProc(其參數值爲NULL,即第一種用法),則lParam爲0,顯然在第二種用法中此值就不爲0了。

 注:部分內容參考《Windows計時器》一文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章