使用MFC编写绘图程序的总结

之前学习了C++面向对象和STL(标准模板库)的知识,苦于没有实战项目来加深理解。于是翻来Ivor Horton 编写的《Visual C++ 2013 入门经典(第7版)》这本书,把书中绘图的项目程序码了一遍。这个项目是一个基于MFC多文档的桌面应用程序,完成这个项目除了增加了对C++面向对象编程的理解,同时也加深了对MFC的理解,在整个过程中我侧重于把握前者。这是第二遍写这本书的代码,第一次C++知识不怎么完善,写完整个项目的感觉像搭积木,而且对类中一些限定词(如const、override、virtual、protected),指针引用和STL中的容器对象不怎么理解,所以这一次重在把握程序设计的整体架构,加深理解面向对象继承和成员函数重载等概念,同时加深了对MFC的了解。

对MFC的理解:

在windows平台上开发桌面应用程序有两种方式:一种是直接面向WindowsAPI,API函数比如WinMain(),WindowProc(),WinMain就是main函数,windowProc是处理各种消息的函数;另一种是基于MFC,MFC是对WindowsAPI的进一步封装,这种方式看不到WinMain(),windowProc()函数,它们在MFC内部处理好了。我们要做的就是对MFC提供的几个类进行派生以及编写自己的类。

 

MFC提供的基类关系图:

 

所以简单来说,按照MFC向导创建的项目,包括单文档和多文档类型都将会看到App类、Doc类、View类、Frame类。DocTemplate类是隐藏的用来连接他们的一个类。基于对话框的结构简单些,其中的Dialog类,和View类、Frame类一样是由CWnd派生的。

 

怎么与Windows通信呢?说白了就是,按鼠标、点击菜单栏、按钮等产生的消息,Windows怎么处理?响应函数写在什么地方?在WindowsAPI中有WindowProc()函数,MFC封装了看不到了之后呢?实际上上面你能看到的类都能处理windows消息,类中DECLARE_MESSAGE_MAP()宏表明这个类里面有作为消息处理程序的函数成员。

 

把消息处理程序放置在什么地方取决于消息的类别。一、标准的Windows消息,以WM_开头(不包括WM_COMMAND),包括重画WM_PAINT,释放鼠标左键WM_LBUTTONUP等,它们总是由最终派生于CWnD的类的对象处理,意思就是不能放在App类,Doc类中;二、对话框里的控件发出的消息,包括点击了按钮,这样的消息顺其自然可以由Dialog类对象处理;三、命令消息,即WM_COMMAND消息,包括点击了菜单栏和工具栏,处理这样的消息比较灵活,可能放在上面可以看到的任一类对象中,比如在此程序就放在了CDoc类中。

对绘图过程的理解:

开始回归到这个绘图项目程序,程序是基于多文档。为了消除零碎的感觉,简要画了类之间的关系图如下:

 

完成的目的就是用鼠标在窗口中绘制各种元素,画直线、矩形、圆等。按左键确定起点;左键不放移动鼠标调整临时元素;右键松手将元素添加进元素对象容器;可以随意变换颜色、元素类型、钢笔线宽等属性。

 

假如没有与鼠标的交互,怎么绘图呢?CView类里的OnDraw(CDC* pDC),我简单的理解就是这个函数提供24小时随时刷新服务。具体地,百度了一下,是这么说的:MFC调用OnDraw()函数是和所有会产生WM_PAINT消息的函数有关,Invalidate()、ReDisplay() 调用时会产生WM_PAINT消息,所以也会使用MFC调用OnDraw()函数。

 

比如画直线的语句是这样的:pDC->MoveTo();pDC->LineTo()。画图操作由不同的元素的类型分别实现,比如画直线和画矩形是不同的;定义元素基类CElement,将Draw()设为虚函数,由子类CLine,CRectangle实现不同的Draw()。所以程序在进行绘图时是OnDraw()根据不同的元素类型调用了不同的Draw()函数。

 

所有元素相同的比如颜色、线宽、起点等属性可以继承自基类。

 

在CDoc类中设置和元素属性相关的成员,比如m_Color,m_PenWidth等,可以由菜单栏改变值。构造元素对象所需的参数可以从CDoc类对象中的这些成员的值中获取。

 

线宽由对话框中单选按钮设置,在CDialog类中设置成员m_PenWidth接收控件里的值。然后CDialog类对象成员m_Penwidth的值传入CDoc类对象里的m_PenWidth。

 

处理鼠标消息,将鼠标动作的响应函数写在CView类里。 在类中增加3个成员变量:m_FirstPoint,m_SecondPoint,m_pTempElement。根据程序的设计目标,左键按下的处理函数将当前的光标位置传递给m_FirstPoint;左键按下而且鼠标移动的处理函数(绘直线的情况)将当前光标位置传递给m_SecondPoint,同时创建临时元素对象m_pTempElement,并调用Draw()函数实时绘图;左键松开的处理函数检测是否存在临时元素对象,若存在将其压入元素容器m_Sketch,消除临时对象,发出重绘命令消息。

对编程技术的理解:

成员函数应该设置为public还是protected,我这样理解:如果只允许派生类访问,则设置为protected,如果存在在其他类中访问的情况,则设置为public。

 

在派生类中重写基类成员函数时,最好加上override,加上之后就代表声称了这个函数就是用来重载基类函数的,不然报错。当然不写也可以。

 

在成员函数的参数和返回值中经常出现引用类型,而且如果不用改变它的值会在前面加上限定const。

 

当然也有很多指针类型,比如CDC* pDC,CxxDoc* pDoc,CElement* pTempElement等。在list容器中存储的也是元素对象的指针,比如这句:std::list<std::shared_ptr<CElement>> m_Sketch,使用了共享指针。

 

const限定的成员函数 ,也就是const加在参数列表的后面,表明这个函数是“只读的”,不会改变类对象里的任何成员变量(static类型修饰的成员变量除外,因为static类型成员是所有对象共同维护的,单个对象的成员函数的权力没这么大)。

最后:

下图1是基于多文档的程序界面效果,下图2是我又用单文档模板写完之后的界面效果:

 

 

因为在单文档程序中一次只存在一个文档,其中的差别在OnDraw()函数中有所体现。

在多文档中就存在文档对象的遍历,用for循环实现:

void CSketcherView::OnDraw(CDC* pDC)
{
	CSketcherDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO:  在此处为本机数据添加绘制代码
	for (auto it = pDoc->begin(); it != pDoc->end();++it)
	{
		for (const auto pElement : *pDoc)
		{
			if (pDC->RectVisible(pElement->GetEnclosingRect()))
			pElement->Draw(pDC);
		}
	}
}

 

而在单文档中程序改写如下:

void CDrawView::OnDraw(CDC* pDC)
{
	CDrawDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;
	std::list<std::shared_ptr<CElement>> Sketch = pDoc->GetSketch();
	const auto& haha = *pDoc;
	for (auto it = Sketch.begin(); it != Sketch.end();++it)
	{
		if (pDC->RectVisible((*it)->GetEnclosingRect()))
		{
			(*it)->Draw(pDC);
		}
	}
	
	// TODO:  在此处为本机数据添加绘制代码
}

 

可以看到list存储的就是CElement对象的指针,然后使用了list的迭代器,它是指向list存储对象的指针,所以it要想调用CElement对象的成员函数,需要这么写(*it)->Draw(pDC)或者(**it).Draw(pDC)。也就是需要对指针多做一次解引用。
 

  • 9
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值