1、Windows的窗口刷新管理
窗口句柄(HWND)都是由操作系统内核管理的,系统内部有一个z-order序列,记录着当前窗口从屏幕底部(假象的从屏幕到眼睛的方向),到屏幕最高层的一个窗口句柄的排序,这个排序不关注父窗口还是子窗口。
当任意一个窗口接收到WM_PAINT消息产生重绘,更新区域绘制完成以后,就搜索它的前面的一个窗口,如果此窗口的范围和更新区域有交集,就向这个窗口发送WM_PAINT消息,周而复始,直到执行到顶层窗口。才算完成。
1.1 父子窗口间的刷新管理
对于一个对话框(主窗口)来说,理论上其所有子窗口都在他的前面——也就是更靠近眼睛的位置),当主窗口接收WM_PAINT绘制完成后,会引起更新区域上所有子窗口的重绘(所有子窗口也是自底向上排序的)。
子窗口是具有WS_CHILD或者WS_CHILDWINDOW样式的窗口。和一般窗口一样,子窗口通过WM_PAINT来绘图。子窗口也维护一个更新区域,应用程序和系统都可以通过设置该更新区域无效来产生WM_PAINT消息。
子窗口的更新和显示区域受到父窗口的影响,其他样式的窗口则不会。系统常常设置父窗口的更新区域的同时设置子窗口的更新区域,使父窗口收到WM_PAINT消息的同时子窗口也能收到WM_PAINT消息。系统把子窗口的位置限制在父窗口的client区域,超出这个区域就会被裁减掉。
无论何时,只要父窗口的更新区域包含了子窗口的一部分,系统就会为子窗口设置更新区域。此时,系统先向父窗口发送WM_PAINT消息,然后向子窗口发送消息让子窗口可以恢复被父窗口覆盖的内容。
但是如果只有子窗口设置了更新区域,系统不会给父窗口也设置。在无效化子窗口时,系统不会给父窗口发WM_PAINT(因为被覆盖住了,根本没有必要)。同样的,如果使被子窗口覆盖住的父窗口的部分区域无效化,系统也不会给父窗口发送WM_PAINT的。在这种情况下,无论子窗口还是父窗口都不会收到WM_PAINT消息。
父子窗口间的刷新,还受父窗口是否设置了WS_CLIPCHILDREN样式影响。
父窗口如果设置了WS_CLIPCHILDREN这个样式的话,当父窗口的更新区域被设置的时候,子窗口的更新区域不会被设置。父窗口作用在子窗口下面的任何绘图全部被裁减掉。
因此,当父窗口无效且收到WM_PAINT消息时,如果没有设置WS_CLIPCHILDREN样式,则所有子窗口都会在父窗口处理WM_PAINT之后收到WM_PAINT重绘消息;如果父窗口带有WS_CLIPCHILDREN样式,则不会引起子窗口重绘。
1.2 兄弟窗口间的刷新管理
如果两个窗口重叠,则两个窗口都会收到WM_PAINT消息。他们收到WM_PAINT消息的顺序与z-index相反,即最上面的(z-order最高)的收到WM_PAINT消息最晚。
应用程序可以为窗口设置WS_CLIPSIBLING样式来避免兄弟窗口的绘制重叠。设置了这个,高z-order的窗口部分就会被上面的窗口裁减掉了,此部分被覆盖的区域就不会被刷新了。
结论
1)WS_CLIPCHILDREN样式主要是用于父窗口,也就是说当在父窗口绘制的时候,父窗口上还有一个子窗口,那么如果设置了这个样式的话,子窗口所在区域父窗口就不负责绘制;
2)所有的overlapped和popup风格的窗口,都有WS_CLIPSIBLINGS属性。也就是说这类风格的窗口,你是去不掉WS_CLIPSIBLINGS样式的,这样就是它不会在其与兄弟窗口重叠的区域绘图;
3)WS_CLIPSIBLINGS样式只适用于同级窗口,实际上还需要和控件的叠放顺序(z order)配合使用才能看出明显的效果。
2、OnEraseBkGnd与OnPaint
2.1 常见的问题
在OnEraseBkGnd中,如果你不调用原来缺省的OnEraseBkGnd只是重画背景则不会有闪烁。而在OnPaint里面,由于它隐含的调用了OnEraseBkGnd,而你又没有处理OnEraseBkGnd函数,这时就和窗口缺省的背景画刷相关了。缺省的OnEraseBkGnd操作使用窗口的缺省背景画刷刷新背景(一般情况下是白刷),而随后你又自己重画背景造成屏幕闪动。
然而OnEraseBkGnd不是每次都会被调用的。如果你调用Invalidate的时候参数为TRUE,那么在OnPaint里面隐含调用BeginPaint的时候就会产生WM_ERASEBKGND消息,如果参数是FALSE,则不会重刷背景。
以上问题解决方法有三个半:
1)用OnEraseBkGnd实现,不要调用原来的OnEraseBkGnd函数。
2)用OnPaint实现,同时重载OnEraseBkGn,其中直接返回TRUE。
3)用OnPaint实现,创建窗口时设置背景刷为空。
4)用OnPaint实现,但是要求刷新时用Invalidate(FALSE)这样的函数。(不过这种情况下,窗口覆盖等造成的刷新还是要闪一下,所以不是彻底的解决方法)。
2.2 关于OnEraseBkGnd的返回值
An application should return nonzero inresponse to WM_ERASEBKGND if it processes the message and erases thebackground; this indicates that no further erasing is required. If theapplication returns zero, the window will remain marked for erasing.(Typically, this indicates that the fErase member of the PAINTSTRUCT structurewill be TRUE.)
2.3 OnPaint的执行
Windows为每个视窗保存一个「绘图信息结构」,这就是PAINTSTRUCT,定义如下:
typedef struct tagPAINTSTRUCT
{
HDC hdc ;
BOOL fErase ;
RECT rcPaint ;
BOOL fRestore ;
BOOL fIncUpdate ;
BYTE rgbReserved[32] ;
} PAINTSTRUCT ;
在程序隐式调用BeginPaint时,Windows会适当填入该结构的各值。应用程序只使用前三个值,其他值由Windows内部使用。Hdc是设备DC句柄。在旧版本的Windows中,BeginPaint的传回值也曾是该值。在大多数情况下,fErase被标志为FALSE(0),这意味著Windows已经擦除了无效矩形的背景。这最早在BeginPaint函数中发生(如果要在窗口消息处理中自己定义一些背景擦除行为,可以自行处理WM_ERASEBKGND消息)。Windows使用WNDCLASS结构的hbrBackground指定的画刷来擦除背景,这个WNDCLASS结构程序在注册窗口类型时使用的。许多Windows程序使用白色画刷。以下叙述设定窗口类型结构中画刷的值:
wndclass.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH) ;
不过,如果程序通过调用Windows函数InvalidateRect使显示区域中的矩形失效,则该函数的最后一个参数会指定是否擦除背景。如果这个参数为FALSE(即0),则Windows将不会擦除背景,并且在调用完BeginPaint后PAINTSTRUCT结构的fErase将为TRUE(非零)。
PAINTSTRUCT结构的rcPaint是RECT型态的结构。它定义了无效矩形的边界,这些值均以像素为单位,并相对于客户区域的左上角。无效矩形是指应该重画的区域。
PAINTSTRUCT结构的rcPaint不仅是无效矩形,它还是一个“裁剪”矩形。这意味着Windows将绘图操作限制在该裁剪矩形内(更确切地说,如果无效矩形区域不为矩形,则Windows将绘图操作限制在这个区域内)。
在处理WM_PAINT消息时,为了在更新的矩形外绘图,可以使用如下函数:
InvalidateRect (hwnd, NULL, TRUE) ;
该函数在BeginPaint调用之前进行,它使整个显示区域变为无效,并擦除背景。但是,如果最後一个参数等於FALSE,则不擦除背景,原有的东西将保留在原处。通常这是Windows程序在无论何时收到WM_PAINT讯息而不考虑rcPaint结构的情况下简单地重画整个显示区域最方便的方法。
3、WM_PAINT消息
在Windows API编程中,WM_PAINT是Windows窗口的一个重要消息,应用程序就是通过响应这个消息来完成窗口的绘制。
The WM_PAINT message is generated by thesystem and should not be sent by an application.The system sends this messagewhen there are no other messages in the application's message queue.
注意:WM_PAINT消息是由系统产生,非要等应用程序的消息队列为空时才发送WM_PAINT消息。
其实系统会在很多的不同的机制下发送WM_PAINT消息,比如调用UpdateWindow函数,第一次创建窗口,改变了窗口的大小,最大化,最小化等等。这些动作的产生都是有系统来控制的,应用程序只是接收消息,并处理消息。
当Window检测到窗口被覆盖的地方需要恢复的时候,它会向用户程序发送一个WM_PAINT消息,消息中包括了需要恢复的区域,然后由用户程序来决定如何恢复被覆盖的内容。窗口过程收到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消息,而不是根据程序是否执行了刷新过程,所以程序也可以不去刷新客户区,而是简单地用一个ValidateRect函数直接让客户区变得有效,以此来“欺骗”Windows已经没有无效区域了,当Windows检查“绘图信息结构”的时候发现没有了无效区域,也就不会继续发送WM_PAINT消息了。
大多数的时候应用也需要能够主动引发窗口中的绘制操 作,比如当窗口显示的数据改变的时候,这一般是通过InvalidateRect和InvalidateRgn函数来完成的。InvalidateRect和InvalidateRgn把指定的区域加到窗口的Update Region中,当应用的消息队列没有其他消息时,如果窗口的Update Region不为空时,系统就会自动产生WM_PAINT消息。
系统为什么不在调用Invalidate时发送WM_PAINT消息呢?又为什么非要等应用消息队列为空时才发送WM_PAINT消息呢?这是因为系统把在窗口中的绘制操作当作一种低优先级的操作,于是尽可能地推后做。不过这样也有利于提高绘制的效率:两个WM_PAINT消息之间通过 InvalidateRect和InvaliateRgn使之失效的区域就会被累加起来,然后在一个WM_PAINT消息中一次得到更新,不仅能避免多次重复地更新同一区域,也优化了应用的更新操作。像这种通过InvalidateRect和InvalidateRgn来使窗口区域无效,依赖于系统在合适的时机发送WM_PAINT消息的机制实际上是一种异步工作方式,也就是说,在无效化窗口区域和发送WM_PAINT消息之间是有延迟的;有时候这种延迟并不是我们希望的,这时我们当然可以在无效化窗口区域后利用SendMessage发送一条WM_PAINT消息来强制立即重画,但不如使用Windows GDI为我们提供的更方便和强大的函数:UpdateWindow和RedrawWindow。UpdateWindow会检查窗口的Update Region,当其不为空时才发送WM_PAINT消息;RedrawWindow则给我们更多的控制:是否重画非客户区和背景,是否总是发送 WM_PAINT消息而不管Update Region是否为空等。
4、BeginPaint
BeginPaint sets the update region of awindow to NULL. This clears the region, preventing it fromgenerating subsequentWM_PAINT messages. If an application processes a WM_PAINT message but does notcall BeginPaint or otherwise clear the update region, the application continuesto receive WM_PAINT messages as long as the region is not empty. In all cases,an application must clear the update region before returning from the WM_PAINTmessage.
BeginPaint函数的作用之一就是将窗口需要重绘的区域设置为空(也就是Update Region置空)。在正常情况下,我们接收到了WM_PAINT消息后,窗口的Update Region都是非空的(如果为空就不需要发送WM_PAINT消息了)。而当你响应这个消息的时候又不调用BeginPaint来清空,窗口的 update Region就一直是非空的,系统就会一直发送WM_PAINT消息。这样就形成了一个处理WM_PAINT消息的死循环。
BeginPaint和WM_ERASEBKGND消息也有关系。当窗口的Update Region被标志为需要擦除背景时,BeginPaint会发送WM_ERASEBKGND消息来重画背景,同时在其返回信息里有一个标志表明窗口背景是否被重画过。当我们用InvalidateRect和InvalidateRgn来把指定区域加到Update Region中时,可以设置该区域是否需要被擦除背景,这样下一个BeginPaint就知道是否需要发送WM_ERASEBKGND消息了。
BeginPaint只能在WM_PAINT处理函数中使用,并且在调用了BeginPaint函数后,必须调用EndPaint函数,他们可是一对的。
5、重绘函数
5.1 InvalidateRect / InvalidateRgn
The InvalidateRect function adds a rectangleto the specified window's update region. The update region represents theportion of the window's client area that must be redrawn.
The invalidated areas accumulate in theupdate region until the region is processed when the next WM_PAINT message occurs or until the region isvalidated by using the ValidateRect or ValidateRgn function.
The system sends a WM_PAINT message to awindow whenever its update region is not empty and there are no other messagesin the application queue for that window.
If the bErase parameter is TRUE for any partof the update region, the background is erased in the entire region, not justin the specified part.
InvalidateRect和InvalidateRgn把指定的区域加到窗口的Update Region中,当应用的消息队列没有其他消息时,如果窗口的Update Region不为空时,系统就会自动产生WM_PAINT消息。
如果指定bErase为TRUE,整个的背景都将会被擦除。
5.2 UpdateWindow
The UpdateWindow function updates the clientarea of the specified window by sending a WM_PAINT message to the window if thewindow's update region is not empty. The function sends a WM_PAINT messagedirectly to the window procedure of the specified window, bypassing theapplication queue. If the update region is empty, no message is sent.
UpdateWindow是直接调用窗口函数立即响应刷新消息,使窗口刷新消息优先被响应(消息队列中如果没有WM_PAINT消息就什么都不执行),一般是在ShowWindow之后调用。
5.3 RedrawWindow
RedrawWindow相当于先调用InvalidateRect,紧接着又调用UpdateWindow,此外RedrawWindow还提供了一些前两者没法做到的功能,如是否重画非客户区和背景,是否总是发送 WM_PAINT消息而不管Update Region是否为空等。
其flags定义如下:
Flag(无效化标记) |
描述 |
RDW_ERASE |
当窗口重绘时将收到WM_ERASEBKGND消息。必须与RDW_INVALIDATE同时使用,否则无效。 |
RDW_FRAME |
与无效区域有交集的非客户区域将收到WM_NCPAINT消息,必须与RDW_INVALIDATE同时使用,否则无效。 在RedrawWindow处理过程中WM_NCPAINT消息不会被发送,除非指定了RDW_UPDATENOW或RDW_EARSENOW |
RDW_INTERNALPAINT |
Post一条WM_PAINT消息,不管Update Region是否为空 |
RDW_INVALIDATE |
无效化参数指定的区域 |
Flag(有效化标记) |
描述 |
RDW_NOERASE |
阻止即将发生的WM_ERASEBKGND消息 |
RDW_NOFRAME |
阻止即将发生的WM_NCPAINT消息,使用它要特别注意,可能会使窗口绘制不正确。 它必须与RDW_VALIDATE同时使用, 通常和RDW_NOCHILDREN一起使用。 |
RDW_NOINTERNALPAINT |
阻止即将发生的NULL无效区域的WM_PAINT消息 |
RDW_VALIDATE |
将参数的指定的区域有效化 |
Flag(刷新时机标记) |
描述 |
RDW_EARSENOW |
如果必要,将在RedrawWindow返回前使被影响的窗口(由RDW_ALLCHILDREN和RDW_NOCHILDREN指定)收到WM_NCPAINT和WM_ERASEBKGND消息 |
RDW_UPDATENOW |
如果必要,将在RedrawWindow返回前使被影响的窗口(由RDW_ALLCHILDREN和RDW_NOCHILDREN指定)收到WM_NCPAINT、WM_ERASEBKGND和WM_PAINT消息 |
Flag(影响标记) |
描述 |
RDW_ALLCHILDREN |
如果有子窗口,子窗口将被影响 |
RDW_NOCHILDREN |
如果有子窗口,子窗口不收影响 |
默认情况下,如果不指定RDW_NOCHILDREN和RDW_ALLCHILDREN,受影响的窗口由WS_CLIPCHILDREN样式决定,如果指定的窗口设置了该样式子窗口不受影响,否则子窗口将会被递归式的影响直到遇到一个具有WS_CLIPCHILDREN样式的窗口。