计算机图形学实验——利用MFC对话框实现多边形绘制与填充(扫描线填充算法)附源码

内容概括:

  • 利用基于对话框的MFC项目
  • 实现鼠标点击绘制多边形
  • 实现扫描线算法填充多边形

源码见Yushan-Ji/ComputerGraphics: ECNU2023秋 计算机图形学课程实验代码 (github.com)

实验内容

  1. 通过鼠标交互输入多边形
  2. 对各种多边形进行填充,包括边界自交的情况

算法描述

多边形绘制

  1. 利用OnLButtonDownOnRButtonDown函数,实现:
    • 左键点击:开始绘制多边形,并连接上一个顶点和当前点击的顶点
    • 右键点击:结束绘制多边形,并连接上一个顶点和第一个顶点
      其中,绘制顶点间的连线利用了MoveToLineTo函数
  2. 另外,为了避免多边形绘制完毕后,程序仍然对鼠标点击事件进行响应,因此需要添加布尔变量IsCompleted,用来监测当前多边形是否绘制完毕
    因此在OnLButtonDownOnRButtonDown函数的最开始,需要添加一个条件判断语句,若IsCompletedTRUE,则表明多边形已绘制完毕,函数直接返回
  3. 将所有顶点信息保存在vector<CPoint> points中,便于后续计算边信息

扫描线填充

y++\n清空scanPoints
获取边表
AET更新
获取当前扫描线\n的所有交点
交点排序
填充当前扫描线
  1. 首先需要根据points中存储的多边形顶点信息,计算出多边形的边信息,得到ET
    • 新建Edge类,存储每条边的信息
          // 边信息
          class Edge 
          {
          public:
          	float x_lower; // 扫描线与边的交点x值(初始值为线段下端点x值)
          	float dx; // 斜率倒数
          	float y_upper; // 线段上端点y值
          	// 构造函数
          	Edge() : x_lower(0.0), dx(0.0), y_upper(0.0) {}
          	Edge(float x_val, float dx_val, float y_val);
          };
      
    • 新建ET表:map<int, vector<Edge>> ET
      • 建立从线段下端点y值y_lower到多边形所有边vector<Edge>的映射,这样有相同下端点的不同边就能够存储在相同键值的map中,便于后续向AET中添加活性边
    • 获取ET表:通过getET()函数实现
      • 需要判断当前边是否为首尾顶点连成的边
      • 计算当前边斜率的倒数dx
        • 若当前边平行于x轴,continue不执行任何操作
        • 若当前边平行于y轴,则dx=0
        • 不是上述两种情况则正常计算
      • 计算x_lowery_upper
  2. 初始化、更新AET
    • 扫描线y初始化为ymin,并将ET表中ymin对应的所有边都添加到AET
    • 对于每一条扫描线,都需要检查AET中是否有非活性边的存在,还要及时添加新的活性边
      • y >= y_upper 时,说明该边需要移除
      • y >= y_lower时,说明该边需要添加到AET
  3. 获取每条扫描线与活性边的交点,存入scanPoints
    • 对于不同边相交的顶点,不存入
    • 对于其他交点,易知交点数量必为偶数,因此成对存入交点
  4. 根据scanPoints中交点的x值升序排列所有交点,并进行填充

核心代码

CPolygonFillDlg类中新添加的public成员:

class CPolygonFillDlg : public CDialogEx
{
  public:
	std::vector<CPoint> points; // 存储多边形的顶点
	std::vector<CPoint> scanPoints; // 存储扫描线与活动边的交点
    
	BOOL IsCompleted; // 标志多边形是否绘制完毕
    
	std::map<int, std::vector<Edge>> ET; // ET表:从线段下端点y值到边信息链表的映射
	std::vector<Edge> AET; // 活动边表
	float YMAX; // 存储多边形最高点
    
	void getET();
	void PolygonFill();
	static bool compareCPoint(const CPoint& a, const CPoint& b);
}

消息函数

PolygonFillDlg.cpp中需要添加按钮点击、鼠标点击的消息关联函数

// 消息关联
BEGIN_MESSAGE_MAP(CPolygonFillDlg, CDialogEx)
	...
	ON_WM_LBUTTONDOWN() // 关联鼠标左键点击事件
	ON_WM_RBUTTONDOWN() // 关联鼠标右键点击事件
	ON_BN_CLICKED(IDC_BUTTON_FILL, &PolygonFill) // 关联按钮点击事件
END_MESSAGE_MAP()

PolygonFillDlg.h中需要添加鼠标点击函数的声明

class CPolygonFillDlg : public CDialogEx
{
    ...
  protected:
    ...
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
    ...
}

多边形绘制

// 鼠标左键点击事件(绘制多边形)
void CPolygonFillDlg::OnLButtonDown(UINT nFlags, CPoint point) 
{
	// 若多边形绘制完毕,忽略该函数,不执行操作
	if (IsCompleted == TRUE) 
	{
		return;
	}
	else 
	{
		// 如果是第一个点,存入points数组中
		if (points.empty()) 
		{
			points.push_back(point);
		}
		// 若不是第一个点,则连接当前点和上一个点
		else 
		{
			CStatic* CANVAS = (CStatic*)GetDlgItem(IDC_STATIC_CANVAS);
			CDC* pDC = CANVAS->GetDC();

			// 获取points数组的最后一个point(就是上一个点),并将当前绘图位置移动到该点
			pDC->MoveTo(points[points.size() - 1]);
			// 连接上一个点和当前点
			pDC->LineTo(point);
			points.push_back(point); // 将当前点加入数组
			ReleaseDC(pDC);
		}
	}
}

// 鼠标右键点击事件(多边形绘制完毕)
void CPolygonFillDlg::OnRButtonDown(UINT nFlags, CPoint point) 
{
	if(IsCompleted == TRUE) 
	{
		return;
	}

	else 
	{
		// 如果前面少于3个点,则无法连接成为多边形,弹出错误信息
		if (points.size() < 3) {
			CString message = _T("顶点数量少于3,无法绘制多边形");
			CString caption = _T("多边形绘制错误");
			MessageBox(message, caption, MB_ICONERROR);
		}

		// 若points已存储有2个以上的顶点,则连接上一个点和第一个点
		else 
		{
			CStatic* CANVAS = (CStatic*)GetDlgItem(IDC_STATIC_CANVAS);
			CDC* pDC = CANVAS->GetDC();
			// 连接第一个点和当前点
			pDC->MoveTo(points[points.size() - 1]);
			pDC->LineTo(points[0]);
			ReleaseDC(pDC);

			// 成功消息
			CString message = _T("多边形绘制完毕");
			CString caption = _T("多边形绘制完毕");
			MessageBox(message, caption, MB_OK);

			IsCompleted = TRUE;
		}
	}
}

扫描线填充

由于扫描线填充涉及到的代码较长,这里不全部展示,仅列出关键的几个步骤

获取边表

void CPolygonFillDlg::getET() 
{
	for (int i = 1; i <= points.size() ; i++) 
	{
		float dx;
		float x_lower, y_lower, y_upper;

		// points中最后一个点和第一个点的连线
		if (i == points.size())
		{
			if (points[i - 1].x == points[0].x)
			{
				continue; // 边平行于x轴,不处理
			}
			else if (points[i - 1].y == points[0].y)
			{
				dx = 0.0;
			}
			else
			{
				dx = (points[i - 1].x - points[0].x) * 1.0 / (points[i - 1].y - points[0].y);
			}

			if (points[0].y <= points[i - 1].y)
			{
				y_lower = points[0].y;
				y_upper = points[i - 1].y;
				x_lower = points[0].x;
			}
			else
			{
				y_lower = points[i - 1].y;
				y_upper = points[0].y;
				x_lower = points[i - 1].x;
			}
			// 插入到Edge边信息中
			Edge e(x_lower, dx, y_upper);
			// 插入到ET表中
			ET[y_lower].push_back(e);
		}

		else 
		{
			// 斜率
			if (points[i - 1].x == points[i].x)
			{
				continue; // 边平行于x轴,不处理
			}
			else if (points[i - 1].y == points[i].y)
			{
				dx = 0;
			}
			else
			{
				dx = (points[i - 1].x - points[i].x) * 1.0 / (points[i - 1].y - points[i].y);
			}

			// 线段上端点的x,y值
			if (points[i - 1].y <= points[i].y)
			{
				y_lower = points[i - 1].y;
				y_upper = points[i].y;
				x_lower = points[i - 1].x;
			}
			else
			{
				y_lower = points[i].y;
				y_upper = points[i - 1].y;
				x_lower = points[i].x;
			}

			// 计算多边形的最高点y值
			if (y_upper >= YMAX)
			{
				YMAX = y_upper;
			}

			// 插入到Edge边信息中
			Edge e(x_lower, dx, y_upper);
			// 插入到ET表中
			ET[y_lower].push_back(e);
		}
	}
}

AET表更新

// 移除已经超出扫描线的边
std::vector<Edge> newAET;
for (auto it = AET.begin(); it != AET.end(); ++it) {
	if (it->y_upper > y) {
		newAET.push_back(*it);
	}
}
AET = newAET;

//检查是否有新的边加入
if (it_ET != ET.end() && y >= it_ET->first) {
	AET.insert(AET.begin(), it_ET->second.begin(), it_ET->second.end());
	it_ET++;
}

获取当前扫描线的所有交点

auto it = AET.begin();
while (it != AET.end())
{
	float x1 = it->x_lower;
	it->x_lower += it->dx;
	it++;

	if (it != AET.end())
	{
		float x2 = it->x_lower;
		it->x_lower += it->dx;
		it++;

		// 扫描线遇到两条线相交的顶点时,不记录交点
		if (x1 != x2)
		{
			scanPoints.push_back(CPoint(x1, y));
			scanPoints.push_back(CPoint(x2, y));
		}
	}
}

交点排序并填充

// 比较函数,按照CPoint对象的x值升序排列
bool CPolygonFillDlg::compareCPoint(const CPoint& a, const CPoint& b) {
	return a.x < b.x;
}
// 对扫描到的所有交点进行升序排列
std::sort(scanPoints.begin(), scanPoints.end(), compareCPoint);

// 获取画板
CStatic* CANVAS = (CStatic*)GetDlgItem(IDC_STATIC_CANVAS);
CDC* pDC = CANVAS->GetDC();
// 成对绘制,i每次+2
for (int i = 0; i < scanPoints.size(); i += 2)
{
	pDC->MoveTo(scanPoints[i]);
	pDC->LineTo(scanPoints[i + 1]);
}
ReleaseDC(pDC);

实验结果

image-20231025213116930

如上图,通过左键点击绘制多边形,右键点击结束绘制

绘制成功后会弹出绘制完毕的提示框

如果多边形顶点少于3个,则弹出错误提示框如下:

image-20231025213258682

随后点击Fill Polygon按钮,运行扫描线填充,结果如下:

  • 凸多边形
    image-20231025213412123
  • 凹多边形
    image-20231025213526340
  • 边相交
    image-20231025213430842
  • 8
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
#include #include #include #include #include //////////////////////////////////////////////////////////////functions///////////////////////////////////////////////// void swap(float &m,float &n) { float temp=n; n=m; m=temp; } int sign(float a,float b)//sign() { int t; if(a>b) { t=1;} else if(adx) { swap(dx,dy); Flag=1; } else Flag=0; float Nerror=2*dy-dx; for(int i=1;i=0) { if(Flag) { x=x+sx;} else y=y+sy; Nerror=Nerror-2*dx; } if(Flag) { y=y+sy;} else x=x+sx; Nerror=Nerror+2*dy; } } ///////////////////////////////////四连通种子填充/////////////////////////////////////////////////// void BoundaryFill4(HDC hdc,int x,int y,int FilledColor,int BoundaryColor) { int CurrentColor; CurrentColor=GetPixel(hdc,x,y); if(CurrentColor!=BoundaryColor&&CurrentColor!=FilledColor) { SetPixel(hdc,x,y,FilledColor); BoundaryFill4(hdc,x+1,y,FilledColor,BoundaryColor); BoundaryFill4(hdc,x-1,y,FilledColor,BoundaryColor); BoundaryFill4(hdc,x,y+1,FilledColor,BoundaryColor); BoundaryFill4(hdc,x,y-1,FilledColor,BoundaryColor); } } ////////////////////////////////////////扫描线填充/////////////////////////////////////////////////// //DrawLine()函数:在(x1,y)和(x2,y)两点之间画一条颜色为FilledColor的横线(用来扫描填充) void drawline(HDC hdc, int x1, int x2,int y0, int FilledColor) { for(int n=x1+1;n<x2;n++) { SetPixel(hdc,n,y0,FilledColor); } } //Scan()函数:扫描线函数,将扫描线图形的交点坐标存在数组中 //数组中同行的点即为该行扫描线图形的交点(一般为2个) //数组中的行代表扫描线的纵坐标 void scan(HDC hdc, int boundarycolor) { int currentcolor; int a[300][2]={0}; for (int j=0;j<300;j++) { for(int i=300;i<700;i++) { currentcolor=GetPixel(hdc,i,j); if((currentcolor==boundarycolor)&&(GetPixel(hdc,i+1,j)!=boundarycolor)&&(i500)) {a[j][1]=i;} } } //利用循环调用DrawLine函数逐行填充两交点之间的点 for(int k=0;k<300;k++) { if((a[k][0]!=0)&&(a[k][1]!=0)) drawline(hdc,a[k][0],a[k][1],k,RGB(255,0,0));} } ///////////////////////////////////////////////边界填充////////////////////////////////////// //Contrary()取反函数:如果点的颜色为白,则将点置为填充色;如果点的颜色为填充色,则将点置为白色 //忽略了边界色,即不对边界点作颜色处理 void contrary(HDC hdc, int x, int y,int FilledColor) { for(int h=x;h<280;h++) { if(GetPixel(hdc,h,y)==RGB(255,255,255)) { SetPixel(hdc,h,y,FilledColor); } else if(GetPixel(hdc,h,y)==FilledColor) { SetPixel(hdc,h,y,RGB(255,255,255)); } } } //borderline()边线函数: 先找出图形的边界 左边和右边,从右到左的顺序调用contrary()函数进行填充 void borderline(HDC hdc, int boundarycolor) { for(int j=280;j<499;j++) { for(int i=80;i100)) { contrary(hdc,i,j,RGB(0,0,255)); } } } for(int m=280;m<499;m++) { for(int n=80;n<280;n++) { int currentcolor=GetPixel(hdc,n,m); if((currentcolor==boundarycolor)&&(GetPixel(hdc,n+1,m)!=boundarycolor)&&(GetPixel(hdc,n-1,m)!=boundarycolor)&&(n<101)) { contrary(hdc,n,m,RGB(0,0,255)); } } } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值