天天看点

要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)

多边形扫描线填充算法(有序边算法实现)

任务内容

·任务说明

编写C++MFC程序,要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充。利用对话框控制线的数量或密度,以及横线或竖线,并可以重复画线或填充。

·输入输出

界面显示

任务完成情况

·实现思路

画鼠标点击画多边形

使用MFC内置的OnLButtonDown(UINT nFlags, CPoint point)函数实现。

首先使用cpoint记录上次鼠标点击的点,然后当鼠标下一次点击后,将上一次的点与这一次的点使用CDC以及CPen画笔连接成线。

在此过程中,使用firstpoint记录第一个点的信息,当第一个点和最后一个点之间的距离小于某一个阈值时,将第一个点和最后一个点连接起来,形成封闭的多边形,至此多边形绘制完毕。

多边形扫描线填充算法(有序边算法)绘制横线

边表创建ET

在扫描线填充算法中,采用了有序边算法。具体实现思想图下:在这个算法中,主要的就是需要建立一个边表和一个活性表。边表使用map<int,list>来存储,EDGE是一个边表的结构体,主要由ymax(边的最大y坐标),xi(交点坐标),dx(这条边斜率的倒数),map之中的int表示的是ymin(边的最小y坐标),由此就建立了边表的信息。如下图所示:

要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)
要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)
顶点问题处理

在建立完边表,其实还存在一个问题,就是如何处理多边形顶点的问题,在对多边形进行求交的过程中,扫描线会和每一条边都有一个交点,然而在顶点处则会出现两个交点,在这个过程中,错误的计算交点个数,会对填充造成影响。如下图所示:

[外链图片转存失败(img-kOuf1RmF-1564287217308)(E2C2A914890948B6BF0ABB026103858D)]

对于以上出现的这种情况,我们只需要更新边表的信息,也就是修改以终点为顶点的边,删除这条边的终点。

水平边处理

对于水平边,也就是两个边的y值相同的边,不加入边表中。

活性边创建AET

首先定义一个std::listAET,来存储每一条扫描线所对应的边表信息。

假设边表中,最小的y值为a,则第一条扫描边为y=a,并将a所对应的边表中的list插入到AET中,然后需要对AET进行排序。

之后就是进行算法部分了,每次从AET中取出一个边并获得与扫描线的交点坐标,也就是对应的x1=xi,并对xi进行更新(每次都加该边的斜率即可),接着获取AET中的第二个节点,x2=xi,后续操作与第一个节点类似。

接着判断这两个交点坐标是否相等,不相等的话则使用容器存下这两个交点坐标。遍历完AET,使用y++,获得第二条扫描线,这个时候需要判断AET中的所有边是否在该扫描线的范围之内,只需判断y值与改变的ymax的大小即可,如果ymax小于y值,则表明该边扫描结束,将改变从AET容器中删除。

接着并将扫描线对应的边表信息加入到AET中,并对AET进行排序,重复上面的动作,直到扫描线的y值大于YMAX(该多边形的最大y值)结束。

对话框与view视图类之间的通信

这里采用了非模态框,原因是因为模态框数据输入完成后会关闭。这里使用SendMessage来实现对话框(SelectDialog)与view(Task2_1View)之间的自定义消息的通信。

首先,我们需要在SelectDialog.h以及Task2_1View.h中加入下面的宏定义:

#define WM_MY_MESSAGE (WM_USER+100)

接着在SelectDialog.cpp中定义#include"MainFrm.h"

测试消息函数(SelectDialog中实现):

表示按下该按钮时,从SelectDialog发送信息到Task2_1View中。

void SelectDialog::OnBnClickedButton1()
{
	space = GetDlgItemInt(IDC_EDIT1);
	checkbox1 = ((CButton *)GetDlgItem(IDC_CHECK1))->GetCheck();
	checkbox2 = ((CButton *)GetDlgItem(IDC_CHECK2))->GetCheck();
	CMainFrame* pMF = (CMainFrame*)AfxGetApp()->m_pMainWnd;  //先通过获取当前框架指针
	CView * active = pMF->GetActiveView();//才能获取当前视类指针
	if (active != NULL)  //获取了当前视类指针才能发送消息
		active->SendMessage(WM_MY_MESSAGE, space,checkbox1);   //使用PostMessage发送消息

}
           

接着在Task2_1View.h中定义消息映射函数:

afx_msg LRESULT OnMyMessage(WPARAM wParam, LPARAM lParam);
           

接着在Task2_1View.cpp中声明响应函数:

BEGIN_MESSAGE_MAP(CTask21View, CView)
  ON_MESSAGE(WM_MyMessage,&CTask21View::OnMyMessage)
END_MESSAGE_MAP()
           

接着添加消息响应函数实现:

LRESULT CTask21View::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
    MessageBox(_T("OnMyMessage!"));
    return 0;
}
           

这两个参数就是从active->SendMessage(WM_MY_MESSAGE, space,checkbox1)中传过来的第二个以及第三个参数,这里的参数形式可以自己定义。

e.g:

LRESULT CTask21View::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
	CString str;
	int space = wParam;
	int checkbox1 = lParam;
	str.Format(_T("间距: %d ;  是否选择横线(0未选择,1选择): %d"), space, checkbox1);
}
           
MFC中实现console控制台的调用

首先添加如下函数并在初始化时进行调用:

void InitConsoleWindow()
{
	AllocConsole();
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	int hCrt = _open_osfhandle((long)handle, _O_TEXT);
	FILE * hf = _fdopen(hCrt, "w");
	*stdout = *hf;
}
           

然后添加:

AllocConsole();                     // 打开控制台资源
freopen("CONOUT$", "w+t", stdout);// 申请写
// 输出内容
freopen("CONIN$", "r+t", stdin);  // 申请读
           

除此之外还需进行配置,不然会报错。配置过程如下:

项目->属性->C/C++ ->预处理器->编辑预处理器定义->加入:_CRT_SECURE_NO_WARNINGS

_DEBUG。

·实现代码

绘制多边形

void CTask21View::OnLButtonDown(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	
	// 暂存的点
	// 定义一个画笔变量
	if (drawflag == -2)
	{
		CPen penBlack;
		CDC*pDC = GetDC();
		penBlack.CreatePen(PS_SOLID, 2, RGB(255, 0, 0));
		CPen*poldPen = pDC->SelectObject(&penBlack);
		// 上次的点
		CPoint tempPoint = cpoint;
		// 这次的点
		cpoint = point;
		//pDC->SetPixel(point, RGB(1, 0, 0));
		if (sum != 0)
		{
			// 将当前点加入到点存储器中
			freopen("CONOUT$", "w+t", stdout);// 申请写
			cout << "point: " << point.x << " " << point.y << endl;
			freopen("CONIN$", "r+t", stdin);  // 申请读
			listPoint.push_back(point);
			pDC->MoveTo(tempPoint.x, tempPoint.y);
			pDC->LineTo(cpoint.x, cpoint.y);
		}
		else
		{
			freopen("CONOUT$", "w+t", stdout);// 申请写
			cout << "point: " << point.x << " " << point.y << endl;
			freopen("CONIN$", "r+t", stdin);  // 申请读
			listPoint.push_back(point);
			firstpoint = point;
		}
		dis = sqrt((firstpoint.x - cpoint.x)*(firstpoint.x - cpoint.x) + (firstpoint.y - cpoint.y)*(firstpoint.y - cpoint.y));
		if (dis < 100)
		{
			pDC->MoveTo(firstpoint.x, firstpoint.y);
			pDC->LineTo(cpoint.x, cpoint.y);
		}
		sum++;
	}
	else
	{
		MessageBox(_T("没有点击绘图哦!"));
	}
	
	CView::OnLButtonDown(nFlags, point);
}
           
多边形扫描线填充有序边算法实现
// 构建边表
void GetNET()
{
	
	for (int i = 0;i < listPoint.size() - 1;i++)
	{
		// 平行边判定
		if (listPoint[i + 1].y == listPoint[i].y)
			continue;
		// 斜率的倒数
		float dx=0;
		double a = listPoint[i + 1].y - listPoint[i].y;
		if (a == 0)
		{
			 dx = 0.0;
		}
		else
		{
			 dx = (listPoint[i + 1].x - listPoint[i].x)*1.0 / (listPoint[i + 1].y - listPoint[i].y);

		}
		// xi低端点的x值,x高端点的x值
		float ymax, xi, ymin;
		if (listPoint[i + 1].y > listPoint[i].y)
		{
			ymax = listPoint[i + 1].y;
			ymin = listPoint[i].y;
			xi = listPoint[i].x;
		}
		else
		{
			ymax = listPoint[i].y;
			ymin = listPoint[i+1].y;
			xi = listPoint[i+1].x;
		}
		EDGE e(ymax, xi, dx);
		ET[ymin].push_back(e);
		if (ymax > YMAX)
		{
			YMAX = ymax;
		}
	}
	// 第一个点与最后一个点所构成的边
	int index = listPoint.size() - 1;
	if (listPoint[0].y != listPoint[index].y)
	{
		float dx = listPoint[index].y - listPoint[0].y == 0 ? 0 : (listPoint[index].x - listPoint[0].x)*1.0 / (listPoint[index].y - listPoint[0].y);
		// xi低端点的x值,x高端点的x值
		float ymax, xi, ymin;
		if (listPoint[index].y > listPoint[0].y)
		{
			ymax = listPoint[index].y;
			ymin = listPoint[0].y;
			xi = listPoint[0].x;
		}
		else
		{
			ymax = listPoint[0].y;
			ymin = listPoint[index].y;
			xi = listPoint[index].x;
		}
		EDGE e(ymax, xi, dx);
		ET[ymin].push_back(e);
	}
}
           
// 更新ET,将特殊点进行特殊处理
void UpdateET()
{
	for (auto it = ET.begin();it != ET.end();)
	{
		// 只存在一条边
		if (it->second.size() == 1)
		{
			EDGE e = *(it->second.begin());
			e.xi += e.dx;
			int ymin = it->first + 1;
			ET.erase(it++);
			ET[ymin].push_back(e);
		}
		else
		{
			it++;
		}
	}
}
           
// 处理活动边表
void ModifyAET()
{
	// 建立活动边表
	//创建活动边表并填充
	std::list<EDGE>AET;
	int y = 0;
	int count = 0;
	//获取首元素
	y = ET.begin()->first;
	cout << "y is " << y << endl;
	cout << " YMAX IS " << YMAX <<" "<< endl;
	AET.insert(AET.begin(), ET.begin()->second.begin(), ET.begin()->second.end());
	AET.sort();
	do {
		auto aet = AET.begin();
		while (aet != AET.end())
		{
			// 获取与扫描线交点的值
			float x1 = aet->xi;
			// 
			aet->xi += aet->dx;
			aet++;
			if (aet != AET.end())
			{
				float x2 = aet->xi;
				aet->xi += aet->dx;
				aet++;
				// 获取填充线坐标
				if (x1 != x2) 
				{
					vector<MyPoint>listp;
					
					listp.push_back(MyPoint(x1, y));
					listp.push_back(MyPoint(x2, y));
					IntePoint.insert(pair<int, vector<MyPoint>>(count, listp));
					count++;
				}
				cout << "x1: " << x1 << " x2: " << x2 << " y: " << y<<" "<<endl;
			}	
		}
		y++;
		// 逆序迭代器,指向c容器的最后一个元素
		// 如果y大于ymax,表示扫描线大于最大y值
		std::list<EDGE>::reverse_iterator it = AET.rbegin();
		for (; it != AET.rend();) {
			if (y >it->ymax)
				it = std::list<EDGE>::reverse_iterator(AET.erase((++it).base()));
			else it++;
		}
		// 找到扫描线对应的活性表
		auto itt=ET.find(y);
		if (itt != ET.end())
			AET.insert(AET.end(), ET[y].begin(), ET[y].end());
		AET.sort();
		// 如过y大于YMAX,直接退出
	} while ((y<YMAX));
}
           

多边形扫描线填充算法(有序边算法)绘制竖线

竖线的绘制方法和横线类似,这里不再赘述,只需将y转变成x,斜率变化为横线时的倒数即可。

·运行结果

没有点击绘图按钮,不会开始绘图

要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)

多边形绘制

要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)

开始填充可以任意间距,选择横线,竖线的选择

要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)
要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)

点击重绘之后

要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)

横线绘制

要求在MFC的视图中利用鼠标画多边形,并按要求利用横线或竖线进行填充多边形扫描线填充算法(有序边算法实现)

·遇到的问题总结

1 在计算斜率的时候没有考虑精度问题,导致填充不成功。

2 循环的时候没有考虑完整,导致绘制了不完全。

·代码下载地址

https://download.csdn.net/download/baidu_40924225/11452699

继续阅读