OnDraw()和OnPaint()兄弟

来源:互联网 发布:编辑照片的软件 编辑:程序博客网 时间:2024/04/28 01:08

http://hi.baidu.com/mikenoodle/blog/item/6d2f08b36b3ed1a1d9335afb.html

 经常有朋友问雷神这样的问题:
我在视图画的图象或者文字,当窗口改变后为什么不见了?
OnDraw()和OnPaint()两个都是解决上面的问题,有什么不同?

雷神在这里一并解答一下吧。
OnDraw()和OnPaint()好象兄弟俩,因为它们的工作类似。

至于不见了的问题简单,因为当你的窗口改变后,会产生无效区域,这个无效的区域需要重画。一般Windows回发送两个消息WM_PAINT(通知客户区有变化)和WM_NCPAINT(通知非客户区有变化)。非客户区的重画系统自己搞定了,而客户区的重画需要我们自己来完成。这就需要OnDraw()或OnPaint()来重画窗口。

OnDraw()和OnPaint()有什么区别呢?
首先:
我们先要明确CView类派生自CWnd类。而OnPaint()是CWnd的类成员,同时负责响应WM_PAINT消息。OnDraw()是CVIEW的成员函数,并且没有响应消息的功能。这就是为什么你用VC成的程序代码时,在视图类只有OnDraw没有OnPaint的原因。

其次:
我们在第《每天跟我学MFC》3的开始部分已经说到了。要想在屏幕上绘图或显示图形,首先需要建立设备环境DC。其实DC是一个数据结构,它包含输出设备(不单指你17寸的纯屏显示器,还包括打印机之类的输出设备)的绘图属性的描述。MFC提供了CPaintDC类和CWindwoDC类来实时的响应,而CPaintDC支持重画。

当视图变得无效时(包括大小的改变,移动,被遮盖等等),Windows 将 WM_PAINT 消息发送给它。该视图的 OnPaint 处理函数通过创建 CPaintDC 类的DC对象来响应该消息并调用视图的 OnDraw 成员函数。通常我们不必编写重写的 OnPaint 处理成员函数。

///CView默认的标准的重画函数
void CView::OnPaint()
{
     CPaintDC dc(this);
     OnPreparDC(&dc);
     OnDraw(&dc); //调用了OnDraw
}

既然OnPaint最后也要调用OnDraw,因此我们一般会在OnDraw函数中进行绘制。下面是一个典型的程序

///视图中的绘图代码首先检索指向文档的指针,然后通过DC进行绘图调用。
void CMyView::OnDraw( CDC* pDC )
{
     CMyDoc* pDoc = GetDocument();
     CString s = pDoc->GetData();   // Returns a CString
     CRect rect;

     GetClientRect( &rect );
     pDC->SetTextAlign( TA_BASELINE | TA_CENTER );
     pDC->TextOut( rect.right / 2, rect.bottom / 2,
                   s, s.GetLength() );
}

最后:
现在大家明白这哥俩之间的关系了吧。因此我们一般用OnPaint维护窗口的客户区(例如我们的窗口客户区加一个背景图片),用OnDraw维护视图的客户区(例如我们通过鼠标在视图中画图)。当然你也可以不按照上面规律来,只要达到目的并且没有问题,怎么干都成。

补充:
我们还可以利用Invalidate(),ValidateRgn(),ValidateRect()函数强制的重画窗口,具体的请参考MSDN吧。

改变窗口大小,或者窗口被遮挡(去除遮挡)时,窗口自动产生WM_PAINT消息重绘有变化的区域(无效区)。
当你需要人工指定窗口重绘时,依此原理,可以指定窗口客户区的某些区域无效,让其重绘。
Invalidate指定整个客户区无效;
InvalidateRect和InvalidateRgn分别指定某个矩形或某区域无效。
这三个函数向窗口发送了WM_PAINT消息进入消息队列。当队列较长时(前面还有很多消息未处理),就不能即时刷新了。
你可以在上面三个函数后加一句UpdateWindow(),这样就可以立即刷新窗口了。

 

 

 

一款属于自己的窗体基础类库,与MFC不同的是用这个类库建立的窗体形状是不规则的(用户定义),且窗体上的所有控件都没有handle(只是一个用户定义的region, 或者说叫hotspot),而是从一个抽象类CControl派生。由于所有可视部分都采用用户贴图(用GDI+),这样就必然要和WM_PAINT打交道。当年用VB编程的时候,对Paint事件就不甚了解,现在果然遇到了不小的问题……经过半天的研究,终于基本弄明白了WM_PAINT的来龙去脉,在这里总结一下。

先看官方教材,在MSDN里的Platform SDK - Windows GDI一章中给出的WM_PAINT发送条件是"The WM_PAINT message is sent when the system or another application makes a request to paint a portion of an application's window."那什么时候系统或其他应用程序会发送重画请求呢?需要重画的"portion"又有多大呢?再往下看,"The message is sent when the UpdateWindow or RedrawWindow function is called, or by the DispatchMessage function when the application obtains a WM_PAINT message by using the GetMessage or PeekMessage function. "这就说明这个WM_PAINT消息既可能由系统发送,也可能由应用程序人工发送。这个很好理解。当窗口的一部分被其它窗口遮盖而复原,或者从最小化状态恢复到正常状态时,系统自然会要求窗口重画。系统重画窗口的条件可以参见MSDN/Platform SDK/Windows GDI/Painting and Drawing/When to Draw in a window. 而有时候我们需要窗口的某些部分作出改变,就得人工要求窗口重画(通过调用UpdateWindow和RedrawWindow)。比如我的基础类库里窗口上没有任何Windows意义上的控件(有hWnd),只是人为定义某一个区域是一个“按钮”,当鼠标指向这个区域时加载hover图像以获得hottrack效果。这时操作系统自然不会认为有重画的必要,但程序却必须重画,这时就得人工发送WM_PAINT消息了。注意不要傻乎乎地直接用SendMessage或PostMessage发送WM_PAINT,后面会解释原因。

由于重画很费时间和资源,并且也不是应用程序的“主业”,因此系统也知道要尽量减少重画的次数。系统只在应用程序的消息队列为空的时候才发送WM_PAINT,这就是为什么当程序死锁时窗口图像不会更新。同样为了减少重画的工作量,Windows提出了update region的概念,"The update region identifies the portion of a window that is out-of-date or invalid and in need of redrawing. The system uses the update region to generate WM_PAINT messages for applications and to minimize the time applications spend bringing the contents of their windows up to date. "也就是说,Windows会判断窗口的哪些区域需要重画,这个区域就是update region. 比如原来在窗口上面的一个窗口现在挪走了,系统就把新露出来的区域定义为update region(这个过程称为invalidate)。系统不断检测一个窗口的update region是否为空,当update region不为空并且应用程序没有消息要处理(消息队列为空)的时候,系统就通过WM_PAINT告诉应用程序“现在没事干了?窗口的一部分需要重画,你把这一部分重画一下”。应用程序重画了窗口之后,把update region重新设置为空(这个过程称为validate),如此不断循环。如果消息队列不为空,系统就把update region不断更新(采用取并集的方法),等消息队列为空的时候一起处理。这就大大减少了重画的次数。

这样你或许就明白了为什么不能直接用SendMessage和PostMessage发送WM_PAINT的原因:由于没有invalidate,系统认为窗口没有更新的必要,于是就对发来的WM_PAINT消息不理不睬。解决方案就是——我们自己invalidate!相关的API就是InvalidateRect()和InvalidateRgn(). 画完了之后用ValidateRect()和ValidateRgn()告诉系统“我画完了”就行了。可以把invalidate过程看成类似CombineRgn()取并集,把validate过程看成取差集即可。还有一些相关的API: GetUpdateRect(), GetUpdateRgn(), ExcludeUpdateRgn(), 从名字就能猜出个大概,各位可以自行去查MSDN.

在WM_PAINT消息处理过程中有两个不得不提到的函数:BeginPaint()和EndPaint(). 只有WM_PAINT消息处理能使用这两个函数。实际上默认消息处理函数DefWindowProc()对WM_PAINT的处理方式就是:

case WM_PAINT:

    PAINTSTRUCT ps;

    BeginPaint (hWnd, &ps);

    EndPaint (hWnd, &ps);

    return 0;

BeginPaint()和EndPaint()之所以不可或缺,就是因为它们实现了validate过程。BeginPaint()的主要任务之一就是validate. 如果在WM_PAINT的消息处理中直接return 0,update region就始终不为空,系统就会不停地发送WM_PAINT消息。EndPaint()负责释放BeginPaint()返回的DC,做好善后工作(比如重新显示BeginPaint()隐藏起来的光标)。

最后还有一点需要额外说明:用WM_PAINT处理重画是异步(asynchronous)的。也就是说,在invalidate之后窗口并不会立即重画而是等到消息队列为空时再重画,这样就有一个时间差。这个事件差有时短到不被注意,但有时就是个大问题(尤其是当程序需要执行耗费时间的任务,如串口I/O)。这时可以采用同步重画法,直接用GetDC()获得hDC执行重画操作。如果非要使用WM_PAINT来同步重画(个人比较喜欢这种方法,和重画有关的代码就应该在WM_PAINT的处理程序里嘛),可以使用UpdateWindow()和RedrawWindow(). 这两个API函数会直接把WM_PAINT送进窗口的消息队列而不是应用程序的消息队列,这样就不用等到最后了。注意前者当update region不为空时才会发送WM_PAINT,后者的控制选项更为丰富。

 

对于窗口程序,一般有个特点:窗口大部分的区域保持不变,只有不分区域需要重新绘制。如果将整个窗口全部刷新的画,就做了许多不必要的工作,因而,MFC采用了一套基于无效区的处理机制。在分析无效区处理之前,我们要明白一个现实,现在的机器还不够牛,如果够牛的话,我们干脆将整个窗口不断的重新绘制好了。事实上即使够牛也不行,对于一个单线程程序,通过一个while循环不断的刷新窗口,程序也无法相应其他消息(除非使用多线程),看来使用无效区的处理机制还是有其必然性的。

     VC程序是基于消息机制的,你所做的任何操作,比如点击鼠标,拖动窗口,首先进入系统的消息队列。这里的系统消息队列包括多个程序的消息,系统再将消息发送给相应的程序。既然是队列,这就有一个先进先出的问题,屏幕上的无效区更新消息出现的频率就会特别高。比如当左上角更新的消息还没有处理,右下角更新的消息已经过来了。为了避免多次处理WM_PAINT消息,系统就将这些窗口更新消息合并到一条,只是将无效区范围变成包括这两次更新无效区范围在内的矩形区域。这样就减少了WM_PAINT消息的处理次数,提高了效率。

     那么,在OnPaint消息处理函数中,又是怎样实现更新无效区的呢?首先,要明白MFC中所有绘图操作都是基于设备描述表(Device Context,简称DC)的,具体信息可参看任何一本VC教材。DC中包含了绘图设备的各种信息,对于屏幕绘图,其实就是有一块内存(显存),专门用来存放要显示到屏幕上的信息,显示器以85HZ的频率(我以前的显示器)将其内容刷新的屏幕上。这里就到了关键点,显示器的刷新是将显存中的内容完全更新到显示器上,不存在无效区处理的问题,那么,无效区的处理一定发生在DC的绘图处理上。事实确实如此,当程序调用OnPaint消息时,首先将无效区范围传递给DC,DC在进行绘图操作时,就只更新无效区范围内的信息,其他地方的不管,这就提高了效率。

     现在你明白OnPaint的处理是怎么一回事了吧?这里还想说一下Invalidate和UpdateWindow的区别。Invalidate在消息队列中加入一条WM_PAINT消息,其无效区为整个客户区。而UpdateWindow直接发送一个WM_PAINT消息,其无效区范围就是消息队列中WM_PAINT消息(最多只有一条)的无效区。效果很明显,调用Invalidate之后,屏幕不一定马上更新,因为WM_PAINT消息不一定在队列头部,而调用UpdateWindow会使WM_PAINT消息马上执行的,绕过了消息队列。如果你调用Invalidate之后想马上更新屏幕,那就加上UpdateWindow()这条语句

原创粉丝点击