VC++深入详解(8):图形绘制

我们先看简单绘图:
对于一个单文档应用程序,我们先为其添加一个菜单:画图,然后为它添加4个菜单项:ID_DOT、IDM_LINE、IDM_RECTANGLE、IDM_ELLIPSE。
我们的目标,是当选中其中的一项时,能够在客户区中绘制相应的图形。

首先,我们需要一个UINT类型的变量m_nDrawType来记录我们选择的是哪个类型。并在这4个菜单的响应函数中为其赋值:

void CCH_10_GranphicView::OnDot() 
{
	// TODO: Add your command handler code here
	m_nDrawType = 1;
}

void CCH_10_GranphicView::OnLine() 
{
	// TODO: Add your command handler code here
	m_nDrawType = 2;
}

void CCH_10_GranphicView::OnRectangle() 
{
	// TODO: Add your command handler code here
	m_nDrawType = 3;
}

void CCH_10_GranphicView::OnEllipse() 
{
	// TODO: Add your command handler code here
	m_nDrawType = 4;
}
其次,我们需要捕获鼠标按下和鼠标抬起的消息。这两个消息会告诉我们两个点,通过这两个点,我们可以构造直线、矩形、椭圆。
我们需要一个CPoint成员变量m_ptOrigin来记录鼠标按下去时的点,然后在鼠标抬起中响应:

void CCH_10_GranphicView::OnLButtonUp(UINT nFlags, CPoint point) 
{
	// TODO: Add your message handler code here and/or call default
	CClientDC dc(this);
	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(m_ptOrigin,RGB(255,0,0));
		break;
	case 2:
		dc.MoveTo(m_ptOrigin);
		dc.LineTo(point.x,point.y);
		break;
	case 3:
		dc.Rectangle(CRect(m_ptOrigin,point));
		break;
	case 4:
		dc.Ellipse(CRect(m_ptOrigin,point));
		break;
	}
	CView::OnLButtonUp(nFlags, point);
}
下面我们实现一个功能:增加一个菜单,通过这个菜单,我们可以修改线的线的宽度、线型、颜色、字体等内容。
先看线宽吧,这个说明白了其他是差不多的。
首先,建立一个对话框,在对话框上放一个静态文本光,命名为“线宽”,然后再放一个编辑框,将其ID改为IDC_LINE_WIDTH。然后我们为这个对话框建立一个从CDialog类派生出来的类CSettingDlg,并将编辑框与这个类的一个UINT型成员变量m_iLinewidth相关联。这样就能用它来记录我们设置的线宽了。然后在我们的view类里面也加上成员变量m_iLineWidth。最后增加一个“设置”菜单项,对其在它的消息响应函数中:

void CCH_10_GranphicView::OnSetting() 
{
	// TODO: Add your command handler code here
	CSettingDlg dlg;
	dlg.m_iLinewidth = m_iLineWidth;//
	if(IDOK == dlg.DoModal())
		m_iLineWidth = dlg.m_iLinewidth;

}
而在OnLButtonUp中:

	CPen pen(PS_SOLID,m_iLineWidth,RGB(255,0,0));//设置画笔颜色
可能有人会觉得奇怪if语句的作用是当点击OK以后,把我们在对话框里设置的颜色传递到view中,可是if之前的那句话是干什么的?它的作用是:确保我们下一次设置时,对话框里显示的是之前设置的颜色。因为我们的dlg对象是一个局部变量,所以每次都会被重新初始化,初始化后的m_iLinewidth在构造函数中被设为0,而view类中的m_iLineWidth的声明周期却是很长的,所以里面记录着上次设置过的值,我们把它传递给新生成的dlg中的m_iLinewidth。
下面看看线型:首先,我们使用一个组框,在里面放置3个单选按钮,并把它们设为一组。然后为其关联一个成员变量m_iLineStyle。同样的,在view类中也创建一个成员变量m_iLineStyle,在OnSetting中

void CCH_10_GranphicView::OnSetting() 
{
	// TODO: Add your command handler code here
	CSettingDlg dlg;
	dlg.m_iLinewidth = m_iLineWidth;
	dlg.m_nLineStyle = m_iLineStyle;
	if(IDOK == dlg.DoModal())
	{
		m_iLineWidth = dlg.m_iLinewidth;
		m_iLineStyle = dlg.m_nLineStyle;
	}

}
在OnLButtonUp中,并没有使用复杂的switch case语句来判断线型,而是直接将m_iLineWidth填进去了:

	CPen pen(m_iLineStyle,m_iLineWidth,RGB(255,0,0));
这是为什么呢?因为在windows中,线型也是用数字定义的,且与我们这里摆放单选按钮的顺序一致:

/* Pen Styles */
#define PS_SOLID            0
#define PS_DASH             1       /* -------  */
#define PS_DOT              2       /* .......  */
#define PS_DASHDOT          3       /* _._._._  */
#define PS_DASHDOTDOT       4       /* _.._.._  */
#define PS_NULL             5
#define PS_INSIDEFRAME      6
#define PS_USERSTYLE        7
#define PS_ALTERNATE        8
#define PS_STYLE_MASK       0x0000000F

在一组单选按钮中,第一个元素的值为0,第二个为1,第三个为2,而0、1、2恰好是windows定义的实现、虚线、点线的编号。


下面,我们看如何设置颜色。windows中的颜色设置对话框看起来很复杂,其实MFC为我们定义了一个与之相关的类CColorDialog,利用这个类,我们可以很方便的设置颜色对话框。我们在view类中增加一个表示颜色的成员:COLORREF类型的m_clr。在菜单上增加一个菜单项,命名为颜色,在其消息响应函数中:

void CCH_10_GranphicView::OnColor() 
{
	// TODO: Add your command handler code here
	CColorDialog dlg;
	dlg.m_cc.Flags |= CC_RGBINIT;
	dlg.m_cc.rgbResult = m_clr;
	if(dlg.DoModal())
	{
		m_clr = dlg.m_cc.rgbResult;
	}
}

而OnLButtonUp响应的改为:

	CPen pen(m_iLineStyle,m_iLineWidth,m_clr);//设置画笔颜色
这里着重介绍一个CColorDialog 的成员变量m_cc它表明了这个类的很多重要信息,其中的一个成员rgbResult,表示的是颜色,而Flags 则是一些标记,通过它来设定某些功能是否能够使用,其中CC_RGBINIT标记意味着颜色对话框的默认颜色可以使用rgbResult成员。通过设置这个标记,我们可以把上一次设置的颜色保留下来。


下面看如何设置字体。我们要完成的操作是:如果我们设置了一种字体,就把这个字体的名字显示出来。与颜色类似,MFC也定义了CFontDialog来封装跟字体相关的操作。其中有一个CHOOSEFONT类型的成员m_cf ,而CHOOSEFONT这个结构有一个LPLOGFONT类型成员变量lpLogFont,它指向LOGFONT结构,这个结构中,有一个TCHAR类型的成员lfFaceName,用来表示字体的名字。

接下来是我们要做的事情。设置一个“字体”菜单项,定义CFont类型的变量m_font来保存字体,定义CSring类型的变量m_strFontName来存储字体的名字,设置菜单项的消息响应函数:

void CCH_10_GranphicView::OnFont() 
{
	// TODO: Add your command handler code here
	CFontDialog dlg;
	if(IDOK == dlg.DoModal())
	{
		m_font.CreateFontIndirect(dlg.m_cf.lpLogFont);
		m_strFontName = dlg.m_cf.lpLogFont->lfFaceName;
	}
	Invalidate();
}
通过Invalidate来引起重绘,而在OnDraw中相应:

void CCH_10_GranphicView::OnDraw(CDC* pDC)
{
	CCH_10_GranphicDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	// TODO: add draw code for native data here
	CFont *pOldFont = pDC->SelectObject(&m_font);
	pDC->TextOut(0,0,m_strFontName);
	pDC->SelectObject(pOldFont);
}
这里有一个问题,就是如果你多次设置字体,程序就会报错,原因很简单,第一次设置字体后,已经通过CreateFontIndirect 函数初始化字体了,而第二次时,又会试图初始化,所以会报错。所以每次在初始化前,先要判断m_font是否已经被初始化过,如果已经被初始化过,那么得先切断这种关联,释放字体资源,然后与新的字体相关联。首先,如何判断已经被关联了。最简单的办法是通过这个类的句柄来判断,其次,如何切断联系:通过它的基类CGdiObject 的成员函数DeleteObject 来实现:

void CCH_10_GranphicView::OnFont() 
{
	// TODO: Add your command handler code here
	CFontDialog dlg;
	if(IDOK == dlg.DoModal())
	{
		if(m_font.m_hObject)
			m_font.DeleteObject();
		m_font.CreateFontIndirect(dlg.m_cf.lpLogFont);
		m_strFontName = dlg.m_cf.lpLogFont->lfFaceName;
	}
	Invalidate();
}
下面看如何创建示例对话框。示例对话框,能让我们直观看见我们选择的颜色、线宽、线型等属性的效果。首先,我们先得理顺逻辑,当编辑框的内容改变时,会发出一个EN_CHANGE通告消息;当单选框的内容改变时,会发送BN_CLICKED通告消息。我们没有必要为每个消息写一遍代码,只需要让这些消息的响应函数调用Invalidate函数,而在OnPaint函数中相应就行了:

void CSettingDlg::OnPaint() 
{
	CPaintDC dc(this); // device context for painting
	
	// TODO: Add your message handler code here
	//如果要读取选择的数据,必须调用这个函数
	UpdateData();
	//设置画笔
	CPen pen(m_nLineStyle,m_iLinewidth,m_clr);
	dc.SelectObject(&pen);
	//获取矩形
	CRect rect;
	//GetWindowRect获取的是屏幕坐标
	GetDlgItem(IDC_SAMPLE)->GetWindowRect(&rect);
	//转化为客户区坐标
	ScreenToClient(&rect);
	dc.MoveTo(rect.left + 20, rect.top + rect.Height() / 2);
	dc.LineTo(rect.right - 20,rect.top + rect.Height() / 2);
	// Do not call CDialog::OnPaint() for painting messages
}
其中为了将颜色也按照用户的需求显示出来,特地在CSettingDlg中增加了成员m_clr,并在OnSetting中将其与视类中保存的颜色关联起来:

	dlg.m_clr = m_clr;


下面看看如何改变对话框和控件的颜色。
先看如何改变整个对话框的颜色。
这里需要用到一个函数OnCtlColor。当子空间要被绘制时,就会向他的父窗口(对话框)发送WM_CTLCOLOR消息:

HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 
{
	HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
	
	// TODO: Change any attributes of the DC here
	

	// TODO: Return a different brush if the default is not desired
	return hbr;
}
可以看到,这个函数先调用基类的OnCtlColor函数,然后返回一个即将被用来画图的刷子。如果想改变背景颜色,只要返回一个我们自己定义好颜色的画刷就行了。我们为CSettingDlg增加一个CBrush类型的成员变量m_brush,在构造函数中初始化它:

	m_brush.CreateSolidBrush(RGB(0,0,255));
然后在函数返回时,返回我们自己的画刷:

//	return hbr;
	return m_brush;
假如我们只想改变某个控件的颜色。那么我们如何判断是哪个控件呢?先把代码列出来再解释:

HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 
{
	HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
	
	// TODO: Change any attributes of the DC here
	if(pWnd->GetDlgCtrlID() == IDC_LINE_STYLE)
	{
		pDC->SetTextColor(RGB(255,0,0));
		return m_brush;
	}

	// TODO: Return a different brush if the default is not desired
	return hbr;

}
每个控件创建时,都会调用OnCtlColor函数,而到底是哪个,怎是通过pWnd指针来区分的。所以如果我们想在某个具体的控件中修改,只要判断他的ID就行了。在这里,我们改变了组框的背景色和字体的颜色。但是因为字体本身也有背景,所以看起来不太舒服,我们可以将字体的背景设为透明:

	pDC->SetBkMode(TRANSPARENT);
下面,我们改变编辑框的颜色。方法与前面的类似:

	if(pWnd->GetDlgCtrlID() == IDC_LINE_WIDTH)
	{
		pDC->SetTextColor(RGB(255,0,0));
//		pDC->SetBkMode(TRANSPARENT);
		pDC->SetBkColor(RGB(0,0,255));
		return m_brush;		
	}
注意,这里需要修改的是编辑框的背景颜色,而不是他的背景模式。

最后看看位图的显示。我们按照以下步骤显示一幅位图:
1.创建位图
2.创建兼容DC
3.将位图选进兼容的DC中
4.将兼容DC中的位图贴到当前的DC中。

我们在最前面的几节讲过DC是干什么的,这里再重申以下。DC全称为Device Context,翻译过来是“设备描述表”或者“设备上下文”。显示图片时都会用到,为什么呢?首先,没有一句C语言可以帮助我们在屏幕上显示图像或者在打印机上打印图像,我们要想显示,最终是调用显卡的驱动程序操作硬件的。不同的显卡,不同的显示器肯定会略有不同,而Windows将这些问题在自己的内部处理了,只给我们提供个一个接口,就是DC,它与设备无关,通过它,我们就能调用这些函数画图了。
那么什么是兼容的DC呢?
所谓兼容DC,就是符合某一标准(比如打印机或者显示器)的逻辑设备。它的本质是一段内存,我们在画图时,需要在这段内存上把图形画好,然后一次性拷贝到DC中。为什么不直接拷贝呢?据说是因为MFC的双缓存技术,直接拷贝会导致屏幕反复闪烁。
首先,我们保存一幅位图,这里使用对桌面的截屏。通过插入资源的方式,将它加入进来,此时,它的ID为:IDB_BITMAP1

在程序中,当擦除背景时,程序会发送一个WM_ERASEBKGND消息,对其响应即可:

BOOL CCH_10_GranphicView::OnEraseBkgnd(CDC* pDC) 
{
	// TODO: Add your message handler code here and/or call default
	CBitmap bitmap;
	bitmap.LoadBitmap(IDB_BITMAP1);
	CDC dcCompatible;
	dcCompatible.CreateCompatibleDC(pDC);
	dcCompatible.SelectObject(&bitmap);
	CRect rect;
	GetClientRect(&rect);
	pDC->BitBlt(0,0,rect.Width(),rect.Height(),
		&dcCompatible,0,0,SRCCOPY);
	return TRUE;
}
注意,我们删除了MFC默认的return 语句。
我们发现,整个窗口显示的不是很完全,通过拉伸窗口,可以看到更多的内容。我们希望将整个桌面平铺在这个SID的窗口中,需要使用StretchBlt 函数:

BOOL CCH_10_GranphicView::OnEraseBkgnd(CDC* pDC) 
{
	// TODO: Add your message handler code here and/or call default
 	CBitmap bitmap;
 	bitmap.LoadBitmap(IDB_BITMAP1);
	BITMAP bmp;
	bitmap.GetBitmap(&bmp);
 	CDC dcCompatible;
 	dcCompatible.CreateCompatibleDC(pDC);
 	dcCompatible.SelectObject(&bitmap);
 	CRect rect;
 	GetClientRect(&rect);
 //	pDC->BitBlt(0,0,rect.Width(),rect.Height(),
 //		&dcCompatible,0,0,SRCCOPY);
	pDC->StretchBlt(0,0,rect.Width(),rect.Height(),
		&dcCompatible,0,0,bmp.bmWidth,bmp.bmHeight,SRCCOPY);
	return TRUE;	

}
其中StretchBlt 与>BitBlt类似,但是多了两个参数,分别是原矩形的宽度和高度。我们通过GetBitmap函数来获得它们,这个函数需要一个BITMAP 结构的地址,然后系统会自动把信息填充到这个地址中,其中就有源位图的宽度和高度。




















你可能感兴趣的:(孙鑫VC++深入详解)