C++ 實習測試: 筆劃 (Stroke) 類別製作 |
時間: 80分鐘 (10:40 上傳時間截止) |
在這個測試裡, 我們要製作一個「筆劃」的類別, 目的是支援一個「1052Quiz1 隨意筆」的繪圖程式來運作, 這個程式執行起來的畫面如下: 你可以點上面的連結 (或是上面這張圖) 在你的機器上執行 (如果沒有安裝 vc2010, 需要有 vc10 redistributable DLLs, 請下載、解壓縮、拷貝 32bit\*.dll 或是 64bit\*.dll 到 c:\windows\system32) 你可以運用滑鼠在視窗裡隨意繪圖, 例如: 這個程式和 Windows 的小畫家不一樣, 小畫家是一個點陣圖的繪圖程式, 整個視窗是一個畫布, 畫出來的都是獨立的點, 每一筆並不是一個物件, 你不能再用滑鼠去點一個筆劃, 去修改原先的東西; 我們這個程式希望是一個向量化的繪圖程式, 所以我們希望用筆劃為單位來存放資料, 像上圖裡面就只有兩個筆劃。這個程式還沒有完整的功能, 目前可以開多個視窗、分割視窗、畫粗線條、畫細線條、清除畫布、存檔、讀檔、列印、預覽列印等等,複製/貼上、選取筆劃、選取區域中所有筆劃、刪除所選取筆劃、修改所選的筆劃的粗細、修改顏色、移動某一筆劃、放大/縮小、移到最上層、下移一層、上移一層,...都還沒有寫, 別擔心, 不是請你現在寫這些, 只是希望你設計一個筆劃的類別來儲存一筆所畫出來的資料, 以便支援這個程式。 這個程式的存檔功能在設計的時候, 存出去的 *.qz1 檔案是二進位模式的檔案, 在其它文字編輯器裡面讀出來會是亂碼, 不過再由這個程式讀進來的話還是可以顯示的。為了這次考試, 我們額外設計了一個功能是用文字模式來儲存 *.txt 的文字檔案如下圖: 選取上面的選單以後可以看到 Windows 標準的檔案選取對話盒: 填入檔案名稱 (例如 CS)、按下存檔可以把筆劃的資料存在 CS.txt 中 這個檔案中有上圖中筆劃的資料 (你可以自己產生資料檔案, 也可以下載下面的文字檔案) 以這個檔案來說, 裡面主要依序包含兩個筆劃的資料,格式說明如括號中文字: 2 (本檔案裡筆劃總數) 64 -45 140 -128 (「含括第一筆的最小長方形邊框」, 左上右下, 請見下圖說明) 5 (第一筆的粗細) 205 (第一筆的點數) 134 -60 (第一筆的第一點的 x 座標與 y 座標) 133 -60 (第一筆的第二點的 x 座標與 y 座標) 131 -57 。。。 。 。 。 126 -103 「含括某一筆劃的最小長方形邊框」所代表的意思如下圖: 也就是含括那一筆劃圖形的最小長方形 - (left, top, right, bottom), 也就是所有這個筆劃的圖形的 x 座標的範圍是 left+width ≤ xi < right-width, y 座標的範圍是 bottom+width ≤ yi < top-width, 這裡因為筆劃有寬度 width, 所以我們直接加上筆劃的寬度來簡單近似這個最小長方形, 也就是最小的長方形是覆蓋那些點座標的長方形四周再放大 width 格的那個長方形, 請注意所有的點的 x 座標都小於 right-width, 所有的點的 y 座標都大於 bottom+width。 |
下面是運用 stdio.h 函式庫來開啟這個檔案並且讀取資料的 C 程式, 程式直接把資料讀到一個自己定義的結構 struct Point 的陣列中 #include <stdio.h> #include <assert.h> struct Point { int x; int y; }; struct BoundingRect { int left; int top; int right; int bottom; }; int isEqual(BoundingRect br1, BoundingRect br2); void calcBoundingRect(int length, int width, Point stroke[], BoundingRect *br); int main() { FILE *fp; int i, j, nStrokes; int widths[2]; int lengths[2]; BoundingRect brs[2], mybrs[2]; Point strokes[2][500]; fp = fopen("CS.txt", "r"); if (fp==0) { printf("無法開啟 CS.txt\n"); return 1; } fscanf(fp, "%d", &nStrokes); for (i=0; i<nStrokes; i++) { fscanf(fp, "%d%d%d%d", &brs[i].left, &brs[i].top, &brs[i].right, &brs[i].bottom); fscanf(fp, "%d", &widths[i]); fscanf(fp, "%d", &lengths[i]); for (j=0; j<lengths[i]; j++) fscanf(fp, "%d%d", &strokes[i][j].x, &strokes[i][j].y); calcBoundingRect(lengths[i], widths[i], strokes[i], &mybrs[i]); printf("%d %d %d %d\n", brs[i].left, brs[i].top, brs[i].right, brs[i].bottom); printf("%d %d %d %d\n", mybrs[i].left, mybrs[i].top, mybrs[i].right, mybrs[i].bottom); assert(isEqual(brs[i], mybrs[i])); } fclose(fp); return 0; } int isEqual(BoundingRect br1, BoundingRect br2) { return (br1.left==br2.left)&& |
請撰寫一個 C++ 程式, 運用 iostream 開啟檔案並且讀取資料, 同時設計一個 Stroke 類別, 這個類別需要管理一筆劃裡面所有的點, 這個類別以及程式的要求如下:
|
下面是一個實作 筆劃 (Stroke) 類別的 C++ 物件化程式 |
#include <iostream> #include <fstream> #include <cassert> #include <vector> #include <cmath> // sqrt #include <cstdlib> // exit using namespace std; struct Point { Point(): x(0), y(0) {} Point(int x, int y): x(x), y(y) {} int x; int y; }; struct BoundingRect { BoundingRect(): left(0),top(0),right(0),bottom(0) {} int left; int top; int right; int bottom; }; class Stroke { public: Stroke(int width); void addPoint(Point point); void finish(); int select1(Point pt, double r); int select2(Point pt, double r); void shift(Point offset); void deletePoint(int idx); static void unitTest(); private: void calcBoundingRect(); double lineDist(int idx, Point pt); bool isBoundingRectEqual(BoundingRect br); vector<Point> m_points; int m_width; bool m_inputMode; BoundingRect m_br; }; Stroke::Stroke(int width) : m_width(width), m_inputMode(true) { } void Stroke::addPoint(Point point) { if (m_inputMode) m_points.push_back(point); } void Stroke::finish() { m_inputMode = false; calcBoundingRect(); } void Stroke::calcBoundingRect() { int i; m_br.left = 10000, m_br.top = -10000; m_br.right = -10000, m_br.bottom = 10000; for (i=0; i<m_points.size(); i++) { if (m_points[i].x < m_br.left) m_br.left = m_points[i].x; if (m_points[i].x > m_br.right) m_br.right = m_points[i].x; if (m_points[i].y < m_br.bottom) m_br.bottom = m_points[i].y; if (m_points[i].y > m_br.top) m_br.top = m_points[i].y; } m_br.left -= m_width; m_br.right += (m_width+1); m_br.top += m_width; m_br.bottom -= (m_width+1); } int Stroke::select1(Point pt, double r) { int i, min=-1; double d, dmin=10000.; for (i=0; i<m_points.size(); i++) if ((d=sqrt((double)(m_points[i].x-pt.x)* (m_points[i].x-pt.x)+ (m_points[i].y-pt.y)* (m_points[i].y-pt.y))) < r) if (d<dmin) dmin=d, min=i; if (min!=-1) return min; else return -1; } double Stroke::lineDist(int idx, Point pt) { if (m_points[idx].x>=m_points[idx+1].x) { if (pt.x>m_points[idx].x||pt.x<m_points[idx+1].x) return 10000; } else { if (pt.x<m_points[idx].x||pt.x>m_points[idx+1].x) return 10000; } if (m_points[idx].y>=m_points[idx+1].y) { if (pt.y>m_points[idx].y||pt.y<m_points[idx+1].y) return 10000; } else { if (pt.y<m_points[idx].y||pt.y>m_points[idx+1].y) return 10000; } if (m_points[idx].x!=m_points[idx+1].x) { double a, b, c; a = (double)(m_points[idx].y-m_points[idx+1].y) / (m_points[idx].x-m_points[idx+1].x); b = -1; c = m_points[idx+1].y-a*m_points[idx+1].x; return fabs(a*pt.x+b*pt.y+c) / sqrt(a*a+b*b); } else if (m_points[idx].y!=m_points[idx+1].y) return fabs((double)pt.x-m_points[idx].x); else return sqrt((double)(m_points[idx].x-pt.x)* (m_points[idx].x-pt.x)+ (m_points[idx].y-pt.y)* (m_points[idx].y-pt.y)); } int Stroke::select2(Point pt, double r) { int i, min=-1; double d, dmin=10000.; for (i=0; i<m_points.size()-1; i++) if ((d=lineDist(i, pt)) < r) if (d<dmin) dmin=d, min=i; if (min!=-1) return min; else return -1; } void Stroke::shift(Point offset) { int i; if (m_inputMode) return; for (i=0; i<m_points.size(); i++) { m_points[i].x += offset.x; m_points[i].y += offset.y; } m_br.left += offset.x; m_br.right += offset.x; m_br.top += offset.y; m_br.bottom += offset.y; } void Stroke::deletePoint(int idx) { if (m_inputMode) return; if (idx>=0&&idx<m_points.size()) m_points.erase(m_points.begin()+idx); calcBoundingRect(); } bool Stroke::isBoundingRectEqual(BoundingRect br) { return (br.left==m_br.left)&& (br.top==m_br.top)&& (br.right==m_br.right)&& (br.bottom==m_br.bottom); } void Stroke::unitTest() { int i, j, nStrokes; int width, length; Point point; BoundingRect br; Stroke *stroke; vector<Stroke *> strokes; ifstream ifs("CS.txt"); if (!ifs) cout << "CS.txt nout found!\n", exit(1); ifs >> nStrokes; for (i=0; i<nStrokes; i++) { ifs >> br.left >> br.top >> br.right >> br.bottom; ifs >> width >> length; stroke = new Stroke(width); for (j=0; j<length; j++) { ifs >> point.x >> point.y; stroke->addPoint(point); } stroke->finish(); assert(stroke->isBoundingRectEqual(br)); strokes.push_back(stroke); } assert(strokes[0]->select1(Point(125,-53),0.001)==13); // (125,-53) assert(strokes[0]->select1(Point(125,-53),2.001)==10); // (127,-53) assert(strokes[0]->select1(Point(125,-52),1.001)==13); // (125,-53) assert(strokes[1]->select1(Point(178,-53),0.001)==4); // (178,-53) assert(strokes[1]->select2(Point(146,-67),0.001)==-1); // 45:(147,-66)~46:(145,-67) assert(strokes[1]->select2(Point(146,-67),0.45)==45); assert(strokes[0]->select1(Point(125,-53),0.001)==13); // (125,-53) strokes[0]->deletePoint(13); assert(strokes[0]->select1(Point(125,-53),0.001)==-1); assert(strokes[1]->select1(Point(180,-52),0.001)==0); strokes[1]->shift(Point(10,5)); assert(strokes[1]->select1(Point(190,-47),0.001)==0); for (i=0; i<nStrokes; i++) delete strokes[i]; cout << "Unit test finished successfully!\n"; } int main() { Stroke::unitTest(); } |
後續: 接下去能做的事情還蠻明顯的, 就是把整個視窗版的程式完成, 不過我們這學期的課程已經塞進去太多東西了, 不太可能在課程或是實習裡面安排時間講這些東西 (期中考後有一次實習會讓你配合一個視窗界面來寫程式), 如果有同學有興趣知道更詳細的設計方法, 可以讓我知道, 需要另外安排時間介紹。 |
回
C++ 物件導向程式設計課程
首頁
製作日期: 03/30/2017 by 丁培毅 (Pei-yih Ting) E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615 海洋大學 電機資訊學院 資訊工程學系 Lagoon |