基于:https://blog.csdn.net/weixin_51599044/article/details/111741111
实现的功能:
绘制图元的方法:
前5种图元的菜单项均在“绘制”菜单里。
1.直线:按下左键作为起点,按住左键拖动鼠标到你想设定的终点,然后放开鼠标即可。
2.矩形:按下左键确定左上角,按住左键拖动鼠标到你想设定的右下角,然后放开鼠标。
3.折线:同直线画法,第一条线要设置起点和终点,后续的线条只需设置终点,结束需要按下右键,并且要再次点击折线按钮或菜单才能绘制新的折线。
4.贝塞尔曲线:先画一条直线,然后依次拖动选择第一个、第二个控制点。
5.圆:按下左键作为圆心,按住左键拖动鼠标设定半径,然后放开鼠标
6.b样条:点击“新增功能”里的“绘制b样条曲线”菜单项,每次鼠标点下即可设置一个控制点(注意每次按下后不需要按住),控制点会被直线连接起来。至少要设置四个控制点才会画出曲线,设置完毕后点击右键完成一条曲线的绘制。
再次绘制样条曲线需要重新点击“绘制b样条”菜单项。
设计思路
说明:GDI函数的调用(参看https://blog.csdn.net/qq_61814350/article/details/133284380)、界面设计、设置消息处理函数(鼠标按下、移动、松开以及点击菜单栏的某项)、解决动态绘制出现的重影(反色笔)、图形的重绘(保存图形,在ondraw函数里调用绘图函数)不再赘述,请参看文章最前面的参考文章和同专栏的【mfc/VS2022】计图实验:绘图工具设计知识笔记系列文章。
画直线
画直线的例子参考文章的基础上进行了修改,主要是加入了自己实现的中点画线法函数(算法思路可以查看专栏里的有关文章),newdraw是调用中点画线法函数的,以及相应的处理斜率的变量。相应的h和cpp文件如下:
#pragma once
class CLine
{
public:CLine();void Set_start_point(CPoint p);void Set_end_point(CPoint p);CPoint Get_start_point();CPoint Get_end_point();double slope;double b;//y=kx+bint slope_is_exist;// 斜率是否存在的标志CPoint Line_start_point;CPoint Line_end_point;public:void Draw(CDC* pDC);void MidLine(int x0, int y0, int x1, int y1, CDC* pDC);void NewDraw(CDC* pDC);};
#include "pch.h"
#include "CLine.h"
CLine::CLine()
{slope = 0; b = 0;slope_is_exist = 1;
}void CLine::Set_start_point(CPoint p)
{Line_start_point = p;
}void CLine::Set_end_point(CPoint p)
{Line_end_point = p;}CPoint CLine::Get_start_point()
{return Line_start_point;
}CPoint CLine::Get_end_point()
{return Line_end_point;
}void CLine::Draw(CDC* pDC)
{if (Line_end_point.x - Line_start_point.x == 0) slope_is_exist = 0;else{slope_is_exist = 1;slope = (1.0 * Line_end_point.y- Line_start_point.y) / (Line_end_point.x - Line_start_point.x);b = Line_end_point.y - slope * Line_end_point.x;}pDC->MoveTo(Line_start_point);pDC->LineTo(Line_end_point);
}void CLine::MidLine(int x1, int y1, int x2, int y2, CDC* pDC)
{int dx, dy, d, Deta1, Deta2, x, y;double k{ 0 };if (x1 != x2) k = (1.0 * y2 - y1) / (x2 - x1);if (x1 > x2 && abs(k) <= 1){x = x2; x2 = x1; x1 = x;y = y2; y2 = y1; y1 = y;}else if (y1 > y2 && abs(k) > 1){x = x2; x2 = x1; x1 = x;y = y2; y2 = y1; y1 = y;}x = x1; y = y1;if (x2 == x1){while (y < y2){pDC->SetPixel(x, y, RGB(0, 0, 0));y++;}return;}dx = x2 - x1; dy = y2 - y1;if (k >= 0 && k <= 1){Deta1 = 2 * dx - 2 * dy; Deta2 = -2 * dy; d = dx - 2 * dy;while (x <= x2){pDC->SetPixel(x, y, RGB(0, 0, 0));x++;if (d < 0){y++;d += Deta1;}elsed += Deta2;}}else if (k <= 0 && k >= -1){/**/Deta1 = -2 * dy; Deta2 = -2 * dx - 2 * dy; d = -dx - 2 * dy;while (x <= x2){pDC->SetPixel(x, y, RGB(0, 0, 0));x++;if (d >= 0){y--;d += Deta2;}elsed += Deta1;}}else if (k > 1){Deta1 = 2 * dx - 2 * dy; Deta2 = 2 * dx; d = 2 * dx - dy;while (y <= y2){pDC->SetPixel(x, y, RGB(0, 0, 0));y++;if (d >= 0){x++;d += Deta1;}elsed += Deta2;}}else if (k < -1){/**/Deta1 = 2 * dx; Deta2 = 2 * dx + 2 * dy; d = 2 * dx + dy;while (y <= y2){pDC->SetPixel(x, y, RGB(0, 0, 0));y++;if (d < 0){x--;d += Deta2;}elsed += Deta1;}//pDC->MoveTo(Line_start_point);//pDC->LineTo(Line_end_point);}}void CLine::NewDraw(CDC* pDC)
{if (Line_end_point.x - Line_start_point.x == 0) slope_is_exist = 0;else{slope_is_exist = 1;slope = (1.0 * Line_end_point.y- Line_start_point.y) / (Line_end_point.x - Line_start_point.x);//这里要加上计算slope的部分,计算交点的时候要判断b = Line_end_point.y - slope * Line_end_point.x;}MidLine(Line_start_point.x, Line_start_point.y, Line_end_point.x, Line_end_point.y, pDC);
}
画矩形:
矩形的数据结构实现起来与直线很类似,因为考虑到其他功能(比如不调用Rectangle()而是用
Polyline()来绘制),所以用了一个数组pts保存了矩形的顶点。并保存了矩形的几何中点,方便计算一个点和矩形的位置关系(比较中点到该点的垂直、水平距离来判断点位于矩形内、外还是上)。消息处理函数与直线差不多,不再赘述。
#pragma once
class CQuare
{
public:CQuare();void Set_first_point(CPoint p);void Get_other_point();void Set_end_point(CPoint p);CPoint Get_first_point();CPoint Get_end_point();CPoint Get_central_point();void Draw(CDC* pDC);CPoint pts[5];//保存矩形顶点,可以用于多义线方式绘制//private:CPoint square_first_point;CPoint square_end_point;CPoint square_central_point;CPoint square_east_point;CPoint square_south_point;
};
#include "pch.h"
#include "CQuare.h"
CQuare::CQuare()
{}
void CQuare::Set_first_point(CPoint p)
{square_first_point = p;
}
void CQuare::Set_end_point(CPoint p)
{square_end_point = p;
}
CPoint CQuare::Get_first_point()
{return square_first_point;
}
CPoint CQuare::Get_end_point()
{return square_end_point;
}
CPoint CQuare::Get_central_point()
{return square_central_point;
}
void CQuare::Get_other_point()
{square_east_point.x = square_end_point.x;square_east_point.y = square_first_point.y;square_south_point.x = square_first_point.x;square_south_point.y = square_end_point.y;
}
void CQuare::Draw(CDC* pDC)
{square_central_point.x = (square_first_point.x+square_end_point.x)/ 2;square_central_point.y = (square_first_point.y + square_end_point.y )/ 2;Get_other_point();pts[0] = square_first_point;pts[1] = square_east_point;pts[2] = square_end_point;pts[3] = square_south_point;pts[4] = square_first_point;pDC->Rectangle(square_first_point.x, square_first_point.y, square_end_point.x, square_end_point.y);
}
画折线(多义线)
类似直线的画法。因为要动态地一条一条的绘制,所以不能直接调用绘制多义线的GDI函数,要一条一条的调用绘制直线的函数。给出相应的类定义:
#pragma once
class CPolyline
{
public:CPolyline();void Set_start_point(CPoint p);void Set_end_point(CPoint p);BOOL Sure_is_first();//private:CPoint Line_start_point;CPoint Line_end_point;BOOL is_first;public:void Draw(CDC* pDC);void ReDraw(CDC* pDC);void chgstartpoint();void selfincre_point_num();int Get_point_num();void Set_point_num(int num);int point_num;int is_closed;//判断多义线是不是画成了三角形之类的封闭图形CPoint polyline_pts[256];
};
说明:在构造函数里初始化状态isfirst为1,表示是第一条线,需要设置起点和终点,在画完第一条线后置为0,只需要设置终点。
is_closed判断是否画成了封闭图形,为1,表示封闭,可以结束一次多义线绘制。
注意:不能在void CPolyline::Set_end_point(CPoint p)自增点的数量,因为不断地在设置终点。用void CPolyline::selfincre_point_num()专门做这个操作,在void CSimpleDrawView::OnLButtonUp(UINT nFlags, CPoint point),即左键松开的时候调用。
void CPolyline::ReDraw(CDC* pDC)为在ondraw()里重绘多义线的函数,因为此时该图形的点已经全部确定,可以直接调用GDI的多义线绘制函数。
void CPolyline::chgstartpoint()将上段线的终点设置为下段要画的线的起点。
相应的类定义如下:
#include "pch.h"
#include "CPolyline.h"
CPolyline::CPolyline()
{is_first = 1;point_num = 0;is_closed = 0;}
void CPolyline::Set_start_point(CPoint p)
{Line_start_point = p;polyline_pts[point_num] = Line_start_point;point_num++;is_first = 0;
}void CPolyline::Set_end_point(CPoint p)//不断地在设置终点,所以不能在这里自增
{Line_end_point = p;polyline_pts[point_num] = Line_end_point;//point_num++;}
BOOL CPolyline::Sure_is_first()
{return is_first;
}
void CPolyline::Draw(CDC* pDC)
{pDC->MoveTo(Line_start_point);pDC->LineTo(Line_end_point);
}
void CPolyline::ReDraw(CDC* pDC)
{pDC->Polyline(polyline_pts, point_num);
}
void CPolyline::chgstartpoint()
{Line_start_point = Line_end_point;
}void CPolyline::selfincre_point_num()
{point_num++;
}int CPolyline::Get_point_num()
{return point_num;
}void CPolyline::Set_point_num(int num)
{point_num = num;
}
消息函数具体的处理
剩余图形的绘制有空再在下篇文章里说明。再说明一下相关的消息函数的处理:
在view的消息处理函数进行单独说明较复杂,故直接给出相关的部分 :
type变量==1(type变量表示不同的使用状态,详见参考的文章)表示用GDI函数绘制的直线,10表示用中点画线法绘制的直线(两者点击的菜单栏不同),2为矩形,3和11为GDI和bresenham绘制的圆,4,5,6,8为多义线,贝塞尔曲线,垂线,b样条曲线。
注意:每条case语句块后记得加break;代码只复制了相关的部分,所以可能存在括号不对应的问题。
void CSimpleDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{CDC* pDC = GetDC();switch (type){case 1:{m_pline = new CLine;m_pline->Set_start_point(point);}break;case 10:{m_pline = new CLine;m_pline->Set_start_point(point);}break;case 2:{m_psquare = new CQuare;m_psquare->Set_first_point(point);}break;case 3:{m_pcircle = new CCircle;m_pcircle->Set_CenPoint(point);}break;case 11:{m_pcircle = new CCircle;m_pcircle->Set_CenPoint(point);}break;case 4:{if (!on_polyline){on_polyline = 1;m_polyline = new CPolyline;}if (m_polyline->Sure_is_first()){m_polyline->Set_start_point(point);}else m_polyline->chgstartpoint();//上次的终点设为起点}break;case 5:{if (!on_polybezier){m_polybezier = new CPolyBezier;on_polybezier = 1;m_polybezier->Set_start_point(point);}}break;case 6:{if (on_verticalline && choose_line == 0 && is_choose){m_verpline = new CVerticalLine;choose_line = 1;m_verpline->Set_start_point(point);}if (on_verticalline && choose_line == 2 && is_choose){CPoint verfoot = m_verpline->Get_foot_point();m_verpline->Set_end_point(verfoot);Add_insect_point(verfoot);m_verpline->Draw(pDC);choose_line = 0;}}break;}if (type != 0) start = true;ReleaseDC(pDC);CView::OnLButtonDown(nFlags, point);
}
鼠标左键按下函数里需要注意的是需要标识多义线是否在画第一条线,要设置起点和终点。由于多义线的点的数量未知,结束一次绘制不能像其他图形一样通过松开左键来结束。这里采用了按下右键时on_polyline = 0;来标识结束。如下面的函数:
void CSimpleDrawView::OnRButtonDown(UINT nFlags, CPoint point)
{CDC* pDC = GetDC();if (type == 4 && on_polyline){Node* repaint = new Node;repaint->now_type = 4;repaint->data = m_polyline;m_line_list.InputFront(repaint);on_polyline = 0;}type = 0;on_polybezier = 0;is_choose = 0;on_verticalline = 0;is_get_cenpoint = 0;on_find_intersection = 0;choose_line = 0;on_fill = 0;CView::OnRButtonDown(nFlags, point);
}
void CSimpleDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{CDC* pDC = GetDC();if (start == true){Node* repaint = new Node;switch (type){case 1:{repaint->now_type = 1;repaint->data = m_pline;m_line_list.InputFront(repaint);}break;case 10:{repaint->now_type = 1;repaint->data = m_pline;m_line_list.InputFront(repaint);}break;case 2:{repaint->now_type = 2;repaint->data = m_psquare;m_line_list.InputFront(repaint);}break;case 3:{repaint->now_type = 3;repaint->data = m_pcircle;m_line_list.InputFront(repaint);}break;case 11:{repaint->now_type = 3;repaint->data = m_pcircle;m_line_list.InputFront(repaint);}break;case 4:{if (m_polyline->Sure_is_first() == 0){m_polyline->selfincre_point_num();if (on_polyline&&m_polyline->is_closed==1){Node* repaint = new Node;repaint->now_type = 4;repaint->data = m_polyline;m_line_list.InputFront(repaint);on_polyline = 0;}}}break;}}go = false;start = false;ReleaseDC(pDC);CView::OnLButtonUp(nFlags, point);
}
OnLButtonUp鼠标左键松开函数里,m_line_list是保存图形的链表,具体说明见参考的文章。需要注意的就是多义线的点的数目的增加是在这里进行的(如前文所述),以及画出的是封闭图形的话就结束这次绘制。
void CSimpleDrawView::OnMouseMove(UINT nFlags, CPoint point)
{if (start == true){if (type == 1){if (go){pDC->SetROP2(R2_NOTXORPEN);m_pline->Draw(pDC);}elsego = true;m_pline->Set_end_point(point);m_pline->Draw(pDC);}if (type == 10){if (go){pDC->SetROP2(R2_NOTXORPEN);m_pline->NewDraw(pDC);}elsego = true;m_pline->Set_end_point(point);m_pline->NewDraw(pDC);}if (type == 2){if (go){pDC->SetROP2(R2_NOTXORPEN);m_psquare->Draw(pDC);}elsego = true;m_psquare->Set_end_point(point);m_psquare->Draw(pDC);}if (type == 3){if (go){pDC->SetROP2(R2_NOTXORPEN);m_pcircle->Draw(pDC);}elsego = true;m_pcircle->Get_Radius(point);m_pcircle->Draw(pDC);}if (type == 11){if (go){pDC->SetROP2(R2_NOTXORPEN);m_pcircle->NewDraw(pDC);}elsego = true;m_pcircle->Get_Radius(point);m_pcircle->NewDraw(pDC);}if (type == 4)//折线段{if (go){pDC->SetROP2(R2_NOTXORPEN);m_polyline->Draw(pDC);}elsego = true;//绘制封闭图形,当鼠标点接近第一个点时,鼠标显示问号,可以绘制封闭的图形if (dis_to_another(point, m_polyline->polyline_pts[0]) <= 8&& m_polyline->point_num>2){SetCursor(LoadCursor(NULL, IDC_HELP));m_polyline->Set_end_point(m_polyline->polyline_pts[0]);m_polyline->is_closed = 1;}else{SetCursor(LoadCursor(NULL, IDC_ARROW));m_polyline->Set_end_point(point);m_polyline->is_closed = 0;}m_polyline->Draw(pDC);}}ReleaseDC(pDC);CView::OnMouseMove(nFlags, point);
}
在鼠标移动函数onmousemove里,采用了参考文章里的反色笔(其实就是用颜色进行异或,相同颜色异或两次就恢复成背景色)处理方法避免动态绘制里出现的重影,也可以采用双缓冲方式(此处不赘述)。
保存图形的链表
在参考文章的基础上实现了node类的串行化,可以将数据存入文件。
类的串行化方法见:https://blog.csdn.net/qq_61814350/article/details/133870684
Node.h和cpp
#pragma once
class Node :public CObject
{DECLARE_SERIAL(Node)
public:void* data;int now_type;Node* next;Node();void Serialize(CArchive& archive);
};
#include "pch.h"
#include "Node.h"
#include "CLine.h"
#include "CCircle.h"
#include "CPolyline.h"
#include"CPolyBezier.h"
#include"CVerticalLine.h"
#include"CIntersectPoint.h"
#include"CQuare.h"
#include"CBSpline.h"
IMPLEMENT_SERIAL(Node, CObject, 1)
Node::Node()
{data = NULL;now_type = 1;next = NULL;
}void Node::Serialize(CArchive& ar)
{if (ar.IsStoring()){ar << now_type;if (now_type == 1){ar << ((CLine*)data)->Get_start_point() << ((CLine*)data)->Get_end_point();}if (now_type == 2){ar << ((CQuare*)data)->Get_first_point() << ((CQuare*)data)->Get_end_point();}if (now_type == 3){ar << ((CCircle*)data)->Get_m_CenPoint() << ((CCircle*)data)->Get_m_Radius();}if (now_type == 4){int pnum = ((CPolyline*)data)->Get_point_num();ar << pnum;for (int i = 0; i < pnum; i++){ar << ((CPolyline*)data)->polyline_pts[i];}}if (now_type == 5){for (int i = 0; i < 4; i++){ar << ((CPolyBezier*)data)->pts[i];}}if (now_type == 6){ar << ((CVerticalLine*)data)->Get_start_point() << ((CVerticalLine*)data)->Get_end_point();}if (now_type == 8){CBSpline* bstemp = ((CBSpline*)data);int tnum = bstemp->nPoints;ar <<tnum <<bstemp->linewidth<<bstemp->is_on_changed;for (int i = 0; i < tnum; i++){ar << bstemp->xcoordinate[i] << bstemp->ycoordinate[i];}}}else{CPoint tempp; int tempr;ar >> now_type;if (now_type == 1){data = new CLine;//注意这里要创建一个类对象让data指向ar >> tempp;((CLine*)data)->Set_start_point(tempp);ar >> tempp;((CLine*)data)->Set_end_point(tempp);}if (now_type == 2){data = new CQuare;ar >> tempp;((CQuare*)data)->Set_first_point(tempp);ar >> tempp;((CQuare*)data)->Set_end_point(tempp);}if (now_type == 3){data = new CCircle;ar >> tempp;((CCircle*)data)->Set_CenPoint(tempp);ar >> tempr;((CCircle*)data)->Set_m_Radius(tempr);}if (now_type == 4){data = new CPolyline;ar >> tempr;((CPolyline*)data)->Set_point_num(tempr);for (int i = 0; i < tempr; i++){ar >> ((CPolyline*)data)->polyline_pts[i];}}if (now_type == 5){data = new CPolyBezier;for (int i = 0; i < 4; i++){ar >> ((CPolyBezier*)data)->pts[i];}}if (now_type == 6){data = new CVerticalLine;ar >> tempp;((CVerticalLine*)data)->Set_start_point(tempp);ar >> tempp;((CVerticalLine*)data)->Set_end_point(tempp);}if (now_type == 8){data = new CBSpline;ar >> ((CBSpline*)data)->nPoints >> ((CBSpline*)data)->linewidth >> ((CBSpline*)data)->is_on_changed;int tnum = ((CBSpline*)data)->nPoints;for (int i = 0; i < tnum; i++){ar >> ((CBSpline*)data)->xcoordinate[i] >> ((CBSpline*)data)->ycoordinate[i];}}}
}
列表的定义:
#pragma once
#include"Node.h"class CSlist
{public:CSlist();~CSlist();Node* first;void InputFront(Node* Pelem);//在前面插入最新的一个节点int Length()const;//判断链表长度bool IsEmpty()const;//判断链表是否为空void MakeEmpty();//将链表清空Node* Locate(int i);//将第i个数据取出int Input_behind_pos(Node* Pelem, int pos);//在第几个数值之后插入新节点int Input_before_pos(Node* Pelem, int pos);void Delete(int pos);};
#include "pch.h"
#include "CSlist.h"#include"Node.h"CSlist::CSlist()
{first = NULL;
}
CSlist::~CSlist()
{MakeEmpty();
}void CSlist::InputFront(Node* Pelem)//头插法
{if (Pelem == NULL) return;Pelem->next = first;first = Pelem;
}
int CSlist::Length()const
{if (first == NULL) return 0;Node* n = first;int length = 1;while (n->next){length++;n = n->next;}return length;
}
bool CSlist::IsEmpty()const
{if (first == NULL){return true;}else{return false;}
}
void CSlist::MakeEmpty()
{Node* d;while (first){d = first;first = first->next;delete d;}return;
}
Node* CSlist::Locate(int i)
{int j = 1;Node* L = first;while (L){if (j == i){return L;}else L = L->next;j++;}}
int CSlist::Input_behind_pos(Node* Pelem, int pos)
{Node* d = Locate(pos + 1);Pelem->next = d;//新节点的next指向原来的下一个节点Node* c = Locate(pos);c->next = Pelem;//上一个节点的next指向新节点return 1;
}
int CSlist::Input_before_pos(Node* Pelem, int pos)
{Node* c = Locate(pos - 1);c->next = Pelem;//上一个节点的next指向新节点Node* d = Locate(pos);Pelem->next = d;//新节点的next指向原来的下一个节点return 1;
}
void CSlist::Delete(int pos)
{Node* a = Locate(pos - 1);Node* b = Locate(pos);Node* c = Locate(pos + 1);a->next = c;delete b;
}
Doc类重写序列化函数:
void CSimpleDrawDoc::Serialize(CArchive& ar)
{POSITION pos = GetFirstViewPosition();CSimpleDrawView* pView = (CSimpleDrawView*)GetNextView(pos);if (ar.IsStoring()){int nCount = pView->m_line_list.Length();ar << nCount;for (int i = 1; i <= nCount; i++){ar << pView->m_line_list.Locate(i);}ar << pView->insect_num;int j = pView->insect_num;for (int i = 0; i < j; i++){ar << pView->m_intersectp[i].Get_point();}}else{int nCount;ar >> nCount;Node* pGraph;for (int i = 1; i <= nCount; i++){ar >> pGraph;pView->m_line_list.InputFront(pGraph);}ar >> pView->insect_num;int j = pView->insect_num;for (int i = 0; i < j; i++){CPoint tempp;ar >> tempp;pView->m_intersectp[i].Set_point(tempp);}}
}