基于C++(MFC)图形编辑界面工具
MFC 图形编辑界面工具
一、背景
喔,五天的实训终于结束了,学校安排的这次实训课名称叫高级程序设计实训,但在我看来,主要是学习了 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、p2p1 = 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、p2p1 = 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);
}