【mfc/VS2022】绘图工具设计-绘制基本图元2

接着:https://blog.csdn.net/qq_61814350/article/details/135609009?spm=1001.2014.3001.5501

画圆

添加了bresenham法绘制圆的函数(该算法详细步骤见专栏相关文章):

void Bresenham_Circle(int xc, int yc, int r, CDC* pDC);
void NewDraw(CDC* pDC);

is_CenPoint用来标志是否已经设置好了圆心(按下了鼠标左键),可以计算半径。

详细定义如下:

#pragma once
class CCircle
{
public:
	CCircle();


	void Set_CenPoint(CPoint p);
	void Get_Radius(CPoint p);
	int Get_m_Radius();
	CPoint Get_m_CenPoint();
	void Draw(CDC* pDC);
	void Set_m_Radius(int r);

	void Bresenham_Circle(int xc, int yc, int r, CDC* pDC);
	void NewDraw(CDC* pDC);


	CPoint m_CenPoint;
	int m_Radius;
	bool is_CenPoint;
};

#include "pch.h"
#include "CCircle.h"
CCircle::CCircle()
{
	is_CenPoint = false;
}
void CCircle::Set_CenPoint(CPoint p)//圆心
{
	is_CenPoint = true;
	m_CenPoint = p;
}
void CCircle::Get_Radius(CPoint p)
{
	if (is_CenPoint)
	{
		double x = (p.x - m_CenPoint.x) * (p.x - m_CenPoint.x);
		double y = (p.y - m_CenPoint.y) * (p.y - m_CenPoint.y);
		m_Radius = (int)sqrt(x + y);
	}
}
int CCircle::Get_m_Radius()
{
	return m_Radius;
}
CPoint CCircle::Get_m_CenPoint()
{
	return m_CenPoint;
}
void CCircle::Draw(CDC* pDC)
{
	pDC->Ellipse(m_CenPoint.x - m_Radius, m_CenPoint.y - m_Radius, m_CenPoint.x + m_Radius, m_CenPoint.y + m_Radius);
}

void CCircle::Set_m_Radius(int r)
{
	m_Radius = r;
}

void CCircle::Bresenham_Circle(int xc, int yc, int r, CDC* pDC)
{
	int x, y, d, d1, d2, direction;
	x = 0, y = r, d = 2 * (1 - r);//初始化
	while (y >= 0)
	{
		pDC->SetPixel(x + xc, y + yc, RGB(0, 0, 0));
		pDC->SetPixel(x + xc, -y + yc, RGB(0, 0, 0));
		pDC->SetPixel(-x + xc, y + yc, RGB(0, 0, 0));
		pDC->SetPixel(-x + xc, -y + yc, RGB(0, 0, 0));
		if (d < 0)
		{
			d1 = 2 * (d + y) - 1;
			if (d1 < 0) direction = 1;
			else direction = 2;
		}
		else if (d > 0)
		{
			d2 = 2 * (d - x) - 1;
			if (d2 <= 0) direction = 2;
			else direction = 3;
		}
		else direction = 2;
		switch (direction)
		{
		case 1:d += 2 * x + 3; x++;  break;
		case 2: d += 2 * (x + 1) - 2 * (y - 1) + 2; x++, y--; break;
		case 3:d += -2 * (y - 1) + 1; y--; break;
		}
	}
}

void CCircle::NewDraw(CDC* pDC)
{
	Bresenham_Circle(m_CenPoint.x, m_CenPoint.y, m_Radius, pDC);
}

在鼠标的消息处理函数里的写法和直线、矩形类似,不再说明。

画贝塞尔曲线

贝塞尔曲线设计的绘制方法是“贝塞尔曲线:先画一条直线,然后依次拖动选择第一个、第二个控制点。”。

由于绘制是用户一个一个地设置四个控制点,调用的又是GDI绘制四个控制点的贝塞尔曲线的函数,所以在设置第三个控制点时(前两个控制点是贝塞尔曲线的起点和终点,设置时调用绘制直线的函数,贝塞尔曲线的性质之一是起点和终点也在贝塞尔曲线的起点和终点),第四个控制点还没有设置,这里设置为自动变成跟第三个控制点一样,等到第三个点设置好,可以再设置第四个控制点。need_control,count1就是用来标识这几个情况的。因为涉及到设置第四个点时,要把设置第三个点时的曲线擦除,所以用pts1数组存了设置第三个点时的四个控制点,同样用反色笔方法来擦除,count1用来控制只擦除一次。

Redraw函数是重绘时调用的,因为控制点已经确定,直接调用GDI函数即可。

详细定义如下:

#pragma once
class CPolyBezier
{
private:

	int need_control;
	int count1;
public:
	CPoint pts[4];
	CPoint pts1[4];//存三个点时的状态,用来擦除上一次的曲线的

    CPolyBezier();
	void Set_start_point(CPoint p);
	void Set_end_point(CPoint p);
	void Set_start_control(CPoint p);
	void Set_end_control(CPoint p);
	CPoint Get_start_point();
	CPoint Get_end_point();
	void Draw(CDC* pDC);
	void ReDraw(CDC* pDC);

	int Get_control_state();
	void Set_control_state(int s);
};

#include "pch.h"
#include "CPolyBezier.h"
CPolyBezier::CPolyBezier()
{
	need_control = 1;
	count1  = 0;
}

void CPolyBezier::Set_start_point(CPoint p)
{
	pts[0].x = p.x;
	pts[0].y = p.y;
}

void CPolyBezier::Set_end_point(CPoint p)
{
	pts[3].x = p.x;
	pts[3].y = p.y;
	//need_control = 2;
}

void CPolyBezier::Set_start_control(CPoint p)
{
	pts[1].x = p.x;
	pts[1].y = p.y;
	pts[2].x = pts[1].x;
	pts[2].y = pts[1].y;
	for (int i = 0; i < 4; i++)
		pts1[i] = pts[i];
	//need_control = 3;
}

void CPolyBezier::Set_end_control(CPoint p)
{
	pts[2].x = p.x;
	pts[2].y = p.y;
	//need_control = 0;
}

CPoint CPolyBezier::Get_start_point()
{
	return pts[0];
}

CPoint CPolyBezier::Get_end_point()
{
	return pts[3];
}

void CPolyBezier::Draw(CDC* pDC)
{
	if (3 == need_control)
	{
		if (count1 == 1)
		{
			pDC->SetROP2(R2_NOTXORPEN);
			pDC->PolyBezier(pts1, 4);
			count1++;
		}
		pDC->PolyBezier(pts, 4);
	}
	else if (2 == need_control)
	{
		if (count1 == 0)
		{
			pDC->SetROP2(R2_NOTXORPEN);
			pDC->MoveTo(pts[0]);
			pDC->LineTo(pts[3]);
			count1++;
		}
		pDC->PolyBezier(pts, 4);
	}
	else if (0 == need_control)
	{
		pDC->PolyBezier(pts, 4);
	}
	else
	{
		pDC->MoveTo(pts[0]);
		pDC->LineTo(pts[3]);
	}
}

void CPolyBezier::ReDraw(CDC* pDC)
{
	pDC->PolyBezier(pts, 4);
}

int CPolyBezier::Get_control_state()
{
	return need_control;
}

void CPolyBezier::Set_control_state(int s)
{
	need_control = s;
}

消息处理函数部分需要注意的是 不是每次按下左键都要重新新建曲线对象,所以用on_polybezier变量的变化来判断一条曲线是否绘制完毕。

相应的消息处理函数部分如下(注意只是部分,省去了前后文): 

void CSimpleDrawView::OnLButtonDown(UINT nFlags, CPoint point)
{
//............//

	case 5:
	{
		if (!on_polybezier)
		{
			m_polybezier = new CPolyBezier;
			on_polybezier = 1;
			m_polybezier->Set_start_point(point);
		}
	}break;

//..........//
}


void CSimpleDrawView::OnLButtonUp(UINT nFlags, CPoint point)
{

//............//

		case 5:
		{

			if (m_polybezier->Get_control_state() == 1)
			{
				m_polybezier->Set_control_state(2);
			}
			else if (m_polybezier->Get_control_state() == 2)
			{
				m_polybezier->Set_control_state(3);
			}
			else if (m_polybezier->Get_control_state() == 3)
			{
				m_polybezier->Set_control_state(0);
				repaint->now_type = 5;
				repaint->data = m_polybezier;
				m_line_list.InputFront(repaint);
				on_polybezier = 0;
			}

		}break;


//............//

}


void CSimpleDrawView::OnMouseMove(UINT nFlags, CPoint point)
{

		if (type == 5)//贝塞尔曲线
		{
			if (go)
			{
				if (m_polybezier->Get_control_state() == 1)
				{
					pDC->SetROP2(R2_NOTXORPEN);
					m_polybezier->Draw(pDC);
				}
				if (m_polybezier->Get_control_state() == 2 && on_polybezier)
				{
					pDC->SetROP2(R2_NOTXORPEN);
					m_polybezier->Draw(pDC);
				}
				if (m_polybezier->Get_control_state() == 3 && on_polybezier)
				{
					pDC->SetROP2(R2_NOTXORPEN);
					m_polybezier->Draw(pDC);
				}
			}
			else
				go = true;

			if (m_polybezier->Get_control_state() == 1)
			{
				m_polybezier->Set_end_point(point);
				m_polybezier->Draw(pDC);
			}
			if (m_polybezier->Get_control_state() == 2 && on_polybezier)
			{
				m_polybezier->Set_start_control(point);
				m_polybezier->Draw(pDC);
			}
			else if (m_polybezier->Get_control_state() == 3 && on_polybezier)
			{
				m_polybezier->Set_end_control(point);
				m_polybezier->Draw(pDC);
			}


		}

}

画b样条曲线

b样条曲线采用三次b样条曲线拼接而成,计算控制点对应的系数如下:

void CBSpline::calculate_base_func(double& f1, double& f2, double& f3, double& f4,double t)
{
	f1 = (1.0 / 6) * ((-1) * t * t * t + 3 * t * t - 3 * t + 1);
	f2 = (1.0 / 6) * (3 * t * t * t - 6 * t * t + 4);
	f3 = (1.0 / 6) * ((-3) * t * t * t + 3 * t * t + 3 * t + 1);
	f4 = (1.0 / 6) * (t * t * t);
}

 名字里有changed的函数,变量是用来在修改b样条时来做操作的。

注意:细分精度时,用浮点数要注意精度丢失问题,不然b样条拼接时会有缝隙,有些点就没计算出来。

b样条曲线与贝塞尔曲线不同,控制点的起点和终点不在曲线上,但这里实现了让控制点的起点和终点在曲线上,方法是额外添加了一个新的起点P-1和终点Pn+1,即xleft,yleft,xright,yright,多绘制出了两段线。然后让原本的起点P0和终点Pn变成P-1P1,Pn-1Pn+1的中点,就能让曲线通过原本的起点和中点(因为三次b样条曲线的性质就是P(0)——曲线的起点,在前三个点组成的三角形的中线上,且位于距三角形底边对着的顶点的1/3处,详见《计算机图形学实用教程》(第四版),苏小红等著,p94和p96有详细的推导和说明)。

同样地,因为b样条的控制点数目也不确定,结束一条曲线的绘制也采用跟多义线绘制(见上一篇文章)一样的方法,在按下右键的函数里面处理。

详细定义如下:

#pragma once
class CBSpline
{
public:
	int xcoordinate[100], ycoordinate[100];//控制点坐标
	CPoint cpts[100];
	int nPoints;//已选择控制点的数量
	int linewidth;//曲线线宽

	int bechangednum;
	int is_on_changed;

	void is_near_control();
public:
	CBSpline();
	CBSpline(CPoint p[],int n);
	void BSpline(CDC* pDC);
	void display(CDC* pDC);//绘图函数
	void setcontrol(CPoint p);
	void changecontrol(CPoint p);
	void finishchangecontrol();

	void drawtoend(CDC* pDC);//画出到最后一个控制点的曲线段

	//void after_change_display(CDC* pDC);

	void calculate_base_func(double& f1, double& f2, double& f3, double& f4,double t);

};

#include "pch.h"
#include "CBSpline.h"

CBSpline::CBSpline()
{
	nPoints = 0;
	linewidth = 2;
	is_on_changed = 0;
	bechangednum = 0;
}

CBSpline::CBSpline(CPoint p[], int n)
{
	nPoints = n;
	linewidth = 2;

	for (int i = 0; i < n; i++)
	{
		xcoordinate[i] = p[i].x;
		ycoordinate[i] = p[i].y;
		cpts[i] = p[i];
	}
}

void CBSpline::BSpline(CDC* pDC)//三次b样条画曲线
{
	CPen newpen, * pOldpen;;
	newpen.CreatePen(PS_SOLID, linewidth, RGB(0, 0, 0));
	pOldpen = pDC->SelectObject(&newpen);//注意保存成原来的画笔
	CPoint pre;
	int j, n = 50;//j表示已经绘制的曲线条数,n为曲线的细分程度
	double t, dt, f1, f2, f3, f4;
	dt = 1.0 / n;//dt表示每次画点后坐标的增量

	int xleft, yleft, xright, yright;//使用三顶点共线技巧使得曲线经过控制点起点终点
	xleft = 2 * xcoordinate[0] - xcoordinate[1], yleft = 2 * ycoordinate[0] - ycoordinate[1];


	t = 0;
	calculate_base_func(f1, f2, f3, f4, t);
	pre.x = f1 * xleft + f2 * xcoordinate[0] + f3 * xcoordinate[1] + f4 * xcoordinate[2],
		pre.y = f1 * yleft + f2 * ycoordinate[0] + f3 * ycoordinate[1] + f4 * ycoordinate[2];
	pDC->MoveTo(pre.x, pre.y);


	for (t = dt; t - 1 <= 1e-6; t += dt)
	{
		//根据B样条曲线的公式绘制每条曲线
		calculate_base_func(f1, f2, f3, f4, t);

		pDC->LineTo(f1 * xleft + f2 * xcoordinate[0] + f3 * xcoordinate[1] + f4 * xcoordinate[2],
			f1 * yleft + f2 * ycoordinate[0] + f3 * ycoordinate[1] + f4 * ycoordinate[2]);
		pre.x = f1 * xleft + f2 * xcoordinate[0] + f3 * xcoordinate[1] + f4 * xcoordinate[2],
			pre.y = f1 * yleft + f2 * ycoordinate[0] + f3 * ycoordinate[1] + f4 * ycoordinate[2];
		//指定需要连线的点
	}

	for (j = 0; j < (nPoints - 3); j++)//曲线条数为n-3条
	{
		t = 0;
		calculate_base_func(f1, f2, f3, f4, t);
		pre.x = f1 * xcoordinate[j] + f2 * xcoordinate[j + 1] + f3 * xcoordinate[j + 2] + f4 * xcoordinate[j + 3],
			pre.y = f1 * ycoordinate[j] + f2 * ycoordinate[j + 1] + f3 * ycoordinate[j + 2] + f4 * ycoordinate[j + 3];
		pDC->MoveTo(pre.x, pre.y);

		for (t = dt; t - 1 <= 1e-6; t += dt)//浮点数有精度损失,所以要用 t-1 <= 1e-6
		{
			//根据B样条曲线的公式绘制每条曲线
			calculate_base_func(f1, f2, f3, f4, t);

			pDC->LineTo(f1 * xcoordinate[j] + f2 * xcoordinate[j + 1] + f3 * xcoordinate[j + 2] + f4 * xcoordinate[j + 3],
				f1 * ycoordinate[j] + f2 * ycoordinate[j + 1] + f3 * ycoordinate[j + 2] + f4 * ycoordinate[j + 3]);
			pre.x = f1 * xcoordinate[j] + f2 * xcoordinate[j + 1] + f3 * xcoordinate[j + 2] + f4 * xcoordinate[j + 3],
				pre.y = f1 * ycoordinate[j] + f2 * ycoordinate[j + 1] + f3 * ycoordinate[j + 2] + f4 * ycoordinate[j + 3];
			//指定需要连线的点
		}
	}

	pDC->SelectObject(pOldpen);//注意还原成原来的画笔
	newpen.DeleteObject();

}

void CBSpline::display(CDC* pDC)
{

	for (int i = 0; i < nPoints - 1; i++)
	{
		pDC->MoveTo(xcoordinate[i], ycoordinate[i]);
		pDC->LineTo(xcoordinate[i + 1], ycoordinate[i + 1]);
	}
	if (nPoints >= 4)
	{
		//如果已经选择的控制点数大于4个,则调用BSpline绘制曲线
		BSpline(pDC);
	}

}

void CBSpline::setcontrol(CPoint p)
{
	xcoordinate[nPoints] = p.x;
	ycoordinate[nPoints] = p.y;
	cpts[nPoints] = p;
	nPoints++;
}

void CBSpline::drawtoend(CDC* pDC)
{
	//将曲线终点落到最后一个点上
	CPen newpen, * pOldpen;
	newpen.CreatePen(PS_SOLID, linewidth, RGB(0, 0, 0));
	pOldpen =pDC->SelectObject(&newpen);

	int xright = 2 * xcoordinate[nPoints - 1] - xcoordinate[nPoints - 2], yright = 2 * ycoordinate[nPoints - 1] - ycoordinate[nPoints - 2];
	double dt, t = 0; CPoint pre; int n = 50;
	double f1 ;double f2;double f3;double f4;
	calculate_base_func(f1, f2, f3, f4, t);
	pre.x = f4 * xright + f1 * xcoordinate[nPoints - 3] + f2 * xcoordinate[nPoints - 2] + f3 * xcoordinate[nPoints - 1],
		pre.y = f4 * yright + f1 * ycoordinate[nPoints - 3] + f2 * ycoordinate[nPoints - 2] + f3 * ycoordinate[nPoints - 1];
	pDC->MoveTo(pre.x, pre.y);

	dt = 1.0 / n;
	for (t = dt; t - 1 <= 1e-6; t += dt)
	{
		//根据B样条曲线的公式绘制每条曲线
		calculate_base_func(f1, f2, f3, f4, t);

		pre.x = f4 * xright + f1 * xcoordinate[nPoints - 3] + f2 * xcoordinate[nPoints - 2] + f3 * xcoordinate[nPoints - 1],
			pre.y = f4 * yright + f1 * ycoordinate[nPoints - 3] + f2 * ycoordinate[nPoints - 2] + f3 * ycoordinate[nPoints - 1];
		pDC->LineTo(pre.x, pre.y);

		//指定需要连线的点
	}

	pDC->SelectObject(pOldpen);
	newpen.DeleteObject();
}

void CBSpline::changecontrol(CPoint p)
{
	if (is_on_changed)
	{
		xcoordinate[bechangednum] = p.x;
		ycoordinate[bechangednum] = p.y;
		cpts[bechangednum] = p;
	}
}

void CBSpline::is_near_control()
{
	if (!is_on_changed)
	{
		is_on_changed = 1;
	}
}

void CBSpline::finishchangecontrol()
{
	is_on_changed = 0;
}

void CBSpline::calculate_base_func(double& f1, double& f2, double& f3, double& f4,double t)
{
	f1 = (1.0 / 6) * ((-1) * t * t * t + 3 * t * t - 3 * t + 1);
	f2 = (1.0 / 6) * (3 * t * t * t - 6 * t * t + 4);
	f3 = (1.0 / 6) * ((-3) * t * t * t + 3 * t * t + 3 * t + 1);
	f4 = (1.0 / 6) * (t * t * t);
}

消息处理函数如下(修改b样条部分还没给全,还涉及到鼠标移动到控制点附近,鼠标变形然后选中该曲线进行修改操作的部分)

void CSimpleDrawView::OnLButtonDown(UINT nFlags, CPoint point)对应的部分


	case 8://b样条绘制
	{
		if (!is_on_drawspline)
		{
			m_bspline = new CBSpline;
			is_on_drawspline = 1;
		}
		if(is_on_drawspline)
		m_bspline->setcontrol(point);
	}
	break;

	case 9://b样条修改
	{

	}
	break;

void CSimpleDrawView::OnLButtonUp(UINT nFlags, CPoint point)对应的部分

		case 8:
		{
			if(is_on_drawspline)
			m_bspline->display(pDC);
		}
		break;

		case 9:
		{
				if (m_CurEditbspline->is_on_changed && m_CurEditbspline != NULL)
				{
					m_CurEditbspline->finishchangecontrol();
				}
		}
		break;

 void CSimpleDrawView::OnMouseMove(UINT nFlags, CPoint point):

		if (type == 9)//b样条曲线修改
		{
			if (m_CurEditbspline->is_on_changed&&m_CurEditbspline!=NULL)
			{
				m_CurEditbspline->changecontrol(point);
				changedrawspline();

			}

		}

绘制曲线时只需要点击设置控制点,所以鼠标移动函数里没有操作。

CBSpline* m_bspline;//当前绘制的样条
CBSpline* m_CurEditbspline;//当前修改的样条,好在遍历保存图形的链表时比较是不是当前要修改的对象

int is_on_drawspline;表示当前是否在画b样条

修改b样条曲线,也就是修改控制点后重绘曲线的函数用了双缓冲来实现动态效果,比起反色笔,双缓冲可以降低闪烁。双缓冲就是先在另一个逻辑缓冲设备上画好了再放到你现在用的设备上,注释已经很详细,不再赘述。注意,主要要记得删除对象和解除笔的绑定。

void CSimpleDrawView::changedrawspline()
{
	//双缓冲
	CDC* pDC = GetDC();
	CRect rect;
	GetClientRect(rect);
	int x0 = rect.Width();//获得中点
	int y0 = rect.Height();

	// 创建一个内存设备上下文
	CDC memDC;
	memDC.CreateCompatibleDC(pDC);

	// 创建一个位图对象
	CBitmap NewBitbmp, * pOldBitmap;
	NewBitbmp.CreateCompatibleBitmap(pDC, x0, y0);
	pOldBitmap = memDC.SelectObject(&NewBitbmp);//兼容位图选入memdc

	//	memDC.FillSolidRect(0, 0, x0, y0, RGB(255, 255, 255));
	memDC.FillSolidRect(rect, pDC->GetBkColor());//按原来背景填充客户区否则是黑色

	//这里可以不设,因为设的PDC
	CPen NewPen, * pOldpen;
	NewPen.CreatePen(PS_SOLID, 4, RGB(0, 0, 0));  //实线,宽度为4像素
	pOldpen = pDC->SelectObject(&NewPen);   //选择创建的画笔

	ReDraw(&memDC);
	m_CurEditbspline->display(&memDC);
	m_CurEditbspline->drawtoend(&memDC);


	pDC->BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY);//内存位图复制到屏幕

	memDC.SelectObject(pOldBitmap);
	NewBitbmp.DeleteObject();
	memDC.SelectObject(pOldpen);
	NewPen.DeleteObject();
	memDC.DeleteDC();
}

void CSimpleDrawView::ReDraw(CDC* pDC)
{
	int i, j;
	j = m_line_list.Length();
	if (m_line_list.IsEmpty() == false)
	{

		for (i = 1; i <= j; i++)
		{
			pDC->SelectStockObject(NULL_BRUSH);//空心笔刷,封闭图形不填充

			Node* paint;
			paint = m_line_list.Locate(i);

			if (paint->now_type == 1)
			{
				((CLine*)paint->data)->Draw(pDC);
			}
			if (paint->now_type == 2)
			{
				((CQuare*)paint->data)->Draw(pDC);
			}
			if (paint->now_type == 3)
			{
				((CCircle*)paint->data)->Draw(pDC);
			}

			if (paint->now_type == 4)
			{
				((CPolyline*)paint->data)->ReDraw(pDC);
			}
			if (paint->now_type == 5)
			{
				((CPolyBezier*)paint->data)->ReDraw(pDC);
			}
			if (paint->now_type == 6)
			{
				((CVerticalLine*)paint->data)->Draw(pDC);
			}

			if (paint->now_type == 8)
			{
				if ((CBSpline*)paint->data != m_CurEditbspline)
				{
					((CBSpline*)paint->data)->display(pDC);
					((CBSpline*)paint->data)->drawtoend(pDC);
				}
			}

		}

	}

}

ReDraw函数除了用在修改b样条函数时的重绘,也可以用在Ondraw的重绘里 

void CSimpleDrawView::OnDraw(CDC* /*pDC*/)
{
	CSimpleDrawDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	CDC* pDC = GetDC();
	ReDraw(pDC);

	ReleaseDC(pDC);/**/



	// TODO: 在此处为本机数据添加绘制代码
}

修改b样条的部分说明起来实在比较麻烦,有空再说吧。

你可能感兴趣的:(计算机图形学实验/作业,mfc,c++)