項目 | 內容 |
---|---|
這個做業屬於哪一個課程 | 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!