以前用mfc画图一直是把别人的代码拿过来就用,用到什么问题再解决之,这次又要做画图的事,正好总结下。
1 如何把图片显示到屏幕上?
先把图片载入到内存中,再更新到dc中,再将其画到屏幕上
2 画图时屏幕闪烁问题
显示图形如何避免闪烁,如何提高显示效率是问得比较多的问题。而且多数人认为MFC的绘图函数效率很低,总是想寻求其它的解决方案。
MFC的绘图效率的确不高但也不差,而且它的绘图函数使用非常简单,只要使用方法得当,再加上一些技巧,用MFC可以得到效率很高的绘图程序。
我想就我长期(呵呵当然也只有2年多)使用MFC绘图的经验谈谈我的一些观点。
2.1显示的图形为什么会闪烁?
我们的绘图过程大多放在OnDraw或者OnPaint函数中,OnDraw在进行屏幕显示时是由OnPaint进行调用的。当窗口由于任何原因需要重绘时,总是先用背景色将显示区清除,然后才调用OnPaint,而背景色往往与绘图内容反差很大,这样在短时间内背景色与显示图形的交替出现,使得显示窗口看起来在闪。如果将背景刷设置成NULL,这样无论怎样重绘图形都不会闪了。当然,这样做会使得窗口的显示乱成一团,因为重绘时没有背景色对原来绘制的图形进行清除,而又叠加上了新的图形。有的人会说,闪烁是因为绘图的速度太慢或者显示的图形太复杂造成的,其实这样说并不对,绘图的显示速度对闪烁的影响不是根本性的。例如在OnDraw(CDC *pDC)中这样写:
这个绘图过程应该是非常简单、非常快了吧,但是拉动窗口变化时还是会看见闪烁。其实从道理上讲,画图的过程越复杂越慢闪烁应该越少,因为绘图用的时间与用背景清除屏幕所花的时间的比例越大人对闪烁的感觉会越不明显。比如:清楚屏幕时间为1s绘图时间也是为1s,这样在10s内的连续重画中就要闪烁5次;如果清楚屏幕时间为1s不变,而绘图时间为9s,这样10s内的连续重画只会闪烁一次。这个也可以试验,在OnDraw(CDC *pDC)中这样写:
呵呵,程序有点变态,但是能说明问题。
说到这里可能又有人要说了,为什么一个简单图形看起来没有复杂图形那么闪呢?这是因为复杂图形占的面积大,重画时造成的反差比较大,所以感觉上要闪得厉害一些,但是闪烁频率要低。那为什么动画的重画频率高,而看起来却不闪?这里,我就要再次强调了,闪烁是什么?闪烁就是反差,反差越大,闪烁越厉害。因为动画的连续两个帧之间的差异很小所以看起来不闪。如果不信,可以在动画的每一帧中间加一张纯白的帧,不闪才怪呢。
2.2如何避免闪烁
在知道图形显示闪烁的原因之后,对症下药就好办了。首先当然是去掉MFC提供的背景绘制过程了。实现的方法很多,
* 可以在窗口形成时给窗口的注册类的背景刷付NULL
* 也可以在形成以后修改背景
* 要简单也可以重载OnEraseBkgnd(CDC* pDC)直接返回TRUE
这样背景没有了,结果图形显示的确不闪了,但是显示也象前面所说的一样,变得一团乱。怎么办?这就要用到双缓存的方法了。双缓冲就是除了在屏幕上有图形进行显示以外,在内存中也有图形在绘制。我们可以把要显示的图形先在内存中绘制好,然后再一次性的将内存中的图形按照一个点一个点地覆盖到屏幕上去(这个过程非常快,因为是非常规整的内存拷贝)。这样在内存中绘图时,随便用什么反差大的背景色进行清除都不会闪,因为看不见。当贴到屏幕上时,因为内存中最终的图形与屏幕显示图形差别很小(如果没有运动,当然就没有差别),这样看起来就不会闪。
2.3如何实现双缓冲
首先给出实现的程序,然后再解释,同样是在OnDraw(CDC *pDC)中:
上面的注释应该很详尽了,废话就不多说了。
2.4如何提高绘图的效率
我主要做的是电力系统的网络图形的CAD软件,在一个窗口中往往要显示成千上万个电力元件,而每个元件又是由点、线、圆等基本图形构成。如果真要在一次重绘过程重画这么多元件,可想而知这个过程是非常漫长的。如果加上了图形的浏览功能,鼠标拖动图形滚动时需要进行大量的重绘,速度会慢得让用户将无法忍受。怎么办?只有再研究研究MFC的绘图过程了。
实际上,在OnDraw(CDC *pDC)中绘制的图并不是所有都显示了的,例如:你在OnDraw中画了两个矩形,在一次重绘中虽然两个矩形的绘制函数都有执行,但是很有可能只有一个显示了,这是因为MFC本身为了提高重绘的效率设置了裁剪区。裁剪区的作用就是:只有在这个区内的绘图过程才会真正有效,在区外的是无效的,即使在区外执行了绘图函数也是不会显示的。因为多数情况下窗口重绘的产生大多是因为窗口部分被遮挡或者窗口有滚动发生,改变的区域并不是整个图形而只有一小部分,这一部分需要改变的就是pDC中的裁剪区了。因为显示(往内存或者显存都叫显示)比绘图过程的计算要费时得多,有了裁剪区后显示的就只是应该显示的部分,大大提高了显示效率。但是这个裁剪区是MFC设置的,它已经为我们提高了显示效率,在进行复杂图形的绘制时如何进一步提高效率呢?那就只有去掉在裁剪区外的绘图过程了。可以先用pDC->GetClipBox()得到裁剪区,然后在绘图时判断你的图形是否在这个区内,如果在就画,不在就不画。
如果你的绘图过程不复杂,这样做可能对你的绘图效率不会有提高。
3 WM_PAINT消息的发送问题
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的处理方式就是:
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,后者的控制选项更为丰富。
4 关于对话框中图片控件的内容更新问题
加以加入定时器,在定时器处理函数中对数据进行处理
5 关于坐标系的问题
关于mfc中图像处理的类的问题,最好不要在mfc本身的dlg类中添加太多的东西,这样会造成代码管理的不方便以及思路的不清晰。