| 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 |