转自 http://blog.sina.com.cn/s/articlelist_1815328704_0_1.html
1.创建4个菜单,为其添加消息响应,用成员变量保存绘画类型。添加LButtonDown和Up消息。
2.当窗口重绘时,如果想再显示原先画的数据,则需要保存数据。为此创建一个新类来记录绘画类型和两个点。class CGraph
{
public:
CPoint m_ptOrigin;//起点
CPoint m_ptEnd;//终点
UINT m_nDrawType;//绘画类型
CGraph();
CGraph(UINT m_nDrawType,CPoint m_ptOrigin,CPoint m_ptEnd);//此为构造函数。
virtual ~CGraph();
};
然后在void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)中加入如下代码
//CGraph graph(m_nDrawType,m_ptOrigin,point);//不能用局部变量
//m_ptrArray.Add(&graph);//加入这种指针数组中
//加入到指针数组中
在GraphicView.h中有如下代码
CPtrArray m_ptrArray;
在OnDraw中重画时调出数据
for(int i=0;i<m_ptrArray.GetSize();i++)
3.在CView::OnPaint()调用了OnDraw(),但在void CGraphicView::OnPaint()中MFC的Wizard没有调用OnDraw(),要注意这个区别。如果你此时想调用,必须手动添加代码。 OnDraw(&dc);
4.让窗口具有滚动条的功能。
第1.将CGraphicView的头文件中的CView全部替换成CSrollView
第2.添加如下的代码
void CGraphicView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
// TOD Add your specialized code here and/or call the base class
SetScrollSizes(MM_TEXT,CSize(800,600));//设置映射模式,设定窗口大小。OK!
}
5.坐标系的转换,此处不再详细介绍,需要时请查阅相关资料。
6.解决重绘时线跑到上面的问题。为什么会错位?因为逻辑坐标和设备坐标没有对应起来。
解决方法:
在OnLButtonDown画完图后,保存之前。调用
7.另外两种方法来保存数据。
一种是用CMetaFileDC
另一种是利用兼容DC,重绘时利用 pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompatible,0,0,SRCCOPY);
将兼容DC的图拷贝到屏幕DC上去。
此处不再详细介绍这两种方法,因为介绍多了容易搞晕。呵呵
如何让CDC上输出的文字、图形具有保持功能,集合类CPtrArray的使用,CPaintDC与CClientDC 的区别与应用,OnPaint与OnDraw在CView中的关系及实现内幕,滚动窗口的实现,坐标空间,映射方式,设备坐标与逻辑坐标的转换。元文件设 备描述表的使用,如何利用兼容DC实现图形的保存和再现。
#先实现一下上节课的绘图功能
#保存所绘制的图像以及图像的重绘00:09
3个要素 绘制的类型,起点,终点
保存这3个要素就可以在OnDraw中对其进行重绘
×新建一个类来保存CGraph
class CGraph
{
public:
CPoint m_ptOrigin; //起点
CPoint m_ptEnd; //终点
UINT m_nDrawType; //类型
CGraph();
CGraph(UINT m_nDrawType,CPoint m_ptOrigin,CPoint m_ptEnd);
virtual ~CGraph();
};
×动态存储这些对象
用MFC的集合类CPtrArray,支持void指针数组。CPtrArray与CObArray相似
MSDN:
The CPtrArray class supports arrays of void pointers.
The member functions of CPtrArray are similar to the member functions of class CObArray. Because of this similarity, you can use the CObArray reference documentation for member function specifics. Wherever you see a CObject pointer as a function parameter or return value, substitute a pointer to void.
CObject* CObArray::GetAt( int <nIndex> ) const;
for example, translates to
void* CPtrArray::GetAt( int <nIndex> ) const;
#增加Add,获取元素GetAt,获取元素数目GetSize
#代码:
//void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
//使用new在堆中分配空间,在栈中分配的话CGraph的对象析构以后内存被回收
CGraph *pGraph=new CGraph(m_nDrawType,m_ptOrigin,point);
m_ptrArray.Add(pGraph); //CPtrArray m_ptrArray;
//void CGraphicView::OnDraw(CDC* pDC)
CBrush *pBrush=CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
pDC->SelectObject(pBrush);
for(int i=0;i<m_ptrArray.GetSize();i++)
{
//void* to CGraph* 类型转换
switch(((CGraph*)m_ptrArray.GetAt(i))->m_nDrawType)
{
case 1:
pDC->SetPixel(((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd,RGB(0,0,0));
break;
case 2:
pDC->MoveTo(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin);
pDC->LineTo(((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd);
break;
case 3:
pDC->Rectangle(CRect(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd));
break;
case 4:
pDC->Ellipse(CRect(((CGraph*)m_ptrArray.GetAt(i))->m_ptOrigin,
((CGraph*)m_ptrArray.GetAt(i))->m_ptEnd));
break;
}
}
#OnDraw函数00:32
OnDraw是个虚函数。
Vc安装目录vc98-》MFC-》SRC-》viewcore.cpp
void CView::OnPaint() //可以在此设置断点看是否能够进入这里
{
// standard paint routine
CPaintDC dc(this);
OnPrepareDC(&dc);
//是个虚函数,在OnDraw和OnPrint调用之前被Framework所调用,屏幕显示时是什么不做,当使用CScrollView时则需要它对设备上下文的属性进行调整
OnDraw(&dc);
//是个虚函数,调用子类函数(多态性)
}
//OnPaint中只能用begainpaint和endpaint(第一课中),但这里构造了一个CPaintDC的dc
// CPaintDC派生自CDC构造时调用了BeginPaint析构时调用了EndPaint
//CPaintDC只能在响应WM_PAINT消息时使用,通常在OnPaint中
//Device Contexts:
//CClientDC和表示了客户区域的设备上下文一起工作,构造时调用GetDC,析构时调用ReleaseDC(GetDC和ReleaseDC用在OnPaint以外的其他地方而不能用BeginPaint和EndPaint)
00:42
在view类中重写OnPaint消息,则系统会调用自定义的OnPaint函数。
void CGraphicView::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: Add your message handler code here
OnPrepareDC(&dc);
OnDraw(&dc);
// Do not call CScrollView::OnPaint() for painting messages
}
#让窗口具有滚动的能力00:44
#在头文件和源文件中手动把view的基类改为CScrollView
#对CScrollView的尺寸进行设置
SetScrollSizes
×映射的模式和映射的方式
※ 坐标空间
Microsoft Windows下的程序运用坐标空间和转换来对图形输出进行缩放、旋转、平移、斜切和反射。
Coordinate Spaces and Transformations
Msdn:……
一个坐标空间是一个平面的空间,通过使用两个相互垂直并且长度相等的轴来定位二维对象。(x和y轴……)
Win32应用程序设计接口(API)使用四种坐标空间:世界坐标系空间、页面空间、设备空间、和物理设备空间。应用程序运用世界坐标系空间对图形输出进行旋转、斜切或者反射。
Win32 API把世界坐标系空间和页面空间称为逻辑空间;最后一种坐标空间(即物理设备空间)通常指应用程序窗口的客户区;但是它也包括整个桌面、完整的窗口(包 括框架、标题栏和菜单栏)或打印机的一页或绘图仪的一页纸。物理设备的尺寸随显示器、打印机或绘图仪所设置的尺寸而变化。
※ 转换
如要在物理设备上绘制输出,Windows把一个矩形区域从一个坐标空间拷贝到(或映射到)另一个坐标空间,直至最终完整的输出呈现在物理设备上(通常是屏幕或打印机)。
如果该应用程序调用了SetWorldTransform函数,那么映射就从应用程序的世界坐标系空间开始;否则,映射 在页面空间中进行。在Windows把矩形区域的每一点从一个空间拷贝到另一个空间时,它采用了一种被称作转换的算法,转换是把对象从一个坐标空间拷贝到 另一个坐标空间时改变(或转变)这一对象的大小、方位、和形态,尽管转换把对象看成一个整体,但它也作用于对象中的每一点或每条线。
Msdn有例子。
※页面空间到设备空间的转换(主要)
页面空间到设备空间的转换是原Windows接口的一部分。这种转换确定与一特定设备描述表相关的所有图形输出的映射方式。
(逻辑到设备)
所谓映射方式是指确定用于绘图操作的单位大小的一种量度转换。映射方式是一种影响几乎任何客户区绘图的设备环境属性。另外还有四种设备环境属性:窗口原点、视口原点、窗口范围和视口范围,这四种属性与映射方式密切相关。
页面空间到设备空间的转换所用的是两个矩形的宽与高的比率,其中页面空间中的矩形被称为窗口,设备空间中的矩形被称为视口,Windows把窗口原点映射到视口原点,把窗口范围映射到视口范围,就完成了这种转换,如下图所示:
#设备空间到物理空间的转换
设备空间到物理空间的转换有几个独特之处:它只限于平移,并由Windows的窗口管理部分控制,这种转换的唯一用途是确保设备空间的原点被映射到物理设备上的适当点上。没有函数能设置这种转换,也没有函数可以获取有关数据。
#默认转换
一旦应用程序建立了设备描述表,并立即开始调用GDI绘图或输出函数,则运用默认页面空间到设备空间的转换和设备空间到客户区(物理设备空间)的转换(在应用程序调用SetWorldTransform函数之前,不会出现世界坐标空间到页面空间的转换)。
默认页面空间到设备空间的转换结果是一对一的映射;即页面空间上给出的一点映射到设备空间的一个点。正如前文讲到的,这种转换没有以矩阵指定,而是通过把视口宽除以窗口宽,把视口高除以窗口高而得出的。在默认的情况下,视口尺寸为1x1个象素,窗口尺寸为1x1页单位。
设备空间到物理设备(客户区、桌面或打印机)的转换结果总是一对一的;即设备空间的一个单位总是与客户区、桌面、或打印机上的一个单位相对应。这一转换的唯一用途是平移。无论窗口移到桌面的什么位置,它永远确保输出能够正确无误地出现在窗口上。
默认转换的一个独特之处是设备空间和应用程序窗口的y轴方向。在默认的状态下,y轴正向朝下,负y方向朝上
#逻辑坐标和设备坐标
几乎在所有的GDI函数中使用的坐标值都是采用的逻辑单位。Windows必须将逻辑单位转换为“设备单位”,即像素。这种转换是由映射方式、窗口和视口的原点以及窗口和视口的范围所控制的。
Windows对所有的消息(如WM_SIZE、WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_LBUTTONUP),所有的非GDI函数和一些GDI函数(例如GetDeviceCaps函数),永远使用设备坐标。
“窗口”是基于逻辑坐标的,逻辑坐标可以是象素、毫米、英寸等单位;“视口”是基于设备坐标(象素)的。通常,视口和客户区是相同的。
缺省的映射模式为MM_TEXT。在这种映射模式下,逻辑单位和设备单位相同。
#SetMapMode改变映射方式
#逻辑坐标和设备坐标的相互转换
窗口(逻辑)坐标转换为视口(设备)坐标的两个公式:
xViewport=(xWindow-xWinOrg)* xViewExt/xWinExt +xViewOrg
// xViewExt视口的x范围xWinExt窗口的x范围,相除为转换因子
yViewport=(yWindow-yWinOrg)*xViewExt/xWinExt+yViewOrg
视口(设备)坐标转换为窗口(逻辑)坐标的两个公式:
xWindow=(xViewPort-xViewOrg)*xWinExt/xViewExt+xWinOrg
yWindow=(yViewPort-yViewOrg)*xWinExt/xViewExt+yWinOrg
在MM_TEXT映射方式下逻辑坐标和设备坐标的相互转换:
窗口(逻辑)坐标转换为视口(设备)坐标的两个公式:
xViewport = xWindow-xWinOrg+xViewOrg
yViewport = yWindow-yWinOrg+yViewOrg
视口(设备)坐标转换为窗口(逻辑)坐标的两个公式:
xWindow = xViewport-xViewOrg+xWinOrg
yWindow = yViewport-yViewOrg+yWinOrg
//转换因子为1
#设置SetScrollSizes 01:15
void SetScrollSizes(
int nMapMode,//映射模式
SIZE sizeTotal,
//滚动视图的总尺寸
const SIZE& sizePage = sizeDefault,
//可以用CSize,点击滚动空白处的时候滚动的数量
const SIZE& sizeLine = sizeDefault
//点击滚动的箭头的时候滚动的数量
);
//什么时候调用呢?窗口创建之后调用
//使用OnInitialUpdate(该函数是当第一个视图和文档关联之后被调用)(虚函数)
//而且它在第一次调用OnDraw的调用之前
void CGraphicView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
// TODO: Add your specialized code here and/or call the base class
SetScrollSizes(MM_TEXT,CSize(800,600));
}
//?当移动到窗口下端划线,窗口重绘时所画的线条位置发生了改变
//坐标点并没有发生改变
//但我们作图的时候使用的是逻辑坐标,windows需要将逻辑坐标改变为设备坐标来输出图形
//而OnPrepareDC(&dc)用来调整显示上下文的属性
//可能就是它改变了上下文属性
//在MFC中查找OnPrepareDC函数(viewscrl.cpp)(先搜索CSrollView)
其中
switch (m_nMapMode)
{
case MM_SCALETOFIT:
pDC->SetMapMode(MM_ANISOTROPIC);
pDC->SetWindowExt(m_totalLog); // window is in logical coordinates
pDC->SetViewportExt(m_totalDev);
if (m_totalDev.cx == 0 || m_totalDev.cy == 0)
TRACE0("Warning: CScrollView scaled to nothing.\n");
break;
default://设置了MM_TEXT就到这里
ASSERT(m_nMapMode > 0);
pDC->SetMapMode(m_nMapMode);
break;
}
CPoint ptVpOrg(0, 0); // assume no shift for printing
if (!pDC->IsPrinting())//是否在打印中?不在打印中则执行
{
ASSERT(pDC->GetWindowOrg() == CPoint(0,0));
// by default shift viewport origin in negative direction of scroll
ptVpOrg = -GetDeviceScrollPosition();//返回值有符号
if (m_bCenter)
{
CRect rect;
GetClientRect(&rect);
// if client area is larger than total device size,
// override scroll positions to place origin such that
// output is centered in the window
if (m_totalDev.cx < rect.Width())
ptVpOrg.x = (rect.Width() - m_totalDev.cx) / 2;
if (m_totalDev.cy < rect.Height())
ptVpOrg.y = (rect.Height() - m_totalDev.cy) / 2;
}
}
pDC->SetViewportOrg(ptVpOrg);//设置视口的原点
#视口和窗口原点的改变01:25?????????????????????
CDC中提供了两个成员函数函数SetViewportOrg和SetWindowOrg,用来改变视口和窗口的原点。(获取则为Get……)
如果将视口原点设置为(xViewOrg,yViewOrg)若(0,-150),则逻辑点(0,0)就会被映射为设备点(xViewOrg,yViewOrg)(设备坐标(相对于左上角))(0,-150)。如果将窗口原点改变为(xWinOrg,yWinOrg)则为(0,150),则逻辑点(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函数中,作为逻辑坐标使用),根据转换公式xViewport = xWindow-xWinOrg+xViewOrg 和yViewport = yWindow-yWinOrg+yViewOrg,得到设备点的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-0+0=680,y坐标为390-(-150)+0=540,将逻 辑坐标(680,540)保存起来,在窗口重绘时,会先调用OnPrepareDC函数,调整显示上下文的属性,将视口的原点设置为了(0,-150), 然后GDI函数用逻辑坐标点(680,540)绘制图形,被Windows转换为设备坐标点(680,390),和原先显示图形时的设备点是一样的,当然 图形就还在原先的地方显示出来。
代码:在保存坐标之前做如下转换:
OnPrepareDC(&dc);
dc.DPtoLP(&m_ptOrigin);
dc.DPtoLP(&point);
#OnPrepareDC会随时根据滚动窗口的位置来调整视口的原点。
#另外两种保存图形和重绘图形的方式01:47
(1)CMetaFileDC 源文件DC
×A Windows metafile contains a sequence of graphics device interface (GDI) commands that you can replay(重放) to create a desired image or text.
×相当于一个画布。绘制的时候是看不见的,绘好之后可以播放它。
×它包含的是绘制图形的各个命令。
CDC::PlayMetaFile用来播放源文件
1#CMetaFileDC m_dcMetaFile;
2#m_dcMetaFile.Create();//参数为NULL则一个内存源文件被创建
3#把绘图时的dc都换成m_dcMetaFile
4#ONDRAW中使用close来返回一个源文件的句柄
HMETAFILE hmetaFile;
hmetaFile=m_dcMetaFile.Close();
pDC->PlayMetaFile(hmetaFile);//播放
//再次创建一个源文件
m_dcMetaFile.Create();
//在源文件dc中播放先前的源文件,从而保存了上次的操作
m_dcMetaFile.PlayMetaFile(hmetaFile);
//删除源文件资源
DeleteMetaFile(hmetaFile);
#把源文件保存到文件中
添加菜单上打开和保存的命令响应
×CopyMetaFile
void CGraphicView::OnFileSave()
{
// TODO: Add your command handler code here
HMETAFILE hmetaFile;
hmetaFile=m_dcMetaFile.Close();
CopyMetaFile(hmetaFile,"meta.wmf");//扩展名一定的吗?
m_dcMetaFile.Create();
DeleteMetaFile(hmetaFile);
}
#打开文件
GetMetaFile××××××××××
//GetEnhMetaFile
void CGraphicView::OnFileOpen()
{
// TODO: Add your command handler code here
HMETAFILE hmetaFile;
hmetaFile=GetMetaFile("meta.wmf");
m_dcMetaFile.PlayMetaFile(hmetaFile);
DeleteMetaFile(hmetaFile);
Invalidate();
}
(2)使用兼容DC
#CDC m_dcCompatible;
#先判断兼容dc是否已经创建
if(!m_dcCompatible.m_hDC)
{
m_dcCompatible.CreateCompatibleDC(&dc);
CRect rect;
GetClientRect(&rect); //客户区的大小
CBitmap bitmap;
//通过指定的宽和高来创建一个兼容位图
bitmap.CreateCompatibleBitmap(&dc,rect.Width(),rect.Height());
//如果要在兼容dc上绘图必须要选入一幅位图,导入的也行
m_dcCompatible.SelectObject(&bitmap);
//m_dcCompatible.BitBlt(0,0,rect.Width(),rect.Height(),&dc,0,0,SRCCOPY);CreateCompatibleBitmap返回的位图对象只包含相应设备描述表中的位图的位图信息头,不包含颜色表和象素数据块。因此,选入该位图对象的设备描述表不能像选入普通位图对象的设备描述表一样应用,必须在SelectObject函数之后,调用BitBlt将原始设备描述表的颜色表及象素数据块拷贝到兼容设备描述表。
m_dcCompatible.SelectObject(pBrush);
}
同样把dc换成m_dcCompatible,由于是内存中dc,同样操作时是看不见的
OnDraw中贴图:
CRect rect;
GetClientRect(&rect);
pDC->BitBlt(0,0,rect.Width(),rect.Height(),&m_dcCompatible,0,0,SRCCOPY);