多边形扫描线填充算法(有序边算法实现)
任务内容
·任务说明
编写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坐标),由此就建立了边表的信息。如下图所示:
顶点问题处理
在建立完边表,其实还存在一个问题,就是如何处理多边形顶点的问题,在对多边形进行求交的过程中,扫描线会和每一条边都有一个交点,然而在顶点处则会出现两个交点,在这个过程中,错误的计算交点个数,会对填充造成影响。如下图所示:
[外链图片转存失败(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,斜率变化为横线时的倒数即可。
·运行结果
没有点击绘图按钮,不会开始绘图
多边形绘制
开始填充可以任意间距,选择横线,竖线的选择
点击重绘之后
横线绘制
·遇到的问题总结
1 在计算斜率的时候没有考虑精度问题,导致填充不成功。
2 循环的时候没有考虑完整,导致绘制了不完全。
·代码下载地址
https://download.csdn.net/download/baidu_40924225/11452699