(孙鑫 十一)图形的保存与重绘

如何让CDC上输出的文字、图形具有保持功能,集合类CPtrArray的使用,CPaintDC与CClientDC的区别与应用,OnPaint与OnDraw在CView中的关系及实现内幕,滚动窗口的实现,坐标空间,映射方式,设备坐标与逻辑坐标的转换。元文件设备描述表的使用,如何利用兼容DC实现图形的保存和再现。

  创建mfc单文档程序的时候,可在第6步选择view、app等有更多功能的类。

1.增加绘图菜单,点 直线 矩形 椭圆(的菜单项)及命令响应(view类的),再增加一个成员变量UINT m_nDrawStyle private,构造为0。在上面的命令响应将它设置为1/2/3/4。
  再增加一个CPoint的m_ptOrigin private保存原来的点坐标,初始化为0。添加WM_LBUTTONDOWN和WM_LBUTTONUP消息处理,在BUTTONDOWN中保存m_ptOrigin,在BUTTONUP中编写switch(m_nDrawStyle)画点那些……
  当窗体重绘的时候要调用OnDraw函数。为了保存一个图形的三要素,插入一个新的类Generic Class,CGraphic。然后给它增加3个成员变量UINT m_nDrawStyle public;CPoint m_ptOrigin;CPoint m_ptEnd;
  在构造函数中构造一个带参数的构造函数,在原构造函数中添加一个CGraphic(UINT m_nDrawStyle,CPoint m_ptOrigin,CPoint m_ptEnd);
  用CPtrArray来存储对象指针数组,用Add增加元素,GetAt获取指定元素……
  在LButtonUp中,增加一个CGraphic类
	CGraphic graph(m_nDrawStyle,m_ptOrigin,point);
  在view类增加一个CPtrArray m_ptrArray private.
  然后在LButtonUp中添加指针元素:m_ptrArray.Add(&graph);
 
  然后在OnDraw函数中,
	CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
	pDC->SelectObject(pBrush);

	for(int i=0;i<m_ptrArray.GetSize();i++)
	{
		switch(((CGraphic *)m_ptrArray.GetAt(i))->m_nDrawStyle)
		{
		case 1:
			pDC->SetPixel(((CGraphic *)m_ptrArray.GetAt(i))->m_ptEnd,RGB(0,0,0));
			break;
		case 2:
			pDC->MoveTo(((CGraphic *)m_ptrArray.GetAt(i))->m_ptOrigin);
			pDC->LineTo(((CGraphic *)m_ptrArray.GetAt(i))->m_ptEnd);
			break;
		case 3:
			pDC->Rectangle(CRect(((CGraphic *)m_ptrArray.GetAt(i))->m_ptOrigin,((CGraphic *)m_ptrArray.GetAt(i))->m_ptEnd ));
			break;
		case 4:
			pDC->Ellipse(CRect(((CGraphic *)m_ptrArray.GetAt(i))->m_ptOrigin,((CGraphic *)m_ptrArray.GetAt(i))->m_ptEnd));
			break;
		}
	}
  但这样仍然在重绘的时候不能显示已经画的,原因是
	CGraphic graph(m_nDrawStyle,m_ptOrigin,point);  
//在OnLButtonUp函数中构造的对象存储在栈内存中,当此函数运行完后就释放这内存了
	m_ptrArray.Add(&graph);  
  这时需要用CGraph *pGraph;pGraph=new CGraph(...);   
//这时CGraph对象是存储在堆内存,当程序退出时才释放
  所以改成这样既可:
	CGraphic* graph=new CGraphic(m_nDrawStyle,m_ptOrigin,point);
	m_ptrArray.Add(graph);

2.实现窗口具有滚动能力(滚动条,可以在开始的时候选择CScrollView类)
CPaintDC在构造的时候调用了BeginPaint,在析构的时候调用了EndPaint,它只可以在回应WM_PAINT消息的时候使用。
  MFC先调用默认OnPaint函数,再OnDraw函数(被OnPaint函数调用)。当我们自己添加OnPaint函数时,就不会调用OnDraw函数了,可以自己调用。
  给view类添加一个WM_PAINT消息,编辑OnPaint函数,需要添加:
	OnPrepareDC(&dc);
	OnDraw(&dc);
  这是我们可以将view类替换成ScrollView类,在头文件中改一处,在源文件中需要用CScrollView替换CView了。但这时运行程序有非法错误,因为滚动条没有设置。
  需要调用void SetScrollSizes( int nMapMode, SIZE sizeTotal, const SIZE& sizePage = sizeDefault, const SIZE& sizeLine = sizeDefault );
//映射模式

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

  下面是运用SetWorldTransform函数进行的一个转换的例子:

Ymax			Ymax			Ymin
	区域			区域			
0			0			0			屏幕(左上角--区域)
							区域
Ymin			Ymin			Ymax

世界坐标系空间		页面空间		设备空间		物理设备


	页面空间到设备空间的转换
  页面空间到设备空间的转换是原windows接口的一部分。这种转换确定与一特定设备描述表相关的所有图形输出的映射方式。
  所谓映射方式是指确定用于绘图操作的单位大小的一种量度转换。映射方式是一种影响几乎任何客户区绘图的设备环境属性。另外还有4中设备环境属性:窗口原点、视口原点、窗口范围和视口范围,这四种属性与映射方式密切相关。
  页面空间到设备空间的转换所用的是两个矩形的宽与高的比率,其中页面空间中的矩形被称为窗口,设备空间中的矩形被称为视口,windows把窗口原点映射到视口原点,把窗口范围映射到视口范围,就完成了这种转换。

	设备空间到物理空间的转换
  设备空间到物理空间的转换有几个独特之处:它只限于平移,并由windows的窗口管理部分控制,这种转换的唯一用途是确保设备空间的原点被映射到物理设备上的适当点上。没有函数能设置这种转换,也没有函数可获取有关数据。

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

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

回到void SetScrollSizes( int nMapMode, SIZE sizeTotal, const SIZE& sizePage = sizeDefault, const SIZE& sizeLine = sizeDefault );
//映射模式有
Mapping Mode		Logical Unit		Positive y-axis Extends...
MM_TEXT			1 pixel			Downward
MM_HIMETRIC		0.01mm			Upward
MM_TWIPS		1/1440 in		Upward
改变映射模式用SetMapMode

	逻辑坐标和设备坐标的相互转换
  窗口(逻辑)坐标转换为视口(设备)坐标的两个公式:Org代表原点,Ext代表范围
xViewport=(xWindow-xWinOrg)*xViewExt/xWinExt+xViewOrg
yViewport=(yWindow-yWinOrg)*yViewExt/yWinExt+yViewOrg

  视口(设备)坐标转换为窗口(逻辑)坐标的两个公式:
xWindow=(xViewPort-xViewOrg)*xWinExt/xViewExt+xWinOrg
yWindow=(yViewPort-yViewOrg)*yWinExt/yViewExt+yWinOrg

	在MM_TEXT映射方式下逻辑坐标和设备坐标的相互转换
窗口(逻辑)坐标转换为视口(设备)坐标的两个方式:
xViewport=xWindow-xWinOrg+xViewOrg
yViewport=yWindow-yWinOrg+yViewOrg
视口(设备)坐标转换为窗口(逻辑)坐标的两个公式:
xWindow=xViewport-xViewOrg+xWinOrg
yWindow=yViewport-yViewOrg+yWinOrg

void SetScrollSizes( int nMapMode, SIZE sizeTotal, const SIZE& sizePage = sizeDefault, const SIZE& sizeLine = sizeDefault );
//sizeTotal--总长;sizePage--滚动的单位……
调用它应该是窗口创建之后,可以在view类增加一个虚函数OnInitialUpdate(在第一个视图和文档关联后调用),它的调用在OnDraw之前。然后在其中设置滚动条:
	SetScrollSizes(MM_TEXT,CSize(800,600));
但这时,当滚动条滚动到下面后,画了线之后,重绘时线会上移(因为OnDraw函数……)

	视口和窗口原点的改变
  CDC中提供了两个成员函数SetViewportOrg和SetWindowOrg,用来改变视口和窗口的原点
  若将视口原点设置为(xViewOrg,yViewOrg),则逻辑点(0,0)就会被映射为设备点(xViewOrg,yViewOrg)。若将窗口原点改变为(xViewOrg,yViewOrg),则逻辑点(xWinOrg,yWinOrg)将会被映射为设备点(0,0),即左上角。
  注意:不管对窗口和视口原点做什么改变,设备点(0,0)始终是客户区的左上角。

 	关于图形错位的说明
  当我们在窗口中点击鼠标左键的时候,得到的是设备坐标(680,390),在MM_TEXT的映射模式下,逻辑坐标和设备坐标是相等的,所以我们利用集合类保存的这个点的左边是以像素为单位,坐标值为(680,390)。在调用OnDraw函数前,在OnPaint函数中调用了OnPrepareDC函数,调整了显示上下文的属性,将视口的原点设置为了(0,-150),这样的话,窗口的原点,也就是逻辑坐标(0,0)将被映射为设备坐标(0,-150),在画线的时候,因为GDI的函数使用的是逻辑坐标,而图形在显示的时候,windows需要将逻辑坐标转化为设备坐标,因此,原先保存的坐标点(680,390)(在GDI函数中,作为逻辑坐标使用),根据转换公式,得到设备点的x坐标是680-0+0=680,设备点的y坐标为390-0+(-150)=240,于是我们看到图形在原先显示地方的上方出现了。

	关于解决方法的说明
  首先我们在绘制图形之后,在保存坐标点之前,调用OnPrepareDC函数,调整显示上下文的属性,将视口的原点设置为(0,-150),这样的话,窗口的原点,也就是逻辑坐标(0,0)将被映射为设备坐标(0,-150),然后我们调用DPtoLP函数将设备坐标(680,390)转换为逻辑坐标,根据设备坐标转换为逻辑坐标的公式:
xWindow=xViewport-xViewOrg+xWinOrg
yWindow=yViewport-yViewOrg+yWinOrg
得到逻辑点的x坐标(680,540)保存起来,在窗口重绘时,会先调用OnPrepareDC函数,调整显示上下文的属性,将视口的原点设置为了(0,-150),然后GDI函数用逻辑坐标点(680,540)绘制图形,被windows转换为设备坐标点(680,390),和原先显示图形时的设备点是一样的,当然图形就还在原先的地方显示出来。
//OnPrepareDC会随时根据滚动窗口的位置来调整视口的原点

  在OnLButtonUp中调用OnPrepareDC,再转换设备点为逻辑点。
	OnPrepareDC(&dc);
	dc.DPtoLP(&m_ptOrigin);
	dc.DPtoLP(&point);  //在保存之前

3.CMetaFileDC源文件DC,从CDC派生而来。
  在view类增加一个成员变量CMetaFileDC m_dcMetaFile private,在view构造函数中:m_dcMetaFile.Create();
  然后在OnLButtonUp中,将dc替换为CMetaFileDC。再把下面存储对象graph以及坐标转换的代码注释掉。
  再注释掉OnDraw函数中以前的代码,加入以下:
	HMETAFILE metafile;
	metafile=m_dcMetaFile.Close();  //保存源文件
	pDC->PlayMetaFile(metafile);  //播放
	m_dcMetaFile.Create();   //新建
	DeleteMetaFile(metafile);  //删除
  但这样先前画的会被删除,这是在新建后,调用
	m_dcMetaFile.PlayMetaFile(metafile); //可以将metafile复制到这个对象中

  然后在菜单栏中分别给打开和保存增加命令响应。
  保存源文件,可用CopyMetaFile拷贝metafile文件到指定格式的文件
HMETAFILE CopyMetaFile(
  HMETAFILE hmfSrc,  // handle to a Windows-format metafile
  LPCTSTR lpszFile   // pointer to a filename string
);
  在“保存”的命令响应中:
	HMETAFILE hmetaFile;
	hmetaFile=m_dcMetaFile.Close();
	CopyMetaFile(hmetaFile,"meta.wmf");
	m_dcMetaFile.Create();
	DeleteMetaFile(hmetaFile);
  在“打开”的命令响应中:
用GetMetaFile();获取元文件

4.兼容DC
  在view类新增一个变量,CDC m_dcCompatible private,然后在OnLButtonUp中:

	if(!m_dcCompatible.m_hDC)
	{
		m_dcCompatible.CreateCompatibleDC(&dc);
		CRect rect;
		GetClientRect(&rect);
		CBitmap bitmap;
		bitmap.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height());
		m_dcCompatible.SelectObject(&bitmap);
		m_dcCompatible.SelectObject(pBrush);
	}   //然后将后面switch中的dc改为m_dcCompatible

  然后在OnDraw函数中:
	CRect rect;
	GetClientRect(&rect);
	pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompatible,0,0,SRCCOPY);
  但这样程序会有问题
  因为CreateCompatibleBitmap返回的位图对象只包含相应设备描述表中的位图的位图信息头,不包含颜色表和像素数据块。因此,选入该位图对象的设备描述表不能想选入普通位图对象的设备描述表一样应用,必须在SelectObject函数之后,调用BitBlt将原始设备描述表的颜色表及像素数据块拷贝到兼容设备描述表。
  即要在OnLButtonUp中,在m_dcCompatible选择了对象后,调用BitBlt函数。
	m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值