注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

jasonyang9的博客

随便写写

 
 
 

日志

 
 

(怀旧系列)VC程序设计(孙鑫老师)听课笔记:11 图形的保存和重绘  

2013-02-12 17:16:03|  分类: programming |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
(怀旧系列)VC程序设计(孙鑫老师)听课笔记:11 图形的保存和重绘 - jasonyang9 - jasonyang9的博客
 
================
图形的保存和重绘
================

和上节课一样,新建一个单文档工程,增加一个“绘图”弹出菜单,4个菜单项:分别是“点”(IDM_DOT)、“直线”(IDM_LINE)、“矩形”(IDM_RECTANGLE)和“椭圆”(IDM_ELLIPSE)。分别给这4个菜单项在View类中增加对应的命令响应函数(OnDot、OnLine、OnRectangle和OnEllipse)。
在这4个命令响应函数中将用户的选择保存起来,为此,在View类中增加一个私有的成员变量UINT m_nDrawType。

CXXXView::CXXXView()
{
m_nDrawType = 0; // 初始化为0
}

void CXXXView::OnDot()
{
m_nDrawType = 1; // “点”为1(其实应该定义几个对应的宏来表示绘图的类型)
}

void CXXXView::OnLine()
{
m_nDrawType = 2; // “直线”为2
}

void CXXXView::OnRectangle()
{
m_nDrawType = 3; // “矩形”为3
}

void CXXXView::OnEllipse()
{
m_nDrawType = 4; // “椭圆”为4
}

增加对WM_LBUTTONDOWN和WM_LBUTTONUP的命令响应函数(为了捕获鼠标左键按下时和放开时的坐标点,用于绘制“直线”、“矩形”和“椭圆”)。同时,为了将坐标点保存下来,增加私有成员变量CPoint m_ptOrigin。

CXXXView::CXXXView()
{
m_nDrawType = 0;
m_ptOrigin = 0; // 初始化为0
}

void CXXXView::OnLButtonDown(UINT nflags, CPoint point)
{
m_ptOrigin = point; // 保存鼠标按下时的坐标点
CView::OnLButtonDown(nflags, point);
}

void CXXXView::OnLButtonUp(UINT nflags, CPoint point) // 在鼠标放开时进行绘图
{
CClientDC dc(this); // 创建DC
CPen pen(PS_SOLID, 1, RGB(255, 0, 0)); // 创建红色的画笔
dc.SelectObject(&pen); // 将画笔选入设备表述表
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH)); // 创建一个指向空画刷的指针
dc.SelectObject(pBrush); // 将空画刷选入设备描述表(使“矩形”和“椭圆”为空心效果)

switch (m_nDrawType)
{
case 1: // 画“点”
dc.SetPixel(point, RGB(255, 0, 0)); // 一个红色的点
break;
case 2: // 画“直线”
dc.MoveTo(m_ptOrigin); // 移动到起点(即鼠标按下时保存的点)
dc.LineTo(point); // 画一条直线到鼠标放开时的点
break;
case 3: // 画“矩形”
dc.Rectangle(CRect(m_ptOrigin, point)); // 用CRect组合2个点为一个矩形对象(注意:这里CRect会自动将CRect转换为LPCRECT,因为CRect重载了一个操作符LPCRECT,当CDC::Rectangle发现传入的参数是一个类对象时,会调用重载的带有LPCRECT的函数,让CRect可以自动将CRect转换为LPCRECT?)
break;
case 4: // 画“椭圆”
dc.Ellipse(CRect(m_ptOrigin, point)); // 类似“矩形”
break;
default:
}

CView::OnLButtonUp(nflags, point);
}


这个绘图程序有个问题,就是在窗口重绘(如:尺寸发生变化时)后,图形消失。
要有一个将所绘图形保存下来的方法。经过分析,可以发现,这个程序中的所有图形都可以用一个绘制的模式(m_nDrawType)和两个点(起点和终点)来表示。
将这3个要素用结构体或类来表示都是可行的。
新建一个类(类型:Generic Class;类名:CGraph),增加3个成员变量(UINT m_nDrawType; CPoint m_ptOrigin; CPoint m_ptEnd;),增加一个带参的构造函数(方便在构造的同时赋值)。

CGraph::CGraph(UINT m_nDrawType, CPoint m_ptOrigin, CPoint m_ptEnd)
{
this->m_nDrawType = m_nDrawType;
this->m_ptOrigin = m_ptOrigin;
this->m_ptEnd = m_ptEnd;
}

利用CPtrArray来存储CGraph类对象。增加一个成员变量CPtrArray m_ptrArray。修改OnLButtonUp函数。

#include "Graph.h"

void CXXXView::OnLButtonUp(UINT nflags, CPoint point) // 在鼠标放开时进行绘图
{
...
CGraph graph(m_nDrawType, m_ptOrigin, point); // 创建一个CGraph对象,存放3个要素
m_ptrArray.Add(&graph); // 用集合类的Add函数保存CGraph对象的地址

CView::OnLButtonUp(nflags, point);
}


在OnDraw函数中取出集合类对象中保存的CGraph对象,实现重绘。

void CXXXView::OnDraw(CDC* pDC)
{
...
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH)); // 创建一个指向空画刷的指针
pDC.SelectObject(pBrush); // 将空画刷选入设备描述表(使“矩形”和“椭圆”为空心效果)

for (int i = 0; i < m_ptrArray.GetSize(); i++)
{
switch ((CGraph*)m_ptrArray.GetAt(i))->m_nDrawType)
{
case 1:
pDC->SetPixel(((CGraph*)m_ptArray.GetAt(i))->m_ptEnd, RGB(0, 0, 0));
break;
case 2:
pDC->MoveTo(((CGraph*)m_ptArray.GetAt(i))->m_ptOrigin);
pDC->LineTo(((CGraph*)m_ptArray.GetAt(i))->m_ptEnd);
break;
case 3:
pDC->Rectangle(CRect(((CGraph*)m_ptArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptArray.GetAt(i))->m_ptEnd));
break;
case 4:
pDC->Ellipse(CRect(((CGraph*)m_ptArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptArray.GetAt(i))->m_ptEnd));
break;
default:
break;
}
...
}


编译运行后发现,程序在窗口重绘后没有任何图形输出。
原因在于OnLButtonUp中的CGraph graph(m_nDrawType, m_ptOrigin, point);产生的是一个局部变量,其内存分配在栈上,当OnLButtonUp函数执行完毕后,graph对象发生析构,内存被释放。虽然用m_ptrArray.Add(&graph)将graph对象的内存地址加入到了集合类对象中保存了下来,但在OnDraw中取出这些地址时,由于地址所指向的graph对象早已析构,不存在了,也就无法获取其成员变量的值,不能绘制出保存的图形。

1、CPtrArray m_ptrArray; <--- 集合类对象
2、CGraph graph(...); <--- 内存首地址0088:4400
3、m_ptrArray.Add(&graph); <--- 保存graph对象的内存首地址0088:4400到集合类对象中
4、CGraph对象发生析构,内存被回收
5、m_ptrArray中保存的地址指向的内存空间已无效(已不是一个graph对象)

解决方法是定义一个指针类对象(指针类对象本身占据栈内存),在堆上分配内存产生CGraph对象(指针类对象保存有这些分配的堆内存的地址),然后将堆内存地址保存到集合类对象中(即使指针类对象生命周期结束,也只是栈内存被释放,堆内存仍未释放),那么集合类对象中保存的堆内存的地址仍然是有效的(堆中对象的生命周期和应用程序一致,或到它们被delete掉为止)。

1、CPtrArray m_ptrArray; <--- 集合类对象
2、CGraph* pGraph; <--- CGraph指针类对象(局部变量,该指针类对象本身占据栈内存,首地址0088:4400)
3、pGraph = new CGraph(...); <--- 在堆上分配内存产生CGraph对象(堆内存首地址1244:EE00,这个地址保存在pGraph中)
4、m_ptrArray.Add(pGraph); <--- 将pGraph中保存的堆内存首地址1244:EE00(即pGraph这个指针变量的值)保存到集合类对象中
5、当OnLButtonUp函数执行完毕,pGraph的内存被回收(0088:4400这个地址指向的空间被释放),但不影响堆上的CGraph对象(内存首地址1244:EE00),且其首地址也已经被保存到集合类对象中,因此仍能索引到堆上的CGraph对象

遵照这个思路,修改OnLButtonUp。

void CXXXView::OnLButtonUp(UINT nflags, CPoint point) // 在鼠标放开时进行绘图
{
...
CGraph *pGraph = new CGraph(m_nDrawType, m_ptOrigin, point); // 在堆上创建一个CGraph对象,将地址赋给pGraph指针
m_ptrArray.Add(pGraph); // 用集合类的Add函数保存堆上CGraph对象的地址

CView::OnLButtonUp(nflags, point);
}


窗口发生重绘时的WM_PAINT消息应该是由OnPaint函数响应,但CView::OnPaint会调用OnDraw,而OnDraw是CView的一个虚函数,所以会由派生类的实例对象调用其OnDraw实现。

void CView::OnPaint()
{
CPaintDC dc(this); // 在构造函数中调用CWnd::BeginPaint,在析构函数中调用CWnd::EndPaint
OnPrepareDC(&dc); // 在OnDraw和OnPrint调用前被框架调用,如果只是为了显示而调用,则什么都不做;在派生类中,该函数被重载,应该在派生类中调用基类的实现。
OnDraw(&dc); // OnDraw是虚函数,如派生类有实现就调用派生类的实现,否则调用父类的实现
}


和CPaintDC类似,CClientDC在构造函数中调用GetDC,在析构函数中调用ReleaseDC。
注意:只有在WM_PAINT消息处理函数中才能调用BeginPaint和EndPaint,其他地方只能用GetDC和ReleaseDC对。

如果在CView的派生类中对WM_PAINT消息进行处理,根据消息映射,由于在派生类中对WM_PAINT进行了捕获,则不会调用CView::OnPaint,也就不会自动调用OnDraw了,这点需要注意。

void CXXXView::OnPaint()
{
CPaintDC dc(this);

OnPrepareDC(&dc);
OnDraw(&dc); // 由于不再调用CView::OnDraw,需要自写代码
}


想让窗口具有滚动功能,可在创建工程时选择CScrollView为View类的基类,也可以在工程创建后手动将View类的父类改为CScrollView(注意:除了CXXXView头文件中的类声明处,还需要在CXXXView源文件中将消息路由等几处的CView替换为CScrollView)。
替换后,程序运行出错,是由于还需对滚动窗口做一些设置。

关于坐标空间:
* Windows下的程序运用坐标空间和转换来对图形输出进行缩放、旋转、平移、斜切和反射(镜像)。
* 一个坐标空间是一个平面的空间,通过使用两个相互垂直且长度相等的轴来定位二维对象。
* Win32应用程序设计接口(API)使用四种坐标空间:世界坐标系空间、页面空间、设备空间和物理设备空间。应用程序运用世界坐标系空间对图形输出进行旋转、斜切或反射(镜像)。
* Win32 API把世界坐标系空间和页面空间称为逻辑空间。物理设备空间通常指应用程序窗口的客户区(即获得客户区域的DC,如GetClientDC),但也包括整个桌面(即获得整个桌面的DC)、完整的窗口(包括框架、标题栏和菜单栏)(即获得整个窗口的DC)或打印机(或绘图仪)的一页纸。物理设备的尺寸随显示器、打印机或绘图仪所设置的尺寸而变化。
关于转换:
* 要在物理设备上绘制输出,Windows把一个矩形区域从一个坐标空间拷贝到(或映射到)另一个坐标空间,直至最终完整的输出呈现在物理设备上(通常是屏幕或打印机)。
* 如果应用程序调用了SetWorldTransform函数,那么映射就从应用程序的世界坐标系空间开始,否则,映射在页面空间中进行。在Windows把矩形区域的每一点从一个空间拷贝到另一个空间时,采用一种称作转换的算法,把对象从一个坐标空间拷贝到另一个坐标空间时改变(或转变)这个对象的大小、方位和形态。尽管转换把对象看成一个整体,但也作用于对象中的每一点或每条线。

     ↑               ↑             丨             丨
     丨               丨             丨             丨
 ——十—→       ——十—→     ——十—→     ——十—→
     丨               丨             丨             丨
     丨               丨             ↓             ↓

世界坐标系空间 → 页面空间   →   设备空间   →   物理设备
具体可参考MSDN提供的关于转换的例子。

* 页面空间到设备空间的转换是原Windows接口的一部分,这种转换确定与一特定设备描述表相关的所有图形输出的映射方式。
* 所谓映射方式是指确定用于绘图操作的单位大小的一种量度转换。映射方式是一种影响几乎任何客户区绘图的设备环境属性。另外还有四种设备环境属性:窗口原点、视口原点、窗口范围和视口范围,这四种属性和映射方式密切相关。
* 页面空间到设备空间的转换所用的是两个矩形的宽和高的比率(转换因子),其中页面空间中的矩形被称为窗口,设备空间中的矩形被称为视口,Windows把窗口原点映射到视口原点,把窗口范围映射到视口范围,就完成了这种转换。
* 设备空间到物理空间的转换有几个独特之处:只限于平移,并由Windows的窗口管理部分控制,这种转换的唯一用途是确保设备空间的原点被映射到物理设备上的适当点上。没有函数能设置这种转换,也没有函数可以获取有关数据。

关于默认转换:
* 一旦应用程序建立了设备描述表并立即开始调用GDI绘图或输出函数,则运用默认页面空间到设备空间的转换和设备空间到客户区的转换(在应用程序调用SetWorldTransform函数之前不会出现世界坐标系空间到页面空间的转换)。
* 默认页面空间到设备空间的转换结果是一对一的映射,即页面空间上给出的一个点映射到设备空间的一个点。这种转换没有以矩阵指定,而是通过把视口宽除以窗口宽,把视口高除以窗口高而得出。在默认情况下,视口尺寸为1x1个像素,窗口尺寸为1x1页单位。
* 设备空间到物理设备(客户区、桌面或打印机)的转换结果总是一对一的,即设备空间的一个单位总是与客户区、桌面或打印机上的一个单位相对应。这种转换的唯一用途是平移,无论窗口移到桌面的什么位置,它永远确保输出能够正确无误地出现在窗口上。(设备空间到物理设备的转换总是一对一的,通常将设备空间看成客户区,主要考虑页面空间到设备空间的转换(映射方式、窗口、视口原点))
* 默认转换的一个独特之处是设备空间和应用程序窗口的Y轴方向,在默认的状态下,Y轴正向朝下,负向朝上。

关于逻辑坐标和设备坐标:
* 几乎在所有的GDI函数中使用的坐标值都是采用逻辑单位,Windows必须将逻辑单位转换为“设备单位”,即像素。这种转换是由映射方式、窗口和视口的原点以及窗口和视口的范围所控制的。
* Windows对所有的消息(如WM_SIZE、WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_LBUTTONUP)、所有的非GDI函数和一些GDI函数(如GetDeviceCaps函数,返回值为像素)永远使用设备坐标。
* “窗口”是基于逻辑坐标的,逻辑坐标可以是像素、毫米、英寸等单位,“视口”是基于设备坐标(即像素)的,通常视口和客户区是相同的。
* 缺省的映射模式为MM_TEXT,在这种映射模式下,逻辑单位和设备单位相同。

Mapping Mode Logical Unit Positive y-axis Extends...
MM_TEXT 1 pixel Downward(向下Y轴正方向)
MM_HIMETRIC 0.01 mm Upward(向上Y轴正方向)
MM_TWIPS 1/1440 In Upward
MM_HIENGLISH 0.001 In Upward
MM_LOMETRIC 0.1 mm Upward
MM_LOENGLISH 0.01 In Upward


用SetMapMode函数设置映射模式。

关于逻辑坐标和设备坐标的相互转换:
* 窗口(逻辑)坐标转换为视口(设备)坐标的两个公式:

xViewport = (xWindow - xWinOrg) * (xViewExt / xWinExt) + xViewOrg
yViewport = (yWindow - yWinOrg) * (yViewExt / yWinExt) + yViewOrg
注意:(xViewExt / xWinExt)和(yViewExt / yWinExt)即转换因子。

* 视口(设备)坐标转换为窗口(逻辑)坐标的两个公式:

xWindow = (xViewPort - xViewOrg) * (xWinExt / xViewExt) + xWinOrg
yWindow = (yViewPort - yViewOrg) * (yWinExt / yViewExt) + yWinOrg
注意:(xWinExt / xViewExt)和(yWinExt / yViewExt)即转换因子。

在MM_TEXT映射方式下逻辑坐标和设备坐标的相互转换:
* 窗口(逻辑)坐标转换为视口(设备)坐标的两个公式:

xViewport = xWindow - xWinOrg + nViewOrg
yViewport = yWindow - yWinOrg + yViewOrg
注意:即转换因子为1。

* 视口(设备)坐标转换为窗口(逻辑)坐标的两个公式:

xWindow = xViewport - xViewOrg + xWinOrg
yWindow = yViewport - yViewOrg + yWinOrg
注意:即转换因子为1。

了解这些后,增加一个虚函数OnInitialUpdate(在窗口创建完成后第一个被调用的函数,在OnDraw前调用)。

void CXXXView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();

SetScrollSizes(MM_TEXT, CSize(800, 600));
}

用SetScrollSizes函数设置了映射模式和窗口尺寸后就有了滚动条和滚动功能。

但如果滚动条被拉到底部时绘制了图形,在窗口重绘(比如被其他窗口覆盖后重新显示,而不是拖滚动条)后,线条位置发生了上移。
(提示:绘制时的图形是由OnLButtonUp函数完成的,而重绘时的图形是由OnDraw函数取出保存在m_ptrArray中的CGraph对象完成的)
在OnLButtonUp函数的CGraph *pGraph = new CGraph(m_nDrawType, m_ptOrigin, point);处设置断点,将滚动条拉到底部,绘制线条,可以发现得到的m_ptOrigin是x=692 y=399(注意:这里的y=399已经有问题了,滚动条拉到底时y的值应该很接近600才对),在OnDraw中的pDC->MoveTo(((CGraph*)m_ptArray.GetAt(i))->m_ptOrigin);处再次设置断点,发现m_ptOrigin同样是x=692 y=399,值并没有发生改变。
由于Windows在作图时(也就是使用GDI函数时)使用的是逻辑坐标,而在输出时需要将逻辑坐标转换为设备坐标。
在OnDraw函数被调用前,也就是在OnPaint中先调用了OnPrepareDC,调整了DC的属性。这个例子中的OnPrepareDC被CScrollView重写,在VIEWSCRL.CPP中可以找到该函数:

CScrollView::OnPrepareDC(...)
{
...
if (!pDC->IsPrinting())
{
...
// by default shift viewport origin in negative direction of scroll
ptVpOrg = -GetDeviceScrollPosition(); // 将视口的原点设置为滚动位置的反方向
...
}
pDC->SetViewportOrg(ptVpOrg); // 设置视口的原点
...
}


关于视口和窗口原点的改变:
* CDC中提供了两个成员函数SetViewportOrg和SetWindowOrg,来改变视口和窗口的原点。
* 如果将视口原点设置为(xViewOrg, yViewOrg),则逻辑点(0, 0)就会被映射为设备点(xViewOrg, yViewOrg)。(指在不改变窗口原点的情况下,即xWinOrg = 0,yWinOrg = 0)

逻辑点(0,0)也就是xWindow = 0,yWindow = 0。
xViewport = xWindow - xWinOrg + xViewOrg = 0 - 0 + xViewOrg = xViewOrg
yViewport = yWindow - yWinOrg + yViewOrg = 0 - 0 + yViewOrg = yViewOrg

* 如果将窗口原点改变为(xWinOrg, yWinOrg),则逻辑点(xWinOrg, yWinOrg)将会被映射为设备点(0, 0),即左上角。(指在不改变视口原点的情况下,即xViewOrg = 0,yViewOrg = 0)

逻辑点(xWinOrg,yWinOrg)也就是xWindow = xWinOrg,yWindow = yWinOrg。
xViewport = xWindow - xWinOrg + xViewOrg = xWinOrg - xWinOrg + xViewOrg = 0 + 0 = 0
yViewport = yWindow - yWinOrg + yViewOrg = yWinOrg - yWinOrg + yViewOrg = 0 + 0 = 0

* 不管对窗口和视口原点作什么改变,设备点(0, 0)始终是客户区的左上角。

调试运行程序,将滚动条拖动到底端,在pDC->SetViewportOrg(ptVpOrg);处设置断点,可以发现ptVpOrg是x=0 y=-150,也就是将视口的原点设置为x=0 y=-150。

xViewport = xWindow - xWinOrg + xViewOrg = xWindow - 0 + 0 = xWindow (即x坐标没有改变)
yViewport = yWindow - yWinOrg + yViewOrg = yWindow - 0 + (-150) = yWindow - 150 (即y坐标上移了150像素)

OnDraw函数中绘制的图形上移的原因就是这里的视口原点被改变了。

关于图形错位的说明:
* 当在窗口中点击鼠标左键时得到的是设备坐标(680,390),在MM_TEXT的映射模式下,逻辑坐标和设备坐标是相等的,所以利用集合类保存的这个点的坐标是以像素为单位,坐标值为(680,390)。(同时,由于第一次绘出的图形是由OnLButtonDown完成的,它并没有调用OnPrepareDC来调整显示上下文的属性,也就是没有改变视口的原点,视口原点和窗口原点都是(0,0),绘出的图形位置正确)
* 在调用OnDraw函数前,在OnPaint函数中调用了OnPrepareDC,调整了显示上下文的属性,将视口的原点设置为了(0,-150),这样,窗口的原点,也就是逻辑坐标(0,0)将被映射为设备坐标(0,-150)。(因为窗口的垂直滚动条被拖动到底部,那么原先应该显示在(0,0)的内容现在应该显示在(0,-150)的地方,也就是窗口内容向上移动了150个像素)
* 在画线的时候,因为GDI函数使用的是逻辑坐标,而图形在显示的时候,Windows需要将逻辑坐标转换为设备坐标,因此,原先保存的坐标点(680,390)(在GDI函数中,作为逻辑坐标使用),根据转换公式xViewport = xWindow - xWinOrg + xViewOrg和yViewport = yWindow - yWinOrg + yViewOrg,得到设备点的x坐标为680 - 0 + 0 = 680,设备点的y坐标为390 - 0 + (-150) = 240。
* 于是看到图形在原先显示地方的上方出现。

关于解决方法的说明:
* 在绘制图形(OnLButtonUp中)后、保存坐标点前,调用OnPrepareDC,调整显示上下文的属性,将视口的原点设置为(0,-150),这样,窗口的原点(也就是逻辑坐标(0,0))将被映射为设备坐标(0,-150)。
* 然后调用DPtoLP函数,将设备坐标(680,390)转换为逻辑坐标。根据设备坐标转换为逻辑坐标的公式:

xWindow = xViewport - xViewOrg + xWinOrg = 680 - 0 + 0 = 680
yWindow = yViewport - yViewOrg + yWinOrg = 390 - (-150) + 0 = 540

  得到逻辑点的x坐标为680,y坐标为540,将逻辑坐标(680,540)保存起来。
* 在窗口重绘时,会先调用OnPrepareDC调整显示上下文的属性,将视口的原点设置为(0,-150),然后GDI函数用逻辑坐标点(680,540)绘制图形,被Windows转换为设备坐标点(680,390),这和原先显示图形时的设备点是一样的,图形在原先的地方显示。

xViewport = xWindow - xWinOrg + xViewOrg = 680 - 0 + 0 = 680
yViweport = yWindow - yWinOrg + yViewOrg = 540 - 0 + (-150) = 390

修改OnLButtonUp。

void CXXXView::OnLButtonUp(UINT nflags, CPoint point) // 在鼠标放开时进行绘图
{
CClientDC dc(this); // 创建DC
CPen pen(PS_SOLID, 1, RGB(255, 0, 0)); // 创建红色的画笔
dc.SelectObject(&pen); // 将画笔选入设备表述表
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH)); // 创建一个指向空画刷的指针
dc.SelectObject(pBrush); // 将空画刷选入设备描述表(使“矩形”和“椭圆”为空心效果)

switch (m_nDrawType)
{
case 1: // 画“点”
dc.SetPixel(point, RGB(255, 0, 0)); // 一个红色的点
break;
case 2: // 画“直线”
dc.MoveTo(m_ptOrigin); // 移动到起点(即鼠标按下时保存的点)
dc.LineTo(point); // 画一条直线到鼠标放开时的点
break;
case 3: // 画“矩形”
dc.Rectangle(CRect(m_ptOrigin, point)); // 用CRect组合2个点为一个矩形对象(注意:这里CRect会自动将CRect转换为LPCRECT,因为CRect重载了一个操作符LPCRECT,当CDC::Rectangle发现传入的参数是一个类对象时,会调用重载的带有LPCRECT的函数,让CRect可以自动将CRect转换为LPCRECT?)
break;
case 4: // 画“椭圆”
dc.Ellipse(CRect(m_ptOrigin, point)); // 类似“矩形”
break;
default:
}


OnPrepareDC(&dc); // 调整显示上下文的属性,将视口的原点设置为(0,-150)(-150是根据滚动窗口的实际位置确定的)这样,窗口的原点(也就是逻辑坐标(0,0))将被映射为设备坐标(0,-150)

dc.DPtoLP(&m_ptOrigin); // 调用DPtoLP函数,将设备坐标转换为逻辑坐标
dc.DPtoLP(&point); // 对2个点都要转换

CGraph *pGraph = new CGraph(m_nDrawType, m_ptOrigin, point); // 在堆上创建一个CGraph对象,将地址赋给pGraph指针
m_ptrArray.Add(pGraph); // 用集合类的Add函数保存堆上CGraph对象的地址

CScrollView::OnLButtonUp(nflags, point);
}


OnPrepareDC会随时根据滚动窗口的位置来调整视口的原点(在窗口重绘时,调用OnPrepareDC时,滚动条处于最上方时,视口原点Y轴方向为0,滚动条下拉时,视口原点Y轴方向为负值)。

重点回顾:在作图时,使用了逻辑坐标点,相当于在页面空间中画图,在输出时需要映射到设备空间,逻辑坐标转换为设备坐标(依据映射模式、窗口原点、视口原点、窗口范围和视口范围来计算)。

另一种保存图形和重绘图形的方法
==============================
CMetaFileDC(从CDC派生),可以包含一系列GDI命令,可以重放或创建图形和文本。(类似图形以命令形式(直线、椭圆、文本等等)画在一副画布上)

在View类中增加一个CMetaFileDC m_dcMetaFile成员变量,并在View类的构造函数中初始化(创建)。

CXXXView::CXXXView()
{
...
m_dcMetaFile.Create(NULL); // 调用Create创建MetaFile,参数为NULL表示创建内存的MetaFile
}

在OnLButtonUp中,将所有的dc改为dcMetaFile,将原先画在屏幕上的内容放入MetaFile。

void CXXXView::OnLButtonUp(UINT nflags, CPoint point)
{
// CClientDC dc(this);
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
m_dcMetaFile.SelectObject(pBrush);

switch (m_nDrawType)
{
case 1:
m_dcMetaFile.SetPixel(point, RGB(255, 0, 0));
break;
case 2:
m_dcMetaFile.MoveTo(m_ptOrigin);
m_dcMetaFile.LineTo(point);
break;
case 3:
m_dcMetaFile.Rectangle(CRect(m_ptOrigin, point));
break;
case 4:
m_dcMetaFile.Ellipse(CRect(m_ptOrigin, point));
break;
default:
}

CScrollView::OnLButtonUp(nflags, point);
}


在OnDraw中关闭MetaFile,获得句柄,进行重放。

void CXXXView::OnDraw(CDC* pDC)
{
CXXXDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

HMETAFILE hmetaFile; // 定义句柄
hmetaFile = m_dcMetaFile.Close(); // 关闭MetaFile,获得句柄
pDC->PlayMetafile(hmetaFile); // 播放,重绘
m_dcMetaFile.Create(); // 创建一个新的MetaFile,为再次在OnLButtonUp中绘图做好准备
m_dcMetaFile.PlayMetaFile(hmetaFile); // 在新建的MetaFile中播放原来的MetaFile的内容,这样就可以将原来的图形保存到新的MetaFile中去(非常聪明的方法)
DeleteMetaFile(hmetaFile); // 删除MetaFile句柄,释放资源
}


这种方法有个问题,就是在OnLButtonUp中只对MetaFile进行了绘图,屏幕上没有输出,直到窗口重绘才能显示出来。(其实在每一次OnLButtonUp的最后调用Invalidate即可使图形重绘出来)

把绘制的MetaFile文档化
----------------------
对ID_FILE_SAVE和ID_FILE_OPEN增加消息响应函数。

void CXXXView::OnFileSave()
{
HMETAFILE hmetaFile; // 定义句柄
hmetaFile = m_dcMetaFile.Close(); // 关闭MetaFile,获得句柄
CopyMetaFile(hmetaFile, "meta.wmf"); // 保存为meta.wmf
m_dcMetaFile.Create(); // 同样不要忘记创建一个新的MetaFile,为再次在OnLButtonUp中绘图做好准备
DeleteMetaFile(hmetaFile); // 删除MetaFile句柄,释放资源
}

void CXXXView::OnFileOpen()
{
HMETAFILE hmetaFile; // 定义句柄
hmetaFile = GetMetaFile("meta.wmf"); // 虽然GetMetaFile已废弃(GetEnhMetaFile),但仍可使用
m_dcMetaFile.PlayMetaFile(hmetaFile); // 在m_dcMetaFile中播放hmetaFile,这样就将其内容保存了下来
DeleteMetaFile(hmetaFile); // 删除MetaFile句柄,释放资源
Invalidate(); // 使窗口重绘,这样就可以触发OnDraw函数绘出图形
}

还可以利用兼容DC来实现
----------------------
在View类中增加CDC m_dcCompatible。

修改OnLButtonUp函数,用兼容DC替代MetaFile。

void CXXXView::OnLButtonUp(UINT nflags, CPoint point)
{
CClientDC dc(this);
CBrush* pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
m_dcMetaFile.SelectObject(pBrush);

if (!m_dcCompatible.m_hDC) // 如果兼容DC还未创建
{
m_dcCompatible.CreateCompatibleDC(&dc); // 创建兼容DC
CRect rect;
GetClientRect(&rect); // 获取当前客户区域尺寸
CBitmap bitmap;
bitmap.CreateCompatibleBitmap(&dc, rect.Width(), rect.Height()); // 创建和dc兼容的一副位图
m_dcCompatible.SelectObject(&bitmap); // 通过兼容位图的尺寸来确定m_dcCompatible显示表面的尺寸
m_dcCompatible.BitBlt(0, 0, rect.Width(), rect.Height(), &dc, 0, 0, SCRCOPY); // 调用BitBlt将原始设备描述表的颜色表和像素数据块拷贝到兼容设备描述表(见注)
m_dcCompatible.SelectObject(pBrush); // 选择透明画刷
}

switch (m_nDrawType)
{
case 1:
m_dcCompatible.SetPixel(point, RGB(255, 0, 0)); // 将所有的dcMetaFile改为m_dcCompatible
break;
case 2:
m_dcCompatible.MoveTo(m_ptOrigin);
m_dcCompatible.LineTo(point);
break;
case 3:
m_dcCompatible.Rectangle(CRect(m_ptOrigin, point));
break;
case 4:
m_dcCompatible.Ellipse(CRect(m_ptOrigin, point));
break;
default:
}

CScrollView::OnLButtonUp(nflags, point);
}


注:CreateCompatibleBitmap返回的位图对象只包含相应设备描述表中的位图的位图信息头,不包含颜色和像素数据块。因此,选入该位图对象的设备描述表不能和选入普通位图对象的设备描述表一样应用,必须在SelectObject函数之后调用BitBlt将原始设备描述表的颜色表和像素数据块拷贝到兼容设备描述表。

修改OnDraw。

void CXXXView::OnDraw(CDC* pDC)
{
CXXXDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

CRect rect;
GetClientRect(&rect); // 获得客户区尺寸
pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &m_dcCompatible, 0, 0, SRCCOPY); // 将兼容DC中的内容拷贝到当前DC中
}

  评论这张
 
阅读(732)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017