2020软工结对项目作业-简单几何形状间交点统计
项目 | 内容 |
---|---|
课程链接 | 2020春季计算机学院软件工程(罗杰 任健) |
作业要求 | 结对项目作业 |
课程目标 | 系统学习软件开发理论和流程,通过实践积累软件开发经验 |
本博客的收获 | 开发了一个简单带有UI的项目,总结结对编程过程的经历和优缺点 |
教学班级 | 005 |
项目地址 | https://github.com/zwx980624/IntersectProject |
UI地址 | https://github.com/zwx980624/IntersectionGUI |
队友地址 | xgnb |
一、估计将在程序的各个模块的开发上耗费的时间
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 720 | 600 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60(同时) | 60(同时) |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 80 | 60 |
· Coding | · 具体编码 | 600 | 500 |
· Code Review | · 代码复审 | 600(同时) | 500(同时) |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 400 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 240 | 300 |
合计 | 2110 | 2020 |
二、结对编程中是如何利用Information Hiding,Interface Design,Loose Coupling方法设计接口
-
information Hiding 是指信息的隐藏和封装
In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface(computer_science) which protects the remainder of the program from the implementation (the details that are most likely to change).
从维基百科中,我们可以看出information Hiding是指外部人员在可以调用接口而不必关心内部的实现,即使内部实现发生变动,对外部人员也不会产生任何影响。在我们设计一些接口时,就采用了这种思想。比如处理输入的接口可以接受文件、字符串、标准输入等任意输入流,调用接口者不需要知道我们内部是怎么处理的,就可以完成对输入图形的处理和保存。还有GUI使用的guiProcess接口,只需要传入字符串和保存交点的vector,不需要额外构建该接口内部使用的各种类,即可完成交点计算。
-
interface Design 指接口设计,包含单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、最少知识原则、开闭原则等。
我们根据以上原则的一部分,设计了如下两个计算核心的接口:
-
guiProcess
函数传入符合格式的字符串,交点集用指针返回,交点个数用返回值返回。
- 由数据格式的设计原则,只使用c++标准库中的容器,而不用自定义数据类型。如CPoint要转成
std::pair<double,double>
表示,保证调用者清晰理解。 - 输入采用特定格式的字符串,符合方便易用原则,如果格式错误,由核心处理并抛出异常
- 由数据格式的设计原则,只使用c++标准库中的容器,而不用自定义数据类型。如CPoint要转成
-
cmdProcess
为了命令行调用方便,采用需求规定的命令行输入格式进行输入输出
-
-
loose Coupling是松耦合
In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services.
我们的dll在抛出异常时会将错误信息存储为标准异常,别人在调用时,不需要专门使用我们设计的异常类,就可以捕捉异常,可以减小耦合度。
三、四、计算模块设计与实现过程及UML。
(一)PipeLine
PreProcess
- ReadShape:读取文件接收全部输入的直线和圆
- Shape construct:根据输入构建形状对象,计算直线斜率。
- Classified by Slope:按斜率将直线分组存起来。
- CalcIns Same Slope:处理在同一直线上的线段、射线的共端点情况。
CalcIntersect
- CalcLines:计算所有直线、射线、线段之间的交点:
- 依次考虑每个平行组,按每条线遍历计算交点。平行组内的线不用计算交点。
- 查交点表,如果存在,就可以不求同一交点的其他线了。
交点表:Map<点,Vector<线>>
维护交点表:新增的交点加入交点表,线加入表中对应的线集 - 射线和线段的交点还要满足在射线和线段范围内才有效
- CalcCircles:所有线算完后,再一个个遍历圆。
- 暴力求其与之前图形的全部交点
- 同样需要考虑射线和线段的范围问题
(二)类间关系图
- CIntersect类:实现控制流,方法包含输入、计算两图形交点、计算交点总数
- CShape类:图形类基类,为每个图形实例创建唯一id,并记录图形的类型
- CLine类:继承图形类基类,作用为表示直线、线段、射线的代数方程参数。
- CCircle类:继承图形类基类,作用为表示圆的代数方程参数。
- 直线方程两种表示方法
- 一般方程:(Ax + By +C = 0)
- 斜截方程:(y = kx + b)
- 圆方程两种表示
- 一般方程: (x^2 + y^2 + Dx + Ey +F = 0)
- 标准方程: ((x-x_0)^2 + (y-y_0)^2 = r^2)
- CSlope类和CBias类:为解决斜率无穷大设计,isInf和isNan为true时表示直线的斜率为无穷,此时k和b的具体值无效。由于要按斜率分组,采用C++STL的
unordered_set
和unordered_map
,CSlope要实现Hash方法
和等于运算符
。 - CPoint类:表示交点,作为map的key,同样需要实现
Hash方法
和等于运算符
。
(三)关键函数
-
inputShapes: 处理输入函数,直线、射线、线段按斜率分组,放到
map<double, set<CLine>>
的_k2lines
中,圆直接放到set<CCircle>
的_circles
里。新增:上一次需求中直线和直线平行不可能产生有限交点,但是此次需求新增的线段和射线就可能产生共线相交在端点的情况,是符合需求说明书的情况,需要特殊考虑。在此函数中实现,后续就可以正常按照平行分组计算了。
-
calcShapeInsPoint:求两个图形交点的函数,分三种情况,返回点的vector。
- 直线与直线
- 直线与圆
- 圆与圆
-
cntTotalInsPoint: 求所有焦点的函数,按先直线后圆的顺序依次遍历求焦点。已经遍历到的图形加入一个
over
集中。- 直线两个剪枝方法:
- 砍平行:依次加入每个平行组,不需计算组内直线交点,只需遍历
over
集中其它不平行直线。 - 砍共点:假若ABC共点,按ABC的顺序遍历,先计算了AB,交点为P;之后计算AC时发现交点也是P,则无需计算BC交点。方法为维护
_insp2shapes
这个map<CPoint, vector<CShape>>
数据结构,为交点到经过它的线集的映射。
- 砍平行:依次加入每个平行组,不需计算组内直线交点,只需遍历
- 再依次遍历圆,暴力求焦点。加到
_insp2shapes
里 - 函数返回
_insp2shapes.size()
即为交点个数。
- 直线两个剪枝方法:
(四)代码说明
具体说明本次需求新增的重要代码
1. 判断求交公式算出的交点在射线、线段范围内
// 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;
}
}
2. 浮点数hash方法
浮点数由于有浮点误差,其hash值将不同,所以应该截取某精度,转换成整形进行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
函数已经不再是花费最多的部分,说明还是很有效果的。其余部分优化也都类似,不断对比分析,删除冗余计算结果。
六、看 Design by Contract,Code Contract 的内容,描述这些做法的优缺点,说明你是如何把它们融入结对作业中的
Design by Contract,也称契约式设计:
It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.
简单来说,就是为每个接口写好各种使用的规范,包括调用前的规范和调用后的结果规范。在曾经的OO课上,我们接触并学习过写函数的约束和规范,个人感觉优点和缺点如下:
-
优点:通过书写前置条件、后置条件和不变式这种逻辑化的语言,可以避免开发人员对模块的实现产生歧义,其次通过合理的设计约束,可以让模块的耦合度更低,更加符合上述提到的三个设计原则。
-
缺点:需要花费大量时间,并且对开发人员的素质要求高。比如本次作业过程中,我们就没有严格使用标准的前置、后置语句,而是使用通俗化的语言描述,否则又是一笔巨大的时间开销。
我们的pipeline函数就采用了契约式设计模式,为了提高效率简化过程,我们采用了比较书面化的语言来描述规范,而没有严格遵循标准得contract格式。如以下几个函数:
// 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
插件测试得到代码覆盖率为:
其中部分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 R 0 0 5 5 R 6 6 -1 -1 "; 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 S 0 0 0 4 S 0 0 4 0 R 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 S 0 -1 0 1 S 0 3 0 6 S 0 0 0 2 "; 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 L 1 2 3 4 C 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 L 1 2 3 F "; 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 C 1 1 0 "; 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 C 1 1 1 C 1 1 1 "; 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 L 1 1 1 2 L 0 1 0 1 "; 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 S 0 0 4 0 R 0 -2 0 -1 S 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()) + "
";
for (int i = 0; i < ui.listWidget->count(); i++)
{
QListWidgetItem *aItem = ui.listWidget->item(i);//获取一个项
QString str = aItem->text();
input += str.toStdString() + "
";
}
// 调用核心接口
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,只是输入输出格式不同,都可以任意调用,保证松耦合。
(二)导出dll
通过此接口将计算核心封装成动态链接库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
L 1 1 0 1
L 1 1 1 0
";
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
}
}
- GUI接口对接
#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;
}
十一、描述结对的过程
由于单个软件存在或多或少的问题,我们综合使用VS Live Share、腾讯会议以及github来进行远程结对编程。
VS Live Share可以更加真实的模拟两个人共同面对同一文件编程的效果,“领航员”也可以更方便的参与到代码的编写中,但VS Live Share无法让被邀请的人观看到程序的运行结果以及整个解决方案的结构。因此,我们辅以腾讯会议共享屏幕,来观看整个项目的架构和编译、运行等信息。然后,我们根据两个人擅长的不同部分,通过github进行代码同步,分别在不同的模块担任“驾驶员”和“领航员”的角色。
结对过程中,由于两人已经在许多课程作业中建立了深厚的合作基础和友谊,互相信任,因此,在遇到一些举棋不定的情况时,为了提高效率,在共同讨论各种方法的优劣之后,多采用断言和说服的方式确定思路。
以下是我们结对过程中同时使用LiveShare和腾讯会议时的截图:
十二、说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里
结对编程的优点:
- 更快的攻破技术难点。由于VS用的不多、QT也是第一次使用,在我们结对编程时经常会碰到一些操作、语言上的问题和障碍。这个时候,我们可以分头查找相关资料,解决问题的速度大大提升。
- 保持愉快的编程氛围,平衡心态。自己一个人编程的时候,经常会因为一些莫名其妙的bug卡几个小时,心情烦躁到想要拍桌子,而且最后无处诉说。但在结对编程的时候,两个人一起开发代码,一起享受debug的”乐趣“,看到有人和自己一起因为共同的代码受苦,心态也好了许多,debug也更加积极,编程氛围也很轻松愉快。
- 加快对新知识的熟悉和学习,降低开发代价。我的搭档是一名大佬,曾啃过《C++ Primer》,因此对于C++比我熟悉的多,在我编程过程中,可以给我提供许多指导,避免了我花费在查找资料上的时间,也预防了很多细节上的错误。
- 结对期间产出的代码质量更高一些。结对编程时,有搭档盯着屏幕看,一些细小的粗心导致的错误,往往能够被立即发现,免得之后被粗心导致的bug卡住。
结对编程的缺点:
- 时间协调问题。由于我们还都是学生,没办法把精力全用在开发项目之中。特别到了大三,每个人选的课几乎都不相同,也都有科研、助教、冯如杯等等一些不同的私事,而且现在还是在家远程结对编程,家事也不可能不管不顾,因此时间的协调比较麻烦,有一些比较简单的、已经确定好设计的部分也就各自抽空完成了。
- 远程结对编程还存在交流不便的问题。有一些细节小问题,比如某一行、某一个字母、某一个数字,如果在线下的话可以直接用手指出,或”夺下“键盘鼠标直接改掉。但是线上交流就不得不用用语言描述和定位,一些很简单的操作,由于着急或者其他原因一时间表达不清楚,就会浪费时间。如果是线下结对编程的话,应该不会存在该问题。
结对编程的每一个人的优缺点:
- 我的搭档的优点
- 学习能力强。充满学习热情,能很快的学习新的语言和开发工具,如本次项目中的QT开发工具。
- 表达能力强。能够快速发现问题并描述出来,也负责与其他队伍交流接口和对拍等事务。
- OOP思想、C++语言细节等基础知识很熟悉。给了我很多帮助
- 我的优点
- debug和测试更有耐心一点。
- 对算法与代码实现的细节关注的更多一些。
- 对作业要求更上心一些。
(分奴嘴脸)
- 我的搭档的缺点
- 不喜欢一些繁琐枯燥的工作
- 我的缺点
- OOP和c++基础不牢固,代码一股面向过程的风格
- 有的时候过于死板和犹豫,需要队友”断言“以提高效率
总结:学识渊博、代码规范、能说会道、规划合理,我的搭档的是我见过最强的搭档。
十三、警告信息消除
使用默认的规则。可以看到已经没有任何错误和警告。
【附加题】 跨组对接调用其他组的计算核心
合作小组中的两位成员:17373124 闫苗、17373299 刘紫涵
我们与其合作,互换了计算核心dll,测试结果如下,可以正常运行,git仓库链接
这是我们简单测试他们的dll的结果:
这是他们测试我们dll的结果:
由于在最初设计时就已经进行过大致的商量和规划,他们采用的gui接口函数格式与我们的不同之处很少,只需修改接口部分少量代码即可正常对接。
IMPORT_DLL int guiProcess(std::vector<std::pair<double, double>>* points,std::vector<string> msg);
- 这是我们测试他们dll的GUI接口对接结果
- 这是他们测试我们dll的GUI接口对接结果