窗口和控件闪烁解决方案

对于MFC程序员来说做UI开发是痛苦的事情,不过大多数情况下我们都需要做这件事情,因为MFC自带的控件实在是太简陋了。这时候我们多半会涉及到自绘控件,随之而来的很可能就是窗口和控件的闪烁问题。这篇文章希望对MFC的窗口和控件闪烁问题做一个尽量全面的总结。

一、闪烁的原因

引起闪烁的原因很多,以至于网上有n多种解决闪烁问题的方法;如果你按照某一种方法做了仍然没有解决你的问题,请不要认定这个方法有问题,而是你没有对上号。如果你对这个解释不满意的话,我们就来深究一下到底是什么引起了闪烁。从原理上讲,闪烁是因为屏幕上连续的两次或多次输出画面差别比较大引起的,这是最根本的原因。因此如果窗口绘制差别不大,即使刷新再频繁,也不会引起闪烁。但是差别较大的画面输出一定会引起闪烁吗?还有一个因素要考虑进来,就是屏幕的刷新频率。根据显卡和显示器的不同,屏幕的刷新周期是不一样的,虽然这个参数的差别对界面开发的影响几乎可以忽略,但是如果你真的从思想上理解了这一点,你就会立即明白为什么双缓冲技术能够帮助我们解决一部分闪烁问题。

二、再谈闪烁的原因

虽然第一部分的描述对我们有一些启发,但我们还是应该更深入一些!哪些情况下会导致我们的窗口或控件输出连续的差别较大的绘制界面呢?

1、绘制界面太复杂,一个刷新周期内绘制不完,每次都输出一部分绘制结果,导致几次刷新闪烁。

我们的绘制过程都是通过很多个绘制语句组成的,如果这些语句加起来的时间大于一个刷新周期,那么就很可能引起闪烁。通常的解决办法是去掉中间过程的刷新,直到最后整体绘制完毕再一次性刷新。是不是似曾相识,这就是双缓冲技术的原理!但是有些情况是双缓冲也无能为力的,后面再讲。

2、绘制过程很简单,但是需要频繁刷新。

这种情况下我们首先需要弄清楚频繁刷新的原因是什么,不同的原因对应不同的解决办法。但是归根结底,我们还是为了减少刷新的次数或者尽量去掉中间输出差别较大的绘制输出。

3、刷新过程。

对于窗口或控件的界面显示,windows系统有一套绘制和刷新的规则,绘制或刷新的时机选择也是影响闪烁的重要因素。如果再与上面两条结合起来,某些情况下引起闪烁的原因确实非常复杂。只有我们分析出问题所在,才能用正确的方法解决之。

三、几种消除闪烁的解决方案

1、尽量减少重复绘制

MFC的窗口和控件刷新有一套很复杂的规则,如果我们能深入理解,正确应用的话就能避免一部分闪烁。比如尽量用 InvalidateRect() 函数代替 Invalidate() 函数,InvalidateRect() 函数只刷新界面上指定的区域,如果我们的界面上只有一小部分需要频繁刷新,那么用这个函数代替 Invalidate() 的话,解决闪烁问题的效果是非常明显的。这个函数已经封装到MFC的CWnd类中(也有API函数)。

void InvalidateRect(LPCRECT lpRect, BOOL bErase = TRUE);

其中,lpRect指向一个方形区域,该区域将被添加到需要更新的区域列表中,bErase指定刷新时是否更新区域背景。

如果我们需要刷新的区域是不规则的,比如是几个区域的组合,或者是某区域中去掉一部分,这时候用 InvalidateRect() 不能满足我们的需求,我们可以用 InvalidateRgn() 函数。

void InvalidateRgn (CRgn* pRgn, BOOL bErase = TRUE);

其中,pRgn指向需要刷新的区域。下面是一段示例代码:

Crect rectClient;

CRgn rgn1, 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就可以了,代码如下:

BOOL CMyWnd::OnEraseBkgnd(CDC* pDC)

{

     return TRUE;

     // return CWnd::OnEraseBkgnd(pDC);  // 注释掉默认语句

}

4、双缓冲

也许你已经听说过双缓冲这种方法了,的确,多数情况下双缓冲能很好的解决我们的窗口闪烁问题,尤其是涉及到窗口自绘的时候。双缓冲的基本原理是首先将复杂的绘制结果输出到内存DC上,然后再一次性输出到真正的窗口DC,这样就避免了由于绘制时间占用多个刷新周期,而导致一次绘制引起短时间多次输出产生闪烁。双缓冲方法结合上一个方法,可以解决大部分自绘窗口的闪烁问题。具体的双缓冲示例代码如下:

void CMyWnd::OnPaint()

{

CPaintDC dc(this);

CRect rectClient;

GetClientRect(&rectClient);

CDC dcMem;

CBitmap bmpMem;

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() 一次性完成所有窗口的调整。

7、拖动和调整大小时的虚线框

当以上方法无效或者实现起来过于复杂,有没有更统一更简洁的方法呢?可能你曾经注意到Windows操作系统有这样一种视觉效果(右击我的电脑-> 属性-> 高级-> 设置 -> 视觉效果-> 自定义,去掉“拖拉时显示窗口内容”选项),当你拖动和调整窗口大小时,并不是即时显示窗口内容,而是出现一个虚线框,当调整结束时才一次性绘制最终界面。这时一个非常好的防止闪烁的方法,我们来看看怎么实现这种效果。

比较复杂的方法是自己画虚线框,响应WM_MOVING消息画虚线框,响应WM_MOVE消息绘制窗口内容,不过这个方法的难度可想而知,具体内容可以查看这个讨论帖 http://topic.csdn.net/u/20070519/13/c4f0e32a-552c-4b66-9e9e-1b68f6c7c104.html。

有没有简单的方法呢?调用 SystemParametersInfo 这个API函数可以改变系统“拖拉时显示窗口内容”项的设置,但是如果我们设置以后,系统其他窗口的行为也将被改变。其实我们只要判断什么时候需要绘制虚线框,此时调用SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, FALSE, NULL, SPIF_SENDWININICHANGE),然后在拖动完毕需要绘制的时候调用 SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, TRUE,
NULL, SPIF_SENDWININICHANGE) 恢复设置就可以了。当然如果希望完全不影响系统原来的设置,我们只要每次都先查询一下系统原设置,然后恢复设置就可以了。

具体处理过程是在CDialog的OnNcLButtonDown消息响应函数中,当用户点击对话框的非客户区时该函数会被调用,而我们移动窗口或者调整窗口大小都是要点击非客户区(标题栏或边框)触发该消息。拖动过程中的处理是在CDialog::OnNcLButtonDown(nHitTest, point)中完成的,因此,我们只要按如下代码实现即可:

void CMyDlg::OnNcLButtonDown(UINT nHitTest, CPoint point)
{
    // 1,查询当前系统“拖动显示窗口内容”设置
    SystemParametersInfo(SPI_GETDRAGFULLWINDOWS, 0, &m_bDragFullWindow, NULL);

    // 2,如果需要修改设置,则在每次进入CDialog::OnNcLButtonDown默认处理之前修改
    if(m_bDragFullWindow)
         SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, FALSE, NULL, NULL);

    // 3,默认处理,系统会自动绘制虚框
    CDialog::OnNcLButtonDown(nHitTest, point); 

    // 4,默认处理完毕后,还原系统设置
    if(m_bDragFullWindow)
         SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, TRUE, NULL, NULL);
}
时间: 2024-10-01 21:09:12

窗口和控件闪烁解决方案的相关文章

C#控件闪烁的解决方法

本文实例讲述了C#控件闪烁的解决方法.分享给大家供大家参考.具体分析如下: 如果你在Form中绘图的话,不论是不是采用的双缓存,都会看到图片在更新的时候都会不断地闪烁,解决方法就是在这个窗体的构造函数中增加以下三行代码: 请在构造函数里面底下加上如下几行: 代码如下: SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); // 禁止擦除背景. SetStyle(Co

C++windows内核编程笔记day09_day10,对话框和窗口基本控件等的使用

//设置字体颜色 SetTextColor(hdc,RGB(255,0,0)); //窗口背景 //wce.hbrBackground=(HBRUSH)(COLOR_WINDOW+1); //wce.hbrBackground=CreateSolidBrush(RGB(0,0,255)); //设置字体背景 SetBkColor(hdc,RGB(0,0,200)); //设置字体背景模式 SetBkMode(hdc,TRANSPARENT);//字体背景透明 //创建字体,成功返回字体,失败返回

【工具】获取窗口与控件信息

[工具]获取窗口与控件信息 常言道,工欲善其事,必先利其器.在脚本中常见的一种操作是操作窗口或控件,在操作之前,首先必须获取目标的各种信息,这时就要用上辅助工具了.本文会介绍一些获取窗口.控件信息的常用工具,这里的先后顺序是随意安排.如果目前用的没什么问题就继续用着,如果对某些地方不满意则可试试其他.简单的截图不容易全面反映整个工具的功能和特色,使用才能获得真实体验. Active Window Info 评论:这个最初来自于 AutoIt3 且安装包中自带的工具,就无需过多介绍了.功能简陋,但

如何防止ListView控件闪烁

beginupdate()和endupdate()之间写代码 ListView1.Items.BeginUpdate; ListView1.Items.Add('AAA'); ListView1.Items.EndUpdate; 来自为知笔记(Wiz) 如何防止ListView控件闪烁,布布扣,bubuko.com

Xceed Docking Windows for .NET窗口分组控件下载及详细介绍

Xceed Docking Windows for .NET是一款窗口停靠和浮动.窗口自动隐藏.窗口分组控件,具有完美的外观,支持MDI应用程序以及tabbed-MDI,可以保存和加载窗口布局,可以包含任何.NET控件. 具体功能: 平稳的窗口自动隐藏功能 支持Visual Studio 窗口停靠界面 支持停靠一个窗口到浮动窗口,不仅是主窗口 停靠窗口根据主窗口自动调整大小 支持为Tabs设置图标 支持设置窗口显示和隐藏速度 创建任何数量的窗口分组以及水平和垂直tabbed 支持设置自动隐藏Ta

C#中父窗口和子窗口之间控件互操作实例

本文实例讲述了C#中父窗口和子窗口之间控件互操作的方法.分享给大家供大家参考.具体分析如下: 很多人都苦恼于如何在子窗体中操作主窗体上的控件,或者在主窗体中操作子窗体上的控件.相比较而言,后面稍微简单一些,只要在主窗体中创建子窗体的时候,保留所创建子窗体对象即可. 下面重点介绍前一种,目前常见的有两种方法,基本上大同小异: 第一种,在主窗体类中定义一个静态成员,来保存当前主窗体对象,例如: 代码如下: public static yourMainWindow pCurrentWin = null

[QT]在子窗口或者控件中绘图

要在子窗口中绘图,有2种方法: 1.重写子窗口的控件类(即继承该类,并重载其paintEvent()方法),实现其paintEvent()方法,然后在ui里面将原来的控件提升(promote to)为新类. (注:使用QPainter画图时,只能指定所属为当前类的引用Qpainter painter = new QPainter(this),所以应重写paintEvent()) 填写好新类的类名及头文件名. 2.使用事件过滤器,重写eventFilter(),在子窗口或控件中注册事件过滤器(in

C#窗体加载和控件加载不同步导致控件闪烁

窗体加载和控件加载不同步导致的控件闪烁现象:// 代码块加在父窗体中的任意位置,解决窗体加载和控件加载不同步导致的控件闪烁问题        protected override CreateParams CreateParams        {            get            {                CreateParams cp = base.CreateParams;                cp.ExStyle |= 0x02000000;     

【WPF】总结窗口和控件拖拽的实现

前文 本文只对笔者学习掌握的一般的拖动问题的实现方法进行整理和讨论,包括窗口.控件等内容的拖动. 希望本文能对一些寻找此问题的解决方法的人和一些刚入门的人一些帮助.笔者为WPF初学者,能得到各位的批评指正也是荣幸万分.有更好更多的方法,劳烦与我分享,不胜感激. 本文的各种实现方法其他博客中也都有提及,很多文章内容详实,有图有代码,笔者就不重复造轮子了.就写写自己的一些理解吧. 关键词 Window, UserControls, drag, Thumb 参考资料 http://www.cnblog