项目 | 内容 |
---|---|
这个做业属于哪一个课程 | 2020春季计算机学院软件工程(罗杰 任健) |
这个做业的要求在哪里 | 结对项目做业 |
我在这个课程的目标是 | 经过这门课锻炼软件开发能力和经验,强化与他人合做的能力 |
这个做业在哪一个具体方面帮助我实现目标 | 体验结对编程的模式 |
因为GUI生成的文件过大(20MB)所以将代码和生成的GUI界面分别放在了两个仓库html
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 5 | 5 |
· Estimate | · 估计这个任务须要多少时间 | 5 | 5 |
Development | 开发 | 1,160 | 1,160 |
· Analysis | · 需求分析 (包括学习新技术) | 180 | 180 |
· Design Spec | · 生成设计文档 | 15 | 15 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 45 | 45 |
· Coding | · 具体编码 | 600 | 600 |
· Test | · 测试(自我测试,修改代码,提交修改 | 240 | 240 |
Reporting | 报告 | 30 | 40 |
· Test Report | · 测试报告 | 10 | 10 |
· Size Measurement | · 计算工做量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 10 | 20 |
合计 | 1,195 | 1,205 |
此次在构思代码上想了好久,学习Qt花费的时间比较多,所以总花费的时间也比较多,时间都用在学习新知识上了。c++
信息隐藏、接口设计和松耦合在面向对象课程中都有学习、应用过,在这里再次应用了。git
Information Hiding程序员
David Parnas在1972年最先提出信息隐藏的观点。他在其论文中指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。github
信息隐藏原则本意是但愿类里面定义的变量和结构应当按照必定的原则分配可见性,从而防止模块内容被恶意篡改,但考虑到此次做业的规模,以及对象类型,每一个几何元素更倾向于相似结构体的结构,并非一个真正的模块,所以在结对编程中咱们考虑了之后仍是将类里面的属性定义成了public,减小了代码的复杂性。正则表达式
Interface Degisn算法
接口设计有六大原则:编程
单一职责原则:应该有且仅有一个缘由引发类的变动。安全
里氏替换原则:全部引用基类的地方必须能透明地使用其子类的对象。ide
依赖倒置原则:面向接口编程
接口隔离原则:创建单一接口,不要创建臃肿庞大的接口。
迪米特法则:一个类应该对本身须要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就调用你提供的public方法,其余一律不关心。
开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
此次做业并无体现出不少面向对象的特性,可是咱们也遵循了迪米特法则、里氏替换原则等,除了防止一个函数过于冗长而拆分红几个小函数以外,各个模块的功能独立。
Loose Coupling
一个松耦合的系统中的每个组件对其余独立组件的定义所知甚少或一无所知。
本次做业在函数调用中,每一个通讯的参数都是基本类型的参数,能够直接调用,没有必要为互相的实现考虑。
//判断是否为数字,不然抛出异常 __declspec(dllexport) bool isNum(std::string s); //判断范围是否合理,不然抛出异常 __declspec(dllexport) bool rangeVaild(int n); //检查直线类型输入是否合法,是则更改x1, x2, y1, y2的值,不然抛出异常 __declspec(dllexport) void inputCheck(ifstream& fileIn, int& x1, int& y1, int& x2, int& y2); //检查圆类型的输入是否合法,是则更改x, y ,r的值,不然抛出异常 __declspec(dllexport) void inputCheck(ifstream& fileIn, int& x, int& y, int& r); // 计算两直线的交点 __declspec(dllexport) Point* calLineLineIst(Line line1, Line line2); // 计算圆与直线的交点 __declspec(dllexport) vector<Point> calLineCircleIst(Line line, Circle circle); // 计算两圆交点 __declspec(dllexport) vector<Point> calCircleCircleIst(Circle circle1, Circle circle2); //计算交点 __declspec(dllexport) MySet calculate(ifstream& fileIn, ofstream& fileOut); //line类,表明直线 class Line; //Ray类,表明射线,继承了Line类 class Ray; //Segment类,表明线段,继承了Ray类 class Segment; //Circle类,表明圆 class Circle
各个接口的做用已在注释中阐明,实现以下:
isNum:经过正则表达式来检查一个数字是否合法
rangeVaild:经过return n > -100000 && n < 100000来检查范围的合法性
inputCheck:经过使用以上两个函数来完成对输入数据的检查并赋值,重载了两个函数,分别针对圆和直线
calLineLineIst:使用公式法来求直线交点
calLineCircleIst:使用公式法来求直线交点
calCircleCircleIst:将两圆交点转换成圆与直线的交点,调用calLineCirclelst来求解
calculate:根据文件流处理输入、使用以上6个函数来检查输入合法性、求解交点,并输出
Line类:设计了两个方法,一个是检查直线是否平行,另外一个是检查直线是否重合,都使用了公式法
Ray类:继承了Line类,重写了父类检查是否重合的方法,并增长了检查点是否在射线上的方法
Line类:继承了Ray类,重写了检查重合与点在线段上的方法
计算模块部分沿用了上一次做业的计算方式,所以没有变化,我采用的是个人搭档的计算方法:博客
须要增长的关键方法为检查交点是否在线段/射线上,以及两条线段\直线\射线之间重合的状况,这个比较复杂,也是经过数学的计算之后分状况讨论。
考虑到射线和线段是一种特殊的直线,由于它们能够当作是直线截断造成的,所以产生了继承的想法,子类经过重写父类方法来实现本身的个性。
性能分析结果:
其中消耗最大的函数为Calculate,而unordered_set的维护了耗费了近一半的时间,为此我也查阅了关于容器效率的资料可是彷佛库函数提供的已是比较优的算法了,而计算手段上也经过减小浮点类型的运算进行了浅层的加速,inputCheck等函数则是为了异常处理而牺牲的性能。
Design by Contract
即契约式设计,在面向对象课程中专门有一个单元让咱们经过JML来体会契约式设计的思想,其强调前置条件、后置条件与不变式,是一种形式约束。也就是说,只要知足了这个条件,那么所设计的模块在理论上就必定是正确的,很是可靠,可是缺点是契约撰写的成本比较高,在复杂的模块中会很麻烦,也变得不易阅读。
Code Contract
和DBC的思想相似,优势也是使得模块变得可靠、安全,可是缺点是须要牺牲一部分的性能,增长代码的复杂度,下降运行效率。
本次做业中为了提升编码效率,咱们没有过多地使用契约式设计的思想,在一开始就明肯定义每一个模块的需求,能够有更高的效率。
单元测试部分和上次相似,可是增长了异常处理部分和直线与射线交点部分,例如测试数字是否合法:
TEST_METHOD(isNumTest) { Assert::IsTrue(isNum("0")); Assert::IsTrue(isNum("1")); Assert::IsTrue(isNum("100")); Assert::IsTrue(isNum("-1")); Assert::IsTrue(isNum("-100")); Assert::IsFalse(isNum("001")); Assert::IsFalse(isNum("-001")); Assert::IsFalse(isNum("a")); Assert::IsFalse(isNum("0a")); Assert::IsFalse(isNum("-0a")); }
经过构造错误的样例各1例来检查正则表达式是否正确
在其余模块的测试也是相似,构造正确样例和错误样例来检查模块功能是否正确,对于没有异常抛出计算模块则和上次做业同样分状况讨论:
单元测试覆盖率:
异常处理模块我设计了8种异常
名称 | 定义 | 例子 | 输出 |
---|---|---|---|
TFException | 输入图形个数过少 | 1 | 请输入至少两个图形! |
DSException | 用来肯定直线两点重合 | L 1 1 1 1 | 用来肯定直线的两点不能重合! |
SLException | 两条直线有无穷的交点 | S 1 1 3 3 R 0 0 2 2 |
有两个几何图形之间有无穷的交点 |
TException | 图形种类错误 | K 1 2 3 4 | 支持的图形种类仅为:C, L, S, R |
INException | 输入非整数 | L 001 a 3 2 | 坐标请输入一个(-100000, 100000)之间的无前导0标准整数 |
RIException | 圆的半径不合法 | C 1 1 -2 | 圆的半径不能够小于或等于0或者大于或等于100000 |
ArgumentError | 参数数量不对 | argc != 5 | 请检查命令格式: \n\tintersect.exe -i <input> -o <output>\n |
FileError | 打开文件失败 | 文件不存在 | 打开文件失败! |
界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。(5')
本项目的图形化界面采用 VS + Qt 进行开发的,图像的绘制主要使用 QPainter
的 paintEvent
机制。下面按照开发的时间顺序,对GUI的设计过程进行详细介绍。
QScrollArea
(滚动区域)与缩放滑块相结合的方法,以达到良好的绘制效果;各组件的布局基本采用Qt Designer进行;
设置按钮,触发可导入文件到 QListWidget
//设置打开文件的按钮 connect(ui.pbtn_open, &QPushButton::clicked, [=]() { //打开文件 m_FilePath = QFileDialog::getOpenFileName(this, "Open", ":\\file-path"); //将文件的路径显示到文本条中 ui.le_filepath->setText(m_FilePath); //将文件内容逐项显示到列表中 showList(); }); //将文件内容逐项显示到列表中 void CoopWorkGUI::showList() { if (m_FilePath.size() == 0) { return; } file.open(QIODevice::ReadOnly); if (!file.atEnd()) { graphic = file.readLine(); } while (!file.atEnd()) { graphic = file.readLine(); graphic.remove('\n'); graphics << graphic; } file.close(); ui.listWidget->clear(); ui.listWidget->addItems(graphics); }
鼠标右击 QListWidget
的 item
,弹出菜单,进行图形的添加和删除,双击可编辑
//设置列表项:双击编辑 connect(ui.listWidget, &QListWidget::itemDoubleClicked, this, &CoopWorkGUI::editListItem); //设置列表项:右键菜单 ui.listWidget->setProperty("contextMenuPolicy", Qt::CustomContextMenu); QMenu* popMenu = new QMenu(this); QAction* atn_add = new QAction(tr("Add"), this); QAction* atn_delete = new QAction(tr("Delete"), this); popMenu->addAction(atn_add); popMenu->addSeparator(); popMenu->addAction(atn_delete); connect(atn_add, &QAction::triggered, this, &CoopWorkGUI::onActionAdd); connect(atn_delete, &QAction::triggered, this, &CoopWorkGUI::onActionDelete); connect(ui.listWidget, &QListWidget::customContextMenuRequested, [=]() { popMenu->exec(QCursor::pos()); isRepaint = true; update(); }); //修改 void CoopWorkGUI::editListItem(QListWidgetItem* item) { item->setFlags(item->flags() | Qt::ItemIsEditable); m_EditIndex = ui.listWidget->currentRow(); isRepaint = true; CoopWorkGUI::update(); } //删除 void CoopWorkGUI::onActionDelete() { QList<QListWidgetItem*> items = ui.listWidget->selectedItems(); if (items.count() <= 0) { return; } if (QMessageBox::Yes == QMessageBox::question(this, QStringLiteral("Remove Item") , QStringLiteral("Remove %1 items").arg(QString::number(items.count())) , QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)) { foreach(QListWidgetItem * var, items) { ui.listWidget->removeItemWidget(var); items.removeOne(var); delete var; } } CoopWorkGUI::update(); } //添加 void CoopWorkGUI::onActionAdd() { ui.listWidget->addItem(tr("")); CoopWorkGUI::update(); }
建立 QSpinBox
(右上角小小的显示窗口)与 QSlider
(右上角的滑动条),实现数字、滑块位置与画板大小的联系
//设置缩放按钮 void(QSpinBox:: * spinboxSignal)(int) = &QSpinBox::valueChanged; connect(ui.spinBox, spinboxSignal, ui.horizontalSlider, &QSlider::setValue); connect(ui.spinBox, spinboxSignal, ui.horizontalSlider, [=]() {isRepaint = true; update(); }); connect(ui.horizontalSlider, &QSlider::valueChanged, ui.spinBox, &QSpinBox::setValue); connect(ui.horizontalSlider, &QSlider::valueChanged, this, &CoopWorkGUI::resizeWidget);
建立能够滚动的画板(直接在 QScollArea
上没法做画)
//设置绘图区域:滚动条 QScrollArea* scrollArea = new QScrollArea(ui.widget); scrollArea->setWidget(ui.wdt_scroll); ui.wdt_scroll->setMinimumSize(1000, 1000); QHBoxLayout* pLayout = new QHBoxLayout(); pLayout->addWidget(scrollArea); pLayout->setMargin(0); pLayout->setSpacing(0); ui.widget->setLayout(pLayout);
绘画逻辑的实现:每次图形列表有更新(添加、删除、从新载入文件),相应地对图形进行更新
//设置事件分发器 ui.wdt_scroll->installEventFilter(this); bool CoopWorkGUI::eventFilter(QObject* obj, QEvent* ev) { if (obj == ui.wdt_scroll && ev->type() == QEvent::Paint && isRepaint) { //画图 isRepaint = false; painteGraphics(); return true; } else { return QWidget::eventFilter(obj, ev); } }
主要测试:列表的增删改、图形的绘制、各按钮的触发是否与预期一致。
测试结果:列表增删改无问题;图形绘制中增长或修改列表项时图形的重绘不及时;各按钮触发与预期一致,可是一样存在图形重绘不及时的状况。
错误溯因:paintEvent
是由 update()
函数所发送的信号触发的,可是当咱们手动调用 update()
函数时,Qt 并不会当即调用 paintEvent
进行重绘,它会先自动进行一个需不须要重绘的判断,决定是否重绘,从而致使图形绘制与预期不符。
美化:主要对界面的控件进行了二次调整,增长了显示交点个数的小显示窗口,并调整了绘制窗口的背景颜色。
界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。(4')
DLL调用:DLL调用花费了较大的精力,最初咱们企图使用 .dll+.h 的显示调用方法,利用QLibrary
的 load
方法加载DLL库,可是很不幸,失败了。因而咱们选用了 .dll+.h+.lib 的隐式调用方法,最终成功加载了DLL,具体调用方法以下,
核心模块的接口定义,加关键字 __declspec(dllexport)
__declspec(dllexport) MySet result(vector<string>) fileIn;
VS项目属性中,配置属性 - 常规 - 配置类型 改成 动态连接库(.dll)
,从新生成解决方案后,在相应目录就会有.lib
和.dll
文件;
将.lib
.dll
和 (须要用到的).h
文件拷贝到GUI项目的目录中;
在GUI的头文件中添加下述语句,隐式调用DLL库
#pragma comment(lib, "CoopWork.lib")
将须要的.h文件包含进GUI项目中,并对接口定义进行相应的修改
__declspec(dllimport) MySet result(vector<string>) fileIn;
如今就能够直接使用接口函数进行计算啦!
完成计算:这一步很简单,就是调用计算模块已经封装好的接口函数,输入图形容器,输出交点容器,遍历交点容器进行交点绘制便可,代码以下
if (!ui.radioButton->isChecked()) { return; } //s_graphics中存放的是当前列表中的图形参数 if (s_graphics.size() > 0) { s_graphics.insert(s_graphics.begin(), to_string(s_graphics.size())); for (int i = 0; i < s_graphics.size(); i++) { qDebug() << QString::fromStdString(s_graphics.at(i)); } try { m_Points = result(s_graphics); qDebug() << m_Points.size(); for each (Point var in m_Points) { painter.drawPoint(QPointF(var.x * m_scale, var.y * m_scale)); qDebug() << var.x << var.y; } ui.label->setText("Total: " + QString::number(m_Points.size())); } catch (const std::exception& e) { qDebug() << e.what(); } }
最终实现的功能
支持从文件导入几何对象的描述
支持几何对象的添加、删除、修改
支持绘制现有几何对象(请见上图)
支持求解现有几何对象交点并绘制
其余功能:右上角缩放滑块能够调整画布大小,滑动条能够调整画布交点。
咱们使用了腾讯会议进行交流:
结对编程 | 我 | 同伴 | |
---|---|---|---|
优势 | 提升编码效率、在开发阶段能尽早发现bug、交流效率较高 | 编码效率高、编程基础扎实、思考较仔细 | 乐于学习新知识、接受能力强、交流积极 |
缺点 | 容易形成思惟定势,两人都没法发现bug、出现分歧较难处理 | 耐心不足 | 有点粗心 |
在博客中指明合做小组两位同窗的学号,截图展现互换后的运行结果和测试结果。此外,博客中还需分析两组不一样的模块合并以后出现的问题,为什么会出现这样的问题,以及是如何根据反馈改进本身模块的。
我方 | 对方 |
---|---|
LJC: 17373456 WXC: 17373459 | SYB: 17373452 SXD: 17231151 |
完成对接的项目地址:https://github.com/AmanogawaSaya/IntersectGUI
在项目文件中的 “DLL_对接” 文件夹中,分开存放了我方的DLL文件与对方的DLL文件,将想要测试的DLL文件复制到与 CoopWorkGUI.exe 同级的目录下,双击.exe便可运行.
A 的核心模块,加上 B 的测试模块和用户界面模块(命令行和 GUI)
B 的核心模块,加上 A 的测试模块和用户界面模块(命令行和 GUI)
因为双方的接口函数都是void类型的函数,所以咱们以最终结果为依据进行了测试,测试结果均正确。
接口不一致
俗话说,“凡事预则立,不预则废”,诚不我欺。最初进行项目规划的时候,没能正确理解做业要求中 “松散耦合” 的含义,没有提早找好对接的小组并对接口进行统一约定。这直接的后果就是,在进行对接的时候,面临了极大的问题:为了下降项目内各函数的耦合度,咱们小组在开发时尽可能避免 全局变量 的使用,采用函数传参的方式进行各个容器和特征值的修改;可是对方小组在开发时大量采用了全局变量,包括图形容器等。
为了实现 “DLL直接交换”,咱们不得不选择了妥协,在新的分支中,增长了对方小组的全部接口。
同时,为了GUI也能进行匹配,咱们从新改写了GUI的计算模块,调用了新的接口。
DLL加载失败
最初DLL加载失败,抛出异常,通过探索,发现这一异常是由X86与X64不一样编译环境有关,咱们在统一的 Release X64
环境下从新构建了项目,同时选择同一版本的 Qt,最后成功调用了对方的 DLL!