关闭

MFC-GDI绘制

标签: mfcdelphiwindowswinapinull图形
6756人阅读 评论(3) 收藏 举报
分类:

最好的GDI入门教程是《Window程序设计》的第五章,如果你没有任何GDI基础,最好精读这一章,因为本文并不会介绍GDI的方方面面,事实上这也是不可能完成的任务。我只将以前学习GDI时遇到的几个难点拿出来讲讲。

GDI对象的用法

GDI对象就是画笔,画刷,字体这类资源,以我的经验,GDI对象的管理是一件麻烦的事,如果操作不当,很容易引起GDI泄漏。

DelphiTPenTBrushTFont三个类来表示画笔画刷和字体,用Canvas表示设备描述表。以TPen为例,一个TPen并不表示一个GDI对象,真正的GDI对象被保存在“池”里面,TPen只是根据自己的属性到“池”里面寻找对应的GDI对象,如果找不到将会创建一个新的,而这些GDI对象都有一个引用计数,代表它被多少个TPen对象引用,只要引用计数为0,这个GDI马上被DeleteObject并从“池”中移走。

各位可以想象DelphiWinAPI封装到什么程度,这就是为什么你即使不会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);

::DeleteObject(hPen);

这样是不是就正确了呢,如果hDCDeleteObject之后还继续绘制的话就违反规则2了,这会导致后面的绘制失去Pen的风格。

更加安全的做法是这样的:

HPEN hPen = ::CreatePen(PS_SOLID, 2, RGB(0xFF, 00, 00));

HPEN hOldPen = ::SelectObject(hDC, hPen);

::Rectangle(hDC, 10, 10, 100, 100);

::DeleteObject(::SelectObject(hOldPen));

SelectObject会返回原有的GDI对象,将它保存起来,最后再用SelectObject恢复回来。

不过事情总不是绝对的,如果hDCDeleteObject之后不再绘制并且将被ReleaseDC的话,其实也可以不保存hOldPen,这样即可以简化代码,也可以提高绘制效率。

有些情况要设的样式比较多,用上面保存旧GDI对象的方式有些麻烦,可以用SaveDCRestoreDC来简化操作,就像下面这样:

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);

::DeleteObject(hPen);

::DeleteObject(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先在虚拟平面上画出矩形,然后利用“某种方式”将矩形从虚拟平面映射到屏幕上的窗口来,这个过程就是映射:

 

映射到何种设备坐标取决于DC是怎么取到的,简单来说如果是通过GetDCBeginPaint则为客户区坐标,如果通过GetWindowDC则为窗口坐标。

问题的复杂性在于逻辑坐标并不像设备坐标那样固定不变,它的单位是可变的,XY轴增长的方向也是可变的,甚至于逻辑坐标的原点也不一定映射为设备坐标的原点。

       逻辑坐标的XY轴的单位与增长方向由SetMapMode决定;逻辑坐标的原点映射到设备坐标什么地方由SetWindowOrgExSetViewportOrgEx决定。

       我们分别讨论这两个问题,为了简化复杂性,当讨论一个问题时,都假定另一个问题为默认情况,比如讨论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_LOENGLISHXY轴是向右向上增长的,且逻辑单位是0.01in,映射方式就像下图所示:

矩形被映射到客户区上边了,要想屏幕显示矩形,须作如下修改:

::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提供了DPtoLPLPtoDP,用于在逻辑坐标和设备坐标之间进行点的转换。现在,代码变成这样:

::SetMapMode(hDC, MM_LOENGLISH);

RECT rcBound;

::SetRect(&rcBound, 0, 0, 100, 100);

::DPtoLP(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)处,只是整个客户区往上滚动了一点。

SetWindowOrgExSetViewportOrgEx指定了原点的映射方式。我用这样的方式来理解原点的映射。比如:

::SetWindowOrgEx(hDC, 100, 100, NULL);

理解为:逻辑坐标的(100,100)映射为设备坐标的(0,0),其他的点以此为依据映射,用下图表示:

       又比如:

       ::SetViewportOrgEx(hDC, 100, 100, NULL);

理解为逻辑坐标的(0,0)映射为设备坐标的(100,100),其他的点以此为依据映射,用下图表示:

 

       又比如:

::SetWindowOrgEx(hDC, 100, 100, NULL);

       ::SetViewportOrgEx(hDC, 200, 200, NULL);

       理解为:逻辑坐标的(100,100)映射为设备坐标的(200,200),其他的点以此为依据映射。

       这样的描述是否有助于对窗口和视口的理解呢?更加详细的介绍请看Windows程序设计的第五章。

       映射方式仅仅是某个DC的属性,如果这个DC释放,则映射方式也没有什么用了,即使存在两个DC,他们都对应同一个窗口,映射方式仍然是独立的。

 

       这篇文章并没有分析MFC如何封装GDI,相比之下,我觉得理解GDI的一些重点知识显得更加重要,理解了这些知识,再看MFCCDC,那不过就是将hDCGDI函数组织起来的数据结构。

 
1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:312910次
    • 积分:4095
    • 等级:
    • 排名:第7834名
    • 原创:72篇
    • 转载:0篇
    • 译文:0篇
    • 评论:396条
    文章分类
    最新评论