C語言Windows程序設計 -> 第十二天 -> 使用計時器
傳統意義上的計時器是指利用特定的原理來測量時間的裝置, 在古代, 常用沙漏、點燃一炷香等方式進行粗略的計時, 在現代科技的帶動下, 計時水平越來越高, 也越來越精確, 之所以需要進行計時是在很多情況下我們需要知道時間已經過去了多少, 舉例說, 上課下課的打鈴、 考試時的計時、車站按時間間隔進行發車等。 不僅在日常生活中會應用到計時, 在一些電子設備中計時的普遍存在, 如手機里的鬧鍾、電子秒表、電子設備的定時關機等, 這些計時的目的都是相同的, 當達到一定時間后執行某件事, 計時器相當於提醒作用, 當達到某個時間后提醒人們或者機器該做某件事了。
在Windows系統中, 計時器作為一種輸入設備存在於系統中, 當每到一個設定的時間間隔后它都會向應用程序發出一個 WM_TIMER 的消息, 以提醒程序規定的間隔時間已經過去了, 計時器在程序中的應用十分廣泛, 舉些我們容易想到的示例:
1>. 游戲這控制物體的移動速度, 比如說某個物體每100毫秒移動某個單位距離;
2>. 文件的自動保存, 當用戶編輯某些文件時5分鍾自動保存一次, 避免因意外情況造成編輯的成果全部丟失;
3>. 實現程序的自動退出, 當程序達到某個設定的時間后程序自動退出;
。。。
一、使用計時器
計時器的使用主要分為創建、處理、銷毀三個部分。
①. 創建: 創建一個計時器並設定其定計時器的任務周期, 例如每5秒向程序發送一條 WM_TIMER 消息 ;
②. 處理: 根據接收到的 WM_TIMER 消息讓程序作出響應的處理 ;
③. 銷毀: Windows的計時器屬於系統資源, 在使用完畢后應及時銷毀。
1>. 計時器的創建
要創建一個計時器可以使用 SetTimer 函數, SetTimer函數的原型:
UINT_PTR SetTimer( HWND hWnd, //窗口句柄 UINT_PTR nIDEvent, //定時器的ID UINT uElapse, //間隔時間, 單位為毫秒 TIMERPROC lpTimerFunc //所使用的回調函數 );
參數說明:
參數一窗口句柄即為接收 WM_TIMER 消息的窗口句柄;
參數二為設置該計時器的ID, 用於與其他的計時器進行區分;
參數三為計時器發送 WM_TIMER 消息的時間間隔, 單位為毫秒, 最大可設置的時間間隔為一個 unsigned long int 型所能容下的數據大小, 為 4 294 967 295 毫秒(約合49.7天), 當設定的時間間隔到了后Windows就會向應用程序的消息隊列放入一個 WM_TIMER 消息 ;
參數四為定時器所使用的回調函數, 當使用回調函數時, 所產生的 WM_TIMER 消息自動調用回調函數進行處理。
其函數的返回值為成功創建的定時器的ID。
你可以在任何時候創建一個新的計時器, 例如在接收到 WM_CREATE 消息時。
創建計時器的三種方式:
方式一: 不使用回調函數
SetTimer( hwnd, nIDEvent, uiMsecInterval, NULL ) ;
創建舉例:
SetTimer( hwnd, 1, 100, NULL ) ;
這樣我們就創建了一個ID為1, 消息頻率為100毫秒, 沒有使用回調函數的計時器, 每當程序運行100毫秒Windows就會向應用程序的消息隊列里放入一個 WM_TIMER 消息。
方式二: 使用回調函數
SetTimer( hwnd, nIDEvent, uiMsecInterval, TimeProc ) ;
創建舉例:
SetTimer( hwnd, 1, 100, TimeProc ) ;
TimeProc即為該定時器所指定使用的回調函數, 它可以是你喜歡的任何名字, 但是函數聲明時的類型必須為 CALLBACK 型, 表示該函數為回調函數, 需要注意的時, 當為定時器使用回調函數時, 該定時器所發出的 WM_TIMER 消息將直接發送給回調函數進行處理並從消息隊列里銷毀該消息。
方式三: 不使用窗口句柄
iTimerID = SetTimer( NULL, 0, uiMsecInterval, TimeProc ) ;
當忽略窗口句柄時, 那么第二個參數計時器ID也應被忽略, 填充0, 由系統隨機分配一個與其他定時器不重復的ID, 返回值即為分配到的ID, 如果返回值為0表示計時器創建失敗, 如果要處理該定時器發出的消息需要配合回調函數使用。
2>. 計時器消息的處理
①. 當不使用回調函數時
當不使用回調函數時程序會收到 WM_TIMER 消息, 這時只要像處理普通消息一樣處理 WM_TIMER 消息就行了, 如果有多個計時器, 可以從 wParam 參數中根據計時器的ID作不同的處理, 例如:
case WM_TIMER: switch(wParam) { case 1: [處理ID為1的計時器] break; case 2: [處理ID為2的計時器] break ; ... } return 0 ;
②. 使用回調函數的計時器
當計時器創建時指定好回調函數時, 回調函數可以像下面的寫法進行:
VOID CALLBACK TimerProc( HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime )
{
[處理 WM_TIMER 消息]
}
當不同的計時器使用同一個回調函數時, 可以根據回調函數的 iTimerID 參數來區分不同的計時器, 形如:
switch(iTimerID) { case 1: //處理ID為1的定時器 [...] break; case 2: //處理ID為2的定時器 [...] break; ... }
3>. 銷毀計時器
在開始部分也已經說了, Windows的計時器屬於系統資源, 在使用完畢后應及時銷毀。銷毀計時器的函數是 KillTimer, 他的函數原型如下:
BOOL KillTimer( HWND hWnd, //窗口句柄 UINT_PTR uIDEvent //計時器ID );
要銷毀一個計時器, 必須知道該計時器的ID, 所以保留計時器的ID也是十分重要的, 你可以在任何時候銷毀一個已經創建的計時器, 包括在處理計時器消息時。 最好在程序退出之前銷毀完所有的已創建的計時器, 一個不錯的辦法是在處理 WM_DESTROY 消息時對於那些沒有銷毀的全部進行銷毀。
需要注意的是, 當成功銷毀一個計時器后, 該計時器所產生的 WM_TIMER 消息並不會從消息隊列中移除, 如果消息隊列中還有沒有處理的 WM_TIMER 消息, 那么即使銷毀了該計時器, 應用程序還是會有可能處理到沒有處理完的 WM_TIMER 消息。
二、重置計時器
在某些情況下我們可能需要改變一件事的處理時間間隔, 如果先銷毀一個計時器再創建一個新的時間間隔的計時器未免有些麻煩, 當需要重新設定某個計時器的時間間隔時只需要再次調用 SetTimer 函數改變其中的 時間間隔 值即可, 當改變某個計時器的時間間隔時需要知道該計時器的ID, 舉例:
1 static int t = 1000 ; //初始間隔為1000毫秒, 即1秒 2 switch(message) 3 { 4 case WM_CREATE: 5 SetTimer( hwnd, 1, t, NULL ) ; //創建一個ID為1, 時間間隔為t的計時器 6 return 0 ; 7 8 case WM_TIMER: 9 t += 1000 ; //每處理一次WM_TIMER消息將時間間隔增加1秒 10 SetTimer( hwnd, 1, t, NULL ) ; //重置ID為1計時器 11 MessageBox( hwnd, TEXT("時間間隔增加一秒!"), TEXT("計時器消息"), MB_OK ) ; 12 return 0 ; 13 }
這段代碼的作用就是首先將計時器的初始間隔時間設為1秒, 然后每處理一次 WM_TIMER 消息后都將消息間隔再增加一秒。
三、使用計時器需要知道的一些問題
1>. 程序運行時會被 WM_TIMER 消息打斷嗎?
或許當我們在執行一個很重要的任務時害怕已經被突然發送來的 WM_TIMER 消息打斷而被迫去處理 WM_TIMER 消息, 實際上這種情況是不會發生的, WM_TIMER 和其他普通的消息一樣, 當計時器發出該消息時Windows會把它放在該程序的消息隊列中, 只有當 while( GetMessage(&msg, NULL, 0, 0) )從消息隊列獲取到該消息時程序才會進行處理。
2>. 使用Windows計時器進行計時是否精確?
使用Windows計時器進行計時並不精確, 主要有兩方面的原因造成的:
①. 時鍾周期的影響
簡單的說, Windows是通過獲取底層的"時鍾滴答"來進行計時的, 而 "時鍾滴答" 是有一定的周期的, 舉例來說, 假如這個滴答周期為55毫秒, 那么每滴答一次Windows就知道55毫秒過去了, 但是它沒法知道10毫秒是什么時候過去的, 如果我們告訴Windows要進行一個100毫秒的計時, 那么Windows會拿100毫秒除以滴答周期55毫秒進行4四舍五入, 得到的結果為2, 然后當兩個滴答過去后Windows才會通知你100毫秒已經過去了, 但實際上已經過去了110毫秒了。
實際上, 在Windows98上, 55毫秒就是那時的計時器周期, 在Windows NT的Windows, 計時器的周期已經縮短到10毫秒左右, 也就是說誤差已經縮小到10毫秒內。
當向計時器設置間隔10毫秒以下的任務時, Windows只能以10毫秒計。
②. 消息處理的速度影響
當 WM_TIMER 消息發送到消息隊列后, 但前面已經積攢了大量的消息, 程序需要把前面的消息處理完才能處理 WM_TIMER 消息, WM_TIMER 消息是低優先級的, 只有當消息隊列中沒有其他消息時程序才能收到並處理他們, 也就是說當計時器發出 WM_TIMER 消息一直到當你收到 WM_TIMER 消息這個過程又將消耗一定的時間, 舉個例子說, 當你計時器設定的時間頻率是 100毫秒, 當這個時間過去后Windows向消息隊列中放入一個 WM_TIMER 消息, 但是在該消息前面還有1個將會耗時1分鍾才能處理完的消息, 那么從消息發出到程序處理實際上已經過去 1分鍾再加上100毫秒了, 遠大於我們期望的時間間隔。
綜上兩個原因, 在Windows中, 用計時器進行精確計時是不准的, 因為他不夠"專一"。
3>. WM_TIMER消息在消息隊列中會大量積存么?
與 WM_PAINT 消息類似, WM_TIMER 消息在消息隊列也同樣不會大量存在, 當連續不斷的產生多個 WM_TIMER 消息時, Windows會把這些連續存在的 WM_TIMER 消息合成一條, 其他的消息將會被拋棄銷毀, 因此我們也同樣不可以根據收到多少 WM_TIMER 消息來計算已經過去了多少時間, 因為我們無法知道 Windows 為我們丟棄了多少 WM_TIMER 消息。
四、計時器使用舉例
1>. 示例一: 定時退出程序
該功能將創建一個計時器, 當程序運行10秒后自動退出程序, 窗口過程部分函數如下:
1 LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) 2 { 3 switch(message) 4 { 5 case WM_CREATE: //處理WM_CREATE消息時完成計時器的創建 6 SetTimer( hwnd, 1, 10000, NULL ) ; //設置一個ID為1, 時間間隔為10秒, 無回調函數的計時器 7 return 0 ; 8 9 case WM_TIMER: //處理WM_TIMER消息 10 KillTimer( hwnd, 1 ) ; //處理 WM_TIMER 消息時銷毀計時器 11 PostQuitMessage( 0 ) ; //在消息隊列中插入退出消息 12 return 0 ; 13 } 14 return DefWindowProc( hwnd, message, wParam, lParam ) ; 15 }
完整的示例代碼:

1 #include<windows.h> 2 3 LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ) ; 4 5 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ) 6 { 7 static TCHAR szAppName[] = TEXT("UseTimer") ; 8 HWND hwnd ; 9 MSG msg ; 10 WNDCLASS wndclass ; 11 12 wndclass.lpszClassName = szAppName ; 13 wndclass.hInstance = hInstance ; 14 wndclass.lpfnWndProc = WndProc ; 15 wndclass.style = CS_HREDRAW | CS_VREDRAW ; 16 wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH) ; 17 wndclass.hIcon = LoadIcon( NULL, IDI_APPLICATION ) ; 18 wndclass.hCursor = LoadCursor( NULL, IDC_ARROW ) ; 19 wndclass.cbClsExtra = 0 ; 20 wndclass.cbWndExtra = 0 ; 21 wndclass.lpszMenuName = NULL ; 22 23 if( !RegisterClass(&wndclass) ) 24 { 25 MessageBox( NULL, TEXT("錯誤, 無法注冊窗口類!"), szAppName, MB_OK | MB_ICONERROR ) ; 26 return 0 ; 27 } 28 29 hwnd = CreateWindow( szAppName, TEXT("UseTimer - Demo"), 30 WS_OVERLAPPEDWINDOW, 31 CW_USEDEFAULT, CW_USEDEFAULT, 32 900, 600, 33 NULL, NULL, hInstance, NULL ) ; 34 35 ShowWindow( hwnd, iCmdShow ) ; 36 UpdateWindow( hwnd ) ; 37 38 while( GetMessage( &msg, NULL, 0, 0) ) 39 { 40 TranslateMessage( &msg ) ; 41 DispatchMessage( &msg ) ; 42 } 43 return msg.wParam ; 44 } 45 46 LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) 47 { 48 switch(message) 49 { 50 case WM_CREATE: //處理WM_CREATE消息時完成計時器的創建 51 SetTimer( hwnd, 1, 10000, NULL ) ; //設置一個ID為1, 時間間隔為10秒, 無回調函數的計時器 52 return 0 ; 53 54 case WM_TIMER: //處理WM_TIMER消息 55 KillTimer( hwnd, 1 ) ; //處理 WM_TIMER 消息時銷毀計時器 56 PostQuitMessage( 0 ) ; //在消息隊列中插入退出消息 57 return 0 ; 58 } 59 return DefWindowProc( hwnd, message, wParam, lParam ) ; 60 }
可以看到, 程序在處理 WM_CREATE 消息時完成了計時器的創建, 計時器的時間間隔為10秒, 當處理到 WM_TIMER 消息時首先銷毀了定時器, 隨后在程序的消息隊列中插入了一個退出消息, 這樣就簡單的完成了程序的自動退出。
2>. 示例二: 定時彈出對話框並定時在客戶區繪制文字
該功能需要實現的是創建兩個計時器, 一個計時器負責提醒程序每間隔3秒在客戶區輸出一行文字, 另一個計時器間隔5秒, 負責彈出一個對話框, 告訴用戶某些信息, 窗口過程函數部分的代碼如下:
1 VOID CALLBACK TimerProc( HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime ) 2 { //定義計時器回調函數 3 MessageBox( hwnd, TEXT("我是負責彈出對話框的計時器! 間隔為5秒!"), TEXT("計時器消息"), MB_OK ) ; 4 } 5 6 LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) 7 { 8 HDC hdc ; 9 PAINTSTRUCT ps ; 10 static int iTimerID ; //記錄計時器ID 11 static int y = 10 ; //記錄已輸出行y坐標 12 13 switch(message) 14 { 15 case WM_CREATE: //處理WM_CREATE消息時完成計時器的創建 16 iTimerID = SetTimer( hwnd, 0, 5000, TimerProc ) ; //設置一個ID隨機分配、時間間隔為5秒, 有回調函數的計時器 17 SetTimer( hwnd, 2, 3000, NULL ) ; //設置一個ID為2, 時間間隔為3秒, 無回調函數的計時器 18 return 0 ; 19 20 case WM_TIMER: //處理WM_TIMER消息 21 switch(wParam) 22 { 23 case 2: //處理ID為2的計時器消息 24 hdc = GetDC( hwnd ) ; 25 TextOut( hdc, 10, y, TEXT("我是來自ID為2的計時器, 間隔為3秒, 我負責繪制文字。"), 26 lstrlen("我是來自ID為2的計時器, 間隔為3秒, 我負責繪制文字。") ) ; 27 y += 20 ; //向下移動20個像素, 模擬文字換行 28 ReleaseDC( hwnd, hdc ) ; 29 ValidateRect( hwnd, NULL ) ; 30 break ; 31 32 /* 33 如果創建了更多的計時器, 這里繼續case計時器的ID, 用來區分不同計時器發來的消息 34 */ 35 } 36 return 0 ; 37 38 case WM_DESTROY: 39 KillTimer( hwnd, iTimerID ) ; //銷毀ID為隨機分配的計時器 40 KillTimer( hwnd, 2 ) ; //銷毀ID為2的計時器 41 PostQuitMessage( 0 ) ; 42 return 0 ; 43 } 44 return DefWindowProc( hwnd, message, wParam, lParam ) ; 45 }
完整的示例代碼:

1 #include<windows.h> 2 3 LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ) ; 4 VOID CALLBACK TimerProc( HWND, UINT, UINT, DWORD ) ; 5 6 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ) 7 { 8 static TCHAR szAppName[] = TEXT("UseTimer") ; 9 HWND hwnd ; 10 MSG msg ; 11 WNDCLASS wndclass ; 12 13 wndclass.lpszClassName = szAppName ; 14 wndclass.hInstance = hInstance ; 15 wndclass.lpfnWndProc = WndProc ; 16 wndclass.style = CS_HREDRAW | CS_VREDRAW ; 17 wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH) ; 18 wndclass.hIcon = LoadIcon( NULL, IDI_APPLICATION ) ; 19 wndclass.hCursor = LoadCursor( NULL, IDC_ARROW ) ; 20 wndclass.cbClsExtra = 0 ; 21 wndclass.cbWndExtra = 0 ; 22 wndclass.lpszMenuName = NULL ; 23 24 if( !RegisterClass(&wndclass) ) 25 { 26 MessageBox( NULL, TEXT("錯誤, 無法注冊窗口類!"), szAppName, MB_OK | MB_ICONERROR ) ; 27 return 0 ; 28 } 29 30 hwnd = CreateWindow( szAppName, TEXT("UseTimer - Demo"), 31 WS_OVERLAPPEDWINDOW, 32 CW_USEDEFAULT, CW_USEDEFAULT, 33 900, 600, 34 NULL, NULL, hInstance, NULL ) ; 35 36 ShowWindow( hwnd, iCmdShow ) ; 37 UpdateWindow( hwnd ) ; 38 39 while( GetMessage( &msg, NULL, 0, 0) ) 40 { 41 TranslateMessage( &msg ) ; 42 DispatchMessage( &msg ) ; 43 } 44 return msg.wParam ; 45 } 46 47 48 LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) 49 { 50 HDC hdc ; 51 PAINTSTRUCT ps ; 52 static int iTimerID ; //記錄計時器ID 53 static int y = 10 ; //記錄已輸出行y坐標 54 55 switch(message) 56 { 57 case WM_CREATE: //處理WM_CREATE消息時完成計時器的創建 58 iTimerID = SetTimer( hwnd, 0, 5000, TimerProc ) ; //設置一個ID隨機分配、時間間隔為5秒, 有回調函數的計時器 59 SetTimer( hwnd, 2, 3000, NULL ) ; //設置一個ID為2, 時間間隔為3秒, 無回調函數的計時器 60 return 0 ; 61 62 case WM_TIMER: //處理WM_TIMER消息 63 switch(wParam) 64 { 65 case 2: 66 hdc = GetDC( hwnd ) ; 67 TextOut( hdc, 10, y, TEXT("我是來自ID為2的計時器, 間隔為3秒, 我負責繪制文字。"), 68 lstrlen("我是來自ID為2的計時器, 間隔為3秒, 我負責繪制文字。") ) ; 69 y += 20 ; //向下移動20個像素, 模擬文字換行 70 ReleaseDC( hwnd, hdc ) ; 71 ValidateRect( hwnd, NULL ) ; 72 break ; 73 74 /* 75 如果創建了更多的計時器, 這里繼續case計時器的ID, 用來區分不同計時器發來的消息 76 */ 77 } 78 return 0 ; 79 80 case WM_DESTROY: 81 KillTimer( hwnd, iTimerID ) ; //銷毀ID為隨機分配的計時器 82 KillTimer( hwnd, 2 ) ; //銷毀ID為2的計時器 83 PostQuitMessage( 0 ) ; 84 return 0 ; 85 } 86 return DefWindowProc( hwnd, message, wParam, lParam ) ; 87 } 88 89 VOID CALLBACK TimerProc( HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime ) 90 { //定義計時器回調函數 91 MessageBox( hwnd, TEXT("我是負責彈出對話框的計時器! 間隔為5秒!"), TEXT("計時器消息"), MB_OK ) ; 92 }
效果:
同樣, 程序在處理 WM_CREATE 消息時創建了兩個計時器, 一個是使用不指定計時器ID方式創建的, 並且使用了計時器回調函數, 該計時器用來負責彈出對話框;
另一個是使用指定計時器ID並且不使用計時器回調函數的方法創建的, 此方式將會把 WM_TIMER 消息發送到 hwnd 窗口, 當case到該消息時, 又使用了switch語句 進行了判斷, 目的是根據不同ID的計時器作出不同的動作, 這里實際上是沒有必要使用switch語句, 因為第一個計時器已經使用了計時器回調函數, 但如果創建的計時器有很多, 使用switch進行判斷就很有必要了。
--------------------
wid, 2012.12.05
上一篇: C語言Windows程序設計 -> 第十一天 -> 使用鼠標