Win32匯編--定時器
1、定時器簡介
在應用程序需要使用定時器時,可以用SetTimer函數向Windows申請一個定時器,要求系統在指定的時間以后“通知”應用程序,如果申請成功的話,系統會以指定的時間周期調用SetTimer函數指定的回調函數,或者向指定的窗口過程發送WM_TIMER消息,和DOS操作系統固定以55ms的間隔觸發中斷服務程序相比,SetTimer函數可以指定的時間間隔更為靈活——以ms為單位,可以指定的時間周期為一個32位的整數,也就是從1~4294 967 295ms,這可是一個將近50天的范圍!
但是在具體的使用中不要被這個參數所迷惑:由于Windows的定時器同樣是基于時鐘中斷的,所以雖然參數的單位是ms,但精度還是55ms,如果指定一個小于55ms的周期,不管是1ms還是54ms,Windows最快也只能在每個時鐘中斷的時候觸發這個定時器,也就是說,實際上這個定時器是以55ms為觸發周期的;另外,當指定一個時間間隔的時候,Windows以和這個間隔最接近的55ms的整數位時間來觸發定時器,假定建立一個周期為1000ms的定時器,定時器的觸發周期實際上不是1s而是989ms(55ms*18)。
使用定時器時還有一個要點就是定時器消息是一個低級別的消息,這表現在兩個方面:首先就是Windows只有在消息隊列中沒有其他消息的情況下才會發送WM_TIMER消息,如果窗口過程忙于處理某個消息沒有返回,使消息隊列中有消息積累起來,那么WM_TIMER消息就會被丟棄,在消息隊列再度空閑的時候,被丟棄的WM_TIMER消息不會被補發;其次,消息隊列中不會有多條WM_TIMER消息,如果消息隊列中已經有一條WM_TIMER消息,還沒來得及處理,又到了定時的時刻,那么兩條WM_TIMER消息會被合并成一條。
所以,應用程序不能依靠定時器來保證某件事情必須在規定的時刻被處理,另外,也不能依賴對定時器消息計數來確定已經過去了多少時間。
2、定時器的使用
//Timer.rc
#include <resource.h>
#define DLG_MAIN 1
#define ICO_1 1
#define ICO_2 2
#define IDC_SETICON 100
#define IDC_COUNT 101
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_1 ICON "1.ico"
ICO_2 ICON "2.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 50, 50, 113, 40
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "定時器例子"
FONT 9 "宋體"
{
ICON ICO_1, IDC_SETICON, 8, 9, 18, 21
LTEXT "計數:", -1, 35, 16, 25, 10
LTEXT "", IDC_COUNT, 62, 16, 40, 10
}
對資源的定義讀者現在一定不會陌生了,這個文件中定義了兩個圖標和一個對話框,對話框中定義了一個圖標框和兩個文本框,其中的一個文本框中的文字為空,這是以后顯示每秒一次的計數值用的。
//Timer.asm
.386
.model flat, stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定義
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ID_TIMER1 equ 1
ID_TIMER2 equ 2
ICO_1 equ 1
ICO_2 equ 2
DLG_MAIN equ 1
IDC_SETICON equ 100
IDC_COUNT equ 101
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 數據段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
hInstance dd ?
hWinMain dd ?
dwCount dd ?
idTimer dd ?
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代碼段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 定時器過程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcTimer proc _hWnd, uMsg, _idEvent, _dwTime
pushad
invoke GetDlgItemInt, hWinMain, IDC_COUNT, NULL, FALSE
inc eax
invoke SetDlgItemInt, hWinMain, IDC_COUNT, eax, FALSE
popad
ret
_ProcTimer endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 窗口過程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcDlgMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
mov eax, uMsg
;**********************************************************************************
.if eax == WM_TIMER
mov eax, wParam
.if eax == ID_TIMER1
inc dwCount
mov eax, dwCount
and eax, 1
inc eax
invoke LoadIcon, hInstance, eax
invoke SendDlgItemMessage, hWnd, IDC_SETICON, STM_SETIMAGE, IMAGE_ICON, eax
.elseif eax == ID_TIMER2
invoke MessageBeep, -1
.endif
;**********************************************************************************
.elseif eax == WM_INITDIALOG
push hWnd
pop hWinMain
invoke SetTimer, hWnd, ID_TIMER1, 250, NULL
invoke SetTimer, hWnd, ID_TIMER2, 2000, NULL
invoke SetTimer, NULL, NULL, 1000, addr _ProcTimer
mov idTimer, eax
;**********************************************************************************
.elseif eax == WM_CLOSE
invoke KillTimer, hWnd, ID_TIMER1
invoke KillTimer, hWnd, ID_TIMER2
invoke KillTimer, NULL, idTimer
invoke EndDialog, hWnd, NULL
;**********************************************************************************
.else
mov eax, FALSE
ret
.endif
mov eax, TRUE
ret
_ProcDlgMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL
invoke ExitProcess, NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
這個程序的基本結構非常簡單,就是一個標準的對話框程序而已,在WM_INITDIALOG中用SetTimer申請了3個定時器,并在WM_CLOSE消息中用KillTimer撤銷這3個定時器。
申請一個定時器使用SetTimer函數,函數的使用方法如下:
invoke SetTimer, hWnd, nIDEvent, uElapse, lpTimerFunc
hWnd參數是WM_TIMER消息發往的窗口句柄;nIDEvent參數是一個用戶指定的任意整數,用來標識一個程序中的多個定時器;uElapse是時間周期,以ms為單位,這個參數是必須指定的;lpTimerFunc是定時器過程,在下面的內容中有詳細介紹。如果定時器建立成功的話,函數的返回值是定時器的標識符。
撤銷定時器的函數是KillTimer,該函數的使用方法是:
invoke KillTimer, hWnd, uIDEvent
參數hWnd和uIDEvent就是建立定時器時使用的數值。
使用SetTimer函數的方法有兩種,第一種方法是要求Windows將WM_TIMER消息發往指定的窗口過程,這時候lpTimerFunc必須為NULL,如例子中的:
invoke SetTimer, hWnd, ID_TIMER1, 250, NULL
invoke SetTimer, hWnd, ID_TIMER2, 2000, NULL
這兩個句子設置了兩個標識分別為ID_TIMER1和ID_TIMER2的定時器,定時周期分別為250ms和2s。在窗口過程收到WM_TIMER消息的時候,wParam中是用SetTimer建立定時器時使用的標識uIDEvent,所以程序可以建立一個分支,通過判斷wParam來處理不同的定時器引起的WM_TIMER的消息。在例子中,當wParam是ID_TIMER1的時候更換圖標框中的圖標,是ID_TIMER2的時候用MessageBeep函數來發出一聲“嘟”的聲音。如果要撤銷用這種方法建立的定時器,那么只需要用建立時的hWnd和uIDEvent參數簡單地調用KillTimer就可以了。
還有一種使用定時器的方法,那就是要求Windows在時間到的時候調用指定的定時器過程,而不是某個窗口過程,那么只需要指定lpTimerFunc參數,如例子中的:
invoke SetTimer, NULL, NULL, 1000, addr _ProcTimer
這句語句要求系統把定時器消息發送到_ProcTimer定時器過程中去,但是,這時候沒有參數用來指定定時器標識,到最后如何用KillTimer撤銷這個定時器呢?答案是SetTimer函數會返回一個標識,程序可以保存這個標識并在KillTimer函數中使用。
當然,這種用法中的定時器標識也可以自己指定,但這時候一定要同時指定hWnd,雖然這個hWnd沒有實際的用途,如果hWnd為NULL,那么即使指定了定時器標識,這個標識也會被忽略,如:
invoke SetTimer, hWnd, ID_TIMER3, 1000, addr _ProcTimer
這個語句定義了一個標識為ID_TIMER3、消息發往_ProcTimer子程序的定時器。
定時器過程是如下定義的:
TimerProc proc hWnd, uMsg, idEvent, dwTime
Windows回調定時器過程的時候會有4個參數,uMsg總是WM_TIMER,hWnd和idEvent是窗口句柄和定時器標識。由于有idEvent參數,所以我們同樣可以把多個定時器消息指向同一個定時器過程中,并且根據idEvent參數構建一個分支來處理不同定時器引發的消息。
程序中還可能遇到一種情況:當在SetTimer中指定的定時器標識已經存在會怎樣呢?答案是Windows會用新的參數代替老的定時器參數,函數執行以后,這個標識的定時器消息將以新的時間周期發送。
注意:例子程序的窗口過程中把WM_TIMER的消息的處理代碼放在第一個分支上,這是對程序的簡單優化,把頻繁發生的消息放到前面可以使程序少執行一系列的比較指令,像WM_CREATE和WM_DESTROY等僅發生一次的消息可以放到分支的最后面。
3、取Windows時間
“定時器”這個詞很容易讓人聯想到時鐘,但是在前面介紹過,定時器是不能用來構造時鐘的,定時器用于時鐘程序中只能是用在定時刷新屏幕這個功能上,要得到系統的時間還是要靠別的方法。
在Win32編程中,和獲得系統時間相關的函數有3個:
invoke GetLocalTime, lpSystemTime
invoke GetSystemTime, lpSystemTime
invoke GetTickCount
它們之間的區別是:
GetTickCout返回的是本次Windows啟動以來的ms數,得到的時間數值直接在eax中返回,由于這是一個32位的整數,可以表示的范圍是1~ffffffffms,所以當Windows連續運行49.7天以后,計數器會清零并重新開始。
GetLocalTime返回當前的時間,GetSystemTime返回當前的格林威治標準時間,這兩個函數返回的時間數據包括年、月、日、時、分、秒、毫秒以及星期,數據比較多,所以無法放在eax中返回,應用程序需要預先設置一個SYSTEMTIME結構的緩沖區,并將緩沖區地址lpSystemTime當參數傳遞給函數,函數會把時間數據返回到這個緩沖區中。
SYSTEMTIME結構的定義如下:
SYSTEMTIME STRUCT
wYear WORD ? ;年
wMonth WORD ? ;月
wDayOfWeek WORD ? ;星期,0=星期日,1=星期一,……
wDay WORD ? ;日
wHour WORD ? ;時
wMinute WORD ? ;分
wSecond WORD ? ;秒
wMilliseconds WORD ? ;毫秒
SYSTEMTIME ENDS
需要注意的是,結構中的字段全部是word類型的,而Win32程序中用的往往是dword型變量,所以在使用這些數據之前往往要先把它們轉換為dword類型,用movzx指令就可以很方便地完成這個工作,如movzx eax, stSystemTime.wYear將wYear字段擴展到32位后放在eax中。
和獲得系統時間的函數相對應,可以用下面的兩個函數設置系統時間:
invoke SetLocalTime, lpSystemTime
invoke SetSystemTime, lpSystemTime
同樣,SetLocalTime中的參數代表本地時間,SetSysTime中的參數代表格林威治標準時間,在調用函數之前,要把需要設置的時間放到一個SYSTEMTIME結構中并把結構地址當做參數傳遞給Windows。