本文是北京航空航天大学计算机学院软件工程课程的结对项目做业,在本次做业中,两位同窗一组,以结对编程的方式共同完成一项需求。html
结对编程是软件工程中的一种开发方法,两我的肩并肩坐在一块儿,共用一块屏幕和一份键盘鼠标,共写一份代码。两我的有不一样的分工,领航者负责指明方向,执行者负责动手写代码,在两人的默契配合下,造成一种无间隙的代码复审模式,使开发出的程序质量更高。python
项目 | 内容 |
---|---|
本做业属于北航软件工程课程 | 博客园班级博客 |
做业要求请点击连接查看 | 结对项目做业 |
班级:006 | Sample |
GitHub项目地址 | IntersectProject |
GUI项目地址 | IntersectionGUI |
同组同窗博客连接 | eitbar |
我在这门课程的目标是 | 得到成为一名软件工程师的能力 |
这个做业在哪一个具体方面帮助我实现目标 | 实践结对编程 |
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 50 |
· Estimate | · 估计这个任务须要多少时间 | 60 | 50 |
Development | 开发 | 1800 | 1630 |
· Analysis | · 需求分析 (包括学习新技术) | 600 | 600 |
· Design Spec | · 生成设计文档 | 60 | 40 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 60 | 50 |
· Coding | · 具体编码 | 600 | 500 |
· Code Review | · 代码复审 | 600(同时) | 500(同时) |
· Test | · 测试(自我测试,修改代码,提交修改) | 400 | 360 |
Reporting | 报告 | 300 | 360 |
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工做量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 240 | 300 |
合计 | 2160 | 2040 |
基于上述原则,咱们设计了以下两个函数做为计算核心模块的接口,能够自由的与命令行和GUI进行对接。知足上述三条设计原则。这两个函数都会调用计算核心模块,不一样之处是输入输出格式。c++
#ifdef IMPORT_DLL #else #define IMPORT_DLL extern "C" _declspec(dllimport) //指的是容许将其给外部调用 #endif IMPORT_DLL int guiProcess(std::vector<std::pair<double,double>> *points, std::string msg); IMPORT_DLL void cmdProcess(int argc, char *argv[]);
guiProcess
函数传入符合格式的字符串,交点集用指针返回,交点个数用返回值返回。
std::pair<double,double>
表示,保证调用者清晰理解。cmdProcess
为了命令行调用方便,采用需求规定的命令行输入格式进行输入输出。注:这两个函数并非分别针对gui和cmd,只是输入输出格式不一样,均可以任意调用,保证松耦合。git
unordered_set
和unordered_map
,CSlope要实现Hash方法
和等于运算符
。Hash方法
和等于运算符
。inputShapes: 处理输入函数,直线、射线、线段按斜率分组,放到map<double, set<CLine>>
的_k2lines
中,圆直接放到set<CCircle>
的 _circles
里。github
【新增】:上一次需求中直线和直线平行不可能产生有限交点,可是这次需求新增的线段和射线就可能产生共线相交在端点的状况,是符合需求说明书的状况,须要特殊考虑。在此函数中实现,后续就能够正常按照平行分组计算了。正则表达式
calcShapeInsPoint:求两个图形交点的函数,分三种状况,返回点的vector。算法
cntTotalInsPoint: 求全部焦点的函数,按先直线后圆的顺序依次遍历求焦点。已经遍历到的图形加入一个over
集中。编程
over
集中其它不平行直线。_insp2shapes
这个map<CPoint, vector<CShape>>
数据结构,为交点到通过它的线集的映射。_insp2shapes
里_insp2shapes.size()
即为交点个数。具体说明本次需求【新增】的重要代码canvas
// require: cx cy belongs to the line // return: if point in shape range bool CLine::crossInRange(double cx, double cy) const { if (type() == "Line") { // 统一接口,直线返回true return true; } else if (type() == "Ray") { // 射线 if (k().isInf()) { // 斜率无穷,比较y值,dcmp为浮点数比较函数,定义见下 if (dcmp(cy, y1())*dcmp(y2(), y1()) != -1) { return true; } } else { // 正常状况,比较x if (dcmp(cx, x1())*dcmp(x2(), x1()) != -1) { return true; } } return false; } else { // 线段 ... //相似于射线,代码略 } } // 浮点数比较函数,相等返回0,前者大返回1,不然返回-1 #define EPS 1e-10 int dcmp(double d1, double d2) { if (d1 - d2 > EPS) { return 1; } else if (d2 - d1 > EPS) { return -1; } else { return 0; } }
浮点数因为有浮点偏差,直接用hash<double>的值将不一样,因此应该截取某精度,转换成整形进行hash。设计模式
#define COLLISION 100000. class SlopeHash { public: std::size_t operator()(const CSlope& s) const { // 乘精度,四舍五入,转整形,算Hash unsigned long long val = dround2ull(s.val() * COLLISION); return std::hash<bool>()(s.isInf()) + (std::hash<unsigned long long>()(val) << 16); } };
咱们随机生成了8000条几何图形数据用于性能测试,其中直线、线段、射线、圆各2000个,最终计算获得交点数为18175002个交点。随机数生成模块为python中random包。
VS2019中使用性能探查器,分析CPU利用率以下:
能够看出,耗费时间最多的函数,是计算输入全部图形的交点总数的函数cntTotalInsPoint
,进入函数入内部查看分析结果:
与我的项目相似,耗费时间最多的仍然是记录交点的数据结构set
对交点的插入。因为已经使用unordered_set
相比于本来的set
时间复杂度已经低了不少,所以固有的数据结构维护时间不可避免。其次咱们还注意到计算两个图形间交点的函数calcShapeInsPoint
占用了较大的时间开销。进入该函数中查看分析结果:
能够看出判断点是否在线段或射线上,花费时间较多,判断点是否在交点上的函数,内部是这样的:
仔细想一想后,发现其实彻底没有必要再这个函数内部再判断一次点是否在线端或射线所处直线上,由于咱们自己会用到这个函数的场景,就是先计算出线段或射线所在直线与其余直线的交点,再判断交点是否在线段或射线上,所以,这个判断属于画蛇添足,是一个能够优化的点。
删除点是否在线上的判断后,再次使用性能分析,结果以下:
能够发现总时间下降了4~5s,再次进入calcShapeInsPoint
函数中查看
能够发现crossInRange
函数已经再也不是花费最多的部分,说明仍是颇有效果的。其他部分优化也都相似,不断对比分析,删除冗余计算结果。
契约式设计一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的而且可验证的接口,这样,为传统的抽象数据类型又增长了先验条件、后验条件和不变式。这种方法的名字里用到的“契约”是一种比喻,由于它和商业契约的状况有点相似。
在面向对象课程中,咱们就接触过契约式设计,将逻辑约束在设计时定义好,编码实现时只须要遵照契约式设计就能够写出正确的代码,减小了出错的可能性。利用一些现有的软件,还能够利用契约式设计作代码正确性的形式证实。
从上次做业开始,个人重要pipeline函数就采用了契约式设计模式,下面举例说明。
//算直线和圆的交点的函数 // calculate Intersections of one circ and one line // need: para1 is CCirc, para2 is CLine // return a vector of intersections. size can be 0,1,2. std::vector<CPoint> calcInsCircLine(const CShape& circ, const CShape& line) { ... }
//算任意两图形交点的函数 // calculate all intersect points of s1 and s2 // return the points as vector // need: s1, s2 should be CLine or CCircle. // special need: if s1, s2 are CLine. They cannot be parallel. std::vector<CPoint> CIntersect::calcShapeInsPoint(const CShape& s1, const CShape& s2) const { ... }
//计算交点总数的函数 // the main pipeline: loop the inputs and fill in _insp2shapes or _insPoints // return the total count of intersect points // need: _k2lines and _circles have been filled int CIntersect::cntTotalInsPoint() { ... }
在设计时我都是先写出契约,以后的代码实现中严格遵照契约中的约束,好比使用calcShapeInsPoint
函数时就必须遵照下面约束,不然将产生错误。
// special need: if s1, s2 are CLine. They cannot be parallel.
首先展现使用OpenCppCoverage
插件测试获得代码覆盖率为:99%
其中部分Uncovered line
是因为VS强大的编译优化,把一些函数在内联处理了,因此执行不到。
在询问了一些同窗而且到网上查询以后,咱们仍是没有找到如何直接将VS的单元测试项目加入OpenCppCoverage
的代码覆盖检测中,所以咱们选择将单元测试代码手动从单元测试项目中移入主函数中。主要测试结构以下:
int main() { ... ... //从单元测试项目中转移至此的单元测试1 ... //从单元测试项目中转移至此的单元测试2 ... }
单元测试中,主要包括文件读写的测试,对一些关键函数如计算交点等的测试,对异常的测试。
部分单元测试代码展现以下:
测试图形之间交点的计算:该部分比较繁杂,须要考虑的状况有如下几种,我的项目中已经出现过的就再也不放测试代码,与上次相似
直线与直线仅有一个交点、没有交点,其中须要包括直线斜率不存在、为0、其余的状况
直线与圆有两个交点、一个交点、没有交点,其中须要包括直线斜率不存在、为0、其余的状况
直线与线段有一个交点、没有交点,其中须要包括直线与线段平行、不平行有一个交点、不平行没有交点的状况。如下例子为一个交点的状况:
CLine t1 = CLine(0, 2, 0, 0, "Seg"); CLine t2 = Cline(3, 2, 4, 2); ans = ins.calcShapeInsPoint(t1, t2); Assert::AreEqual(1, (int)ans.size()); Assert::AreEqual(true, CPoint(0, 2) == ans[0]);
直线与射线有一个交点、没有交点,其中须要包括直线与射线平行、不平行有一个交点、不平行没有交点的状况。如下例子为没有交点的状况:
CLine t1 = CLine(0, 2, 0, 0, "Ray"); CLine t2 = Cline(3, 2, 3, 4); ans = ins.calcShapeInsPoint(t1, t2); Assert::AreEqual(0, (int)ans.size());
圆与圆有两个交点、一个交点、没有交点,其中须要包括外离、外切、相交、内切、内含的状况
圆与线段有两个交点、一个交点、没有交点,其中须要包含线段所在直线与圆相离、相切、相交以及不一样交点数的状况。如下例子为两个交点的状况:
CCircle c = CCircle(0, 0, 2); CLine t1 = Cline(2, 0, 0, 2, "Seg"); ans = ins.calcShapeInsPoint(t1, c); Assert::AreEqual(2, (int)ans.size()); Assert::AreEqual(true, CPoint(0, 2) == ans[0]); Assert::AreEqual(true, CPoint(2, 0) == ans[1]);
圆与射线有两个交点、一个交点、没有交点,其中须要包含射线所在直线与圆相离、相切、相交以及不一样交点数的状况。如下例子为一个交点的状况:
CCircle c = CCircle(0, 0, 2); CLine t1 = Cline(0, 0, 0, 2, "Ray"); ans = ins.calcShapeInsPoint(c, t1); Assert::AreEqual(1, (int)ans.size()); Assert::AreEqual(true, CPoint(0, 2) == ans[0]);
线段与线段有一个交点、没有交点,其中须要包含两个线段所在直线之间的各类关系以及不一样交点数的状况,须要特别考虑线段与线段共线时端点重合的状况。如下例子为共线时一个交点的状况:
CLine t1 = Cline(0, 0, 0, 2, "Seg"); CLine t2 = Cline(0, -2, 0, 0, "Seg"); ans = ins.calcShapeInsPoint(t1, t2); Assert::AreEqual(1, (int)ans.size()); Assert::AreEqual(true, CPoint(0, 0) == ans[0]);
线段与射线有一个交点、没有交点,其中须要包含线段与射线所在直线之间的各类关系以及不一样交点数的状况,须要特别考虑线段与射线共线时端点重合的状况。如下例子为共线时一个交点的状况:
CLine t1 = Cline(0, 2, 0, 0, "Seg"); CLine t2 = Cline(0, 0, 0, -2, "Ray"); ans = ins.calcShapeInsPoint(t1, t2); Assert::AreEqual(1, (int)ans.size()); Assert::AreEqual(true, CPoint(0, 0) == ans[0]);
射线与射线有一个交点、没有交点,其中须要包含两个射线所在直线之间的各类关系以及不一样交点数的状况,须要特别考虑射线与射线共线时端点重合的状况。如下例子为共线时没有交点的状况:
CLine t1 = Cline(0, 0, 2, 0, "Ray"); CLine t2 = Cline(-1, 0, -2, 0, "Ray"); ans = ins.calcShapeInsPoint(t1, t2); Assert::AreEqual(0, (int)ans.size());
交点数统计测试:测试各类状况下,如不一样图形之间有重合交点、图形数量不多、图形数量不少等,交点统计是否正确。举例:
TEST_METHOD(TestMethod10) { ifstream fin("../test/test10.txt"); if (!fin) { Assert::AreEqual(132, 0); } CIntersect ins; ins.inputShapes(fin); int cnt = ins.cntTotalInsPoint(); Assert::AreEqual(433579, cnt); }
异常单元测试:对各类异常状况的单元测试,主要是经过构造异常数据传入输入函数,捕捉其抛出的异常与预期异常的信息进行比较,将在第八章中详细说明,这里仅放出几个样例。
测试射线与射线重合的状况(其中之一)
TEST_METHOD(TestMethod_RR1) { string strin = "2\nR 0 0 5 5\nR 6 6 -1 -1\n"; ShapeCoverException s(2, "R 6 6 -1 -1", "R 0 0 5 5"); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
测试射线与线段重合的状况(其中之一)
TEST_METHOD(TestMethod_SR1) { string strin = "3\nS 0 0 0 4\nS 0 0 4 0\nR 0 -2 0 -1"; ShapeCoverException s(3, "R 0 -2 0 -1", "S 0 0 0 4"); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
测试线段与线段重合的状况(其中之一)
TEST_METHOD(TestMethod_SS1) { string strin = "3\nS 0 -1 0 1\nS 0 3 0 6\nS 0 0 0 2\n"; ShapeCoverException s(3, "S 0 0 0 2", "S 0 -1 0 1"); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
计算模块异常处理大体分类以下,均继承c++标准异常:
N
、N
不符合规范、N
与图形数量不匹配等异常类的UML图以下:
关键异常详细介绍:
ShapeNumberException
当没法从输入流中读入N
、读入的N
范围不符合规范、N
与实际输入的图形数量不匹配时,抛出该异常。
该异常构造方式与标准异常相同,传入错误信息字符串,可使用what()
函数获取错误信息,获取到的错误信息与传入错误信息相同。如:
单元测试举例以下:
TEST_METHOD(TestMethod_N6) { string strin = "1\nL 1 2 3 4\nC 1 1 2"; ShapeNumberException s("The number of graphics is larger than N."); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
IllegalFormatException
当读入过程当中某行既不是空行、也不符合规定的输入格式即(L <x1> <y1> <x2> <y2>
、R <x1> <y1> <x2> <y2>
、S <x1> <y1> <x2> <y2>
、C <x> <y> <r>
四种格式中的一种)时,抛出该异常。
该异常构造方式为,传入格式错误的图形序号和该行字符串,可使用what()
函数获取错误信息,获取到的错误信息为序号、字符串以及相应修改建议。如:
单元测试举例以下:
TEST_METHOD(TestMethod_ILLShape) { string strin = "1\n\nL 1 2 3 F\n"; IllegalFormatException s(1,"L 1 2 3 F"); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
OutRangeException
当读入图形信息时,图形的参数超过规定的范围,抛出该异常。
该异常构造方式为,传入参数超过规定范围的图形序号和该行字符串,可使用what()
函数获取错误信息,获取到的错误信息为序号、字符串以及相应修改建议。如:
单元测试举例以下:
TEST_METHOD(TestMethod_OutRange) { string strin = "1\n\nC 1 1 0\n"; OutRangeException s(1, "C 1 1 0"); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
ShapeRepeatedException
当读入某一图形,发现与已有图形彻底相同时,抛出该异常。
该异常构造方式为,传入出现重复的图形序号和该行字符串,可使用what()
函数获取错误信息,获取到的错误信息为序号、字符串以及相应修改建议。如:
单元测试举例以下:
TEST_METHOD(TestMethod_ShapeRe) { string strin = "2\n\nC 1 1 1\nC 1 1 1\n"; ShapeRepeatedException s(2, "C 1 1 1"); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
IllegalLineException
当读入直线、线段、射线,且发现描述线类图形的两个点重合时,抛出该异常。
该异常构造方式为,传入端点重合的图形序号和该行字符串,可使用what()
函数获取错误信息,获取到的错误信息为序号、字符串以及相应修改建议。如:
单元测试举例以下:
TEST_METHOD(TestMethod_IllLine) { string strin = "2\n\nL 1 1 1 2\nL 0 1 0 1\n"; IllegalLineException s(2, "L 0 1 0 1"); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
ShapeCoverException
当读入直线、线段、射线等与已有线类图形重合(即有无限个交点)时,抛出该异常。
该异常构造方式为,传入该图形(指新读入的图形)序号、该图形字符串信息、与其重合的图形字符串信息,可使用what()
函数获取错误信息,获取到的错误信息为序号、该图形、与其重合的图形信息及相应修改建议。如:
单元测试举例以下:
TEST_METHOD(TestMethod_RS1) { string strin = "3\nS 0 0 4 0\nR 0 -2 0 -1\nS 0 0 0 4"; ShapeCoverException s(3, "S 0 0 0 4", "R 0 -2 0 -1"); InputHandlerException std = s; istringstream in(strin); if (!in) { Assert::AreEqual(132, 0); } CIntersect ins; try { ins.inputShapes(in); Assert::AreEqual(132, 0); } catch (InputHandlerException e) { Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0); } }
首先给出界面模块的总体外观
输入面板模块:提供添加、删除、清空功能
计算核心接口:添加图形后,点击求解交点
调用计算核心模块,返回交点个数对话框,并在绘图面板中绘制交点,后文将详述接口函数的设计。
绘图面板模块:在添加图形时绘制图形,在计算交点后绘制交点。
错误反馈模块:反馈计算核心模块抛出的异常,例如
产生无数交点
输入数据范围错误
本项目的GUI采用c++的Qt库实现,对比之前使用过的MFC,Qt有易上手,跨平台,界面美观等特色。对于不多写图形程序的我来讲,采用Qt是一个很合适的选择。下面分别介绍各个模块的关键代码实现。
输入面板模块:充分利用Qt ui设计器提供的诸多功能强大的组件
整个输入面板置于可浮动拖出的Dock组建(窗体左侧部分),对于屏幕分辨率低的用户,可将输入面板分离出主窗口,让绘图面板占据所有主窗口
输入数据部分选用QComboBox和QSpinBox等组件
按钮利用Qt提供的QToolButton组件,方便定义样式和在工具栏重用
代码主要定义添加,删除,清空按钮的槽函数,以文件添加为例展现代码
void IntersectionGUI::on_actAddFile_triggered() { if (lastPath == QString::fromLocal8Bit("")) { //记录上次打开的路径 lastPath = QDir::currentPath();//获取系统当前目录 } //获取应用程序的路径 QString dlgTitle = QString::fromLocal8Bit("选择一个文件"); //对话框标题 QString filter = QString::fromLocal8Bit("文本文件(*.txt);;全部文件(*.*)"); //文件过滤器 QString aFileName = QFileDialog::getOpenFileName(this, dlgTitle, lastPath, filter); if (!aFileName.isEmpty()) { lastPath = aFileName; // 读入文件数据,文件格式与需求定义相同 std::ifstream fin(aFileName.toLocal8Bit()); int N; std::string line; fin >> N; std::getline(fin, line); while (N--) { std::getline(fin, line); QString qline = QString::fromStdString(line); if (!isShapeStrValid(qline)) { // 正则表达式简单判断输入格式 QString dlgTitle = QString::fromLocal8Bit("输入格式错误"); QMessageBox::critical(this, dlgTitle, qline); break; } draw_shape_from_str(qline); //在绘图面板上绘图 // 图形列表中添加一行 QListWidgetItem * aItem = new QListWidgetItem(); //新建一个项 aItem->setText(qline); //设置文字标签 aItem->setCheckState(Qt::Unchecked); //设置为选中状态 aItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); ui.listWidget->addItem(aItem); //增长一个项 } } }
求解交点
按钮后,调用槽函数on_actSolve_triggered
void IntersectionGUI::on_actSolve_triggered() { // 从图形列表中构造给核心接口的输入字符串 std::string input; input += std::to_string(ui.listWidget->count()) + "\n"; for (int i = 0; i < ui.listWidget->count(); i++) { QListWidgetItem *aItem = ui.listWidget->item(i);//获取一个项 QString str = aItem->text(); input += str.toStdString() + "\n"; } // 调用核心接口 std::vector<std::pair<double, double> > points; int cnt = 0; try { cnt = guiProcess(&points, input); // 调用核心接口dll函数 } catch (std::exception e) { // 反馈计算核心抛出的异常 QString dlgTitle = QString::fromLocal8Bit("计算出现错误"); QMessageBox::critical(this, dlgTitle, e.what()); return; } // 绘制交点 for (auto vit = points.begin(); vit != points.end(); ++vit) { int w, h; xy2wh((int)(vit->first), (int)(vit->second), w, h); draw_point(w, h, Qt::red); } // 反馈交点总数 QString dlgTitle = QString::fromLocal8Bit("计算结果"); QString strInfo = QString::fromLocal8Bit("交点总数为:"); strInfo += strInfo.asprintf("%d", cnt); QMessageBox::information(this, dlgTitle, strInfo,QMessageBox::Ok, QMessageBox::NoButton); }
绘图面板模块:采用QLabel和QPixmap组件进行绘图,主要的绘图函数有如下几种
最难写的函数就是绘制直线、射线,Qt自带的绘制直线函数drawLine
其实是给定两点绘制线段,因此想要达到绘制直线和线段的效果就必须求出与画板边界的交点。先根据方向算左右方向的边界,若是发现碰的不是左右边界,再算上下边界。
void IntersectionGUI::draw_ray(int x1, int y1, int x2, int y2, QColor const c, int const w) { QPainter Painter(&curPixmap); Painter.setRenderHint(QPainter::Antialiasing, true); //反走样 Painter.setPen(QPen(c, w)); if (x2 == x1) { // 竖直 if (y2 > y1) { y2 = REAL_SIZE; } else { y2 = -REAL_SIZE; } } else if (y1 == y2) { // 水平 if (x2 > x1) { x2 = REAL_SIZE; } else { x2 = -REAL_SIZE; } } else { // 向右上倾斜 先算左右边界交点,超范围就算上下交点 double k = (double)(y2 - y1) / (x2 - x1); double b = y1 - k * x1; if (x2 > x1) { double y_ = REAL_SIZE * k + b; if (y_ > REAL_SIZE) { y2 = REAL_SIZE; x2 = (y2 - b) / k; } else if (y_ < -REAL_SIZE) { y2 = -REAL_SIZE; x2 = (y2 - b) / k; } else { x2 = REAL_SIZE; y2 = y_; } } else { ... } // 相似略 } QPoint p1 = xy2whPoint(x1, y1); QPoint p2 = xy2whPoint(x2, y2); Painter.drawLine(p1, p2); ui.canvas->setPixmap(curPixmap); }
为了设计松耦合,咱们将核心部分设计了以下两个函数做为接口,命令行和GUI均可以经过这两个接口访问计算核心。
#ifdef IMPORT_DLL #else #define IMPORT_DLL extern "C" _declspec(dllimport) //指的是容许将其给外部调用 #endif IMPORT_DLL int guiProcess(std::vector<std::pair<double,double>> *points, std::string msg); IMPORT_DLL void cmdProcess(int argc, char *argv[]);
guiProcess
函数传入符合格式的字符串,交点集用指针返回,交点个数用返回值返回。cmdProcess
采用需求规定的命令行输入格式进行输入输出注:这两个函数并非分别针对gui和cmd,只是输入输出格式不一样,均可以任意调用,保证松耦合。
经过此接口将计算核心封装成动态连接库calcInterface.dll
和库calcInterface.lib
int main(int argc, char *argv[]) { typedef int (*pGui)(vector<pair<double,double>>* points, string a); typedef void (*pCmd)(int argc, char* argv[]); HMODULE hDLL = LoadLibrary(TEXT("calcInterface.dll")); vector<pair<double, double>>points; string tmp = "2\nL 1 1 0 1\nL 1 1 1 0\n"; if (hDLL) { pGui guiProcess = (pGui)GetProcAddress(hDLL, "guiProcess"); pCmd cmdProcess = (pCmd)GetProcAddress(hDLL, "cmdProcess"); try { int ans1 = guiProcess(&points, tmp); // 测试接口函数1 for (int i = 0; i < points.size(); i++) { cout << points[i].first << " " << points[i].second << endl; } cout << ans1 << endl; } catch (exception t) { cout << t.what() << endl; } cmdProcess(argc, argv); //测试接口函数2 } }
#pragma comment(lib,"calcInterface.lib") _declspec(dllexport) extern "C" int guiProcess(std::vector<std::pair<double, double>> *points, std::string msg); _declspec(dllexport) extern "C" void cmdProcess(int argc, char *argv[]); // 调用核心接口 std::vector<std::pair<double, double> > points; int cnt = 0; try { cnt = guiProcess(&points, input); // 调用核心接口dll函数 } catch (std::exception e) { // 反馈计算核心抛出的异常 QString dlgTitle = QString::fromLocal8Bit("计算出现错误"); QMessageBox::critical(this, dlgTitle, e.what()); return; }
咱们与另外一个小组合做,互换了计算核心dll,测试结果以下,能够正常运行,git仓库连接
他们采用的gui接口函数格式与咱们的彻底相同,只需将对应的dll和lib替换便可直接运行。
IMPORT_DLL int guiProcess(std::vector<std::pair<double, double>>* points,std::string msg); IMPORT_DLL void cmdProcess(int argc, char *argv[]);
命令行对接结果
GUI接口对接结果
因为单个软件存在或多或少的问题,咱们综合使用VS Live Share、腾讯会议以及github来进行远程结对编程。
结对过程当中,因为两人已经在许多课程做业中创建了深厚的合做基础和友谊,互相信任,所以,在遇到一些犹豫不定的状况时,为了提升效率,在共同讨论各类方法的优劣以后,多采用断言和说服的方式肯定思路。
如下是咱们结对过程当中部分截图:
我搭档的优势
个人优势
个人缺点
搭档的缺点
总结:逻辑清晰、代码规范、耐心细心、规划合理,个人搭档的是我见过最强的搭档。
使用默认的规则。能够看到已经没有任何错误和警告。
在设计接口数据类型时,咱们产生了一些异议,与其余小组讨论,意见也不是很一致。大致分为如下两种。
松式设计
所谓松式设计指采用万能数据类型,如字符串、整形等做为输入输出。
紧式设计
采用自定义数据类型做为接口,提供特定的构造函数,提供检错判断等。
讨论以后,咱们最终采用了折中的办法,用c++STL中的vector<pair<double,double>>
这种数据类型返回点集,即不直接用纯粹字符串,也不使用自定义数据类型。较方便地解决了本次需求。
可是,若是有些状况下没法采用STL来描述某些接口的数据类型,又该采用什么样的方式设计呢?