MFC-GDI绘制

发布时间:2011-3-29 20:28    发布者:1770309616
关键词: MFC-GDI绘制
最好的GDI入门教程是《Window程序设计》的第五章,如果你没有任何GDI基础,最好精读这一章,因为本文并不会介绍GDI的方方面面,事实上这也是不可能完成的任务。我只将以前学习GDI时遇到的几个难点拿出来讲讲。
GDI对象的用法
GDI对象就是画笔,画刷,字体这类资源,以我的经验,GDI对象的管理是一件麻烦的事,如果操作不当,很容易引起GDI泄漏。
Delphi用TPen,TBrush,TFont三个类来表示画笔画刷和字体,用Canvas表示设备描述表。以TPen为例,一个TPen并不表示一个GDI对象,真正的GDI对象被保存在“池”里面,TPen只是根据自己的属性到“池”里面寻找对应的GDI对象,如果找不到将会创建一个新的,而这些GDI对象都有一个引用计数,代表它被多少个TPen对象引用,只要引用计数为0,这个GDI马上被DeleteObject并从“池”中移走。
各位可以想象Delphi对WinAPI封装到什么程度,这就是为什么你即使不会Windows编程也可以很快上手Delphi。这样封装的好处是非常明显的,你不必理会什么时候需要DeleteObject,你只要向Pen设置你喜欢的颜色,宽度,风格,然后调用Canvas的函数来绘制就行了。但它的坏处也非常明显,你设的样式越多,表明“池”中的GDI对象也会越多,Delphi程序的GDI普遍偏高就缘于此,更可怕的是我们对这种偏高的GDI数量有些束手无策。
MFC则完全不一样,它仅仅利用栈对象的自动消毁来简化GDI对象的管理,其余的差不多就是一层简单的包装。所以你必须了解GDI的用法,有下面三条规则,这是从Windows程序设计引过来的:
1.         最后必须删除自己创建的所有GDI对象。
2.         当GDI对象正在一个有效的DC中使用时,不要删除它。
3.         不要删除现有对象(StockObject)。
我们用简单的例子来说明这三条规则,请看下例:
HPEN hPen = ::CreatePen(PS_SOLID, 2, RGB(0xFF, 00, 00));
::SelectObject(hDC, hPen);
::Rectangle(hDC, 10, 10, 100, 100);
参照上面三条规则,明显违反了第一条规则,创建一个画笔之后,最后没有调用DeleteObject消毁hPen,这会发生什么事情呢,GDI对象不断的泄漏,在XP系统下GDI达到10000时程序就死掉了。
把代码修改如下:
HPEN hPen = ::CreatePen(PS_SOLID, 2, RGB(0xFF, 00, 00));
::SelectObject(hDC, hPen);
::Rectangle(hDC, 10, 10, 100, 100);
:eleteObject(hPen);
这样是不是就正确了呢,如果hDC在DeleteObject之后还继续绘制的话就违反规则2了,这会导致后面的绘制失去Pen的风格。
更加安全的做法是这样的:
HPEN hPen = ::CreatePen(PS_SOLID, 2, RGB(0xFF, 00, 00));
HPEN hOldPen = ::SelectObject(hDC, hPen);
::Rectangle(hDC, 10, 10, 100, 100);
:eleteObject(::SelectObject(hOldPen));
SelectObject会返回原有的GDI对象,将它保存起来,最后再用SelectObject恢复回来。
不过事情总不是绝对的,如果hDC在DeleteObject之后不再绘制并且将被ReleaseDC的话,其实也可以不保存hOldPen,这样即可以简化代码,也可以提高绘制效率。
有些情况要设的样式比较多,用上面保存旧GDI对象的方式有些麻烦,可以用SaveDC和RestoreDC来简化操作,就像下面这样:
HPEN hPen = ::CreatePen(PS_SOLID, 2, RGB(0, 0, 0));
HBRUSH hBrush = ::CreateSolidBrush(RGB(0xFF, 0, 0));
int nDCSave = ::SaveDC(hDC);   
::SelectObject(hDC, hPen);
::SelectObject(hDC, hBrush);
::Rectangle(hDC, 10, 10, 100, 100);
::RestoreDC(hDC, nDCSave);
:eleteObject(hPen);
:eleteObject(hBrush);
SaveDC将当前的DC样式保存起来,直到RestoreDC时恢复回来。
规则3比较容易理解,凡是用GetStockObject取出来的GDI对象都不必手动删除。
映射方式
       映射方式是GDI里最难理解的概念,其中涉及到设备坐标,逻辑坐标,窗口,视口这些术语。
如果不使用GDI函数,我们所要面对的只是设备坐标。设备坐标可以分为三种,屏幕,窗口,客户区,这之间的区别仅仅是原点的位置,屏幕坐标以屏幕左上角为原点,窗口坐标以窗口左上角为原点,客户区坐标以客户区左上角为原点,此外,设备坐标的XY轴都向右向下增长,并且单位是像素。究竟要使用哪种坐标由所调用的函数或所处理的消息决定。比如,当我们调用GetCursorPos时,取得的点以屏幕坐标为准;而处理WM_MOUSEMOVE时,参数所指的点以窗口所在的客户区坐标为准。需要窗口坐标的情况不是很多,常用于GetWindowDC逻辑坐标向设备坐标的映射。
如果使用GDI函数就必须理解逻辑坐标,因为GDI函数的位置参数都以逻辑坐标为准。比如这个函数:
BOOL Rectangle(
  HDC hdc,         // handle to DC
  int nLeftRect,   // x-coord of upper-left corner of rectangle
  int nTopRect,    // y-coord of upper-left corner of rectangle
  int nRightRect,  // x-coord of lower-right corner of rectangle
  int nBottomRect  // y-coord of lower-right corner of rectangle
);
后面的四个参数都是逻辑坐标。假想一个虚拟的平面,这个平面使用逻辑坐标,Rectangle先在虚拟平面上画出矩形,然后利用“某种方式”将矩形从虚拟平面映射到屏幕上的窗口来,这个过程就是映射:
o_image001.gif
映射到何种设备坐标取决于DC是怎么取到的,简单来说如果是通过GetDC或BeginPaint则为客户区坐标,如果通过GetWindowDC则为窗口坐标。
问题的复杂性在于逻辑坐标并不像设备坐标那样固定不变,它的单位是可变的,XY轴增长的方向也是可变的,甚至于逻辑坐标的原点也不一定映射为设备坐标的原点。
       逻辑坐标的XY轴的单位与增长方向由SetMapMode决定;逻辑坐标的原点映射到设备坐标什么地方由SetWindowOrgEx或SetViewportOrgEx决定。
       我们分别讨论这两个问题,为了简化复杂性,当讨论一个问题时,都假定另一个问题为默认情况,比如讨论XY轴的单位和增长方向时,假定逻辑坐标的原点就映射为设备坐标的原点,反过来也一样。
       MapMode的默认值是MM_TEXT,这和设备坐标是一样的,即单位是像素,XY轴向右向下增长,如果hDC是客户区的设备描述表,我们调用Rectangle(hDC, 10, 10, 100, 100),矩形就正确无误地在客户区的(10, 10, 100,100)处显示出来。
       现在用SetMapMode将映射方式设为MM_LOENGLISH,再调用Rectangle:
::SetMapMode(hDC, MM_LOENGLISH);
::Rectangle(hDC, 0, 0, 100, 100);
这时客户区并没有出现矩形,因为MM_LOENGLISH的XY轴是向右向上增长的,且逻辑单位是0.01in,映射方式就像下图所示:

o_image002.gif
矩形被映射到客户区上边了,要想屏幕显示矩形,须作如下修改:
::SetMapMode(hDC, MM_LOENGLISH);
::Rectangle(hDC, 0, 0, 100, -100);
但最终结果仍然不是一个100×100像素的矩形,因为MM_LOENGLISH的逻辑单位是0.01in,相当于0.96像素,最终结果是96×96像素的矩形,正确的代码是这样的:
::SetMapMode(hDC, MM_LOENGLISH);
int npx_X = ::GetDeviceCaps(hDC, LOGPIXELSX);
int npx_Y = ::GetDeviceCaps(hDC, LOGPIXELSY);
::Rectangle(hDC, 0, 0, 100/(npx_X*0.01), -100/(npx_Y*0.01));
这个转换仅对于MM_LOENGLISH有效,其他的映射方式要作不同的转换,为了简化这种转换,Windows提供了DPtoLP和LPtoDP,用于在逻辑坐标和设备坐标之间进行点的转换。现在,代码变成这样:
::SetMapMode(hDC, MM_LOENGLISH);
RECT rcBound;
::SetRect(&rcBound, 0, 0, 100, 100);
toLP(hDC, (LPPOINT)&rcBound, 2);
::Rectangle(hDC, rcBound.left, rcBound.top, rcBound.right, rcBound.bottom);
注意rcBound指定的是设备坐标,但Rectangle需要逻辑坐标,所以用DPtoLP转换一下。
DPToLP是非常有用的函数,MiniDraw在绘图时用到这个函数,用于处理滚动条出现的情况。
绝大多数情况下,我们都用MM_TEXT作为映射方式,这种方式下逻辑坐标单位与方向都与设备坐标一样,理解起来比较容易。

第二个问题是逻辑坐标的原点映射到设备坐标什么地方?默认都是原点映射到原点,即逻辑坐标的(0,0)对应设备坐标的(0,0)。
什么情况下需要改变这种映射方式,我想最常见的是客户区出现滚动条的时候。比如一个图形的位置是(100, 100),我们将滚动条往下拉一点,此时图形的实际位置可能变成(0,0),但我们仍然会认为图形就在(100,100)处,只是整个客户区往上滚动了一点。
SetWindowOrgEx和SetViewportOrgEx指定了原点的映射方式。我用这样的方式来理解原点的映射。比如:
::SetWindowOrgEx(hDC, 100, 100, NULL);
理解为:逻辑坐标的(100,100)映射为设备坐标的(0,0),其他的点以此为依据映射,用下图表示:

o_image003.gif
       又比如:
       ::SetViewportOrgEx(hDC, 100, 100, NULL);
理解为逻辑坐标的(0,0)映射为设备坐标的(100,100),其他的点以此为依据映射,用下图表示:
o_image004.gif
       又比如:
::SetWindowOrgEx(hDC, 100, 100, NULL);
       ::SetViewportOrgEx(hDC, 200, 200, NULL);
       理解为:逻辑坐标的(100,100)映射为设备坐标的(200,200),其他的点以此为依据映射。
       这样的描述是否有助于对窗口和视口的理解呢?更加详细的介绍请看Windows程序设计的第五章。
       映射方式仅仅是某个DC的属性,如果这个DC释放,则映射方式也没有什么用了,即使存在两个DC,他们都对应同一个窗口,映射方式仍然是独立的。

       这篇文章并没有分析MFC如何封装GDI,相比之下,我觉得理解GDI的一些重点知识显得更加重要,理解了这些知识,再看MFC的CDC,那不过就是将hDC和GDI函数组织起来的数据结构。
本文地址:https://www.eechina.com/thread-60399-1-1.html     【打印本页】

本站部分文章为转载或网友发布,目的在于传递和分享信息,并不代表本网赞同其观点和对其真实性负责;文章版权归原作者及原出处所有,如涉及作品内容、版权和其它问题,我们将根据著作权人的要求,第一时间更正或删除。
您需要登录后才可以发表评论 登录 | 立即注册

厂商推荐

关于我们  -  服务条款  -  使用指南  -  站点地图  -  友情链接  -  联系我们
电子工程网 © 版权所有   京ICP备16069177号 | 京公网安备11010502021702
快速回复 返回顶部 返回列表