8.2 使用计时器的三种方法

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P265

        如果程序在整个运行过程中需要一个计时器,在 WinMain 函数中或处理 WM_CREATE 消息时,调用 SetTimer 函数:在离开 WinMain 函数时或是处理 WM_DESTROY 消息时,调用 KillTimer 函数。基于调用 SetTimer 参数的不同,可才采取以下三种方法之一使用计时器。

8.2.1  方法一

        这是最简单的方法,它导致 Windows 将 WM_TIMER 消息发送到应用程序的窗口过程。相应的 SetTimer 的调用方法如下:

setTimer (hwnd, 1, uiMsecInterval, NULL);
第一个参数是窗口句柄,它指向接收 WM_TIMER 消息的窗口过程。第二个参数是一个计时器 ID,它不能为 0。在这个例子中,我随意地将它设置为 1。第三个参数是一个 32 位的无符号整数,它指定以毫秒为单位的时间间隔。当这个参数等于 60 000 时,它表示每分钟发出一个 WM_TIMER 消息。

        你可以在任何时刻(甚至当处理 WM_TIMER 消息时)停止 WM_TIMER 消息,方法如下:

killTimer (hwnd, 1);
第二个参数是相应 SetTimer 使用的计时器 ID。我们建议在程序终止之前,也就是在处理 WM_DESTROY 消息时,最好终止所有的工作计时器。

        当窗口过程收到 WM_TIMER 消息时,wParam 等于计时器的 ID(在以上的例子里是 1),lParam 是 0。如果你需要设置多个计时器,那么每个计时器应使用不同的 ID。wParam 的值将帮助区分送到窗口过程的 WM_TIMER 消息。为了使程序易于理解,可以使用 #define 指令定义不同的计时器 ID。

#define TIMER_SEC 1
#define TIMER_MIN 2
然后通过两个 SetTimer 函数调用分别设置两个计时器:
SetTimer (hwnd, TIMER_SEC, 1000, NULL);
SetTimer (hwnd, TIMER_MIN, 6000, NULL);
WM_TIMER 的处理逻辑如下:
case WM_TIMER:
    switch (wParam)
    {
    case TIMER_SEC:
        [每秒钟一次的处理]
        break;
    case TIMER_MIN:
        [每分钟一次的处理]
        break;
    }
    return 0;

        如果需要改变一个已有的计时器的时间间隔,可使用不同的时间间隔值再次调用 SetTimer 函数。如果一个时钟程序需要实现显示秒数或隐藏秒数的功能,就可以使用该方法。可以简单地把计时器的时间间隔设置在 1000 毫秒和 60000 毫秒之间。

        图 8-1 展示了运用计时器的简单程序。这个名为 BEEPER1 的程序将计时器的间隔设置为一秒钟。每收到 WM_TIMER 消息时,它会将客户区的颜色从蓝变到红或从红变为蓝,同时调用 MessageBeep 函数产生一个响铃声。(虽然 MessageBeep 通常与 MessageBox 一起使用,但它实际上是一个通用的响铃函数。在装有声卡的 PC 中,可使用各种 MB_ICON 参数来调用 MessageBeep 产生响铃声。当用户在控制面板上做不同选择时,它会发出不同的声音。这些 MB_ICON 参数常常作为 MessageBox 的参数使用。)

        BEEPER1 在处理 WM_CREATE 消息时设置计时器。在处理 WM_TIMER 消息时,BEEPER1 调用 MessageBeep,反转 bFlipFlop 的值,并使窗口无效而产生 WM_PAINT 消息。在处理 WM_PAINT 消息期间,BEEPER1 调用 GetClientRect 函数得到 RECT 结构以确定窗口的尺寸,并调用 FillRect 为窗口着色。

/*--------------------------------------------------------
   BEEPER1.C --  Timer Demo Program No. 1
                 (c) Charles Petzold, 1998
  --------------------------------------------------------*/
#include 

#define ID_TIMER 1

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("Beeper1") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }

     hwnd = CreateWindow (szAppName, TEXT ("Beeper1 Timer Demo"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          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)
{
     static BOOL  fFlipFlop = FALSE;
     HBRUSH       hBrush;
     HDC          hdc;
     PAINTSTRUCT  ps;
     RECT         rc;

     switch (message)
     {
     case WM_CREATE:
            SetTimer(hwnd, ID_TIMER, 1000, NULL);
            return 0;

     case WM_TIMER:
        MessageBeep(-1);
        fFlipFlop = !fFlipFlop;
        InvalidateRect(hwnd, NULL, FALSE);
        return 0;

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;

          GetClientRect(hwnd, &rc);
          hBrush = CreateSolidBrush(fFlipFlop ? RGB(255, 0, 0) : RGB(0, 0, 255));
          FillRect(hdc, &rc, hBrush);

          EndPaint (hwnd, &ps) ;
          DeleteObject(hBrush);
          return 0 ;

     case WM_DESTROY:
          KillTimer(hwnd, ID_TIMER);
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

        BEEPER1 用发声来表明收到的每一个 WM_TIMER 消息,因此你可以先运行 BEEPER1 程序,同时执行一下 Windows 操作系统内的其他一些功能,以此进一步理解 WM_TIMER 的古怪行为。

        下面做一个很有启发性的实验(在 Win8.0下实验并不可以,在计算机属性-->高级系统设置里面配置相应参数):首先从控制面板上启动【显示】程序,选择【效果】标签,确认 BEEPER1 程序【拖动时显示窗口内容】没有被选中。现在试着移动 BEEPER1 的窗口或改变窗口大小,它会使程序进入“model message loop”(模态消息循环)状态。通过在 Windows 消息循环中,而不是在程序的消息循环中捕获到所有的消息,Windows 有效地阻止了对移动窗口或改变窗口尺寸的操作的任何干扰。从 Windows 消息循环来的并送往应用程序窗口的大多数消息都会被清除掉,这就是 BEEPER1 停止发出 嘟嘟声的原因。完成移动窗口或改变尺寸后,你会发现 BEEPER1 并未得到先前丢失的所有 WM_TIMER 消息,虽然最初的两个消息会相差不到一秒钟。

        当【拖动时显示窗口内容】被选中,Windows 的模态消息循环会试图把一些本会错过的消息传送给你的窗口过程,这种方式有时工作很正常,但有时不正常。

8.2.2  方法二

        第一种设定计时器的方法把 WM_TIMER 消息送给正常的窗口过程。第二种方法则让你指挥 Windows 把计时器消息发送到程序中的另一个函数。

        收到计时器消息的函数被称为“回调”函数。这是程序中被 Windows 调用的函数。你告诉 Windows 这个函数的地址,Windows 以后就会调用这个函数。这听上去很熟悉,其实一个程序的才窗口过程就属于回调函数的一种。注册窗口类时告诉 Windows 你的窗口过程的地址后,只要 Windows 向你的程序发送消息,就会自动调用此函数。

        SetTimer 不是唯一使用回调功能的 Windows 函数。CreateDialog 和 DialogBox 函数(将在第 11 章讨论)都使用回调函数处理对话框的消息;有几个 Windows 函数(EnumChildWindow、EnumFonts、EnumObjects、EnumProps 和 EnumWindow)会传递枚举信息到回调函数;还有几个不常用的函数 GrayString、LineDDA 和 SetWindowHookEx)也要求使用回调函数功能。

        类似于窗口过程,回调函数必须定义为 CALLBACK 类型,因为 Windows 是从程序的代码空间以外调用这个函数的。送到回调函数的参数和从回调函数返回的数据是由该函数要实现的功能所决定的。当回调函数与计时器同时使用时,它的参数实际上与窗口过程的参数是一样的,只不过它们的定义不同。但是,计时器的回调函数并不返回数值给 Windows。

        我们给回调函数起名为 TimerProc。(可选择任何名字,只要不与其他的函数同名 。)这个函数只处理 WM_TIMER 消息:

VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime)
{
	[处理 WM_TIMER 消息]
}
TimerProc 函数的 hwnd 参数是你调用 SetTimer 时指定的窗口句柄。Windows 只发送 WM_TIMER 消息到 TimerProc,所以消息参数总是 WM_TIMER。iTimerID 值是计时器的 ID。 dwTime 是从 GetTickCount 函数返回的值,它记录了自从 Windows 启动到现在所逝去的毫秒数

        正如我们在 BEEPER1 看到的,第一种设置计时器的方法要求以如下方式调用 SetTimer 函数:

SetTimer (hwnd, iTimerID, iMsecInterval, NULL);
而使用回调函数处理 WM_TIMER 消息时,SetTimer 函数的第四个参数必须设定为回调函数的地址:

SetTimer (hwnd, iTimerID, iMsecInterval, TimerProc);

        看一下示例代码,便可以理解这些步骤是怎样结合在一起的。BEEPER2 程序在功能上与 BEEPER1 相同,唯一的区别是 Windows 发送计时器消息到 TimerProc 而不是到 WndProc。注意,在程序的开始处,声明 WndProc 的同时也声明了 TimerProc 函数。

/*--------------------------------------------------------
   BEEPER2.C --  Timer Demo Program No. 2
                 (c) Charles Petzold, 1998
  --------------------------------------------------------*/
#include 

#define ID_TIMER 1

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 ("Beeper2") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }

     hwnd = CreateWindow (szAppName, TEXT ("Beeper2 Timer Demo"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          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)
{
     static BOOL  fFlipFlop = FALSE;
     HBRUSH       hBrush;
     HDC          hdc;
     PAINTSTRUCT  ps;
     RECT         rc;

     switch (message)
     {
     case WM_CREATE:
            SetTimer(hwnd, ID_TIMER, 1000, TimerProc);
            return 0;

     case WM_DESTROY:
          KillTimer(hwnd, ID_TIMER);
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime)
{
     static BOOL  fFlipFlop = FALSE;
     HBRUSH       hBrush;
     HDC          hdc;
     RECT         rc;

     MessageBeep(-1);
     fFlipFlop = !fFlipFlop;

     GetClientRect(hwnd, &rc);

     hdc    = GetDC(hwnd);
     hBrush = CreateSolidBrush(fFlipFlop ? RGB(255, 0, 0) : RGB(0, 0, 255));

     FillRect(hdc, &rc, hBrush);
     ReleaseDC(hwnd, hdc);
     DeleteObject(hBrush);
}

8.2.3  方法三

        第三种设置计时器的方法与第二种方法相似,只不过 SetTimer 的 hwnd 参数被设置为 NULL,而且第二个参数(正常情况下是计时器的 ID)被忽略了。此外,这个函数会返回计时器的 ID:

iTimerID = SetTimer (NULL, 0, iMsecInterval, TimerProc);
如果 SetTimer 函数返回的 iTimerID 为 0,表示没有可用的计时器,这样的情况是极罕见的。

        传给 KillTimer 的第一个参数(通常是窗口的句柄)也必须是 NULL。计时器的 ID 必须是从 SetTimer 返回的值:

KillTimer (NULL, iTimerID);

        传给 TimerProc 计时器函数的 hwnd 参数也将是 NULL。这种设置计时器的方法很少用到。如果在程序中,需要在不同的时刻调用很多次 SetTimer,但又不想记录哪些计时器 ID 已经被使用过,那么这种方法可能会派上用场

        知道如何使用 Windows 计时器之后,可以看看一些有用的计时器应用程序了。

你可能感兴趣的:(《Windows,程序设计》学习之旅)