目錄
第1章定時器
1.1 創建定時器
請使用API函數 SetTimer 來創建定時器,其原型如下:
UINT SetTimer(HWND hWnd,UINT nIDEvent,UINT uElapse,TIMERPROC lpTimerFunc);
有這么兩種用法
1、SetTimer(hWnd,nID,uElapse,NULL);定時給窗口 hWnd 寄送(PostMessage) WM_TIMER 消息;
2、SetTimer(hWnd,nID,uElapse,TimerProc); 不論 hWnd 是否為 NULL,定時調用 TimerProc 函數。
1.2 銷毀定時器
銷毀定時器請使用KillTimer函數,其原型如下:
BOOL KillTimer(HWND hWnd,UINT uIDEvent);
第1個參數應與SetTimer的第1個參數保持一致;
第2個參數:如果SetTimer的第1個參數是一個有效的窗口句柄,則此參數應與SetTimer的第2個參數保持一致。否則此參數應為SetTimer的返回值。
1.3 定時器的運作
不論 SetTimer(hWnd,nID,uElapse,NULL) 還是 SetTimer(NULL,nID,uElapse,TimerProc),其實質都是處理WM_TIMER消息。
1.3.1 產生WM_TIMER消息
WM_TIMER消息並不是 Windows 系統定時、自動增加到消息隊列的,而是調用GetMessage或PeekMessage的時候,才會產生WM_TIMER消息。請參考如下代碼:
void CALLBACK TimerProc(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime) { }
UINT TestTimer() { MSG msg; UINT nTimer = SetTimer(NULL,100,1000,TimerProc);
Sleep(3050); TRACE(_T("Tick=%d\n"),GetTickCount()); PeekMessage(&msg,NULL,0,0,PM_NOREMOVE); Sleep(1050); TRACE(_T("Tick=%d\n"),GetTickCount()); PeekMessage(&msg,NULL,0,0,PM_NOREMOVE);
while(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { if(msg.message == WM_TIMER) { TRACE(_T("Timer=%d\n"),msg.time); } } KillTimer(NULL,nTimer); return 0; } |
在Windows XP下,運行結果為:
Tick=7356593 Tick=7357656 Timer=7356593 Timer=7357656 |
雖然兩次Sleep的時間合計有4秒多,但消息隊列中WM_TIMER的個數並不是4個而是2個。而且這兩個WM_TIMER的時刻與兩次GetTickCount的時刻完全相等。合理的解釋是:在調用PeekMessage(&msg,NULL,0,0,PM_NOREMOVE);時,WM_TIMER消息才被創建並增加到消息隊列。如果調用GetMessage或PeekMessage(&msg,NULL,0,0,PM_REMOVE);則創建的WM_TIMER消息不會被增加到消息隊列。
1.3.2 分發WM_TIMER消息
通過GetMessage或PeekMessage獲得消息之后,一般會調用TranslateMessage和DispatchMessage 進行消息處理。
TranslateMessage對 WM_TIMER 消息不做任何處理。
DispatchMessage(&msg) 負責分發 WM_TIMER 消息,其處理邏輯如下:
if(msg.lParam) {//如果SetTimer的第4個參數不為NULL,則調用這個回調函數 TIMERPROC pfn = (TIMERPROC)msg.lParam; pfn(msg.hwnd,WM_TIMER,msg.wParam,msg.time); } else {//交給窗口過程去處理 WNDPROC pfn = (WNDPROC)GetWindowLong(msg.hwnd,GWL_WNDPROC); CallWindowProc(pfn,msg.hwnd,WM_TIMER,msg.wParam,msg.lParam); } |
也就是說:如果SetTimer的第4個參數不為 NULL,則第1個參數所指定的 hwnd 將無法接收、處理 WM_TIMER 消息。
1.4 WM_TIMER 消息的重入
所謂重入就是當前的消息還沒有處理完畢就進入下一個消息的處理。因為WM_TIMER消息是入隊消息,所以一般情況下,對WM_TIMER的處理是不會重入的。但也有特殊情況,請參考如下代碼:
//定義定時器處理函數 void CALLBACK TimerProc(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime) { TRACE(_T("進入 OnTimer=%lu\n"),dwTime); Sleep(3500); MSG msg; while(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } Sleep(1000); TRACE(_T("離開 OnTimer=%lu\n"),dwTime); } //設置定時器 SetTimer(100,1000,TimerProc); |
本來TimerProc一秒被調用一次,現在情況發生了變化:在TimerProc內部,Sleep(3500)后再調用PeekMessage會立即產生WM_TIMER消息。DispatchMessage會再次調用TimerProc函數處理這個消息。結果就是TimerProc函數無限制的遞歸調用自己,永遠不會返回,最終會因為棧空間溢出而導致程序異常退出。
為了防止TimerProc函數的重入並可能引起的程序崩潰,就需要阻止重入TimerProc函數。可行的方法之一如下:
void CALLBACK TimerProc(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime) { static bool bWorking = false; //是否正在處理 WM_TIMER 消息 if(bWorking) {//如果正在處理 WM_TIMER 消息則返回,這樣就防止了重入 return; } bWorking = true; //標記正在處理 WM_TIMER 消息 ... ... ... //處理 WM_TIMER 消息 bWorking = false; //標記 WM_TIMER 消息已經處理完畢 } |