写在前面:
关于vs2022中MFC App的创建与配置,以及对话框设计、组件ID号设置见我的上一篇博客利用MFC库实现直线绘制(DDA算法 & Bresenham算法)

一、问题描述

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

二、算法描述

1. 圆

易知 \( f_{circle}(x,y)=x^2+y^2-r^2 \)

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

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

我们可以通过检查 \( p_k=f_{circle}(x_k+1,y_k-\frac{1}{2}) \) 的符号来判断下一个点绘制的位置

设置该段圆弧绘制的起始点为 \( (0,r) \) ,并据此计算出 \( p_0=f_{circle}(0+1,r-\frac{1}{2})=\frac{5}{4}-r \)

计算可知

$$ 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} $$

其中

$$ 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} $$

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

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

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

通过检查 \( p_k=f_{circle}(x_k-\frac{1}{2},y_k+1) \) 的符号判断下一个点绘制的位置

设置该段圆弧绘制的起始点为 \( (r,0) \) ,并据此计算出 \( p_0=f_{circle}(r-\frac{1}{2},0+1)=\frac{5}{4}-r \)

计算可知

$$ 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} $$

其中

$$ 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 $$

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

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

2. 椭圆

易知 \( f_{ellipsoid}=b^2x^2+a^2y^2-a^2b^2 \) ,其中a表示x轴上的半轴长,b表示y轴上的半轴长

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

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

通过检查 \( p_k=f_{ellipsoid}(x_k+1,y_k-\frac{1}{2}) \) 的符号判断下一个点绘制的位置

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

$$ p_0=f_{ellipsoid}(0+1,b-\frac{1}{2})=b^2+\frac{1}{4}a^2-a^2b $$

计算可知

$$ 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} $$

其中

$$ 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} $$

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

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

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

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

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

通过检查 \( p_k=f_{ellipsoid}(x_k-\frac{1}{2},y_k+1) \) 的符号判断下一个点绘制的位置

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

$$ p_0=f_{ellipsoid}(a-\frac{1}{2},0+1)=a^2+\frac{1}{4}b^2-b^2a $$

计算可知

$$ 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} $$

其中

$$ 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 $$

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

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

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

三、核心代码

关于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",即可绘制:

image.png

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

image.png

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

image.png


ysji
2 声望4 粉丝

在遍地六便士的街上,他抬头看见了月亮


引用和评论

0 条评论