📄 见招拆招《windows程序设计》(七) .txt
字号:
见招拆招《Windows程序设计》(七)
相关的例子:下载>>> 作者:Zoologist 于2008-4-14上传
--------------------------------------------------------------------------------
定时器
--------------------------------------------------------------------------------
前面一期的《用MasmPlus学汇编》提到过定时器,因此,这一章对我们来说非常简单。这一期的例子程序非常有意思,仔细研究能学到很多编程上的技巧。
Microsoft Windows定时器是一种输入设备,它周期性地在每经过一个指定的时间间隔后就通知应用程序一次。您的程序将时间间隔告诉Windows,例如「每10秒钟通知我一声」,然后Windows给您的程序发送周期性发生的WM_TIMER消息以表示时间到了。
初看之下,Windows定时器似乎不如键盘和鼠标设备重要,而且对许多应用程序来说确实如此。但是,定时器比您可能认为的要重要得多,它不只用于计时程序,比如出现在工具栏中的Windows时钟和这一章中的两个时钟程序。下面是Windows定时器的其它应用,有些可能并不那么明显:
多任务虽然Windows 是一个优先权式的多任务环境,但有时候如果程序尽快将控制传回给Windows效率会更高。如果一个程序必须进行大量的处理,那么它可以将作业分成小块,每接收到一个WM_TIMER消息处理一块。这样的方法可以做到模拟出一个“多任务”。
维护更新过的状态报告程序可以利用定时器来显示持续变化信息的「实时」更新,比如关于系统资源的变化或某个任务的进展情况。
实作「自动储存」功能定时器提示Windows程序在指定的时间过去后保存使用者的工作,以防意外断电或者其他事情发生后导致任务丢失。
终止程序展示版本的执行一些程序的Demo版本被设计成在其开始后,多长时间结束,比如说,30分钟。如果时间已到,那么定时器就会通知应用程序。当然,如果你反编译这样的Demo版,找到定时器设定,将它去掉或者修改为一个足够长的时间
步进移动游戏中的图形对象或计算机辅助教学程序中的连续显示,需要按指定的速率来处理。利用定时器可以消除由于处理器速度不同而造成的不一致。
多媒体播放CD声音、声音或音乐的程序通常在背景播放声音数据。一个程序可以使用定时器来周期性地检查已播放了多少声音数据,并据此协调屏幕上的视觉信息。
另一项应用可以保证程序在退出窗口消息处理程序后,能够重新得到控制。在大多数时情况下,程序不能够知道何时下一个消息会到来。
定时器入门
您可以通过呼叫SetTimer函数为您的Windows程序分配一个定时器。SetTimer有一个时间间隔范围为1毫秒到4,294,967,295毫秒(将近50天)的整数型态参数,这个值指示Windows每隔多久时间给您的程序发送WM_TIMER消息。例如,如果间隔为1000毫秒,那么Windows将每秒给程序发送一个WM_TIMER消息。
当您的程序用完定时器时,它呼叫KillTimer函数来停止定时器消息。在处理WM_TIMER消息时,您可以通过呼叫KillTimer函数来编写一个「限用一次」的定时器。KillTimer呼叫清除消息队列中尚未被处理的WM_TIMER消息,从而使程序在呼叫KillTimer之后就不会再接收到WM_TIMER消息。
系统和定时器
Windows定时器是PC硬件和ROM BIOS架构下之定时器一种相对简单的扩充。回到Windows以前的MS-DOS程序写作环境下,应用程序能够通过拦截者称为timer tick的BIOS中断来实作时钟或定时器。一些为MS-DOS编写的程序自己拦截这个硬件中断以实作时钟和定时器。这些中断每54.915毫秒产生一次,或者大约每秒18.2次。这是原始的IBM PC的处理器频率4.772720 MHz被218所除而得出的结果。
Windows应用程序不拦截BIOS中断,相反地,Windows本身处理硬件中断,这样应用程序就不必进行处理。对于目前拥有定时器的每个程序,Windows储存一个每次硬件timer tick减少的计数。当这个计数减到0时,Windows在应用程序消息队列中放置一个WM_TIMER消息,并将计数重置为其最初值。
因为Windows应用程序从正常的消息队列中取得WM_TIMER消息,所以您的程序在进行其它处理时不必担心WM_TIMER消息会意外中断了程序。在这方面,定时器类似于键盘和鼠标。驱动程序处理异步硬件中断事件,Windows把这些事件翻译为规律、结构化和顺序化的消息。
在Windows 98中,定时器与其下的PC定时器一样具有55毫秒的分辨率。在Microsoft Windows NT中,定时器的分辨率为10毫秒。
Windows应用程序不能以高于这些分辨率的频率(在Windows 98下,每秒18.2次,在Windows NT/XP下,每秒大约100次)接收WM_TIMER消息。在SetTimer呼叫中指定的时间间隔总是截尾后tick数的整数倍。例如,1000毫秒的间隔除以54.925毫秒,得到18.207个tick,截尾后是18个tick,它实际上是989毫秒。对每个小于55毫秒的间隔,每个tick都会产生一个WM_TIMER消息。
定时器消息不是异步的
因为定时器使用硬件定时器中断,程序写作者有时会误解,认为他们的程序会异步地被中断来处理WM_TIMER消息。
然而,WM_TIMER消息并不是异步的。WM_TIMER消息放在正常的消息队列之中,和其它消息排列在一起,因此,如果在SetTimer呼叫中指定间隔为1000毫秒,那么不能保证程序每1000毫秒或者989毫秒就会收到一个WM_TIMER消息。如果其它程序的执行事件超过一秒,在此期间内,您的程序将收不到任何WM_TIMER消息。您可以使用本章的程序来展示这一点。事实上,Windows对WM_TIMER消息的处理非常类似于对WM_PAINT消息的处理,这两个消息都是低优先级的,程序只有在消息队列中没有其它消息时才接收它们。
WM_TIMER还在另一方面和WM_PAINT相似:Windows不能持续向消息队列中放入多个WM_TIMER消息,而是将多余的WM_TIMER消息组合成一个消息。因此,应用程序不会一次收到多个这样的消息,尽管可能在短时间内得到两个WM_TIMER消息。应用程序不能确定这种处理方式所导致的WM_TIMER消息「遗漏」的数目。
这样,WM_TIMER消息仅仅在需要更新时才提示程序,程序本身不能经由统计WM_TIMER消息的数目来计时(在本章后面,我们将编写两个每秒更新一次的时钟程序,并可以看到如何做到这一点)。
为了方便起见,下面在讨论时钟时,我将使用「每秒得到一次WM_TIMER消息」这样的叙述,但是请记住,这些消息并非精确的tick中断。
定时器的使用:三种方法
如果您需要在整个程序执行期间都使用定时器,那么您将得从WinMain函数中或者在处理WM_CREATE消息时呼叫SetTimer,并在退出WinMain或响应WM_DESTROY消息时呼叫KillTimer。根据呼叫SetTimer时使用的参数,可以下列三种方法之一使用定时器。
方法一
这是最方便的一种方法,它让Windows把WM_TIMER消息发送到应用程序的正常窗口消息处理程序中,SetTimer呼叫如下所示:
SetTimer (hwnd, 1, uiMsecInterval, NULL) ;
第一个参数是其窗口消息处理程序将接收WM_TIMER消息的窗口句柄。第二个参数是定时器ID,它是一个非0数值,在整个例子中假定为1。第三个参数是一个32位无正负号整数,以毫秒为单位指定一个时间间隔,一个60,000的值将使Windows每分钟发送一次WM_TIMER消息。
您可以通过呼叫
KillTimer (hwnd, 1) ;
在任何时刻停止WM_TIMER消息(即使正在处理WM_TIMER消息)。此函数的第二个参数是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, 60000, NULL) ;
WM_TIMER的处理如下所示:
caseWM_TIMER:
switch (wParam)
{
case TIMER_SEC:
//每秒一次的处理
break ;
case TIMER_MIN:
//每分钟一次的处理
break ;
}
return 0 ;
如果您想将一个已经存在的定时器设定为不同的时间间隔,您可以简单地用不同的时间值再次呼叫SetTimer。在时钟程序里,如果显示秒或不显示秒是可以选择的,您就可以这样做,只需简单地将时间间隔在1000毫秒和60 000毫秒间切换就可以了。
程序8-1显示了一个使用定时器的简单程序,名为BEEPER1,定时器的时间间隔设定为1秒。当它收到WM_TIMER消息时,它将显示区域的颜色由蓝色变为红色或由红色变为蓝色,并通过呼叫MessageBeep函数发出响声。(虽然MessageBeep通常用于MessageBox,但它确实是一个全功能的鸣叫函数。在有声卡的PC机上,一般可以使用不同的MB_ICON参数作为MessageBeep的一个参数以用于MessageBox,来播放使用者在「控制面板」的「声音」程序中选择的不同声音)。
BEEPER1在窗口消息处理程序处理WM_CREATE消息时设定定时器。在处理WM_TIMER消息处理期间,BEEPER1呼叫MessageBeep,翻转bFlipFlop的值并使窗口无效以产生WM_PAINT消息。在处理WM_PAINT消息处理期间,BEEPER1通过呼叫GetClientRect获得窗口大小的RECT结构,并通过呼叫FillRect改变窗口的颜色。
程序8-1 BEEPER1
BEEPER1.Asm
;MASMPlus 代码模板 - 普通的 Windows 程序代码
.386
.Model Flat, StdCall
Option Casemap :None
Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc
includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
ID_TIMER equ 1
.DATA
szAppName db "Beeper1",0
fFlipFlop BOOL FALSE
.DATA?
hInstance dd ?
.CODE
START: ;从这里开始执行
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke WinMain,hInstance,NULL,NULL,SW_SHOWDEFAULT
invoke ExitProcess,0
WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,iCmdShow:DWORD
LOCAL wndclass :WNDCLASSEX
LOCAL msg :MSG
local hWnd :HWND
mov wndclass.cbSize,sizeof WNDCLASSEX
mov wndclass.style,CS_HREDRAW or CS_VREDRAW
mov wndclass.lpfnWndProc,offset WndProc
mov wndclass.cbClsExtra,0
mov wndclass.cbWndExtra,0
push hInst
pop wndclass.hInstance
invoke LoadIcon,NULL,IDI_APPLICATION
mov wndclass.hIcon,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wndclass.hCursor,eax
invoke GetStockObject,WHITE_BRUSH
mov wndclass.hbrBackground,EAX
mov wndclass.lpszMenuName,NULL
mov wndclass.lpszClassName,offset szAppName
mov wndclass.hIconSm,0
invoke RegisterClassEx, ADDR wndclass
.if (EAX==0)
invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),addr szAppName,MB_ICONERROR
ret
.endif
invoke CreateWindowEx,
NULL,
ADDR szAppName, ;window class name
CTXT("Beeper1 Timer Demo"), ;window caption
WS_OVERLAPPEDWINDOW, ;window style
CW_USEDEFAULT, ;initial x position
CW_USEDEFAULT, ;initial y position
CW_USEDEFAULT, ;initial x size
CW_USEDEFAULT, ;initial y size
NULL,;parent window handle
NULL,;window menu handle
hInstance,;program instance handle
NULL;creation parameters
mov hWnd,eax
invoke ShowWindow,hWnd,iCmdShow
invoke UpdateWindow,hWnd
StartLoop:
invoke GetMessage,ADDR msg,NULL,0,0
cmp eax, 0
je ExitLoop
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
jmp StartLoop
ExitLoop:
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hwnd:DWORD,message:DWORD,wParam :DWORD,lParam :DWORD
LOCAL hBrush :HBRUSH
LOCAL hdc :HDC
LOCAL ps :PAINTSTRUCT
LOCAL rc :RECT
.if message == WM_CREATE
invoke SetTimer,hwnd,ID_TIMER,1000,NULL
ret
.elseif message == WM_TIMER
invoke MessageBeep,-1
mov eax,fFlipFlop
not eax
mov fFlipFlop,eax
invoke InvalidateRect,hwnd,NULL,FALSE
ret
.elseif message == WM_PAINT
invoke BeginPaint,hwnd,addr ps
mov hdc,eax
invoke GetClientRect,hwnd,addr rc
.if (fFlipFlop == FALSE)
mov eax, 0FF0000h
.else
mov eax, 00000FFh
.endif
invoke CreateSolidBrush,eax
mov hBrush,eax
invoke FillRect,hdc,addr rc,hBrush
invoke EndPaint,hwnd,addr ps
invoke DeleteObject,hBrush
ret
.elseif message == WM_DESTROY
invoke KillTimer,hwnd,ID_TIMER
invoke PostQuitMessage,NULL
ret
.endif
invoke DefWindowProc,hwnd, message, wParam, lParam
ret
WndProc endp
END START
因为BEEPER1每次收到WM_TIMER消息时,都用颜色的变换显示出来,所以您可以通过呼叫BEEPER1来查看WM_TIMER消息的性质,并完成Windows内部的一些其它操作。
例如,首先呼叫控制面板的 显示程序,选择效果,确定 拖曳时显示窗口内容复选框没有被选中。现在,试着移动或者缩放BEEPER1窗口,这将导致程序进入「模态消息循环」。Windows通过在内部消息而非您程序的消息循环中拦截所有消息,来禁止对移动或者缩放操作的任何干扰。通过此循环到达程序窗口的大多数消息都被丢弃,这就是BEEPER1停止蜂鸣的原因。当完成了移动与缩放之后,您将会注意到BEEPER1不能取得它所丢弃的所有WM_TIMER消息,尽管前两个消息的间隔可能少于1秒。
在「拖曳时显示窗口内容」复选框被选中时,Windows中,的模态消息循环会试图给您的窗口消息处理程序传递一些丢失的消息。这样做有时工作得很好,有时却不行。
方法二
设定定时器的第一种方法是把WM_TIMER消息发送到通常的窗口消息处理程序,而第二种方法是让Windows直接将定时器消息发送给您程序的另一个函数。
接收这些定时器消息的函数被称为「callback」函数,这是一个在您的程序之中但是由Windows呼叫的函数。您先告诉Windows此函数的地址,然后Windows呼叫此函数。这看起来也很熟悉,因为程序的窗口消息处理程序实际上也是一种callback函数。当注册窗口类别时,要将函数的地址告诉Windows,当发送消息给程序时,Windows会呼叫此函数。
SetTimer并非是唯一使用callback函数的Windows函数。CreateDialog和DialogBox函数使用callback函数处理对话框中的消息;有几个Windows函数(EnumChildWindow、EnumFonts、EnumObjects、EnumProps和EnumWindow)把列举信息传递给callback函数;还有几个不那么常用的函数(GrayString、LineDDA和SetWindowHookEx)也要求callback函数。
像窗口消息处理程序一样,callback函数也必须定义为CALLBACK,因为它是由Windows从程序的程序代码段呼叫的。callback函数的参数和callback函数的传回值取决于callback函数的目的。跟定时器有关的callback函数中,输入参数与窗口消息处理程序的输入参数一样。定时器callback函数不向Windows传回值。
我们把以下的callback函数称为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,dwTimer值是与从GetTickCount函数的传回值相容的值。这是自Windows启动后所经过的毫秒数。
在BEEPER1中已经看到过,用第一种方法设定定时器时要求下面格式的SetTimer呼叫:
SetTimer (hwnd, iTimerID, iMsecInterval, NULL) ;
您使用callback函数处理WM_TIMER消息时,SetTimer的第四个参数由callback函数的地址取代,如下所示:
SetTimer (hwnd, iTimerID, iMsecInterval, TimerProc) ;
我们来看看一些范例程序代码,这样您就会了解这些东西是如何组合在一起的。在功能上,除了Windows发送一个定时器消息给TimerProc而非WndProc之外,程序8-2所示的BEEPER2程序与BEEPER1是相同的。注意,TimerProc和WndProc一起被声明在程序的开始处。
程序8-2 BEEPER2
BEEPER2.Asm
;MASMPlus 代码模板 - 普通的 Windows 程序代码
.386
.Model Flat, StdCall
Option Casemap :None
Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc
includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
LOWORD MACRO bigword;; Retrieves the low word from double word argument
mov eax,bigword
and eax,0FFFFh ;; Get low word
ENDM
HIWORD MACRO bigword ;; Retrieves the high word from double word
mov ebx,bigword
shr ebx,16;; Shift 16 for high word to set to high word
ENDM
RGB MACRO red, green, blue ;; Get composite number from red green and blue bytes
mov al,blue ;; ,,,blue
shl eax,8 ;; ,,blue,
add al,green;; ,,blue,green
shl eax,8 ;; ,blue,green,
add al,red ;; ,blue,green,red
and eax,0FFFFFFh;; Mask out top byte to complete COLORREF dword
ENDM
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
ID_TIMER equ 1
.DATA
szAppName db "DigClock",0
fSevenSegment BYTE 1, 1, 1, 0, 1, 1, 1,\
0, 0, 1, 0, 0, 1, 0,\
1, 0, 1, 1, 1, 0, 1,\
1, 0, 1, 1, 0, 1, 1,\
0, 1, 1, 1, 0, 1, 0,\
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -