C++ MFC学习笔记(第二课) 单文档程序画图

C++ MFC学习笔记(第二课) 单文档程序画图

在第一课学习了基于对话框的程序后,第二课学习建立一个单文档程序画图。

1.新建项目

同样是新建一个MFC项目,应用程序类型选择单个文档,项目样式选择MFC standard。
C++ MFC学习笔记(第二课) 单文档程序画图_第1张图片

2.画线条

进入到界面,与基于对话框的程序不同,没有什么控件可选,我们可以直接点击类视图,找到C名称View的类,然后直接添加函数。对于画线步骤就是鼠标左键点击,鼠标左键松开,鼠标移动这三个事件。我们右键点击类向导,然后再消息中找到WM_MOUSEMOVE,WM_LBUTTONDOWN,WM_LBUTTONUP三个消息并选择添加处理程序,这样在C名称View.h和cpp文件有有相关的函数声明,接下来我们补充函数过程。
C++ MFC学习笔记(第二课) 单文档程序画图_第2张图片C++ MFC学习笔记(第二课) 单文档程序画图_第3张图片
在cpp文件中,我们找到相关函数,发现参数表中都有一个point的参数,这个参数记录了鼠标在窗口的位置。我们画线的步骤是:单击鼠标左键,然后移动鼠标,最后松开鼠标左键。因为onMouseMove函数是鼠标在窗口中移动便会调用。所以我们可以添加在类C名称View类中添加一个int类型的数据成员作为标志(可以在类视图中右键添加变量,或者在头文件声明)。当值为1时,onMouseMove中画线的步骤才会调用。然后在鼠标点击时记录起点位置,在类中添加一个CPoint类型的数据成员。

标志在OnLButtonDown函数置1,OnLButtonUp函数中置0

在窗口画线需要创建CClientDC的对象,对象可以调用下面函数来画线。

函数 功能
MoveTo(CPoint a) 将当前位置放在a点
LineTo(CPoint b) 从当前位置到b点画一条线

CClientDC是CDC类的一个派生类,CDC类定义了一个设备描述对象,并提供了对设备描述对象进行操作的成员函数以及对与窗口客户区有关的显示区进行操作的成员函数。CClineDC类它只能在窗口的客户区(即窗口中除了边框、标题栏、菜单栏以及状态栏外的中间部分)中进行绘图。
创建一个CClientDC对象时用CClintDC 对象名(this),this一般指向本窗口或当前活动视图
最后代码如下

void CDRAWView::OnLButtonDown(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	flag = 1;
	start = point;	//start是CPoint类型,记录线条的起始点。
	CView::OnLButtonDown(nFlags, point);
}


void CDRAWView::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	flag = 0;
	CView::OnLButtonUp(nFlags, point);
}


void CDRAWView::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	if (flag == 1)
	{
		dc.MoveTo(start);
		dc.LineTo(point);
	}
	CView::OnMouseMove(nFlags, point);
}

但到执行的时候,我们会看到点击鼠标后,不断移动鼠标就会有很多条线出来,与我们只显示鼠标最后停留位置到起点的线不一样。是因为鼠标移动过程中,不断调用OnMOUSEMOVE的函数,这样就不断从起点到鼠标当前位置画线。
C++ MFC学习笔记(第二课) 单文档程序画图_第4张图片
我们为了解决这个问题,View类中可以引入一个CPoint类型的数据,负责记录上一条线的终点位置,然后OnMOUSEMOVE函数中,画出一条线后,鼠标未松开,鼠标移动,首先就会用与背景相反的颜色再画一次,这样相当于上一条线的位置颜色两次取反变回原来的颜色。这样就能实现只显示一条直线。下面是类CClient的一个成员函数

函数 功能
SetROP2(a) 设置当前绘图模式为a,并且绘画一次成功后,返回调用前的绘图模式。

a有不同的绘图模式,我们这里会用到R2_NOT。意味当前绘制的像素值设为与屏幕像素值的相反。
详情参考:https://baike.baidu.com/item/SetROP2/3341728?fr=aladdin
改善后的代码

void CDRAWView::OnLButtonDown(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	flag = 1;
	start = point;	//start是CPoint类型,记录线条的起始点。
	old = point;
	CView::OnLButtonDown(nFlags, point);
}


void CDRAWView::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	flag = 0;
	CView::OnLButtonUp(nFlags, point);
}


void CDRAWView::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	if (flag == 1)
	{
		dc.SetROP2(R2_NOT);
		dc.MoveTo(start);
		dc.LineTo(old);//负责覆盖上一次的线

		dc.MoveTo(start);
		dc.LineTo(point);
		old = point;
	}
	CView::OnMouseMove(nFlags, point);
}

C++ MFC学习笔记(第二课) 单文档程序画图_第5张图片
这样就能够基本实现画线的功能,但是这里又会有另一个问题,因为画的线是取背景相反的颜色,此时两条线的交点就会是空心的。因为第二条线与第一条线的交点处,画的是与背景相反地颜色,就是与黑相反地颜色就是白色。这个不细看看不出来,用电脑的放大镜一看就很明显了。这时候,应该在鼠标松开左键后再对线条进行一次绘画就可以了。
在这里插入图片描述

void CDRAWView::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	flag = 0;
	CClientDC dc(this);
	dc.MoveTo(start);
	dc.LineTo(point);	//再将线条绘画一次
	CView::OnLButtonUp(nFlags, point);
}

此时可能将代码这样改变,开始就是画白线条来覆盖上一次的黑线条,然后画黑线条表示最终的线条。将OnMouseMove函数改为如下:

void CDRAWView::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CClientDC dc(this);
	if (flag == 1)
	{
		dc.SetROP2(R2_WHITE);
		dc.MoveTo(start);
		dc.LineTo(old);//负责覆盖上一次的线

		dc.SetROP2(R2_BLACK);
		dc.MoveTo(start);
		dc.LineTo(point);
		old = point;
	}
	CView::OnMouseMove(nFlags, point);
}

这样一般在两条直线交点处没有问题,但没有问题的条件也挺苛刻。代码这样改,就每次开始画的都是白线,假如现在画的线覆盖在另一条线上,交叉的部分只要不是最后现在画的最终直线。就会像碰到橡皮一样被涂抹掉。
C++ MFC学习笔记(第二课) 单文档程序画图_第6张图片
C++ MFC学习笔记(第二课) 单文档程序画图_第7张图片

3.存储线条

此时还有一个问题,当窗口大小调整,或经过最小化后再打开,原来的线条都会不见。因为每次调整窗口过后都是新的窗口,我们需要每画一条线后就要存储一条直线,每当窗口改变时,再将存储的线条一一画出。

①新建一个表示线条的类

这时候我们可以用到一个CObArray类,这个类可以支持CObject类的指针数组,每次画完一条线就可以存储一条到这个类的对象里面,当要重新把线画出来也能从这个指针数组里面取得。
因为这个类支持CObject类的指针,所以我们自己定义一个myline类,并且以CObject为基类,这时候才能够添加进去。myline类有起点坐标的x和y,终点坐标的x和y,也有一个画线的函数。

#pragma once
#include 
#include 
class Myline :
	public CObject
{
public:
	int s_x;
	int s_y;
	int e_x;
	int e_y;
	Myline(int, int, int, int);
	Myline();
	void draw(CDC*);
};

cpp文件中
#include "pch.h"
#include "Myline.h"

Myline::Myline()
{
}
Myline::Myline(int a, int b, int c, int d)
{
	s_x = a;
	s_y = b;
	e_x = c;
	e_y = d;
}

void Myline::draw(CDC* dc)	//要在窗口绘图,所以参数为CDC类,CClient类也可以
{
	dc->MoveTo(s_x, s_y);
	dc->LineTo(e_x, e_y);
}

②在CDocument类中添加函数和成员

CDocument类与CView类管理视图不同,是负责管理数据,封装了和视图窗口以及框架窗口之间的操作。我们可以在CDocument类中添加CObArray类的数据成员array,然后添加在把线条添加进array的函数,获得线条个数的函数,以及到最后需要调用的获取线条的函数。此时操作在C名称DOC.h和cpp文件中操作。
CObArray相关函数:

函数 功能
array.Add() 在数组尾部增加元素,如果需要就扩展数组
array.GetUpperBound() 返回最大有效索引
array.GetSize() 获取数组中的元素个数
array.GetAt(i) 返回索引i的值
头文件添加
public:
	CObArray array;
	void Addline(int, int, int, int);
	int GetTotal();
	Myline* Getline(int);

cpp中实现函数
void CDRAWDoc::Addline(int a, int b, int c, int d)
{
	Myline* p = new Myline(a, b, c, d);
	array.Add(p);	//array.Add()接受的是CObject类的指针
}

int CDRAWDoc::GetTotal()
{
	return array.GetSize();		//返回线条个数
}

Myline* CDRAWDoc::Getline(int index)
{
	if (index<0 || index>array.GetUpperBound())
		return 0;	//判断索引有效性
	else 
		return (Myline *)array.GetAt(index);	//返回索引对应的值,不转换的话是CObject类型,应该强行将指针类型转换
}

③完善CView类中的函数

此时我们在CView类中的OnLButtonUp函数中每完成一次线条,将线条添加进去。

函数 功能
GetDocument() 获得与当前视图类相关联的文档类对象
void CDRAWView::OnLButtonUp(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	flag = 0;
	CClientDC dc(this);
	dc.MoveTo(start);
	dc.LineTo(point);	//再将线条绘画一次
	CDRAWDoc* p = GetDocument();	//p获得与当前视图类相关联的文档类对象
	p->Addline(start.x, start.y, point.x, point.y);	//添加线条
	CView::OnLButtonUp(nFlags, point);
}

CView类每当窗口发生变化时会调用类中的OnDraw函数,因此每次调整窗口后,将存储的线条重新绘制就需要在这个函数继续完善。参数中默认将标识符注释掉,此时将注释符删去就可以在函数中使用CDC类对象实现绘图。这里的形参pDC就可以传给myline类中的draw函数进行绘图了。

void CDRAWView::OnDraw(CDC* pDC)
{
	CDRAWDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;
	int index = pDoc->GetTotal();//获取线条个数
	while (index--)
		pDoc->Getline(index)->draw(pDC);//绘制线条
	// TODO: 在此处为本机数据添加绘制代码
}

现在调整窗口后原有线条也能够保留,但只是保存在内存中,但是如果要将这次的线条以文件形式保存在本地磁盘中,然后下次能够通过打开文件复原的话还需要一些操作。

4.存储信息在本地文件

CArchive类可以实现文件的读写功能,可以存储基本类型数据以及从CObject派生出来的数据类型存储。想要使用类的功能实现存储,我们只需要将自己定义的类以CObject为基类,然后对Serialize()函数复写。Serialize函数,文档在打开或存储的时候都会调用。
声明的时候要加上DECLARE_SERIAL(自己类名)
实现的时候加上IMPLEMENT_SERIAL(自己类名,父类名,1)
代码的实现有点像输入输出流中对文件的读写,到时窗口存储文件时要自己指定格式,如果是txt格式,打开后会是乱码,有点类似二进制文件。
详情参考:http://www.cppblog.com/cxiaojia/archive/2013/03/02/198177.html
将自己的类改为如下:

#pragma once
#include 
#include 
class Myline :
	public CObject
{
public:
	int s_x;
	int s_y;
	int e_x;
	int e_y;
	Myline(int, int, int, int);
	Myline();
	void draw(CDC*);
	DECLARE_SERIAL(Myline)	//声明
	void Serialize(CArchive& ar);	//基类中也有这个函数,这里要重载。
};

cpp中添加
IMPLEMENT_SERIAL(Myline,CObject,1)
void Myline::Serialize(CArchive& ar)
{
	if (ar.IsStoring()) {
		ar << s_x << s_y << e_x << e_y;
	}	//存储状态,序列化过程,将内存中的对象存储
	else {
		ar >> s_x >> s_y >> e_x >> e_y;
	}	//打开状态,反序列化过程,将文件中的对象恢复到内存中
}

然后在CDoc类也是有一个Serialize的函数,且打开和存储时调用的是CDoc类中的这个函数。在C名称Doc.cpp文件中找到Serialize函数在此函数中对负责存储的CObArray类(也是CObject类的派生类,也有Serialize函数)对象array调用Serialize函数即可。

void CDRAWDoc::Serialize(CArchive& ar)
{
	array.Serialize(ar);
}

C++ MFC学习笔记(第二课) 单文档程序画图_第8张图片
保存后,重新运行再打开保存的文件就可以加载上次画的线了。
如果保存的格式是txt,直接打开会是乱码。
在这里插入图片描述

你可能感兴趣的:(#,C++,MFC)