计算机图形学——利用MFC库绘制圆和椭圆(中点画圆算法)

写在前面:关于对话框设置和组件ID设置等相关知识见专栏第一篇文章《直线绘制》

一、问题描述

利用中点画圆算法,在MFC库中绘制圆和椭圆

二、算法描述

1. 圆

易知 f c i r c l e ( x , y ) = x 2 + y 2 − r 2 f_{circle}(x,y)=x^2+y^2-r^2 fcircle(x,y)=x2+y2r2

利用轴对称,只需要绘制第一象限内的1/4圆弧,然后对称绘制到其他三个象限即可。

第一象限上半部分的1/8圆弧(以x为增量)

我们可以通过检查 p k = f c i r c l e ( x k + 1 , y k − 1 2 ) p_k=f_{circle}(x_k+1,y_k-\frac{1}{2}) pk=fcircle(xk+1,yk21) 的符号来判断下一个点绘制的位置

设置该段圆弧绘制的起始点为 ( 0 , r ) (0,r) (0,r) ,并据此计算出 p 0 = f c i r c l e ( 0 + 1 , r − 1 2 ) = 5 4 − r p_0=f_{circle}(0+1,r-\frac{1}{2})=\frac{5}{4}-r p0=fcircle(0+1,r21)=45r

计算可知

p k + 1 − p k = { 2 x k + 1 + 1 p k < 0 2 x k + 1 + 1 − 2 y k + 1 p k ≥ 0 p_{k+1}-p_k= \begin{cases} 2x_{k+1}+1 & p_k<0 \\ 2x_{k+1}+1-2y_{k+1} & p_k\geq 0 \end{cases} pk+1pk={2xk+1+12xk+1+12yk+1pk<0pk0

其中

x k + 1 = x k + 1 ,   y k + 1 = { y k p k < 0 y k − 1 p k ≥ 0 x_{k+1}=x_k+1, \ y_{k+1}= \begin{cases} y_k & p_k<0 \\ y_k-1 & p_k\geq 0 \end{cases} xk+1=xk+1, yk+1={ykyk1pk<0pk0

假设当前 ( x k , y k ) (x_k,y_k) (xk,yk) 确定,那么通过 p k p_k pk的正负来确定下一个点 ( x k + 1 , y k + 1 ) (x_{k+1},y_{k+1}) (xk+1,yk+1)

设置循环,从起点开始绘制,终止条件为 x > y x>y x>y (也就是说第一象限上半部分圆弧的绘制必须保证 x ≤ y x\leq y xy

第一象限下半部分的1/8圆弧(以y为增量)

通过检查 p k = f c i r c l e ( x k − 1 2 , y k + 1 ) p_k=f_{circle}(x_k-\frac{1}{2},y_k+1) pk=fcircle(xk21,yk+1) 的符号判断下一个点绘制的位置

设置该段圆弧绘制的起始点为 ( r , 0 ) (r,0) (r,0) ,并据此计算出 p 0 = f c i r c l e ( r − 1 2 , 0 + 1 ) = 5 4 − r p_0=f_{circle}(r-\frac{1}{2},0+1)=\frac{5}{4}-r p0=fcircle(r21,0+1)=45r

计算可知

p k + 1 − p k = { 2 y k + 1 + 1 p k < 0 2 y k + 1 + 1 − 2 x k + 1 p k ≥ 0 p_{k+1}-p_k= \begin{cases} 2y_{k+1}+1 & p_k<0 \\ 2y_{k+1}+1-2x_{k+1} & p_k\geq 0 \end{cases} pk+1pk={2yk+1+12yk+1+12xk+1pk<0pk0

其中

x k + 1 = { x k p k < 0 x k − 1 p k ≥ 0 ,   y k + 1 = y k + 1 x_{k+1}= \begin{cases} x_k & p_k<0 \\ x_k-1 & p_k\geq 0 \end{cases},\ y_{k+1}=y_k+1 xk+1={xkxk1pk<0pk0, yk+1=yk+1

假设当前 ( x k , y k ) (x_k,y_k) (xk,yk) 确定,那么通过 p k p_k pk的正负来确定下一个点 ( x k + 1 , y k + 1 ) (x_{k+1},y_{k+1}) (xk+1,yk+1)

设置循环,从起点开始绘制,终止条件为 x < y x<y x<y (也就是说第一象限下半部分圆弧的绘制必须保证 y ≤ x y\leq x yx

2. 椭圆

易知 f e l l i p s o i d = b 2 x 2 + a 2 y 2 − a 2 b 2 f_{ellipsoid}=b^2x^2+a^2y^2-a^2b^2 fellipsoid=b2x2+a2y2a2b2 ,其中a表示x轴上的半轴长,b表示y轴上的半轴长

利用轴对称,只需绘制第一象限内的1/4椭圆弧,即可对称绘制到另外三个象限

第一象限上半部分椭圆弧(以x为增量)

通过检查 p k = f e l l i p s o i d ( x k + 1 , y k − 1 2 ) p_k=f_{ellipsoid}(x_k+1,y_k-\frac{1}{2}) pk=fellipsoid(xk+1,yk21) 的符号判断下一个点绘制的位置

设置该段圆弧绘制的起始点为 ( 0 , b ) (0,b) (0,b) ,并据此计算出

p 0 = f e l l i p s o i d ( 0 + 1 , b − 1 2 ) = b 2 + 1 4 a 2 − a 2 b p_0=f_{ellipsoid}(0+1,b-\frac{1}{2})=b^2+\frac{1}{4}a^2-a^2b p0=fellipsoid(0+1,b21)=b2+41a2a2b

计算可知

p k + 1 − p k = { b 2   ( 2 x k + 1 + 1 ) p k < 0 b 2   ( 2 x k + 1 + 1 ) − 2 a 2 y k + 1 p k ≥ 0 p_{k+1}-p_k= \begin{cases} b^2 \ (2x_{k+1}+1) & p_k<0 \\ b^2 \ (2x_{k+1}+1)-2a^2y_{k+1} & p_k\geq 0 \end{cases} pk+1pk={b2 (2xk+1+1)b2 (2xk+1+1)2a2yk+1pk<0pk0

其中

x k + 1 = x k + 1 ,   y k + 1 = { y k p k < 0 y k − 1 p k ≥ 0 x_{k+1}=x_k+1, \ y_{k+1}= \begin{cases} y_k & p_k<0 \\ y_k-1 & p_k\geq 0 \end{cases} xk+1=xk+1, yk+1={ykyk1pk<0pk0

假设当前 ( x k , y k ) (x_k,y_k) (xk,yk) 确定,那么通过 p k p_k pk的正负来确定下一个点 ( x k + 1 , y k + 1 ) (x_{k+1},y_{k+1}) (xk+1,yk+1)

设置循环,从起点 ( 0 , b ) (0,b) (0,b) 开始绘制

由于椭圆的切线斜率为 − b 2 x a 2 y -\frac{b^2x}{a^2y} a2yb2x当切线斜率为 − 1 -1 1 时,标志着以x为增量的绘制结束

因此终止条件为 b 2 x > a 2 y b^2x>a^2y b2x>a2y(也就是说第一象限上半部分椭圆弧的绘制必须保证 b 2 x ≤ a 2 y b^2x\leq a^2y b2xa2y

第一象限下半部分椭圆弧(以y为增量)

通过检查 p k = f e l l i p s o i d ( x k − 1 2 , y k + 1 ) p_k=f_{ellipsoid}(x_k-\frac{1}{2},y_k+1) pk=fellipsoid(xk21,yk+1) 的符号判断下一个点绘制的位置

设置该段圆弧绘制的起始点为 ( a , 0 ) (a,0) (a,0) ,并据此计算出

p 0 = f e l l i p s o i d ( a − 1 2 , 0 + 1 ) = a 2 + 1 4 b 2 − b 2 a p_0=f_{ellipsoid}(a-\frac{1}{2},0+1)=a^2+\frac{1}{4}b^2-b^2a p0=fellipsoid(a21,0+1)=a2+41b2b2a

计算可知

p k + 1 − p k = { a 2   ( 2 y k + 1 + 1 ) p k < 0 a 2   ( 2 y k + 1 + 1 ) − 2 b 2 x k + 1 p k ≥ 0 p_{k+1}-p_k= \begin{cases} a^2 \ (2y_{k+1}+1) & p_k<0 \\ a^2 \ (2y_{k+1}+1)-2b^2x_{k+1} & p_k\geq 0 \end{cases} pk+1pk={a2 (2yk+1+1)a2 (2yk+1+1)2b2xk+1pk<0pk0

其中

x k + 1 = { x k p k < 0 x k − 1 p k ≥ 0 ,   y k + 1 = y k + 1 x_{k+1}= \begin{cases} x_k & p_k<0 \\ x_k-1 & p_k\geq 0 \end{cases},\ y_{k+1}=y_k+1 xk+1={xkxk1pk<0pk0, yk+1=yk+1

假设当前 ( x k , y k ) (x_k,y_k) (xk,yk) 确定,那么通过 p k p_k pk的正负来确定下一个点 ( x k + 1 , y k + 1 ) (x_{k+1},y_{k+1}) (xk+1,yk+1)

设置循环,从起点 ( a , 0 ) (a,0) (a,0) 开始绘制

类似的,易知终止条件为 a 2 y > b 2 x a^2y>b^2x a2y>b2x(也就是说第一象限上半部分椭圆弧的绘制必须保证 a 2 y ≤ b 2 x a^2y\leq b^2x a2yb2x

三、核心代码

关于CircleDlg.h和对于对话框内组件的ID设置,此处略去,仅展示CircleDlg.cpp中的相关代码

1. 圆

// 点击按钮开始绘制圆
void CCircleDlg::ButtonCircle() {

	// 从编辑框中获取输入的半径并转换为整型
	CString Radius;
	GetDlgItemText(IDC_EDIT_CIRCLE_R, Radius);
	int R = _ttoi(Radius);

	// 获取绘图矩形
	CRect AreaCircle;
	GetDlgItem(IDC_STATIC_AREA_CIRCLE)->GetClientRect(AreaCircle);

	// 检查是否会越界并绘制
	if (R > 0 && R <= AreaCircle.Width()/2 && R <= AreaCircle.Height()/2) {
		DrawCircle(R);
	}
	else {
		CString message = _T("半径超出绘图框的边界,请重新输入");
		CString caption = _T("圆绘制错误");
		MessageBox(message, caption, MB_ICONERROR);
	}

}

// 画圆
void CCircleDlg::DrawCircle(int r) {

	// 获取画板
	CStatic* Area = (CStatic*)GetDlgItem(IDC_STATIC_AREA_CIRCLE);
	CDC* pdc = Area->GetDC();

	// 获取绘图矩形
	CRect AreaCircle;
	GetDlgItem(IDC_STATIC_AREA_CIRCLE)->GetClientRect(AreaCircle);
	double dx = AreaCircle.Width() / 2.0;
	double dy = AreaCircle.Height() / 2.0;

	// 初始化P0和起始点
	double p = 5.0 / 4 - r;
	int x = 0, y = r;

	// 先绘制1/8个圆弧(第一象限上半部分),并根据轴对称绘制另外3个1/8圆弧
	for (int x = 0; x <= y; ) {

		// 绘制当前像素点
		pdc->SetPixel(x + dx, y + dy, RGB(0, 0, 0));

		// 轴对称绘制其他像素点
		pdc->SetPixel(-x + dx, y + dy, RGB(0, 0, 0));
		pdc->SetPixel(-x + dx, -y + dy, RGB(0, 0, 0));
		pdc->SetPixel(x + dx, -y + dy, RGB(0, 0, 0));

		// 根据Pk的值决定下一个像素点
		if (p < 0) {
			x += 1;
			p = p + 2.0 * x + 1;
		}
		else {
			x += 1;
			y -= 1;
			p = p + 2.0 * (x - y) + 1;
		}
	}
	
	// 绘制第一象限下半部分的1/8圆弧,并根据轴对称绘制剩下3个1/8圆弧
	// 起始点选取(r,0),重新初始化起始点和P0
	x = r, y = 0;
	p = 5.0 / 4 - r;

	for (y = 0; y <= x; ) {

		// 绘制当前像素点
		pdc->SetPixel(x + dx, y + dy, RGB(0, 0, 0));

		// 轴对称绘制其他像素点
		pdc->SetPixel(-x + dx, y + dy, RGB(0, 0, 0));
		pdc->SetPixel(-x + dx, -y + dy, RGB(0, 0, 0));
		pdc->SetPixel(x + dx, -y + dy, RGB(0, 0, 0));

		// 根据Pk的值决定下一个像素点
		if (p <= 0) {
			y += 1;
			p = p + 2 * y + 1;
		}
		else {
			x -= 1;
			y += 1;
			p = p + 2 * (y - x) - 1;
		}
	}

	// 释放设备
	Area->ReleaseDC(pdc);

}

2. 椭圆

// 椭圆的生成按钮
void CCircleDlg::ButtonEllipsoid() {

	// 从编辑框中获取输入的长短轴并转换为整型
	CString xAxis, yAxis;
	GetDlgItemText(IDC_EDIT_ELLPISOID_A, xAxis);
	GetDlgItemText(IDC_EDIT_ELLPISOID_B, yAxis);
	int A = _ttoi(xAxis); // x轴方向,记为长轴
	int B = _ttoi(yAxis); // y轴方向,记为短轴

	// 获取绘图区
	CRect AreaEllipsoid;
	GetDlgItem(IDC_STATIC_AREA_ELLIPSOID)->GetClientRect(AreaEllipsoid);

	// 检查是否会越界并绘制
	if (A > 0 && B > 0 && A <= AreaEllipsoid.Width()/2 && B <= AreaEllipsoid.Height()/2) {
		DrawEllipsoid(A, B);
	}
	else {
		CString message = _T("轴长超出绘图框的边界,请重新输入");
		CString caption = _T("椭圆绘制错误");
		MessageBox(message, caption, MB_ICONERROR);
	}

}

// 画椭圆
void CCircleDlg::DrawEllipsoid(int a, int b) {
	
	// 获取画板
	CStatic* Area = (CStatic*)GetDlgItem(IDC_STATIC_AREA_ELLIPSOID);
	CDC* pdc = Area->GetDC();

	// 获取绘图矩形
	CRect AreaEllipsoid;
	GetDlgItem(IDC_STATIC_AREA_ELLIPSOID)->GetClientRect(AreaEllipsoid);
	double dx = AreaEllipsoid.Width() / 2.0;
	double dy = AreaEllipsoid.Height() / 2.0;

	// 初始化P0和起始点(0,b)
	double p = b * b - a * a * b + (1.0 / 4) * a * a;
	int x = 0, y = b;

	// 先绘制第一象限上半部分的椭圆弧,并根据轴对称绘制另外3个椭圆弧
	for (int x = 0; b * b * x <= a * a * y; ) {
		// 绘制当前像素点
		pdc->SetPixel(x + dx, y + dy, RGB(0, 0, 0));

		// 轴对称绘制其他像素点
		pdc->SetPixel(-x + dx, y + dy, RGB(0, 0, 0));
		pdc->SetPixel(-x + dx, -y + dy, RGB(0, 0, 0));
		pdc->SetPixel(x + dx, -y + dy, RGB(0, 0, 0));

		// 根据Pk的值决定下一个像素点
		if (p < 0) {
			x += 1;
			p = p + b * b * (2 * x + 1);
		}
		else {
			x += 1;
			y -= 1;
			p = p + b * b * (2 * x + 1) - 2 * a * a * y;
		}
	}

	// 绘制第一象限下半部分的椭圆弧,并根据轴对称绘制剩下3个
	// 起始点选取(a,0),重新初始化起始点和P0
	x = a, y = 0;
	p = a * a - b * b * a + 0.25 * b * b;

	for (y = 0; a * a * y <= b * b * x; ) {

		// 绘制当前像素点
		pdc->SetPixel(x + dx, y + dy, RGB(0, 0, 0));

		// 轴对称绘制其他像素点
		pdc->SetPixel(-x + dx, y + dy, RGB(0, 0, 0));
		pdc->SetPixel(-x + dx, -y + dy, RGB(0, 0, 0));
		pdc->SetPixel(x + dx, -y + dy, RGB(0, 0, 0));

		// 根据Pk的值决定下一个像素点
		if (p < 0) {
			y += 1;
			p = p + a * a * (2 * y + 1);
		}
		else {
			x -= 1;
			y += 1;
			p = p + a * a * (2 * y + 1) - 2 * b * b * x;
		}
	}

	// 释放设备
	Area->ReleaseDC(pdc);

}

四、实验结果

如下图,分别在输入框中输入不同的值并点击"GENERATE",即可绘制:

运行结果

若输入的半径超过绘图框边界,则弹出错误提示:

圆:错误提示

若输入的椭圆轴长超过绘图框边界,类似的弹出错误提示:

椭圆:错误提示