天天看点

GDI+ 双缓冲 的起因以及解决办法

重绘导致原因:UpdateData、Invalidate、InvalidateRect和UpdateWindow函数。

1. UpdateData重绘控件函数

 UpdateData(TRUE)——刷新控件的值到对应的变量。(外部输入值交给内部变量)

 即:控件的值—>变量。  

UpdateData(FALSE) —— 拷贝变量值到控件显示。(变量的最终运算结果值交给外部输出显示)

 即:变量值—>控件显示。 

2. Invalidate()

      该函数的作用是使整个窗口客户区无效。窗口的客户区无效意味着需要重绘,例如,如果一个被其它窗口遮住的窗口变成了前台窗口,那么原来被遮住的部分就是无效的,需要重绘。这时Windows会在应用程序的消息队列中放置WM_PAINT消息。MFC为窗口类提供了WM_PAINT的消息处理函数OnPaint,OnPaint负责重绘窗口。视图类有一些例外,在视图类的OnPaint函数中调用了OnDraw函数,实际的重绘工作由OnDraw来完成。参数bErase为TRUE时,重绘区域内的背景将被擦除,否则,背景将保持不变。 

3. InvalidateRect

    用InvalidateRect函数只重绘部分区域,而且不重绘背景(第二个参数用FALSE)就可以解决大部分的屏闪问题。

    比如:CRect rect(10,47,10+120,47+70);

    InvalidateRect(rect,FALSE);

 4. UpdateWindow函数

UpdateWindow( )的作用是使窗口立即重绘。调用Invalidate等函数后窗口不会立即重绘,这是由于WM_PAINT消息的优先级很低,它需要等消息队列中的其它消息发送完后才能被处理。调用UpdateWindow函数可使WM_PAINT被直接发送到目标窗口,从而导致窗口立即重绘。注意:函数绕过应用程序的消息队列,直接发送WM_PAINT消息给指定窗口的窗口过程,如果更新区域为空,则不发送消息。

解决方法:

       双缓冲是一种基本的技术。我们知道,如果窗体在响应WM_PAINT消息的时候要进行复杂的图形处理,那么窗体在重绘时由于过频的刷新而引起闪烁现象。解决这一问题的有效方法就是双缓冲技术。

      因为窗体在刷新时,总要有一个擦除原来图象的过程,它利用背景色填充窗体绘图区,然后在调用新的绘图代码进行重绘,这样一擦一写造成了图象颜色的反差。当WM_PAINT的响应很频繁的时候,这种反差也就越发明显。于是我们就看到了闪烁现象。(

                      当窗口由于任何原因需要重绘时,

总是先用背景色将显示区清除,然后才调用OnPaint,而背景色往往与绘图内容

反差很大,这样在短时间内背景色与显示图形的交替出现,使得显示窗口看起来

在闪。如果将背景刷设置成NULL,这样无论怎样重绘图形都不会闪了。

当然,这样做会使得窗口的显示乱成一团,因为重绘时没有背景色对原来

绘制的图形进行清除,而又叠加上了新的图形。) 我们会很自然的想到,避免背景色的填充是最直接的办法。但是那样的话,窗体上会变的一团糟。因为每次绘制图象的时候都没有将原来的图象清除,造成了图象的残留,于是窗体重绘时,画面往往会变的乱七八糟。所以单纯的禁止背景重绘是不够的。我们还要进行重新绘图,但要求速度很快,于是我们想到了使用BitBlt函数。它可以支持图形块的复制,速度很快。我们可以先在内存中作图,然后用此函数将做好的图复制到前台,同时禁止背景刷新,这样就消除了闪烁。以上也就是双缓冲绘图的基本的思路。

 如何提高绘图的效率

    电力系统的网络图形的CAD软件,在一个窗口中往往要显示成千上万个电力元件,而每个元件又是由点、线、圆等基本图形构成。如果真要在一次重绘过程重画这么多元件,可想而知这个过程是非常漫长的。如果加上了图形的浏览功能,鼠标拖动图形滚动时需要进行大量的重绘,速度会慢得让用户将无法忍受。怎么办?只有再研究研究MFC的绘图过程了。

    实际上,在OnDraw(CDC *pDC)中绘制的图并不是所有都显示了的,例如:你

在OnDraw中画了两个矩形,在一次重绘中虽然两个矩形的绘制函数都有执行,但是很有可能只有一个显示了,这是因为MFC本身为了提高重绘的效率设置了裁剪区。裁剪区的作用就是:只有在这个区内的绘图过程才会真正有效,在区外的是无效的,即使在区外执行了绘图函数也是不会显示的。因为多数情况下窗口重绘的产生大多是因为窗口部分被遮挡或者窗口有滚动发生,改变的区域并不是整个图形而只有一小部分,这一部分需要改变的就是pDC中的裁剪区了。因为显示(往内存或者显存都叫显示)比绘图过程的计算要费时得多,有了裁剪区后显示的就只是应该显示的部分,大大提高了显示效率。但是这个裁剪区是MFC设置的,它已经为我们提高了显示效率,在进行复杂图形的绘制时如何进一步提高效率呢?那就只有去掉在裁剪区外的绘图过程了。可以先用pDC->GetClipBox()得到裁剪区,然后在绘图时判断你的图形是否在这个区内,如果在就画,不在就不画。

如果你的绘图过程不复杂,这样做可能对你的绘图效率不会有提高。

啰嗦一下:

首先,ShowWindow本身是不会产生重画消息的,它的作用仅仅是把窗口显示出来。不过,当窗口显示的时候,Windows会自动探测窗口的内容是否需要重画、以及需要重画的区域组成,比如你的窗口位置直接在屏幕外,或者你的窗口被别的窗口完全挡住,当然就不需要重画,如果你的窗口只露出一部分,那么就只有这一部分需要重画。这个过程与你移动窗口、切换窗口的时候Windows所做的事情是一样的——自动判定你的窗口有哪一部分原来不显示而现在需要显示,然后对这部分区域调用InvalidateRect()。这个函数的作用并不是立刻重画这些区域,而是对这些区域做上标记。多次调用这个函数,新标记的区域会与以前标记的区域合并。

之后,当你的消息队列完全空了的时候,假若windows又发现你窗口所标记的重画区域不为空,那么Windows就在你的消息队列里放一个WM_PAINT消息,让你重画。根据这一流程可知,假若我们的消息队列一直很忙的话,那么窗口是没机会获得WM_PAINT消息的。其次,假定消息队列里有若干个消息,每个都导致一部分窗口区域需要重画,那么最后只会重画一次,只不过重画的范围是几个区域的合并。再有,某些特殊情况下,有可能会不希望窗口被重画、或者至少其中某一部分不要重画,那么你可以在消息队列被取空之前(尚未发出WM_PAINT),用ValidateRect把窗口的某一部分乃至全部都取消标记。如果所有以前被标记的部分全被你取消掉了,那么等消息队列空了以后,也不会再有WM_PAINT发出了。

当你在处理WM_PAINT消息进行重画的时候,BeginPaint的一个重要作用,就是在它返回的DC里,用原来标记的区域制作一个剪裁区域(ClipRegion),从而使你的所有重画操作都被限定在这一个区域中。这是一个很重要的特性。举例来说,假若你的窗口用一张位图作为背景,处理WM_PAINT的时候用BitBlt之类的方法往屏幕上贴图。如果某一次有一个别的窗口仅仅盖住了你窗口的一个小角,当它拿开的时候,如果没有剪裁区域的话,那么就会对整个窗口贴图,这不仅很慢,而且会引起你窗口中的各个子窗口的闪烁。但有了剪裁区域的话,你的代码虽然还是在对整个窗口贴图,但实际上只有位于剪裁区域内的那部分操作有效,其它的都被Windows放弃了,所以速度会快得多。有时我们自己也需要更新窗口的显示内容,这时候也是通过调用InvalidateRect来做。不过,很多人在这种情况下习惯于将整个窗口统统Inalidate,这样做倒是很方便,不过这是一个很不好的习惯。除非你需要更新的内容波及到整个窗口,否则应该仅仅把需要改变的那部分Invalidate。

上面说的剪裁区域仅仅是BeginPaint的一个作用,BeginPaint还有其它作用,都是跟重画这个任务紧密连接的,因此,在响应WM_PAINT消息的时候,必须使用BeginPaint所获取的DC句柄来画图,绝不能用GetDC等其它方式。相对应的,这个句柄也必须使用EndPaint来释放。如果在响应WM_PAINT的时候没有调用BeginPaint和EndPaint(例如用GetDC和ReleaseDC来画图),其中一个副作用就是:重画区域的标记不会被取消。于是当你响应完这一个WM_PAINT之后,Windows会发现你的窗口还有区域被标记为重画,于是再次发出WM_PAINT,于是你就永无休止地重画下去了。

从上述可知,单纯一个ShowWindow,照样会正确重画窗口内容,只不过重画是在消息队列取空之后。有时我们希望窗口被立即重画,而不是去等待那个不确定的消息队列,此时就需要用到UpdateWindow。这个函数的作用只有一个:假若当前被标记为重画的区域存在(不存在的话它什么也不做),那么立刻让Windows使用SendMessage的方式来对你的窗口发送WM_PAINT。

说道这里,就要说一下SendMessage与PostMessage的区别了。PostMessage是把消息放到消息队列尾部,然后通过程序的消息环逐个从消息队列里取出来进行处理。SendMessage却不是这样,它实际上根本不经过消息队列。对SendMessage的处理分两种情况:

1、由本线程发出的SendMessage,例如在自己的消息处理过程中调用UpdateWindow,从而发出的WM_PAINT。对于这种情况,SendMessage实际上直接调用你窗口的消息处理函数。也就是说,在进行消息处理的时候对本窗口SendMessage,实际上是递归调用。整个过程是:消息环取出消息A(假定A是PostMessage放进消息队列的)-> 处理消息A -> SendMessage对本线程的窗口发送消息B -> 处理消息B -> 继续处理消息A -> 消息环再取下一个消息。

2、由另一个线程发出的SendMessage。此时当然不能直接调用了。这种情况下,windows会把发送消息的线程挂起,然后,当接收消息的线程调用PeekMessage或者GetMessage的时候,这两个函数会立刻调用你窗口的消息处理函数,直到处理完毕(此时发送线程的SendMessage才返回),PeekMessage或者GetMessage才会去检查消息队列,并从中取出一个返回。

总之,不论哪种情况,SendMessage发送的消息,跟消息队列均没有任何关系,而且也不通过消息环执行(前者是直接调用,后者是在PeekMessage或GetMessage函数内部调用)。