前言

很快啊,我大意了哈,这学期这么快就过完了。我表示很伤心,这意味着我要面对许多考试,没有多少时间用来照顾我的博客了😭 。入门了一学期的c++,我还是颇有感受的。
当然,我也有一个课设作业——基于MFC的简单图形编辑。根据我浅薄的Delphi知识以及了解的简单WIN32知识,觉得没啥难度,但是我还是花了两天的课余时间来解决它,真很难受,我把我途中遇到的问题以及解决办法记录下来。感谢你看到这篇文章,如果我的学习经历能帮助到你,我很开心。
由于篇幅和时间的限制(要考试了),我决定抽空不间断的更新完成这篇文章,我的源码我也会上传到我的github:查看源码,想要直接看我源码学习的同学直接下载就好了。当然大家如果有什么问题直接可以在评论区问我哇。

下载源码

运行结果

运行操作

  • ctrl+左键新建图形
  • 左键双击修改图形
  • 右键双击删除图形

11月25日更新

课设要求

image.png

遇到的困难

类对象的序列化

Serialize,就这个单单的作业来说,我们是为了有效的保存读取数据

文档类和视图类的数据交互
  • 当在CDrawingView视图中需要访问CDrawingDoc文档内部的(图形)对象时,可调用CDrawingView::GetDocument(),从而得到CDrawingDoc文档对象的指针pDoc
  • pDoc->SetModifiedFlag();pDoc->UpdateAllViews(NULL);
    更新文档数据,并且用发送OnPaint 消息 来调用调用 CDrawingView::OnDraw()
绘图和文字输出
CShape类的设计思路
对话框和数据和视图类的数据交换
判断点是否在多边形内部
  • 叉乘法
  • 交点法
判断按下了ok

直接判断shapeDlg.DoModal()==IDOK

需求分析

11月27日更新

从程序外部看,需要实现的功能有:

  • 编辑图形,包括图形对象的新建、删除和修改等3项功能。
  • 文件操作,要求实现程序菜单中文件的新建、打开、保存、另存为、关闭等功能。

Shape 的大概规划

需要对6种图形进行类的设计,包括:正方形、矩形、圆、椭圆、正三角形、文本等。我们可以大概规划一下Shape一些公用的方法并且可以设计出一个抽象类,来提供一个模板。如果你虚函数还不是很懂,可以看一下这篇文章,c++(七)多态性:虚函数。CShape类的设计如下:
image.png

//图上的shape为一个结构体
struct shape
{
	int Type;
	int OrgX;
	int OrgY;
	COLORREF   BorderColor;
	int BorderType;
	int BorderWidth;
	COLORREF  FillColor;
	int FillType;
	int Height;
	int Width;
	CString Str;
};
//ElementType 枚举类型
enum  ElementType { NOTSET = 0, SQUARE, RECTANGLE, CIRCLE, ELLIPSE, TRIANGLE, TEXT };

新建以及一些准备工作

构思个差不多,我们就慢慢开始敲代码吧。新建!!!
如果你的Vs2019还不能编写MFC或者 你还不熟悉vs2019编写MFC 你可以,看看这一篇文章--- MFC 使用VS2019编写MFC程序

  1. 选择 单文档 标准样式
    我们这个小程序非常简单,用不上多文档,当然你想也可以弄成多文档。
    image.png
  2. 为你的后面的文件添加上个性的后缀名
    image.png
  3. 下面的随意
    image.png
  4. 我们改成有滚动条的View
    image.png
    image.png

新建完成运行一次测试

image.png
11月28日

Cshape类框架搭建以及序列化

有了最基本的思路我们就在开始搞CShape类
image.png
首先我们要使用序列化技术,不知道的可以看这一篇文章:MFC 序列化技术 Serialization
按照前面的思路,我们定义一个抽象函数CShape

#include<afxwin.h>
enum  ElementType { NOTSET = 0, SQUARE, RECTANGLE, CIRCLE, ELLIPSE, TRIANGLE, TEXT };
struct shape
{
	int Type;
	int OrgX;
	int OrgY;
	COLORREF   BorderColor;
	int BorderType;
	int BorderWidth;
	COLORREF  FillColor;
	int FillType;
	int Height;
	int Width;
	CString Str;
};

class CShape : public CObject
{
public:
	CShape();
	virtual void Draw(CDC* pDC) = 0;//绘制
	virtual bool IsMatched(CPoint pnt) = 0;//点是否落在图形内部
	virtual void Serialize(CArchive& ar) = 0;//序列化存储到数组里面
	virtual void SetShapePos(int x,int y,int w,int h=0, CString s = L"")=0;//设置基本的数据
	virtual void SetPen(int w=0, int c=0, COLORREF cc=RGB(255,0,255)) = 0;//设置画笔
	virtual void SetBrush(int c = 0, COLORREF cc = RGB(0, 0, 0)) = 0;//设置笔刷
	virtual shape GetShape();//获得数据成员
protected:
	ElementType Type;//图元类型
	int OrgX;//原点坐标
	int OrgY;
	COLORREF   BorderColor;//边界颜色
	int BorderType;//边界线型--实线、虚线、虚点线等
	int BorderWidth;//边界宽度
	COLORREF  FillColor;//填充颜色
	int FillType;//填充类型--实心、双对角、十字交叉等
};
//实现CShape();
CShape::CShape()
{
	Type = NOTSET;
	OrgX = 0;
	OrgY = 0;
	BorderType = 0;
	FillType = 6;
	BorderWidth = 0;
	BorderColor = RGB(0, 0, 0);
	FillColor = RGB(255, 255, 255);
}//这里都是我们默认的参数方便我们子类的构造函数调用
shape CShape::GetShape()//获得数据成员
{
shape t;
return t;
}

第一个子类CSquare

继承CShape ,并且能够支持序列化

class CSquare : public CShape
{
public:
	CSquare();
	CSquare(int x, int y, int w);
	void Draw(CDC* pDC);//绘制正方形
	bool IsMatched(CPoint pnt);//重载点pnt是否落在图元内
	virtual void Serialize(CArchive& ar);//序列化正方形图元
	virtual void SetShapePos(int x, int y, int w, int h, CString s);
	virtual void SetPen(int w, int c, COLORREF cc);
	virtual void SetBrush(int c, COLORREF cc );
	virtual shape GetShape();
private:
	int width;//自己独有的数据成员
	DECLARE_SERIAL(CSquare)//声明类CSquare支持序列化
};

DECLARE_SERIAL(CSquare)//声明类CSquare支持序列化
这一句是要写在这里哦!
然后 在Cpp文件里面加上这一句
IMPLEMENT_SERIAL(CSquare, CObject, 1)//实现类CSquare的序列化,指定版本为1

完成函数的定义

我们这里可以先定义一部分,其他的用空的函数体代替

CSquare::CSquare() :CShape::CShape()
{
	Type = SQUARE; width = 0;
};
CSquare::CSquare(int x, int y, int w) :CShape::CShape()
{
	Type = SQUARE;
	OrgX = x;
	OrgY = y;
	width = w;
}
void CSquare::Serialize(CArchive& ar)//保存数据
{
	if (ar.IsStoring())
	{
		ar << (WORD)Type;
		ar << OrgX << OrgY;
		ar << BorderColor;
		ar << BorderType;
		ar << BorderWidth;
		ar << FillColor;
		ar << FillType;
		ar << width;
	}
	else
	{
		WORD w;
		ar >> w;
		Type = (ElementType)w;
		ar >> OrgX >> OrgY;
		ar >> BorderColor;
		ar >> BorderType;
		ar >> BorderWidth;
		ar >> FillColor;
		ar >> FillType;
		ar >> width;
	}
}
void CSquare::Draw(CDC* pDC)//绘制图形函数
{
	CPen pen, * pOldPen;
	pen.CreatePen(BorderType, BorderWidth, BorderColor);//初始化画笔的属性
	pOldPen = (CPen*)pDC->SelectObject(&pen);//保存 画笔原来的数据 等会要还原原来的属性
	CBrush brush, * pOldBrush;
	if (FillType >= HS_HORIZONTAL && FillType <= HS_DIAGCROSS)	//HS_HORIZONTAL = 0;HS_DIAGCROSS = 5; 宏定义 最大为5 代表每一个 填充的方式
		brush.CreateHatchBrush(FillType, FillColor);
	else
		brush.CreateSolidBrush(FillColor);
	pOldBrush = (CBrush*)pDC->SelectObject(&brush);
	pDC->Rectangle(OrgX - width / 2, OrgY - width / 2, OrgX + width / 2, OrgY + width / 2);//两个顶点的坐标
	pDC->SelectObject(pOldPen);//Old还原。这样做是为了其它Windows程序,因为有的Windows程序直接使用默认的画笔和画刷进行绘图
	pDC->SelectObject(pOldBrush);
}

bool CSquare::IsMatched(CPoint pnt)//图元匹配函数
{
	return false;
}
void CSquare::SetShapePos(int x, int y, int w, int h = 0, CString s = L"")
{

}
void CSquare::SetPen(int w = 0, int c = 0, COLORREF cc = RGB(255, 255, 255))
{

}
void CSquare::SetBrush(int c = 0, COLORREF cc = RGB(0, 0, 0))
{

}
shape CSquare::GetShape()
{
	shape t;
	return t;
}

如果你对绘图还不熟练,可以看一下这一篇:MFC 图形接口 GDI
这样我们就初步完成了shape

把数据存到Doc里面 并且能在 使Doc和view关联起来

我们在给DrawingDoc添加 用于存数据的

CObArray m_Elements;//其中m_Elements是文档用来保存图元对象的数组
DrawingDoc在生成的时候就已经可以支持序列化的
在 这个函数里面加入语句'virtual void Serialize(CArchive& ar);'
m_Elements.Serialize(ar);//调用被保存的shape的Serialize()
image.png

onDraw

在veiw的绘图函数添加

void CDrawingView::OnDraw(CDC* pDC)
{
	CDrawingDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;
	// TODO:  在此处为本机数据添加绘制代码
	for (int i = 0; i < pDoc->m_Elements.GetCount(); i++)
	{
		CShape* p = (CShape*)pDoc->m_Elements[i];
		p->Draw(pDC);
	}
}

最后我们添加一个鼠标单击的事件处理函数,测试一下代码

void CDrawingView::OnLButtonDown(UINT nFlags, CPoint point)
{
	// TODO:  在此添加消息处理程序代码和/或调用默认值
	if ((nFlags&MK_CONTROL) == MK_CONTROL)//Ctrl键按下
	{
		CDrawingDoc* pDoc = GetDocument();
		ASSERT_VALID(pDoc);
		if (!pDoc)	return;
		CClientDC dc(this);
		CPoint pntLogical = point;
		OnPrepareDC(&dc);
		dc.DPtoLP(&pntLogical);//DP->LP进行转换
		// ----- 测试代码 begin -----
		CShape * p = new CSquare(pntLogical.x, pntLogical.y, 100);
		pDoc->m_Elements.Add(p);
		pDoc->SetModifiedFlag();
		pDoc->UpdateAllViews(NULL);
		// ----- 测试代码 end -----
	}
	CScrollView::OnLButtonDown(nFlags, point);
}

重点介绍:

pDoc->m_Elements.Add(p);
pDoc->SetModifiedFlag(true);//true可以缺省
pDoc->UpdateAllViews(NULL);

pDoc 是文档类指针,我们需要在前面 获取一下
CDrawingDoc* pDoc = GetDocument();

后面两句:

SetModifiedFlag(true);
在对文档作了修改之后调用该函数。连续调用以确保在关闭之前框架提示用户保存这些变化。
UpdateAllViews(NULL);
指向修改文档的视图,如果所有视图被更新,则设为NULL告诉Veiw要从新调用onDraw了;

运行一下

image.png

析构函数

我们使用了CArray,所以我们要在析构函数里加入删除CArray的语句

CDrawingDoc::~CDrawingDoc()
{
	for (int i = 0; i < m_Elements.GetSize(); i++)
	{
		CShape* p = (CShape*)m_Elements[i];
		delete(p);
	}
	m_Elements.RemoveAll();
	CDocument::DeleteContents();
}

到这里你就完成了,保存数据 显示图像了 其他的图形也和CSquare如法炮制一下!

补充

我们仔细看一下CShape类,除了构造函数都是虚函数,虚函数可以动态的调用我们想要调用的函数。所以我们只需要声明一个指向CShape的指针p,p=new <不同的子类> !!!
参考代码

// TODO: 在此添加消息处理程序代码和/或调用默认值
	CDrawingDoc* pDoc = GetDocument();//获取文档指针
	ASSERT_VALID(pDoc);
	if (!pDoc)	return;
	CClientDC dc(this);
	CPoint pntLogical = point;
	OnPrepareDC(&dc);
	dc.DPtoLP(&pntLogical);//转坐标
	if ((nFlags & MK_CONTROL) == MK_CONTROL)//Ctrl键按下,按位与运算,如果 nFlags== MK_CONTROL 的话 那么就 (nFlags & MK_CONTROL) == MK_CONTROL
	{
		shapeDlg.m_X = pntLogical.x;
		shapeDlg.m_Y = pntLogical.y;
		shapeDlg.m_ShapeType = 0;
		if (shapeDlg.DoModal() == IDOK)
		{ 
			CShape* p = nullptr;
			
			switch (shapeDlg.m_ShapeType)
			{
			case 0:
				break;
			case 1:
				p = new CSquare;
				break;
			case 2:
				p = new CRectangle;
				break;
			case 3:
				p = new CCircle;
				break;
			case 4:
				p = new CEllipse;
				break;
			case 5:
				p = new CTriangle;
				break;
			case 6:
				p = new CText;
				break;
			default:
				
				break;
			}
			if (p)
			{
				p->SetShapePos(shapeDlg.m_X, shapeDlg.m_Y, shapeDlg.m_W, shapeDlg.m_H, shapeDlg.m_Text);
				p->SetPen(shapeDlg.m_L, shapeDlg.m_LineType, shapeDlg.m_ColorButton_Line.GetColor());
				p->SetBrush(shapeDlg.m_FillType,shapeDlg.m_ColorButton_Fill.GetColor());

				pDoc->m_Elements.Add(p);
				pDoc->SetModifiedFlag();
				pDoc->UpdateAllViews(NULL);
			}

虚函数详细请看c++(七)多态性:虚函数

判断一个点是否在图形内

三角形为例子

方法1

做这个点的平行 X Y 轴的水平线 ,判断他们的和三角形的焦点个数

  1. 如果个数都为2 那么就在三角形内部
  2. 如果某一个平行边的焦点为无数个,那么一定在他的边上
    我们已知条件显然很充足,知道三个顶点足以判断,简单的数学公式

方法2 叉乘法

我们现在需要解决的问题是非常简单的,首先是个正三角形,而且不是斜着的,所以使用叉乘法是非常易懂的。理论上这个方法可以判断任何的凸多边形的。
EABDECED442AAB5C8E5F23886A280535.jpg

原理

如图,假设你从A点出发,沿着AC->CB->BA绕行了一圈,那么你会发现M点一直在你的右手边!但是这个在数学上如何判断呢?我们就判断AC X AM和 AC X AB 方向是否一致就好了。
参考原码

//我这里自己新建了向量类Vector2,可以去在github上看原码
bool CTriangle::IsMatched(CPoint pnt)//图元匹配函数
{
	Vector2 AC(width / 2, (int)(-width / sqrt(3) - width / sqrt(3) / 2)); //CB(), BA();
	Vector2 CB(width / 2, (int)(width / sqrt(3) + width / sqrt(3) / 2));
	Vector2 BA(-width, 0);
	Vector2 AM(pnt.x - OrgX + width / 2, pnt.y - int(OrgY + width / sqrt(3) / 2));
	Vector2 BM(pnt.x - OrgX - width / 2, pnt.y - int(OrgY + width / sqrt(3) / 2));
	Vector2 CM(pnt.x - OrgX, pnt.y - int(OrgY - width / sqrt(3)));
	if (AC.Cross((-BA)) * AC.Cross(AM) > 0)
		if (CB.Cross((-AC)) * CB.Cross(CM) > 0)
			if (BA.Cross((-CB)) * BA.Cross(BM) > 0)
				return true;
	return false;
}

如何添加对话框

如何给view添加对话框见:MFC 使用VS2019编写MFC程序

如何控制控件上的数据

方法一 添加控件变量

(1)给控件添加变量(有2种:控件类型、或 值类型),并采用DoDataExchange()在控件和变量之间进行数据交换。建议使用类向导,它会帮你自动生成代码。
注意:一个控件最好不要同时添加值类型和控件类型的2种变量,不要同时做2种交换。
采用值类型变量,如:
DDX_CBIndex(pDX, IDC_COMBO_TYPE, m_type); //int m_type; 表示选项的顺序号(从0开始)

DDX_LBIndex(pDX, IDC_LIST_PENTYPE, m_pen_type);
DDX_LBIndex(pDX, IDC_LIST_BRUSHTYPE, m_brush_type);

采用控件类型变量,如:

DDX_Control(pDX, IDC_COMBO_TYPE, cblist_type); //CComboBox cblist_type;
DDX_Control(pDX, IDC_LIST_BRUSHTYPE, list_brushtype); //CListBox list_brushtype
DDX_Control(pDX, IDC_LIST_PENTYPE, list_pentype);

方法2 指针!

不使用控件变量和DoDataExchange(),而直接利用列表框控件类的成员函数。
根据控件ID获取控件对象(指针)的方法:
组合列表框:CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_TYPE);
列表框:CListBox* pListBox = (CListBox*)GetDlgItem(IDC_LIST_PENTYPE);
设置(按顺序号),如:pComboBox->SetCurSel(1);
获取(顺序号),如:pComboBox->GetCurSel();

结语

如果你看到这里还有一些疑惑可以看一看这写文章

支持我!

其实我写这篇文章的是,看大家刚好都需要,我想蹭一波热度,啊哈哈哈
最重要的是总结自己所学的知识,这样记忆更加深刻一些。
你可选择如下方法支持我

  1. 在评论区支持我
  2. 点开我的微信小程序,增加一点访问量
  3. 下方微信打赏
  4. 在留言板留下你的足迹

79A59EF56224B22753145CEA8E88BAA2-dfc4efa7ed3d4a6480b9233445c2e872

Q.E.D.


努力学习的小菜鸟