前言
很快啊,我大意了哈,这学期这么快就过完了。我表示很伤心,这意味着我要面对许多考试,没有多少时间用来照顾我的博客了😭 。入门了一学期的c++,我还是颇有感受的。
当然,我也有一个课设作业——基于MFC的简单图形编辑
。根据我浅薄的Delphi知识以及了解的简单WIN32知识,觉得没啥难度,但是我还是花了两天的课余时间来解决它,真很难受,我把我途中遇到的问题以及解决办法记录下来。感谢你看到这篇文章,如果我的学习经历能帮助到你,我很开心。
由于篇幅和时间的限制(要考试了),我决定抽空不间断的更新完成这篇文章,我的源码我也会上传到我的github:查看源码,想要直接看我源码学习的同学直接下载就好了。当然大家如果有什么问题直接可以在评论区问我哇。
运行操作
- ctrl+左键新建图形
- 左键双击修改图形
- 右键双击删除图形
11月25日更新
课设要求
遇到的困难
类对象的序列化
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类的设计如下:
//图上的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程序
- 选择 单文档 标准样式
我们这个小程序非常简单,用不上多文档,当然你想也可以弄成多文档。
- 为你的后面的文件添加上个性的后缀名
- 下面的随意
- 我们改成有滚动条的View
新建完成运行一次测试
11月28日
Cshape类框架搭建以及序列化
有了最基本的思路我们就在开始搞CShape类
首先我们要使用序列化技术,不知道的可以看这一篇文章: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()
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了;
运行一下
析构函数
我们使用了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 轴的水平线 ,判断他们的和三角形的焦点个数
- 如果个数都为2 那么就在三角形内部
- 如果某一个平行边的焦点为无数个,那么一定在他的边上
我们已知条件显然很充足,知道三个顶点足以判断,简单的数学公式
方法2 叉乘法
我们现在需要解决的问题是非常简单的,首先是个正三角形,而且不是斜着的,所以使用叉乘法是非常易懂的。理论上这个方法可以判断任何的凸多边形的。
原理
如图,假设你从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();
结语
如果你看到这里还有一些疑惑可以看一看这写文章
- MFC 使用VS2019编写MFC程序
- MFC 类向导在帮你做什么?简单控件的使用以及添加
- MFC 图形接口 GDI
- MFC 序列化技术 Serialization
- 点我获取原码
- 评论区评论,基本有时间就回复
当然你发现任何错误直接可以在评论区指出,或者联系我的邮箱。
支持我!
其实我写这篇文章的是,看大家刚好都需要,我想蹭一波热度,啊哈哈哈
最重要的是总结自己所学的知识,这样记忆更加深刻一些。
你可选择如下方法支持我
- 在评论区支持我
- 点开我的微信小程序,增加一点访问量
- 下方微信打赏
- 在留言板留下你的足迹