窗口和控件闪烁解决方案.docx
- 文档编号:3975801
- 上传时间:2022-11-26
- 格式:DOCX
- 页数:12
- 大小:37.32KB
窗口和控件闪烁解决方案.docx
《窗口和控件闪烁解决方案.docx》由会员分享,可在线阅读,更多相关《窗口和控件闪烁解决方案.docx(12页珍藏版)》请在冰豆网上搜索。
窗口和控件闪烁解决方案
打断你们一下,又一位大神,@心羽,跟楼上完全两种不同的风格,他主张全部用贴图解决问题,也让我大开眼界,谢谢你,让我们用最热烈的掌声欢迎他
,
自绘对话框重绘是会闪,请问如何避免闪屏?
-VC/MFC/界面
收藏人:
峰中之王
2010-10-23 | 阅:
转:
|
大
中
小
| 分享
如果一个程序出现闪烁现象,会让人觉得程序编写人员很马虎,缺乏对细节的足够重视。
Windows程序的任何部分都没有任何理由出现闪烁现象。
这篇文章的目的是告诉读者如何使用相关的技术防止窗口出现闪烁效果。
什么是闪烁
闪烁可以这样定义:
当后面一幅图像以很快的速度画在前面一幅图像上时,在后面图像显示前,你可以很快看到前面那一个图像,这样的现象就是闪烁。
我认为,闪烁会让使用者对程序很不满,原因是:
如果用户接口编码如此糟糕,那么程序的其他部分呢,如何能相信数据的正确性呢?
一个具有平滑,快速相应的程序会给用户带来信心,这个道理很简单。
程序出现闪烁可以由多种形式造成,最常见的原因是窗口大小发生改变时,其内容重画造成闪烁。
仅仅画一次
这是一个黄金法则,在任何计算机(Windows或者你使用的任何操作系统)上处理画法逻辑都需要遵循,即永远不要将同一像素画两次。
一个懒惰的程序员常常不愿意在画法逻辑上投入过多精力,而是采用简单的处理逻辑。
要避免闪烁,就需要确保不会出现重复绘制的情况发生。
现在,WIndows和计算机还是很笨的,除非你给他们指令,否则他们不会做任何事情。
如果闪烁的现象发生,那是因为你的程序刻意地多绘制了屏幕的某些区域造成的.这个现象可能是因为一些明确的命令,或者一些被你忽视了的地方。
如果程序有闪烁的现象出现,你需要你知道如何找到好的方案去解决这个问题。
WM_ERASEBKGND
通常,首先需要怀疑的是WM_ERASEBKGND消息。
当一个窗口的背景需要被擦除时,这个消息会被发送。
这是因为窗口的绘画通常经历了两个过程
WM_ERASEBKGND:
清除背景
WM_PAINT:
在上面绘制内容
这两个过程让窗体在绘制内容时变得很简单,即:
每次当收到WM_PAINT消息时,你知道已经有了一个新画布等待去绘制。
然而,画窗口两次(一次是通过WM_ERASEBKGND画背景,另外一次是WM_PAINT)将会导致窗口出现比较糟糕的闪烁现象。
只要看看标准的编辑框-打开Windows的写字板并改变窗口大小,就可以看到那种闪烁的效果。
那么,如何避免窗口背景的重刷呢?
有如下两种方法:
设置窗口背景刷子为NULL(当注册Windows类时,设置WNDCLASS结构中的hbrBackground成员为零)
在WM_ERASEBKGND消息处理时返回非零值
以上任何一种方法都可以阻止WM_ERASEBKGND消息去清除窗口。
其中,第二个方案的通常可以以如下代码实现:
caseWM_ERASEBKGND:
return1;
当你标记窗口内容无效并试图更新时,还有如下办法可以防止WM_ERASEBKGND消息:
InvalidateRect函数的最后一个参数可以指明在下一次窗口重画时,是否窗口的部分背景会被重刷。
将该参数置为False可以防止当窗口需要重画时系统发出WM_ERASEBKGND消息。
InvalidateRect(hwnd,&rect,FALSE);
不该画的时候一定不要画
有一个比较普遍的现象:
即使窗口中只有一个小的部分发生了改变,往往所有的部分都会被重画。
比如,经常地,当窗口大小被改变时,一些(不是所有)的程序会重画所有的窗口。
通常,这是个是不必要的,这是因为当窗口大小被改变时,经常是之前窗口的内容是不变的,仅仅是改变大小造成的一个小的边界区域需要重画。
此时,没有必要重画所有区域。
如果在这里多注意,多考虑,就可以使用好的算法以使得一次只有最小的部分被画。
系统中每个窗口都有更新区域。
这个区域描述了窗口中变得无效需要重画的地方。
如果一个窗口仅仅其需要更新的区域,不多绘制其他地方,那么窗口的绘制效果将会非常快。
有几种方法可以获得窗口的更新区域。
通过GetUpdateRgn函数可以获得准确的更新区域,这个函数返回的结果可以使矩形的区域也可以是非矩形的区域。
通过GetUpdateRect函数可以获得需要更新的最小矩形区域。
通常使用矩形的更新区域比较容易。
第三个方法是在BeginPaint/EndPaint中得到PAINTSTRUCT结构,从而得到准确的更新区域信息。
一个常规的画法函数是这样的:
PAINTSTRUCTps;
HDChdc;
caseWM_PAINT:
hdc=BeginPaint(hwnd,&ps);
//dopainting
EndPaint(hwnd,&ps);
return0;
BeginPaint函数初始化PS(PAINTSTRUCT)结构,其中,成员rcPaint是一个RECT结构,描述了包含了需要更新的最小矩形区域(就像GetWindowRect函数)。
如果仅仅在这个矩形区域上绘制窗口,速度上绘有很好地提高。
现在,当使用BeginPaint/EndPaint时Windows会自动剪切掉画在更新区域外面的部分。
这意味着,你没有机会画到更新区域以外的地方。
可能你会认为,如果是这样的话,花功夫确保代码不试图画到更新区域外是没有意义的,反正没有画出任何东西来。
然而,你仍然可以避免不必要的API调用和相关计算,所以,我认为放一些精力在如何工作地更快上是绝对值得的。
如果还是不能解决
有些时候,当你花了很多努力去考虑非常好的画法时,发现窗口还是会被全部刷新。
这通常是由两个Window类的属性造成的:
CS_VREDRAW和CS_HREDRAW。
如果有其中一个标志被设置时,那么当窗口水平或者竖直方向有大小被改变时,其内容每次都会被重新刷新。
所有,你需要关掉这两个标志,解决的唯一的方式是在创建窗体和窗体类被注册时,确保这两个属性不被设置。
WNDCLASSEXwc;
wc.cbSize=sizeof(wc);
wc.style=0;/*CS_VREDRAW|CS_HREDRAW;*/
...
RegisterClassEx(&wc);
上面的例子描述了当窗体类被注册时,这两个属性不被设置的实现方法。
有一点需要注意:
如果主窗体有了这两个属性,即使子窗体没有重画标志,会导致所有子窗体在其大小被改变时会被重绘。
可以通过以下方式避免这个情况发生:
剪切子窗体
有时,闪烁的原因是因为当重画时,父窗体没有剪切其子窗体区域。
这样的结果导致,整个父窗口内容被重画,而子窗体又被显示在了上面(造成闪烁)。
这个可以通过在父窗体上设置WS_CLIPCHILDREN来解决。
当这个标志被设置时,被子窗体占据的任何区域将会被排除在更新区域外。
因此,即使你尝试在子窗体所在的位置上绘制(父窗口的内容),BeginPaint中的剪切区域也会阻止其绘制效果。
双缓冲和内存设备描述表(MemoryDeviceContext,简称Memory-DC)
常见的彻底避免闪烁的方法是使用双缓冲。
其基本的思路是:
将窗体的内容画在屏幕外的一个缓冲区内,然后,将该缓冲区的内容再传递到屏幕上(使用BilBlt函数)。
这是一个非常好的减少闪烁的方法,但是经常被滥用,特别是当程序员并不真正地理解如何有效地绘制窗口时。
典型的双缓冲代码如下:
HDChdcMem;
HBITMAPhbmMem;
HANDLEhOld;
PAINTSTRUCTps;
HDChdc;
....
caseWM_PAINT:
//GetDCforwindow
hdc=BeginPaint(hwnd,&ps);
//Createanoff-screenDCfordouble-buffering
hdcMem=CreateCompatibleDC(hdc);
hbmMem=CreateCompatibleBitmap(hdc,win_width,win_height);
hOld=SelectObject(hdcMem,hbmMem);
//DrawintohdcMem
//Transfertheoff-screenDCtothescreen
BitBlt(hdc,0,0,win_width,win_height,hdcMem,0,0,SRCCOPY);
//Free-uptheoff-screenDC
SelectObject(hdcMem,hOld);
DeleteObject(hbmMem);
DeleteDC(hdcMem);
EndPaint(hwnd,&ps);
return0;
这个方法比较慢,因为在每次窗体需要重画的时候内存设备描述表(Memory-DC)都需要被重新创建。
更有效的方法是,仅仅创建内存设备描述表(Memory-DC)一次,并使其足够大到能满足任何时候的整个窗体刷新。
当程序结束时,再销毁这个内存设备描述表(Memory-DC)。
这两种方法都存在对内存开销的问题,特别是如果内存设备描述表(Memory-DC)是针对真个屏幕的大小。
双缓冲也需要两倍的时间去画。
这是因为其第一次是在内存设备描述表(Memory-DC)上画,然后再使用BitBlt画回到屏幕上。
当然,好的显卡会使BitBlt更快,但是仍然会耗CPU时间。
如果程序需要显示相当复杂的信息,比如像网页,那么你应该使用内存设备描述表(Memory-DC)。
比如IE,如果不使用双缓冲,是没有办法在绘制网页时不闪烁的。
没有必要将双缓冲技术用于整个窗体的绘制中。
可以这样设想,窗口中仅仅有一个小部分包含了复杂的图形对象(比如半透明的位图或者其他)。
你应该将内存设备描述表(Memory-DC)仅仅用于着一个小区域,其他区域使用常规的方法。
有时,通过仔细的思考,经常可以避免使用双缓冲而直接将结果画到屏幕上。
只要你不破坏黄金法则,即“永远不要将一个像素画两次”,就可以防止闪烁的出现。
避免过度绘制
我想说的关于这个话题是这样的:
有一个需要自己定义画法的窗体的标题栏。
首先,你画了标题,接着在上面画一些其他的图形。
现在,只要标题需要被重画,就会出现闪烁现象。
这是因为你没有合乎黄金法则。
这里,标题被很快地显示在其他图形在上面绘制时,导致了闪烁。
有两种技术可以组织这种类型的闪烁。
第一个是使用剪切,第二个是使用你的大脑。
使用剪切时,你可以使用ExcludeClipRect函数在设备描述表中去标记一个特定的区域。
当一个区域被标记上时,即使在该区域上面重画也不会产生效果。
一旦背景已经被绘制了,可以通过SelectClipRgn移掉该标记的区域,其他图形能被画到前面标记的区域上。
通过准确的标记(剪切),可以在很多时候被避免过度绘制。
另外一个方案就是找更聪明的解决办法。
比如,当你需要画一个表格,通常应该先画空的背景,再画网格线从而产生表格。
但是,这个方法会使网格线产生闪烁,这是因为在网格线被画之前,下面背景被很快地显示了一下。
然而可以使用不同的做法达到想要的结果。
即,不是一次画一个大的空背景,而是画一系列的空方块,每一个方块边是被一个像素的宽度分开。
这样,当画网格线时,他们刚好能被画到一个之前没有画过的地方。
其结果是不会有闪烁现象,因为没有像素被画了超过两次。
使用你的头脑去想一个好的算法可能需要长一点的时间,但是却是值得的,因为这能让结果更好。
结论
希望你再也不会问:
“为什么我的窗体会闪烁”这样的问题。
我已经讲解了闪烁的主要原因和解决办法。
如果你遇到了闪烁的问题,你应该能找到原因并且使用这里提到的技术来解决了
窗口和控件闪烁解决方案
(2010-07-0509:
42:
38)
转载▼
标签:
窗口闪烁
it
分类:
开发
对于MFC程序员来说做UI开发是痛苦的事情,不过大多数情况下我们都需要做这件事情,因为MFC自带的控件实在是太简陋了。
这时候我们多半会涉及到自绘控件,随之而来的很可能就是窗口和控件的闪烁问题。
这篇文章希望对MFC的窗口和控件闪烁问题做一个尽量全面的总结。
一、闪烁的原因
引起闪烁的原因很多,以至于网上有n多种解决闪烁问题的方法;如果你按照某一种方法做了仍然没有解决你的问题,请不要认定这个方法有问题,而是你没有对上号。
如果你对这个解释不满意的话,我们就来深究一下到底是什么引起了闪烁。
从原理上讲,闪烁是因为屏幕上连续的两次或多次输出画面差别比较大引起的,这是最根本的原因。
因此如果窗口绘制差别不大,即使刷新再频繁,也不会引起闪烁。
但是差别较大的画面输出一定会引起闪烁吗?
还有一个因素要考虑进来,就是屏幕的刷新频率。
根据显卡和显示器的不同,屏幕的刷新周期是不一样的,虽然这个参数的差别对界面开发的影响几乎可以忽略,但是如果你真的从思想上理解了这一点,你就会立即明白为什么双缓冲技术能够帮助我们解决一部分闪烁问题。
二、再谈闪烁的原因
虽然第一部分的描述对我们有一些启发,但我们还是应该更深入一些!
哪些情况下会导致我们的窗口或控件输出连续的差别较大的绘制界面呢?
1、绘制界面太复杂,一个刷新周期内绘制不完,每次都输出一部分绘制结果,导致几次刷新闪烁。
我们的绘制过程都是通过很多个绘制语句组成的,如果这些语句加起来的时间大于一个刷新周期,那么就很可能引起闪烁。
通常的解决办法是去掉中间过程的刷新,直到最后整体绘制完毕再一次性刷新。
是不是似曾相识,这就是双缓冲技术的原理!
但是有些情况是双缓冲也无能为力的,后面再讲。
2、绘制过程很简单,但是需要频繁刷新。
这种情况下我们首先需要弄清楚频繁刷新的原因是什么,不同的原因对应不同的解决办法。
但是归根结底,我们还是为了减少刷新的次数或者尽量去掉中间输出差别较大的绘制输出。
3、刷新过程。
对于窗口或控件的界面显示,windows系统有一套绘制和刷新的规则,绘制或刷新的时机选择也是影响闪烁的重要因素。
如果再与上面两条结合起来,某些情况下引起闪烁的原因确实非常复杂。
只有我们分析出问题所在,才能用正确的方法解决之。
三、几种消除闪烁的解决方案
1、尽量减少重复绘制
MFC的窗口和控件刷新有一套很复杂的规则,如果我们能深入理解,正确应用的话就能避免一部分闪烁。
比如尽量用 InvalidateRect() 函数代替 Invalidate() 函数,InvalidateRect() 函数只刷新界面上指定的区域,如果我们的界面上只有一小部分需要频繁刷新,那么用这个函数代替 Invalidate() 的话,解决闪烁问题的效果是非常明显的。
这个函数已经封装到MFC的CWnd类中(也有API函数)。
voidInvalidateRect(LPCRECTlpRect,BOOLbErase=TRUE);
其中,lpRect指向一个方形区域,该区域将被添加到需要更新的区域列表中,bErase指定刷新时是否更新区域背景。
如果我们需要刷新的区域是不规则的,比如是几个区域的组合,或者是某区域中去掉一部分,这时候用 InvalidateRect() 不能满足我们的需求,我们可以用 InvalidateRgn() 函数。
voidInvalidateRgn(CRgn*pRgn,BOOLbErase=TRUE);
其中,pRgn指向需要刷新的区域。
下面是一段示例代码:
CrectrectClient;
CRgnrgn1,rgn2;
GetClientRect(rectClient);
rgn1.CreateRectRgnIndirect(rectClient);
rgn2.CreateRectRgnIndirect(m_rectButton);
rgn1.CombineRgn(&rgn1, &rgn2, RGN_XOR);
InvalidateRgn(&rgn1,FALSE);
有的时候我们的窗口上有很多控件,如果是由我们负责控件刷新(比如窗口设置了WS_CLIPCHILDREN风格),我们最好判断不同情况下确实需要刷新的控件,而不是简单的将所有控件全部刷新一遍,以此将闪烁的影响减小到最小。
2、正确选择窗口重绘时机
Windows有很多刷新和重绘的函数,但是他们的特性和运行方式不尽相同,我们需要了解调用这些函数的注意事项,否则很可能因为实际情况跟我们的预期不同而引起闪烁。
Windows系统是通过WM_PAINT消息来通知界面重绘的,该消息一般由系统自动产生,比如当窗口被创建、改变大小、最大化、移动、覆盖等等,另外当UpdateWindow等函数被调用时也会产生WM_PAINT消息。
当窗口重绘时,并不一定整个窗口区域都需要刷新,而只是需要更新的那一部分,这部分区域叫做“无效区域”。
系统在发现消息队列空闲时会检查无效区域,如果存在就会发送WM_PAINT消息进行刷新。
Invalidate()、InvalidateRect()、InvalidateRgn()这些函数都只是产生无效区域,而并没有发送WM_PAINT消息,也就是说我们调用这些 Invalidate() 函数时,并不一定会使窗口立即刷新,而是要等到下次WM_PAINT消息进入到消息队列时才行。
如果要使重绘立即执行,可以调用 UpdateWindow() 函数或者 RedrawWindow() 函数强制刷新。
Windows的窗口重绘时,会首先判断是否需要刷新背景,如果需要则首先刷新窗口背景,然后进入OnPaint()函数进行窗口内容的绘制。
这个过程中如果操作不当,也有可能引起闪烁。
当我们遇到闪烁问题,可以从以上窗口绘制机制中查找是否某些步骤的操作引起了闪烁。
比如我们在对一个CListCtrl控件进行频繁操作时(比如添加多个项或者修改内容),可以先调用 SetRedraw(FALSE),在操作全部完成后,再调用 SetRedraw(TURE) 完成一次性刷新。
3、控制窗口背景刷新
Windows窗口背景刷新默认情况下是系统帮你完成的,如果我们的窗口绘制内容和背景差别比较大,或者在刷新背景和刷新窗口绘制之间有一个明显的时间间隔,就有可能引起闪烁。
这个时候我们可能要禁止系统默认的背景绘制,而在窗口绘制函数中自行处理背景。
这时只要重载 OnEraseBkgnd() 函数,并直接返回TRUE就可以了,代码如下:
BOOLCMyWnd:
:
OnEraseBkgnd(CDC*pDC)
{
return TRUE;
// returnCWnd:
:
OnEraseBkgnd(pDC); // 注释掉默认语句
}
4、双缓冲
也许你已经听说过双缓冲这种方法了,的确,多数情况下双缓冲能很好的解决我们的窗口闪烁问题,尤其是涉及到窗口自绘的时候。
双缓冲的基本原理是首先将复杂的绘制结果输出到内存DC上,然后再一次性输出到真正的窗口DC,这样就避免了由于绘制时间占用多个刷新周期,而导致一次绘制引起短时间多次输出产生闪烁。
双缓冲方法结合上一个方法,可以解决大部分自绘窗口的闪烁问题。
具体的双缓冲示例代码如下:
voidCMyWnd:
:
OnPaint()
{
CPaintDCdc(this);
CRectrectClient;
GetClientRect(&rectClient);
CDCdcMem;
CBitmapbmpMem;
dcMem.CreateCompatibleDC(&dc);
bmpMem.CreateCompatibleBitmap(&dc, rectClient.Width(), rectClient.Height());
dcMem.SelectObject(&bmp);
// 此处将绘制内容输出到dcMem上
// dcMem.FillRect(rectClient, &brush);
dc.BitBlt(0, 0, rectClient.Width(), rectClient.Height(), &dcMem, 0, 0, SRCCOPY);
bmpMem.DeleteObject();
dcMem.DeleteDC();
}
5、合理设置WS_CLIPCHILDREN和WS_CLIPSIBLINGS风格
当我们的窗口界面有多层窗口组成时(比如包含多个控件的对话框),用到自绘窗口可能会经常碰到闪烁问题。
因为多层窗口会涉及到很多遮挡,重绘时一般涉及到主窗口和子窗口等多个窗口,而这些窗口的刷新可能不会在一个刷新周期内完成,从而引起闪烁。
这时我们可以通过设置WS_CLIPCHILDREN和WS_CLIPSIBLINGS这两个窗口风格来控制刷新行为。
Clip是裁剪的意思,两个属性的具体含义如下:
带有WS_CLIPCHILDREN风格表示裁剪掉子窗口的区域,即当该窗口重绘时,它的子窗口区域不刷新,而留给子窗口自己去刷新;
带有WS_CLIPSIBLINGS风格(只用于子窗口)表示裁剪掉兄弟窗口的区域,即当该窗口重绘时,与兄弟窗口重叠的区域将不会被刷新。
根据这些窗口行为,我们就能优化我们的界面刷新,控制一些窗口的刷新时机,或者减少重叠区域的重复刷新。
比如当对话框窗口放置了大量控件时,我们可以给对话框加上WS_CLIPCHILDREN风格来阻止一些不必要的刷新。
6、多层次窗口调整大小
如果窗口包含很多子窗口,当我们调整窗口大小时,可能要同时调整子窗口的位置和大小。
此时若使用 MoveWindow() 或 SetWindowPos() 等函数进行调整,由于这些函数会等窗口刷新完才返回,因此当有大量子窗口时,这个过程肯定会引起闪烁。
这时我们可以应用 BeginDeferWindowPos(),DeferWindowPos() 和 EndDeferWindowPos() 三个函数解决。
首先调用 BeginDeferWindowPos(),设定需要调整的窗口个数;然后用 DeferWindowPos() 移动窗口(并非立即移动窗口);最后调用 EndDeferWindowPos() 一次性完成所有窗口
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 窗口 控件 闪烁 解决方案