【引言】:这是以前在作VC/EVC开发时候遇到的一个郁闷的问题的思考,刚好最近在VC#开发时候遇到了类似的问题,因此就总结出来,期望能够给遇到同样问题的开发者一些帮助和提示。
问题缘起
半年前,在作Mobile GPS项目(移动手持设备上的GPS/GIS项目)的时候,为了实现PDA上的地图下载功能,我们将GIS地图数据转化为XML文件,在经过相关技术的优化终于做到了将XML的地图解析并绘制到PDA屏幕上。但是问题出现了:当在响应一个绘制地图的菜单(Button)项目的时候,地图绘制出来了,但是当有新窗体在上面经过(模拟器上运行)或者是触摸笔擦写PDA屏幕的时候,画好的地图被擦写掉了!
很显然地,为了调试这个Bug,我在VC下面去重复这样的实现(当然是简单的模拟),问题还是存在。但是将直接绘图操作放在OnPaint()中则不会出现这样的问题,但是这不能解决应用问题(图像应该是一步步绘制的,并且在不同的时候有不同的显示)。
原因揭密
这个问题的原因是Windows对WM_PAINT消息的处理,WM_PAINT消息的相关知识这里不罗嗦(可以参考相关资料),需要强调和说明的是:
1) 应用程序第一次运行的时候WM_PAINT会被自动调用,也就是说你在OnPaint()中实现的绘图操作会被调用;
2) 当窗体的大小被调整(例如最小化、拉伸等),或者被遮挡后又显示出来(问题中的情况)的时候会调用OnPaint()函数。
于是上面的问题的原因就是:当窗体被遮挡后,WM_PAINT消息使得窗体重画,但是我们的绘画逻辑不是放在OnPaint()中实现(是在菜单项或者Button的响应里面实现的),因此调用后不能获得对应的绘图显示。
解决方案
问题出现了,项目还要做,客户的需求还是要满足,因此就得想办法解决了。在弄懂了整个问题的原因后,解决方案也就慢慢又了眉目:既然是因为系统调用OnPaint引起的,那就在OnPaint和绘图逻辑间做一个权衡:
1) 将绘图过程放在OnPaint中实现,当然可以是在另外函数中实现,在这里调用即可;2) 菜单(Button)只控制绘图逻辑:设置一个BOOL变量m_bDraw来标志是否进行绘图,初次可以设计为FALSE。菜单和Button的响应逻辑中只是修改这个BOOL变量,例如标志为TRUE,提示绘制图形;这个问题看上去已经解决了,但是这样实现后,发现当菜单项(Button)点击后,图像没有绘制出来。一思量,确实应该是这样:因为菜单项(Button)逻辑只是修改了m_bDraw的值,并没有去显示调用OnPaint消息。于是你会发现,当你有上面问题中的操作(例如最小化、遮挡等)时候,图像如期而至(因为这会出发WM_PAINT消息)。
知道原因,解决方案就出来了:在菜单项(Button)响应逻辑中除了改变m_bDraw的状态,还要发送WM_PAINT消息,以调用OnPaint函数。于是,只需要在2)中响应逻辑中除了改变BOOL变量值,再加上出发WM_PAINT消息的操作,你可以有以下3种选择:1) 直接发送WM_PAINT消息,PostMessage(),SendMessage()函数发送WM_PAINT消息。使用以上两函数发送WM_PAINT消息,能将WM_PAINT消息发送到WINDOWS程序消息队列中,当WINDOWS将WM_PAINT消息发送给具体的消息处理函数时,如果窗口的无效区域为空则WINDOWS将不理睬该消息。若存在无效区域,则调用窗口处理函数处理。要注意的这里需要存在无效区域,因此要调用2)中的函数使得窗体(或者部分)无效,其处理过程与2)相同,将WM_PAINT消息送入消息处理队列。与3)不同的是WM_PAINT并不立即处理;2) 调用相应的API实现WM_PAINT消息的发送:Invalidate(),InvalidateRect(),InvalidateRgn():以上函数将窗口的特定区域标定为无效,当WINDOWS检测到窗口中存在无效区域时将向消息队列发送WM_PAINT 消息。我当时用的就是Invalidate()函数;
3) UpdateWindow():该函数调用后WINDOWS将向窗口发送一个非队列化的WM_PAINT消息,它不经过消息循环而直接发送给了窗口消息处理函数。如果窗口无效区域不存在,WINDOWS将不理睬该消息。注意这里因为要使得窗口无效区不存在,因此还是调用Invalidate(),InvalidateRect(), InvalidateRgn()函数,和2)中不同的是这里的WM_PAINT消息会被立即处理,而2)中是加入消息处理队列。
简单起见,你可以使用2)中方案进行问题解决。
问题扩展
上面提到的都是在VC6.0和EVC 3.0下面的问题和解决方案,最近在做VC#开发的时
候,也遇到同样的问题,其原因和上面当然是一样的。解决方案也类似,只不过VC(EVC)中绘图使用CDC(或其子类CPaintDC、CClientDC等)而VC#使用Graphics,解决方案也差不多,不同的是:显示发送WM_PAINT消息的方法略有不同:在上面的发送WM_PAINT消息的方案2)中,你可以是调用以下的API之一:
1) Invalidate():一共有7个重载的函数体,可以根据实际情况调用(相当于是上面的3个API功能);
2) Update():注意这个操作和解决方案中的3)相似,因此必须还要调用1)中的Invalidate,与1)不同的是这里WM_PAINT消息理解被触发;
3) Refresh():这个操作相当于Invalidate(true)+ Update(),因此也可以取得同样的效果。
需要说明的是:当你在VC#的Windows Form下面测试上面的问题时候,你会发现另外
一个郁闷的事情:当窗体大小被调整的时候,看上去似乎图像被重画了N次,但是每次都不彻底。问题的原因是:当窗体被扩大时,Windows只绘制那些新近显露出来的矩形区域,而假定原有的矩形区域无需重绘。这个问题可以通过设置Form风格来处理,在Form的构造函数里InitializeComponent()后面加上改变更个语句:SetStyle(ControlStyles.ResizeRedraw,true);一切就OK了。当然如果你要问为什么默认的不是这种风格,那是因为这种做法的效率很低下(想想就知道了)的原因。
几点说明
本文在VC 6.0和VS 2003中均进行了测试,你可以通过以下方式构造测试程序:
1) VC 6.0下,你可以新建一个基于Dialog或者Single Document的MFC程序,在前者你可以通过响应一个Button,后者通过一个简单的菜单响应,绘制一个椭圆,看实际的问题和解决方案的可行性;
2) 在VS 2003中,新建一个VC#的Windows应用程序即可,同样通过响应一个Button绘制椭圆,以获得1)中效果。