传统意义上的计时器是指利用特定的原理来测量时间的装置, 在古代, 常用沙漏、点燃一炷香等方式进行粗略的计时, 在现代科技的带动下, 计时水平越来越高, 也越来越精确, 之所以需要进行计时是在很多情况下我们需要知道时间已经过去了多少, 举例说, 上课下课的打铃、 考试时的计时、车站按时间间隔进行发车等。 不仅在日常生活中会应用到计时, 在一些电子设备中计时的普遍存在, 如手机里的闹钟、电子秒表、电子设备的定时关机等, 这些计时的目的都是相同的, 当达到一定时间后执行某件事, 计时器相当于提醒作用, 当达到某个时间后提醒人们或者机器该做某件事了。
在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 }
完整的示例代码:
VOID CALLBACK TimerProc( HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime ) { //定义计时器回调函数 MessageBox( hwnd, TEXT("我是负责弹出对话框的计时器! 间隔为5秒!"), TEXT("计时器消息"), MB_OK ) ; } LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) { HDC hdc ; PAINTSTRUCT ps ; static int iTimerID ; //记录计时器ID static int y = 10 ; //记录已输出行y坐标 switch(message) { case WM_CREATE: //处理WM_CREATE消息时完成计时器的创建 iTimerID = SetTimer( hwnd, 0, 5000, TimerProc ) ; //设置一个ID随机分配、时间间隔为5秒, 有回调函数的计时器 SetTimer( hwnd, 2, 3000, NULL ) ; //设置一个ID为2, 时间间隔为3秒, 无回调函数的计时器 return 0 ; case WM_TIMER: //处理WM_TIMER消息 switch(wParam) { case 2: //处理ID为2的计时器消息 hdc = GetDC( hwnd ) ; TextOut( hdc, 10, y, TEXT("我是来自ID为2的计时器, 间隔为3秒, 我负责绘制文字。"), lstrlen("我是来自ID为2的计时器, 间隔为3秒, 我负责绘制文字。") ) ; y += 20 ; //向下移动20个像素, 模拟文字换行 ReleaseDC( hwnd, hdc ) ; ValidateRect( hwnd, NULL ) ; break ; /* 如果创建了更多的计时器, 这里继续case计时器的ID, 用来区分不同计时器发来的消息 */ } return 0 ; case WM_DESTROY: KillTimer( hwnd, iTimerID ) ; //销毁ID为随机分配的计时器 KillTimer( hwnd, 2 ) ; //销毁ID为2的计时器 PostQuitMessage( 0 ) ; return 0 ; } return DefWindowProc( hwnd, message, wParam, lParam ) ; }
可以看到, 程序在处理 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 }
完整的示例代码:
#include<windows.h> LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ) ; VOID CALLBACK TimerProc( HWND, UINT, UINT, DWORD ) ; int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ) { static TCHAR szAppName[] = TEXT("UseTimer") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.lpszClassName = szAppName ; wndclass.hInstance = hInstance ; wndclass.lpfnWndProc = WndProc ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH) ; wndclass.hIcon = LoadIcon( NULL, IDI_APPLICATION ) ; wndclass.hCursor = LoadCursor( NULL, IDC_ARROW ) ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.lpszMenuName = NULL ; if( !RegisterClass(&wndclass) ) { MessageBox( NULL, TEXT("错误, 无法注册窗口类!"), szAppName, MB_OK | MB_ICONERROR ) ; return 0 ; } hwnd = CreateWindow( szAppName, TEXT("UseTimer - Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 900, 600, NULL, NULL, hInstance, NULL ) ; ShowWindow( hwnd, iCmdShow ) ; UpdateWindow( hwnd ) ; while( GetMessage( &msg, NULL, 0, 0) ) { TranslateMessage( &msg ) ; DispatchMessage( &msg ) ; } return msg.wParam ; } LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) { HDC hdc ; PAINTSTRUCT ps ; static int iTimerID ; //记录计时器ID static int y = 10 ; //记录已输出行y坐标 switch(message) { case WM_CREATE: //处理WM_CREATE消息时完成计时器的创建 iTimerID = SetTimer( hwnd, 0, 5000, TimerProc ) ; //设置一个ID随机分配、时间间隔为5秒, 有回调函数的计时器 SetTimer( hwnd, 2, 3000, NULL ) ; //设置一个ID为2, 时间间隔为3秒, 无回调函数的计时器 return 0 ; case WM_TIMER: //处理WM_TIMER消息 switch(wParam) { case 2: hdc = GetDC( hwnd ) ; TextOut( hdc, 10, y, TEXT("我是来自ID为2的计时器, 间隔为3秒, 我负责绘制文字。"), lstrlen("我是来自ID为2的计时器, 间隔为3秒, 我负责绘制文字。") ) ; y += 20 ; //向下移动20个像素, 模拟文字换行 ReleaseDC( hwnd, hdc ) ; ValidateRect( hwnd, NULL ) ; break ; /* 如果创建了更多的计时器, 这里继续case计时器的ID, 用来区分不同计时器发来的消息 */ } return 0 ; case WM_DESTROY: KillTimer( hwnd, iTimerID ) ; //销毁ID为随机分配的计时器 KillTimer( hwnd, 2 ) ; //销毁ID为2的计时器 PostQuitMessage( 0 ) ; return 0 ; } return DefWindowProc( hwnd, message, wParam, lParam ) ; } VOID CALLBACK TimerProc( HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime ) { //定义计时器回调函数 MessageBox( hwnd, TEXT("我是负责弹出对话框的计时器! 间隔为5秒!"), TEXT("计时器消息"), MB_OK ) ; }
效果:
同样, 程序在处理 WM_CREATE 消息时创建了两个计时器, 一个是使用不指定计时器ID方式创建的, 并且使用了计时器回调函数, 该计时器用来负责弹出对话框;
另一个是使用指定计时器ID并且不使用计时器回调函数的方法创建的, 此方式将会把 WM_TIMER 消息发送到 hwnd 窗口, 当case到该消息时, 又使用了switch语句 进行了判断, 目的是根据不同ID的计时器作出不同的动作, 这里实际上是没有必要使用switch语句, 因为第一个计时器已经使用了计时器回调函数, 但如果创建的计时器有很多, 使用switch进行判断就很有必要了。