本文是北京航空航天大學計算機學院軟件工程課程的結對項目做業,在本次做業中,兩位同窗一組,以結對編程的方式共同完成一項需求。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來描述某些接口的數據類型,又該採用什麼樣的方式設計呢?