WIN32汇编语言教程:第07章 图形操作 · 7.1 GDI原理(2)
3. 探讨WM_PAINT消息
当客户区被覆盖并重新显示的时候,Windows并不是在所有的情况下都发送WM_PAINT消息,下面是几种不同的情况:
● 当鼠标光标移过窗口客户区以及图标拖过客户区这两种情况,Windows总是自己保存被覆盖的区域并恢复它,并不需要发送WM_PAINT消息通知用户程序。
● 当窗口客户区被自己的下拉式菜单覆盖,或者被自己弹出的对话框覆盖后,Windows会尝试保存被覆盖的区域并在以后恢复它,如果因为某种原因无法保存并恢复的话,Windows会发送一个WM_PAINT消息通知程序。
● 别的情况造成窗口的一部分从不可见变到可见,如程序从最小化的状态恢复,其他的窗口覆盖客户区后移开,用户改变了窗口的大小和用户按动滚动条等,在这些情况下,Windows会向窗口发送WM_PAINT消息。
● 一些函数会引发WM_PAINT消息,如UpdateWindow,InvalidateRect以及InvalidateRgn函数等。
窗口过程收到WM_PAINT消息后,并不代表整个客户区都需要被刷新,有可能客户区被覆盖的区域只有一小块,这个区域就叫做“无效区域”,程序只需要更新这个区域。
和WM_TIMER消息类似,WM_PAINT消息也是一个低级别的消息,虽然它不会像WM_TIMER消息一样被丢弃,但Windows总是在消息循环空的时候才把WM_PAINT放入其中,实际上,Windows为每个窗口维护一个“绘图信息结构”,无效区域的坐标就在其中,每当消息循环空的时候,如果Windows发现存在一个无效区域,就会放入一个WM_PAINT消息。
无效区域的坐标并不附带在WM_PAINT消息的参数中,在程序中有其他方法可以获取,WM_PAINT消息只是通知程序有个区域需要更新而已,所以Windows也不会同时将两条WM_PAINT消息放入消息循环,当Windows要放入一条WM_PAINT消息的时候,如果发现已经存在一个无效区域了,那么它只需要把新旧两个无效区域合并计算出一个新的无效区域就可以了,消息循环中还是只需要一条WM_PAINT消息。
由于存在“无效区域”这样一个东西,所以程序在WM_PAINT消息中对客户区刷新完毕后工作并没有结束,如果不使无效区域变得有效,Windows会在下一轮消息循环中继续放入一个WM_PAINT消息。还记得4.4.2节中的实验4吗,如果没有这个环节,WM_PAINT消息就会源源不断地发过来!那个实验中我们并没有去刷新客户区,而是简单地用一个ValidateRect函数直接让客户区变得有效,以此来“欺骗”Windows已经没有无效区域了,当Windows检查“绘图信息结构”的时候发现没有了无效区域,也就不会继续发送WM_PAINT消息了。
WM_PAINT消息的处理流程一般是:
.if eax == WM_PAINT ;eax为uMsg invoke BeginPaint,hWnd,addr stPS ;刷新客户区的代码 invoke EndPaint,hWnd,addr stPS xor eax,eax ret
读者可以发现中间并没有调用ValidateRect来使无效区域变得有效,这是因为BeginPaint函数和EndPaint函数隐含有这个功能,如果不是以BeginPaint/EndPaint当做消息处理代码的头尾的话,那么在WM_PAINT消息返回的时候就必须调用ValidateRect函数。
BeginPaint函数的第二个参数是一个绘图信息结构的缓冲区地址,Windows会在这里返回绘图信息结构,结构中包含了无效区域的位置和大小,绘图信息结构的定义如下:
PAINTSTRUCT STRUCT hdc DWORD ? fErase DWORD ? rcPaint RECT <> fRestore DWORD ? fIncUpdate DWORD ? rgbReserved BYTE 32 dup(?) PAINTSTRUCT ENDS
其中hdc字段是窗口的设备环境句柄(在下一节中将要讲到),rcPaint字段是一个RECT结构,它指定了无效区域矩形的对角顶点,fErase字段如果为非零值,表示Windows在发送WM_PAINT消息前已经用背景色擦除了无效区域,后面3个字段是Windows内部使用的,应用程序不必去理会它们。
7.1.2 设备环境
好了,解决了“When”的问题,让我们来考虑一个新的问题,在DOS操作系统中,向屏幕输出数据实际上是把输出内容拷贝到视频缓冲区中,在第01章的图1.1中就已经说明:如果在文本模式下显示信息,只需要把内容拷贝到B8000h处的内存中;显示图形信息,可以把图形数据拷贝到A0000h处的内存中。
在Windows中,GDI接口把程序和硬件分隔开来,在Win32编程中,再也不能通过直接向视频缓冲区拷贝数据的办法来显示信息了,那么,究竟该往哪里输出图形呢——这就是“Where”的问题。答案是:通过“设备环境”来输出图形。
1. 什么是设备环境
在Windows中,所有与图形相关的操作都是用统一的方法来完成的(不然就不能称为“图形设备接口”了)。不管是绘画屏幕上的一个窗口,还是把图形输出到打印机,或者对一幅位图进行绘画,使用的绘图函数都是相同的,为了实现方法上的统一,必须将所有的图形对象看成是一个虚拟的设备,这些设备可能有不同的属性,如黑白打印机和彩色屏幕的颜色深度是不同的,不同打印机的尺寸和分辨率可能是不同的,绘图仪只支持矢量而不支持位图等。不同设备的不同属性就构成了一个绘图的“环境”,就像DOS操作系统中把视频缓冲区当做图形操作的对象一样,这个绘图的“环境”就是Win32编程中图形操作的对象,把它叫做“设备环境”。设备环境实际上是一个数据结构,结构中保存的就是设备的属性,当对设备环境进行图形操作的时候,Windows可以根据这些属性找到对应的设备进行相关的操作。
在实际使用中,通过“设备环境”可以操作的对象很广泛,除了可以是打印机或绘图仪等硬件设备外,也可以是窗口的客户区,包括大大小小的所有可以被称为窗口的按钮与控件等的客户区,也可以是一个位图。总之,任何需要用到图形操作的东西都可以通过“设备环境”进行绘图。
为了更好地理解“设备环境”是什么,先来看一个例子,例子的代码在所附光盘的Chapter07\DcCopy目录中,DcCopy.asm中的代码如下:
.386 .model flat,stdcall option casemap:none ;#################################################################### ; Include 文件定义 ;#################################################################### include windows.inc include gdi32.inc includelib gdi32.lib include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib ;#################################################################### ID_TIMER equ 1 ;#################################################################### ; 数据段 ;#################################################################### .data? hInstance dd ? hWin1 dd ? hWin2 dd ? .const szClass1 db 'SourceWindow',0 szClass2 db 'DestWindow',0 szCaption1 db '请尝试用别的窗口覆盖本窗口!',0 szCaption2 db '本窗口图像拷贝自另一窗口',0 szText db 'Win32 Assembly, Simple and powerful !',0 ;#################################################################### .code ;#################################################################### ; 定时器过程 ;#################################################################### _ProcTimer proc _hWnd,uMsg,_idEvent,_dwTime local @hDc1,@hDc2 local @stRect:RECT invoke GetDC,hWin1 mov @hDc1,eax invoke GetDC,hWin2 mov @hDc2,eax invoke GetClientRect,hWin1,addr @stRect invoke BitBlt,@hDc2,0,0,@stRect.right,@stRect.bottom,\ @hDc1,0,0,SRCCOPY invoke ReleaseDC,hWin1,@hDc1 invoke ReleaseDC,hWin2,@hDc2 ret _ProcTimer endp ;#################################################################### ; 窗口过程 ;#################################################################### _ProcWinMain proc uses ebx edi esi,hWnd,uMsg,wParam,lParam local @stPs:PAINTSTRUCT local @stRect:RECT local @hDc mov eax,uMsg mov ecx,hWnd ;******************************************************************** .if eax == WM_PAINT && ecx == hWin1 invoke BeginPaint,hWnd,addr @stPs mov @hDc,eax invoke GetClientRect,hWnd,addr @stRect invoke DrawText,@hDc,addr szText,-1,\ addr @stRect,\ DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint,hWnd,addr @stPs ;******************************************************************** .elseif eax == WM_CLOSE invoke PostQuitMessage,NULL invoke DestroyWindow,hWin1 invoke DestroyWindow,hWin2 ;******************************************************************** .else invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .endif ;******************************************************************** xor eax,eax ret _ProcWinMain endp ;#################################################################### _WinMain proc local @stWndClass:WNDCLASSEX local @stMsg:MSG local @hTimer invoke GetModuleHandle,NULL mov hInstance,eax invoke RtlZeroMemory,addr @stWndClass,sizeof @stWndClass ;******************************************************************** invoke LoadCursor,0,IDC_ARROW mov @stWndClass.hCursor,eax push hInstance pop @stWndClass.hInstance mov @stWndClass.cbSize,sizeof WNDCLASSEX mov @stWndClass.style,CS_HREDRAW or CS_VREDRAW mov @stWndClass.lpfnWndProc,offset _ProcWinMain mov @stWndClass.hbrBackground,COLOR_WINDOW + 1 mov @stWndClass.lpszClassName,offset szClass1 invoke RegisterClassEx,addr @stWndClass invoke CreateWindowEx,WS_EX_CLIENTEDGE,offset szClass1,\ offset szCaption1,WS_OVERLAPPEDWINDOW,\ 450,100,300,300,\ NULL,NULL,hInstance,NULL mov hWin1,eax invoke ShowWindow,hWin1,SW_SHOWNORMAL invoke UpdateWindow,hWin1 ;******************************************************************** mov @stWndClass.lpszClassName,offset szClass2 invoke RegisterClassEx,addr @stWndClass invoke CreateWindowEx,WS_EX_CLIENTEDGE,offset szClass2,\ offset szCaption2,WS_OVERLAPPEDWINDOW,\ 100,100,300,300,\ NULL,NULL,hInstance,NULL mov hWin2,eax invoke ShowWindow,hWin2,SW_SHOWNORMAL invoke UpdateWindow,hWin2 ;******************************************************************** ; 设置定时器 ;******************************************************************** invoke SetTimer,NULL,NULL,100,addr _ProcTimer mov @hTimer,eax ;******************************************************************** ; 消息循环 ;******************************************************************** .while TRUE invoke GetMessage,addr @stMsg,NULL,0,0 .break .if eax == 0 invoke TranslateMessage,addr @stMsg invoke DispatchMessage,addr @stMsg .endw ;******************************************************************** ; 清除定时器 ;******************************************************************** invoke KillTimer,NULL,@hTimer ret _WinMain endp ;#################################################################### start: call _WinMain invoke ExitProcess,NULL ;#################################################################### end start