VC++ - 窗口重绘

 
 

一、简单介绍

1、何时重绘?常见的无非两种情况

(1)产生无效区时,此时会受到WM_PAINT消息,这是必须重绘无效区。

(2)没有产生无效区,但是我就是想往窗口上写东西时。

而对于每种情况所需要的函数也不相同

(1)对于产生无效区的重绘,必须调用BeginPaint得到DC,最后调用EndPaint释放DC.

(2)没有无效无效区的重绘,调用GetDC、GetWindowDC(可绘制非客户去)、CClientDC等方式得到DC,最后记得依然记得释放DC(注意释放的方式)。

同样都是重绘,为什么还要分情况调用不同的函数呢?其实很简单,产生无效区时并不是只要得到DC(像GetDC那样)就可以进行绘制了,在其他还有一些细致的工作要做,比如将无效区变为有效(避免一直发送WM_PAINT消息),得到无效区域信息等等,这些都是BeginPaint需要完成的工作。

所以笔者觉得,一般只要知道在什么情况下改用什么函数进行重绘就行了。

若想寻根究底,请见第二版本 - 深入讲解


二、深入讲解

其实MFC的重绘并不复杂(流程如下):

1、窗口无效,可以使用InvalidateRect等函数将窗口某些区域强制无效,以达到强制重绘效果!

2、发送WM_ERASEBKGND消息擦除(重绘)背景,我们可以设置不重绘背景,例如在通过InvalidateRect等函数将窗口某些区域强制无效时可以指定其第三个参数来控制是否重绘背景,如InvalidateRect(hWnd,&rect,FALSE);。

窗口接收到此消息后正常情况下会被DefWindowProc函数默认处理,在DefWindowProc中会调用OnEraseBkgnd消息处理函数对背景进行擦除(重绘)。如果我们希望修改窗口的背景(如实现图片背景)时可以重载OnEraseBkgnd函数,并在此函数中绘制相应图案已到达视觉上的特殊背景效果!详见《WM_ERASEBKGND消息详解》

3、紧接着发送WM_PAINT消息重绘窗口内容,内容的重绘在OnPaint中完成。有点需要说明的是,在基于对话框程序中是使用OnPaint函数进行内容重绘,但在基于文档程序中是在OnDraw函数中进行内容重绘的。这只是错觉,其实原理一样的,只不过OnPaint仅适用于屏幕,而OnDraw适用于任何设备!所以大多说内容我们一般使用OnDraw完成绘制。两者区别详见《OnPaint与OnDraw区别》


注意:其实在重绘的最开始实现发送WM_NCPAINT消息的(在WM_ERASEBKGND消息之前发送),用来实现对窗口费客户区域的重绘,只不过此消息完全由系统控制!所以我们避开不谈!

在理解重绘时我们可以吧重绘背景和重绘内容分开来看:

重绘背景:发送WM_ERASEBKGND消息,由DefWindowProc中的OnEraseBkgnd默认处理。

内容重绘:发送WM_PAINT消息,由OnPaint函数处理,在OnPaint中会先后调用BeginPaint、OnDraw、EndPaintBeginPaint、EndPaint这两个函数分别被封装在了CPaintDC类中的构造函数和析构函数中了),BeginPaint与EndPaint作用详见以下内容。


1>>什么时候重绘(窗口产生无效区域)

当拖动窗口的一个顶点改变了窗口的大小、窗口由最小化恢复到最大化、窗口的一部分被其它窗口遮住又重新显示、调用MoveWindow函数改变了窗口大小、窗口移动到桌面之外的部分被拖回重新显示时,窗口就会变得无效。 无效区域是整个客户区,因此默认窗口处理函数DefWindowProc会擦除整个客户区。

【注意】

拖动窗口标题栏移动窗口,只要窗口没有移动到屏幕之外,那么这两个消息都不产生)

 

【重点】

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

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

       WM_PAINT消息优先级很低,一般当消息队列为空(无其他消息存在时)时才会进行重绘。

 

2>>重绘时发送消息

当窗口无效时,Windows会给窗口发出WM_ERASEBKGND消息和WM_PAINT消息,而且WM_ERASEBKGND先发出一次或者几次,紧接着是WM_PAINT

消息处理过程

先发出WM_ERASEBKGND消息若干次后再发出WM_PAINT消息,WM_ERASEBKGNDWM_PAINT之间没有其它消息WM_ERASEBKGND消息的后面一定是WM_PAINT

1、 WM_ERASEBKGND消息的处理:

调用DefWindowProc会擦除窗口背景(即绘制背景),同时会使窗口变得有效。此消息会执行OnEraseBkgnd消息处理函数,我们可以在此函数中设置窗口的背景,实例请见《WM_ERASEBKGND消息详解》,前提是此类窗口能够响应OnEraseBkgnd函数。

【重点】

      说白了WM_ERASEBKGND消息的存在就是用于在重绘我们的内容之前先把背景绘制了,这也为我们提供了修改窗口背景的途径,只要在执行WM_ERASEBKGND消息处理函数之前把此窗口的画刷改了就行。InvalidateRect(hWnd,&rect,TRUE)就为我们提供了方法,最后一个参数为TRUE则说明在重绘我们的内容前先发送(其实就是发送WM_PAINT之前先发送WM_ERASEBKGND),这样修改窗口背景就很方便:

       ——————————————————————————————————

       得到窗口句柄hWnd

       创建窗口DC

       创建想要的画刷;

       得到此窗口上需要重绘的区域rect

       将画刷资源选入到DC设备描述表;

       调用InvalidateRect(hWnd,&rect,TRUE)

    ——————————————————————————————————

2、 WM_PAINT消息的处理:

首先执行BeginPaint函数只做了两件事情:

1)。使窗口无效区域变得有效,从而使Windows不再发送WM_PAINT消息(直到窗口大小改变等,使窗口再次变得无效)。如果窗口一直无效,则Windows会不停地发送WM_PAINT消息!

 

2)。填充PAINTSTRUCT结构。填充这个结构的目的,是让程序员可以根据ps变量中的标志值进行某些操作

使用BeginPaint返回的dc绘制内容。

调用EndPaint释放。

所以,窗口重绘会引发WM_ERASEBKGNDWM_PAINT消息;前者是可选择是否发送的(某些函数,如InvalidateRect设置),而后者一定发送;

窗口的擦出背景是通过DefWindowProc(其实是DefWindowProc中的OnEraseBkgnd消息处理函数而非BeginPaint

》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》

【重点】

BeginPaint补充】

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


     此外BeginPaint也是InvalidateRect函数实现局部区域重绘的关键,当调用InvalidateRect(hWnd,&rect,TRUE)函数进行强制局部重绘,在OnPaint中会调用BeginPaint函数来对显示区域进行裁剪以得到需要重回的rect区域,所以要想实现强制局部强制重绘(InvalidateRect)就必须使用CPaintDC进行重绘,不然调用InvalidateRect的结果还是对整个显示区域进行全部重绘!


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

因此我们在OnPaint函数中只能使用CPaintDC dc(this); 的dc来实现内容绘制。具体代码如下:

void CDSCAMDEMODlg::OnPaint() { if (IsIconic()) { CPaintDC dc(this); // device context for painting SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); // Center icon in client rectangle int cxIcon = GetSystemMetrics(SM_CXICON); int cyIcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.Width() - cxIcon + 1) / 2; int y = (rect.Height() - cyIcon + 1) / 2; //Draw the icon dc.DrawIcon(x, y, m_hIcon); }else { CDialog::OnPaint(); }

在OnPaint中我们都是使用 CPaintDC dc(this);提供的DC进行绘制的, CPaintDC 中得到DC的方法其实是在此类构造函数中封装了BeginPaint函数,在创建 CPaintDC 对象时会调用BeginPaint函数返回一个DC,我们必须使用此DC完成所有绘制。如果非要在OnPaint中自己弄一个DC出来并使用此DC进行一些绘制:

void CDSCAMDEMODlg::OnPaint() { if (IsIconic()) { CPaintDC dc(this); // device context for painting SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); // Center icon in client rectangle int cxIcon = GetSystemMetrics(SM_CXICON); int cyIcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.Width() - cxIcon + 1) / 2; int y = (rect.Height() - cyIcon + 1) / 2; //Draw the icon dc.DrawIcon(x, y, m_hIcon); }else { CDialog::OnPaint(); } //CDialog::OnPaint(); if(RUNMODE_STOP==m_RunMode) { if(m_ShowDCMImageFlag) ShowDCMImage(); if(m_bLoadReDraw) DrawLoadImage(); } CClientDC cdc(this);//非要自己搞出来一个! cdc.SetBkMode(TRANSPARENT); //-------------得到并显示当前时间------------ //GetSysCurTime(); char *week[7]={"星期天","星期一","星期二","星期三","星期四","星期五","星期六"}; CTime curTime=CTime::GetCurrentTime(); CString ctime=curTime.Format("%H:%M:%S"); char dtime[100]; sprintf(dtime,"%d/%d/%d %s",curTime.GetYear(),curTime.GetMonth(), curTime.GetDay(),week[curTime.GetDayOfWeek()-1]); RECT rc; this->GetClientRect(&rc); cdc.TextOut(rc.right-140,rc.bottom-55,ctime); cdc.TextOut(rc.right-140,rc.bottom-35,dtime); //------------------------------------------- }

那么就会出现闪烁的显像,原因就是上面所述的那样。

所以我们必须了解多种DC的不同,有些DC只适用于 绘制,而有些DC只适合用来 重绘 !请详见 《各种DC区别》
不要搞混了一般的 绘制重绘啊!!!平时我们使用CClientDC时是直接往一个窗体上贴图或写文字,这种只是一般的绘制过程,并不涉及到窗体重绘,不会发送WM_ERASEBKGND、WM_PAINT消息。

现在想想,我们创建完一个窗口是怎么样让他显示出来的:

ShowWindow();

UpdateWindow();

其实,单纯一个ShowWindow,照样会正确重画窗口内容,只不过重画是在消息队列取空之后(因为WM_PAINT优先级太低了)。有时我们希望窗口被立即重画,而不是去等待那个不确定的消息队列,此时就需要用到UpdateWindow。这个函数的作用只有一个:假若当前被标记为重画的区域存在(不存在的话它什么也不做),那么立刻让Windows使用SendMessage的方式来对你的窗口发送WM_PAINT
说道这里,就要说一下SendMessagePostMessage的区别了。PostMessage是把消息放到消息队列尾部,然后通过程序的消息环逐个从消息队列里取出来进行处理。SendMessage却不是这样,它实际上根本不经过消息队列。对SendMessage的处理分两种情况:
    1
、由本线程发出的SendMessage,例如在自己的消息处理过程中调用UpdateWindow,从而发出的WM_PAINT。对于这种情况,SendMessage实际上直接调用你窗口的消息处理函数。也就是说,在进行消息处理的时候对本窗口SendMessage,实际上是递归调用。整个过程是:消息环取出消息A(假定APostMessage放进消息队列的)-> 处理消息A -> SendMessage对本线程的窗口发送消息B -> 处理消息B -> 继续处理消息A -> 消息环再取下一个消息。
    2
、由另一个线程发出的SendMessage。此时当然不能直接调用了。这种情况下,windows会把发送消息的线程挂起,然后,当接收消息的线程调用PeekMessage或者GetMessage的时候,这两个函数会立刻调用你窗口的消息处理函数,直到处理完毕(此时发送线程的SendMessage才返回),PeekMessage或者GetMessage才会去检查消息队列,并从中取出一个返回。
    
总之,不论哪种情况,SendMessage发送的消息,跟消息队列均没有任何关系,而且也不通过消息环执行(前者是直接调用,后者是在PeekMessageGetMessage函数内部调用)。

 

3>>父窗口重绘与子窗口重绘

       子窗口的重绘不会引起父窗口的重绘。但是父窗口的重绘会引起子窗口的重绘,其过程时:父窗口接收到WM_PAINT消息发生重绘,当父窗口重绘完成后会发送WM_PAINT信息给每个子窗口,然后子窗口各自重绘。但可以通过设置父窗口的属性来选择当父窗口被重绘后是否对子窗口也进行重绘:

       当指定父窗口Window Styles包含WS_CLIPCHILDREN或在资源管理器父窗口的属性栏中设置Clip Childrentrue可设置当父窗口发生重绘时子窗口不重绘,设为false时子窗口亦会重绘。这时针对vs2010的设置。

当一个窗口由顶层窗口变成非顶层窗口时,此窗口将全部无效,即会全部重绘

当一个窗口一直是顶层窗口,当其一部分移动超出显示器屏幕之外的时候只是超出的那部分无效,其他部分不重绘!

 

4>>重绘细节

       窗口背景的重绘注意由背景画刷决定,而使用MFC类向导添加资源时可能导致窗口画刷的修改。例如:

       通过资源管理器加载图片时同时也会改变Picture控件(窗口)的背景画刷。而通过画图函数手动绘制图片时不会改变Picture控件(窗口)的背景画刷!

       有事这种细节会产生很大影响,参见“DTCMdemo-Error.txt”错误记录。

【重点】

         我们应该将窗口手动重绘过程放置在对应窗口的OnPaintOnDraw中。

 

5>>相关函数

SendMessage

PostMessage不同,它实际上根本不经过消息队列。对SendMessage的处理分两种情况:
    1
、由本线程发出的SendMessage,例如在自己的消息处理过程中调用UpdateWindow,从而发出的WM_PAINT。对于这种情况,SendMessage实际上直接调用你窗口的消息处理函数。也就是说,在进行消息处理的时候对本窗口SendMessage,实际上是递归调用。整个过程是:消息环取出消息A(假定APostMessage放进消息队列的)-> 处理消息A -> SendMessage对本线程的窗口发送消息B -> 处理消息B -> 继续处理消息A -> 消息环再取下一个消息。
    2
、由另一个线程发出的SendMessage。此时当然不能直接调用了。这种情况下,windows会把发送消息的线程挂起,然后,当接收消息的线程调用PeekMessage或者GetMessage的时候,这两个函数会立刻调用你窗口的消息处理函数,直到处理完毕(此时发送线程的SendMessage才返回),PeekMessage或者GetMessage才会去检查消息队列,并从中取出一个返回。
总之,不论哪种情况,SendMessage发送的消息,跟消息队列均没有任何关系,而且也不通过消息环执行(前者是直接调用,后者是在PeekMessageGetMessage函数内部调用)。

 

PostMessage

PostMessage是把消息放到消息队列尾部立刻返回,然后通过程序的消息环逐个从消息队列里取出来进行处理。

Invalidate

该函数的作用是使整个窗口客户区无效。窗口的客户区无效意味着需要重绘,Windows会在应用程序的消息队列中放置WM_PAINT消息,这并不意味着立即重绘。

UpdateWindow

UpdateWindow( )的作用是使窗口立即重绘。函数嵌套执行。

InvalidateRect

InvalidateRect(hWnd,&rect,TRUE)hWnd发出WM_PAINT的消息,强制指定区域的重绘;rect是你指定的重回区域,此区域外的不重绘,这样防止客户区的一个局部的改动,而导致整个客户区的重绘而导致闪烁,如果最后的参数为TRUE,则还像hWnd发送WM_ERASEBKGND消息,在客户区域重绘之前先重绘背景,即先将背景搞好在搞显示内容。

validateRect

       该函数更新指定窗口的无效矩形区域,使之有效。



http://blog.163.com/dingmz_frcmyblog/blog/static/217304023201322241312383

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页