MFC图形编辑界面工具
//2021年4月28日更新
今天收拾电脑,从硬盘里找到了这个项目,现在上传到github上https://github.com/wkfvawl/MFC-Drawing
//2020年6月17 日更新
这篇博客最近好像挺火?不少师弟师妹咨询我MFC的内容,额,博主很久也没有用MFC搞事情了,现在甚至都没有开发环境,加上到期末了,事情很多,实在没有精力回顾帮大家解决问题,今年这种情况特殊,我也很能理解18级的师弟师妹们,教授实训课的应该还是张帆老师吧,很好的老师,可以多向他请教,B站好像也有相关的教程,方法总比困难多,加油!
一、背景
喔,五天的实训终于结束了,学校安排的这次实训课名称叫高级程序设计实训,但在我看来,主要是学习了Visual C++ .NET所提供的MFC(Microsoft Foundation Class)库所提供的类及其功能函数的使用。写这一篇博客的目的是针对实训中出现的问题做一些说明,方便以后查看,并且对这次实训做一些总结。这一次的实训对我来说其实挺难受的,真正用来学习使用VS和MFC的时间只有三天,加上下个周是考试周,还有几门课没有复习完,这几天基本上是连轴转,中午也泡在实验室里,唉啊还是自己太菜了。最后我们需要提交一个课程设计程序,因为时间的原因,我选择了最简单的图形界面编辑工具,这个程序其实在C++的课程设计上就有这个,但当时我还不会windows图形界面的编程,现在想想这两个课程设计其实完全可以是一份(捂脸)。
最后做出来的界面是这样的:
在功能上:
- 能够在windows的界面下画图,能够画直线、空心矩形、、圆角矩形、空心圆形、填充矩形、填充圆形、填充圆角矩形和文字。
- 能够改变画图是画笔用的颜色、线宽、线型和填充用的颜色、字体。
- 能够保存、打开所做的图形文件
- 拥有菜单、工具栏、鼠标右键等编辑界面。
二、程序说明
1.工具栏说明
2.画图菜单
在画图菜单下,能够选择画直线、空心矩形、空心圆形、空心圆角矩形、填充矩形、填充圆形、填充圆角矩形。
3.文本菜单
文本输入菜单下有两个选项一个是文本输入、一个是字体设置,分别对应着两个对话框。
文本输入对话框,能够根据指定的x、y横纵坐标来定位输入位置,打印输入的相应信息。
而字体设置,调用系统自带的对话框,完成对字体类型、字形和字体大小的设置。
4.画笔设置菜单
画笔设置菜单下有画笔颜色、画笔类型、画笔宽度三个选项。其中画笔类型又包含实线、虚线、点线、点划线、双点划线五个选项。画笔类型根据查阅课本内容和上网搜索得知,只有在宽度为1的时候,才能显示除实线外的其他画笔类型,当宽度大于1时画出来的都是实线类型的线条。
颜色设置,调用系统自带的对话框,完成对画笔、画刷颜色的选择。同时选用该对话框能够实现自定义颜色。
画笔宽度设置对话框是自己设置的对话框,输入相应的画笔宽度,实现画笔宽度的改变。
5.界面下鼠标右键
右击鼠标会有鼠标右键菜单,其功能选项与功能栏所给的功能是一样的,选择画直线、空心矩形、空心圆形、空心圆角矩形、填充矩形、填充圆形、填充圆角矩形和文本。
三、鼠标拖动绘画
该程序的基础功能就是能够拖动鼠标来绘制图形,这里面实际上用到的是橡皮筋技术。在鼠标拖动中,每当鼠标的位置发生了改变,需要清除已经绘制的线段,课本已经该出了实现该过程的代码。当然之前需要在视图View类中添加鼠标左键按下,鼠标移动,鼠标左键抬起的消息映射。
void CShirrView::OnLButtonDown(UINT nFlags, CPoint point) { //将鼠标左键按下位置存储到p1、p2 p1 = p2 = point; b=true; //设置绘图标志 pdc->SetROP2(R2_NOTXORPEN);//设置绘图模式为R2_NOTXORPEN,注意背景为白色 CView::OnLButtonDown(nFlags, point); } void CShirrView::OnMouseMove(UINT nFlags, CPoint point) { if (!b) return; //如果不是绘图状态,返回 //P1为鼠标左键按下位置,P2为鼠标上次位置 //即按前次位置重绘了一次,模式是R2_NOTXORPEN //最终效果是白色,由于底色为白,实际效果是清除了上次的线段 pdc->MoveTo(p1.x,p1.y); pdc->LineTo(p2.x,p2.y); p2 = point; //p1仍为鼠标左键按下位置,P2为当前鼠标位置 pdc->MoveTo(p1.x,p1.y); pdc->LineTo(p2.x,p2.y); //从P1到鼠标当前位置绘制线段 CView::OnMouseMove(nFlags, point); } void CShirrView::OnLButtonDown(UINT nFlags, CPoint point) { //将鼠标左键按下位置存储到p1、p2 p1 = p2 = point; b=true; //设置绘图标志 pdc->SetROP2(R2_NOTXORPEN);//设置绘图模式为R2_NOTXORPEN,注意背景为白色 CView::OnLButtonDown(nFlags, point); } void CShirrView::OnMouseMove(UINT nFlags, CPoint point) { if (!b) return; //如果不是绘图状态,返回 //P1为鼠标左键按下位置,P2为鼠标上次位置 //即按前次位置重绘了一次,模式是R2_NOTXORPEN //最终效果是白色,由于底色为白,实际效果是清除了上次的线段 pdc->MoveTo(p1.x,p1.y); pdc->LineTo(p2.x,p2.y); p2 = point; //p1仍为鼠标左键按下位置,P2为当前鼠标位置 pdc->MoveTo(p1.x,p1.y); pdc->LineTo(p2.x,p2.y); //从P1到鼠标当前位置绘制线段 CView::OnMouseMove(nFlags, point); }
上面的代码是用来画直线的,能够完成画直线的功能,那么就可以照猫画虎实现画矩形、画圆的功能了,这些图形都需要起点和终点的坐标作为画图的参数。
同时我们要明白鼠标相应这些函数是在当前视图中执行的,也就是说,我们一打开该程序,只要在视图中点击移动鼠标,这些函数其实都会相应执行到,那么我们该怎么去设计选择不同的图形?
其实这很简单,改造鼠标移动消息相应函数和鼠标左键抬起消息响应函数即可!我们可以给不同的图形一个编号,按下选择图形的按钮后,相对应的消息相应函数就会改变那个编号,鼠标移动消息相应函数和鼠标左键抬起消息响应函数根据这个编号来绘制不同的图形就可以了!
那鼠标左键按下消息响应函数不用去改造吗?
是不用改造的,因为鼠标一开始按下只是为了获取起点的坐标,而是不去画图形,所以这个对所有的图形都适用。
在这之前需要记录好每一个选择图形按键的ID,和消息响应函数,同时在消息响应函数中完成了CDC对象指针pdc的构造。
/* 1 画直线 2 画矩形 3.画空心圆形 4.画填充矩形 5.画填充圆形 6.画圆角矩形 7.画填充圆角矩形 直线 ID_LINE, 矩形 ID_RECTANGLE 圆形 ID_CIRCLE 填充矩形 ID_TRECTANGLE 填充圆形 ID_TCIRCLE 圆角矩形 ID_YTRECTANGLE 填充圆角矩形 ID_TYTRECTANGLE */ void CWkfDrawingView::OnLine() { // TODO: 在此添加命令处理程序代码 MyDrawStyle = 1; pdc=new CClientDC(this);//构造对象 b=false; } void CWkfDrawingView::OnRectangle()//画矩形 { MyDrawStyle = 2; pdc=new CClientDC(this);//构造对象 b=false; } void CWkfDrawingView::OnCircle()//画空心圆形 { MyDrawStyle = 3; pdc=new CClientDC(this);//构造对象 b=false; } void CWkfDrawingView::OnTrectangle() { MyDrawStyle = 4; pdc=new CClientDC(this);//构造对象 b=false; } void CWkfDrawingView::OnTcircle() { MyDrawStyle = 5; pdc=new CClientDC(this);//构造对象 b=false; } void CWkfDrawingView::OnYtrectangle() { MyDrawStyle = 6; pdc=new CClientDC(this);//构造对象 b=false; } void CWkfDrawingView::OnTytrectangle() { MyDrawStyle = 7; pdc=new CClientDC(this);//构造对象 b=false; }
下面给出鼠标按下消息相应函数、鼠标移动消息相应函数和鼠标左键抬起消息响应函数的代码,MyStart 和MyEnd是视图类的两个CPoint类型的成员变量,用来保存起点和终点的坐标。
void CWkfDrawingView::OnLButtonDown(UINT nFlags, CPoint point)//鼠标按下 { // TODO: 在此添加消息处理程序代码和/或调用默认值 MyStart = MyEnd = point; pdc=new CClientDC(this); pdc->SetROP2(R2_NOTXORPEN); b = true; CView::OnLButtonDown(nFlags, point); } void CWkfDrawingView::OnMouseMove(UINT nFlags, CPoint point)//鼠标移动 { // TODO: 在此添加消息处理程序代码和/或调用默认值 /*pdc->MoveTo(MyStart.x,MyStart.y); pdc->LineTo(MyEnd.x,MyEnd.y);*/ if(!b) return ; CPen pen(GP.type, GP.width, GP.pencolor); OldPen=pdc->SelectObject(&pen); if(MyDrawStyle==1) { pdc->SelectStockObject(NULL_BRUSH); pdc->MoveTo(MyStart.x,MyStart.y); pdc->LineTo(MyEnd.x,MyEnd.y); MyEnd=point; pdc->MoveTo(MyStart.x,MyStart.y); pdc->LineTo(MyEnd.x,MyEnd.y); } else if(MyDrawStyle==2) { pdc->SelectStockObject(NULL_BRUSH); pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); MyEnd = point; pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); } else if(MyDrawStyle==3) { pdc->SelectStockObject(NULL_BRUSH); pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); MyEnd = point; pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); } else if(MyDrawStyle==4) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SelectObject(&bsh); pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); MyEnd = point; pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); bsh.DeleteObject(); } else if(MyDrawStyle==5) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SelectObject(&bsh); pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); MyEnd = point; pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); bsh.DeleteObject(); } else if(MyDrawStyle==6) { pdc->SelectStockObject(NULL_BRUSH); pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); MyEnd = point; pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); } else if(MyDrawStyle==7) { CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SelectObject(&bsh); pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); MyEnd = point; pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); bsh.DeleteObject(); } CView::OnMouseMove(nFlags, point); } void CWkfDrawingView::OnLButtonUp(UINT nFlags, CPoint point)//鼠标抬起 { GPen g; g.start = MyStart; g.end = MyEnd; g.width = MyWidth; g.type = type; g.style = MyDrawStyle; g.pencolor = GP.pencolor; if(MyDrawStyle==1) { // TODO: 在此添加消息处理程序代码和/或调用默认值 pdc->SetROP2(R2_COPYPEN);//当前颜色覆盖背景颜色 pdc->MoveTo(MyStart.x,MyStart.y); pdc->LineTo(point.x,point.y); g.c = GP.pencolor; b=false;//解除绘图关系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==2) { pdc->SetROP2(R2_COPYPEN); pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); g.c = GP.pencolor; b=false;//解除绘图关系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==3) { pdc->SetROP2(R2_COPYPEN); pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); g.c = GP.pencolor; b=false;//解除绘图关系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==4) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SetROP2(R2_COPYPEN); pdc->SelectObject(&bsh); pdc->Rectangle(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); g.c =GP.pencolor; b=false;//解除绘图关系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==5) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SetROP2(R2_COPYPEN); pdc->SelectObject(&bsh); pdc->Ellipse(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y); g.c =GP.pencolor; b=false;//解除绘图关系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==6) { pdc->SetROP2(R2_COPYPEN); pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); g.angle=a; g.c = GP.pencolor; b=false;//解除绘图关系 CView::OnLButtonUp(nFlags, point); } else if(MyDrawStyle==7) { //pdc->SelectObject(&newBrush); CBrush bsh; bsh.CreateSolidBrush(GP.pencolor); pdc->SetROP2(R2_COPYPEN); pdc->SelectObject(&bsh); pdc->RoundRect(MyStart.x,MyStart.y,MyEnd.x,MyEnd.y,a.x,a.y); g.angle=a; g.c = GP.pencolor; b=false;//解除绘图关系 CView::OnLButtonUp(nFlags, point); } GetDocument()->Mylist.AddTail(g);//保存信息 Invalidate(); }
上面的代码中也嵌入了画笔和画刷的内容,画笔和画刷有一个特性就是一旦被定义和创建后,之后所绘制的图形就会之间用上了,所以要注意画笔和画刷的使用,而填充图形和空心图形的区别就是有没有画刷。
CBrush bsh;//定义画刷 bsh.CreateSolidBrush(GP.pencolor);//创建画刷 pdc->SelectObject(&bsh);//选择画刷
画笔的使用
CPen pen(GP.type, GP.width, GP.pencolor);//画笔的定义和创建,三个参数画笔的类型、画笔宽度、画笔的颜色 pdc->SelectObject(&pen);//选择画笔
上面还有一些代码是关于图形保存和重绘的,之后进行说明。
四、文件保存和读取——文档串行化
文档类中中提供了文档串行化(Serialize)函数能够将对象当前的状态由成员变量的值表示写入硬盘中,下次再从硬盘中读取对象的状态,从而重建对象。
但在这里的对象是什么呢?
是图形,可是图形的种类很多,如果将每一个图形的信息都通过结构体定义出来,那么会有很多的结构体来表示不同的图形,这里我选择了一种方法,将所有图形的参数,不管是特有的参数还是共有的参数,都统统封装到一个结构体中,为这个结构体创建链表,修改串行化函数就可以了!
//为了让视图和文档都认识GPen这个存储图片信息的结构体,需要在Stdafx.h中添加代码 struct GPen//保存画笔参数全局变量 { int type;//画笔类型 int width;//画笔宽度 COLORREF pencolor;//画笔颜色 COLORREF c; CPoint start,end;//直线、矩形和椭圆的起始点 int style;//图形的类型 CPoint angle;//圆角矩形角度 };
为文档Doc类添加Gpen的链表:
CList <GPen,GPen> Mylist;
文档类的串行化Serialize函数:
void CWkfDrawingDoc::Serialize(CArchive& ar) { int i; if (ar.IsStoring())//保存 { // TODO: 在此添加存储代码 ar<<Mylist.GetCount(); GPen g; POSITION pos = Mylist.GetHeadPosition(); for(i = 0; i<Mylist.GetCount(); i++) { g = Mylist.GetNext(pos); ar<<g.type<<g.width<<g.pencolor<<g.c<<g.start<<g.end<<g.style<<g.angle; } } else//读取 { // TODO: 在此添加加载代码 int count; ar>>count; GPen g; POSITION pos = Mylist.GetHeadPosition(); for(i = 0; i<count; i++) { ar>>g.type>>g.width>>g.pencolor>>g.c>>g.start>>g.end>>g.style>>g.angle; Mylist.AddTail(g); } } }
打开之前保存文件需要有一个重绘函数,我们之前画图都只是在鼠标移动和鼠标左键抬起的时候画图,现在画图都要在视图类中的OnDraw中重绘了,这也就是之前的鼠标左键抬起消息响应函数中,最后需要将所画的图形信息保存到链表中的原因了。(鼠标抬起了,这个图形才真正被画出来)
GetDocument()->Mylist.AddTail(g);//保存信息 Invalidate();
之后的哪一行代码就是刷新,去执行OnDraw函数了。
void CWkfDrawingView::OnDraw(CDC* pDC)//加载文件重绘函数 { int i; CWkfDrawingDoc* pDoc = GetDocument(); pdc=new CClientDC(this); ASSERT_VALID(pDoc); if (!pDoc) return; GPen g; POSITION pos = pDoc->Mylist.GetHeadPosition(); for(i = 0; i<pDoc -> Mylist.GetCount(); i++) { g = pDoc -> Mylist.GetNext(pos); CPen p(g.type,g.width,g.pencolor); pdc->SelectObject(&p); pdc->MoveTo(g.start.x,g.start.y); if(g.style==1)//画直线 { pdc->SelectStockObject(NULL_BRUSH); pdc->LineTo(g.end.x,g.end.y); } if(g.style==2)//画矩形 { pdc->SelectStockObject(NULL_BRUSH); pdc->Rectangle(g.start.x,g.start.y,g.end.x,g.end.y); } if(g.style==3)//画圆形 { pdc->SelectStockObject(NULL_BRUSH); pdc->Ellipse(g.start.x,g.start.y,g.end.x,g.end.y); } if(g.style==4)//画填充矩形 { CBrush bsh; bsh.CreateSolidBrush(g.pencolor); pdc->SelectObject(&bsh); pdc->Rectangle(g.start.x,g.start.y,g.end.x,g.end.y); bsh.DeleteObject(); } if(g.style==5)//画填充圆形 { CBrush bsh; bsh.CreateSolidBrush(g.pencolor); pdc->SelectObject(&bsh); pdc->Ellipse(g.start.x,g.start.y,g.end.x,g.end.y); bsh.DeleteObject(); } if(g.style==6)//画圆角矩形 { pdc->SelectStockObject(NULL_BRUSH); pdc->RoundRect(g.start.x,g.start.y,g.end.x,g.end.y,g.angle.x,g.angle.y); } if(g.style==7)//画填充圆角矩形 { CBrush bsh; bsh.CreateSolidBrush(g.pencolor); pdc->SelectObject(&bsh); pdc->RoundRect(g.start.x,g.start.y,g.end.x,g.end.y,g.angle.x,g.angle.y); bsh.DeleteObject(); } pdc->SelectObject(OldPen); } }
五、几个对话框
颜色对话框和字体对话框是系统给的,我这里给出其按键的消息响应函数。其中的MyFont和是Pcolor是视图类的两个成员变量CFont MyFont COLORREF Pcolor
void CWkfDrawingView::OnFont()//字体设置 { CFontDialog dlg; if(IDOK==dlg.DoModal()) { if(MyFont.m_hObject) { MyFont.DeleteObject(); } MyFont.CreateFontIndirect(dlg.m_cf.lpLogFont);//字体信息 MyFontName=dlg.m_cf.lpLogFont->lfFaceName;//字体的名称 } } void CWkfDrawingView::OnPancolor()//画笔颜色设置 { CColorDialog dlg(0,CC_FULLOPEN); if(dlg.DoModal()) { Pcolor = dlg.GetColor();//从颜色对话框中获取颜色信息 GP.pencolor=Pcolor; } else if(dlg.DoModal()==IDCANCEL) {} }
对于自定义的对话框,有画笔的宽度设置,画笔类型设置,文本输入,代码如下:
void CWkfDrawingView::OnTxt()//文本输入 { // TODO: 在此添加命令处理程序代码 CTxtlog dlg; if(dlg.DoModal()==IDOK) { int X=dlg.MyX; int Y=dlg.MyY; CString String=dlg.MyString; pdc=new CClientDC(this);//构造对象 pdc->SetTextColor(GP.pencolor);//设置文件颜色 pdc->SelectObject(&MyFont); pdc->TextOut(X,Y,String); } else if(dlg.DoModal()==IDCANCEL) {} } void CWkfDrawingView::OnLineW()//画笔宽度 { // TODO: 在此添加命令处理程序代码 CLWidth dlg; if(dlg.DoModal()==IDOK) { GP.width=dlg.width;//更新画笔的宽度 MyWidth=dlg.width; } else if(dlg.DoModal()==IDCANCEL) {} } /* PS_SOLID 实线 PS_DASH 虚线 PS_DOT 点线 PS_DASHDOT 点化线 PS_DASHDOTDOT 双点化线 */ void CWkfDrawingView::OnSolid()//线条类型 { // TODO: 在此添加命令处理程序代码 type=PS_SOLID; GP.type=type; pdc=new CClientDC(this); } void CWkfDrawingView::OnDash() { // TODO: 在此添加命令处理程序代码 type=PS_DASH; GP.type=type; } void CWkfDrawingView::OnDot() { // TODO: 在此添加命令处理程序代码 type=PS_DOT; GP.type=type; } void CWkfDrawingView::OnDashdot() { // TODO: 在此添加命令处理程序代码 type=PS_DASHDOT; GP.type=type; } void CWkfDrawingView::OnDashdotdot() { // TODO: 在此添加命令处理程序代码 type=PS_DASHDOTDOT; GP.type=type; }
但对于文本输入和字体宽度设置,需要从对话框中获取信息,保存到变量中,这就需要交换函数,在这之前需要将自定义的对话框设置一个对话框类,与对话框资源相关联,所有的代码处理都在对话框类中进行。以文本输入为例,添加文本输入对话框类
#pragma once // CTxtlog 对话框 class CTxtlog : public CDialog { DECLARE_DYNAMIC(CTxtlog) public: CTxtlog(CWnd* pParent = NULL); // 标准构造函数 virtual ~CTxtlog(); // 对话框数据 enum { IDD = IDD_TEXT }; protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持 DECLARE_MESSAGE_MAP() public: int MyX; public: int MyY; public: CString MyString; };
修改其中的数据交换函数为:
void CTxtlog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Text(pDX, ID_TXTX, MyX); DDX_Text(pDX, ID_TXTY, MyY); DDX_Text(pDX, ID_TXTS, MyString); }
源代码:https://github.com/wkfvawl/MFC-Drawing