GDI+中的座標系
1 什麼是座標系
座標系就是確定一組數據位置的標尺。按按照維數分爲2維平面座標系和3維空間座標系。其實2維座標系也是z=0的3維座標系的特例。
座標系有三要素,一是原點,二是方向,三是單位大小。如果兩個座標系這三點完全一樣,那麼這兩個座標系就完全相同。關於座標系和點的關係,我們可以這麼理解:點本身是固定的,但在不同座標系下的表示是不同的。那麼爲什麼要定義那麼多的座標系呢,答案是爲了描述方便。比如描述一個圓,如果把座標系原點放到圓心,那麼對圓的描述就是 x2+y2=r2。而如果原點不在圓心,那麼圓描述就成了:(x-x0)2+(y-y0)2=r2。
2 座標系變換與矩陣運算
既然可以找到描述形狀方便的座標系,那麼問題也來了。比如要同時描述兩個形狀,如兩個圓,而且這兩個圓是有相對位置的,比如是自行車的兩個輪子。
儘管兩個圓各自在自己的座標系裏都能很方便的描述,但是要建立兩者之間的關係時,卻遇到了麻煩。因爲要計算兩個圓的位置關係,必須把兩個圓放到同一個座標系下描述才行。所以就引出了座標系變換的概念。在此例中,可以把第二個圓也放到第一個座標系下描述,方法就是把第二個座標系放到第一個座標系中合適的位置(兩個座標系的關係),然後根據兩個座標系的關係,推算出第二個圓在第一個座標系中的描述。
這種方式對於CAD中的任務分隔特別重要,比如做汽車設計的公司,可以把不同的部件分配給不同的人來做。設計人員接到任務後,自由選擇合適的座標系來描述負責的部件。等所有部件設計完成以後,再把所有的部件轉換的整車座標系上。座標系間的轉換(2維和3維)是非常有規律的,有數學基礎的人可以自己推導公式,沒有數學基礎的也沒有關係,各種圖形庫都已經把座標系變換公式做成了函數API供程序調用。比如OpenGL提供了三維座標系間的各種變換API,GDI+則提供了2維座標的變換API。需要了解的是,座標系間的變換,一般是通過矩陣運算完成的,感興趣的讀者可以參考任何講解OpenGL座標變換算法的書籍,重要的是矩陣運算可以通過硬件流水線完成,這就是圖形顯示中的顯卡硬件加速的一部分。當然矩陣運算不光應用於座標系轉換,還廣泛運用於其他計算領域,因此有人提出了用GPU代替CPU來進行大規模科學計算的方案。
3 GDI+中的三種座標系
作爲Windows中圖形顯示的關鍵部件,GDI+代表了Windows下2維圖形API。三維則是D3D的領域了。圖形API要提供的函數大概是兩類,一是繪圖函數,二是座標系轉換函數。GDI+提供了很多繪圖函數,如DrawRectangle,DrawEclipse,DrawString等等。所有這些函數中都需要位置或大小參數,對於這些參數含義的理解是很重要的。
3.1 調用者自定義座標系(world)
一是參數的單位是什麼?位置參數的座標系是什麼?答案很有意思:不確定。因爲這些東西有調用者自由確定。那麼GDI+怎麼根據這些不確定的參數繪製圖形呢?答案是調用者要提供自己定義的座標系和PAGE座標系的關係。
3.2 Page座標系
Page座標系附屬在某一個窗口或控件上,是一個固定的座標系,原點位於窗口的左上角,x軸方向向右,y軸方向向下。單位爲cm,inch或pixel,根據實際情況設定。GDI+提供了Page座標系和World座標系間的轉換API。含義是把world座標系放到Page座標系合適的位置。回到前面講過的汽車分部件設計的例子,此處Page座標系就是最後的整車座標系,GID+提供的就是把各個部件(GDI+繪製函數繪製的圖形)連同其座標系一起放到整車(Page)座標系裏。
這是很合理的方式。在利用GDI+作圖時也要按照這種思路來做。具體說來,先把整個圖形分解成各個小的圖形,在畫某一個小的圖形時不要考慮它最終在Page座標系的位置,只要按照你自己設想的座標系來調用GDI+的繪圖函數就可以了。
當所有的圖形都繪製完畢後,在把這些小的圖形統統放到Page座標系裏。具體就是,調用繪製小圖形的代碼之前調用GDI+的xxxTransform()系列函數把小圖形的建模座標系放置到Page座標系裏,在繪製小圖形的代碼之後,調用ResetTransform()。
講到這裏,也許大家會有疑問了,GDI+最後是如何把Page座標系的圖形繪製到屏幕上的呢,這就是顯示器的Device座標系。
3.3Device座標系
對於Page座標系和Device座標系的轉換,應用程序員不需要了解了,GDI+已經把這部分隱藏了。
4 GDI+中座標系的轉換實例
4.1題目
利用GDI+繪製如下圖形:
4.2 分析
仔細看上面的圖形,不難發現,此圖形有6部分組成:頭,左臂,右臂,身體,左腿,右腿。分別把各個部分分給6個設計師去建模,然後把各個模型連同其建模座標系一起放到到Page座標系中。如下圖:
4.3代碼
private PointF pHead;
private PointF pBody;
private PointF pLeftArm;
private PointF pRightArm;
private PointF pLeftLeg;
private PointF pRightLeg;
private SizeF sHead = new SizeF(30, 30); //頭大小30cm
private SizeF sBody = new SizeF(50, 70); //身體大小
private SizeF sArm = new SizeF(10, 60); //胳膊大小
private SizeF sLeg = new SizeF(20, 70); //腿大小
public void DrawHead(PaintEventArgs e)
{
e.Graphics.DrawEllipse(Pens.Red, -sHead.Width / 2.0f, -sHead.Height / 2.0f, sHead.Width, sHead.Height);
}
public void DrawBody(PaintEventArgs e)
{
e.Graphics.DrawRectangle(Pens.Black, 0, 0, sBody.Width, sBody.Height);
}
public void DrawLeftArm(PaintEventArgs e)
{
e.Graphics.DrawRectangle(Pens.Black, 0, 0, sArm.Width, sArm.Height);
}
public void DrawRightArm(PaintEventArgs e)
{
e.Graphics.DrawRectangle(Pens.Black, 0, 0, sArm.Height, sArm.Width);
}
public void DrawLeftLeg(PaintEventArgs e)
{
e.Graphics.DrawRectangle(Pens.Black, 0, 0, sLeg.Width, sLeg.Height);
}
public void DrawRightLeg(PaintEventArgs e)
{
e.Graphics.DrawRectangle(Pens.Black, 0, 0, sLeg.Height, sLeg.Width);
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
pHead = new PointF(this.Width / 2.0f, 100f); //放置頭座標系
e.Graphics.TranslateTransform(pHead.X, pHead.Y);
DrawHead(e); //調用負責頭建模的代碼
e.Graphics.ResetTransform(); //重置矩陣
pBody = new PointF(pHead.X - sBody.Width/2.0f, pHead.Y+sHead.Height/2.0f);
e.Graphics.TranslateTransform(pBody.X, pBody.Y);
DrawBody(e);
e.Graphics.ResetTransform();
pLeftArm = pBody;
e.Graphics.TranslateTransform(pLeftArm.X, pLeftArm.Y);
e.Graphics.RotateTransform(45);
DrawLeftArm(e);
e.Graphics.ResetTransform();
pRightArm = new PointF(pBody.X + sBody.Width, pBody.Y);
e.Graphics.TranslateTransform(pRightArm.X, pRightArm.Y);
e.Graphics.RotateTransform(45);
DrawRightArm(e);
e.Graphics.ResetTransform();
pLeftLeg = new PointF(pBody.X, pBody.Y + sBody.Height);
e.Graphics.TranslateTransform(pLeftLeg.X, pLeftLeg.Y);
e.Graphics.RotateTransform(45);
DrawLeftLeg(e);
e.Graphics.ResetTransform();
pRightLeg = new PointF(pBody.X + sBody.Width, pBody.Y + sBody.Height);
e.Graphics.TranslateTransform(pRightLeg.X, pRightLeg.Y);
e.Graphics.RotateTransform(45);
DrawRightLeg(e);
e.Graphics.ResetTransform();
}
}
5 MSDN中的幾個不妥
由於GDI存在的時間很長,而GDI+誕生後爲了兼容,一部分函數採用了與GDI相同的名稱,甚至參數,參數的含義在MSDN中也按照原來的解釋,導致了一些容易讓讀者誤解的地方。
5.1繪圖函數中關於左上角upper-left corner的描述
我們知道,GDI+的繪圖函數使用的座標系是由建模人員隨意定義的,以函數
publicvoid DrawRectangle(
Pen pen,
float x,
float y,
float width,
float height
)
爲例,MSDN中對x,y解釋如下:
x
Type: System. Single
The x-coordinate of the upper-left corner of the rectangle to draw.
y
Type: System. Single
The y-coordinate of the upper-left corner of the rectangle to draw.
然而此處的x,y真的是矩形左上角座標嗎?換句話說,左上是從什麼角度看的。如下圖:
在A1座標系中,DrawRectange()函數中的x,y確實代表了矩形的左上角。A2和A3是A1經過旋轉以後得到的座標系,此時x,y在建模者看來顯然是右下角和左下角。而對於更常見的笛卡爾右手座標系B來說,(x,y)也不是左上角。左上角的概念僅在Page座標系中成立。關於(x,y)的正確解釋應該是:
矩形四個角中,x,y值都最小的那個角的座標。
5.2 MatrixOrder問題
5.2.1不帶MatrixOrder參數的變換函數順序執行的理解
前面提到過其實對座標系的轉換就是矩陣的乘法運算。這裏面涉及到了變換的順序和矩陣乘法的順序的問題。講解之前一定要確立座標系變換的視角:把建模座標系放置到Page座標系中,從Page座標系一步步變化成最終座標系。
可以通過兩步完成:
(1)平移至虛線位置;
TranslateTransform(0, dy);
(2)旋轉一定角度。
RotateTransform(45);
如果我們把(1)(2)順序反過來,那麼最終的結果如下圖:
所以說先平移、旋轉的順序很重要。當前的操作是在前一步完成之後的新座標系中進行的。對應於矩陣算法相當於左乘。
5.2.2 帶MatrixOrder參數的函數的理解
publicvoid TranslateTransform(
float dx,
float dy,
MatrixOrder order
)
在MSDN中對於MatrixOrder解釋爲:
其實際效果是,Prepend相當於無此參數的版本,也就是說此次調用是在前面操作結果的基礎上操作的;相當於矩陣左乘。
Append相當於此次操作先於前面的操作起作用,也就是先進行當前的操作,在此基礎上進行此次調用前面的操作。相當於矩陣右乘。在OpenGL中都是採用Append模式的。
如此看來,上述英文解釋正好相反了。這個不能說是錯,可能是從不同的視角來看問題,我們的視角是:從Page座標系一步步變化成最終座標系。
5.3 Graphics.Transform屬性
這個屬性比較特殊。MSDN的解釋爲:
獲取或設置此Graphics的幾何世界變換的副本。
獲取的是Graphics變換矩陣的副本,而不是變換矩陣本身,也就是每次獲取時,新建一個和變換矩陣成員值相同的矩陣對象,並返回。
問題是設置。設置的不是副本,而是Graphics變換矩陣本身。
內部實現僞代碼
class Graphics
{
private Matrix transform;
public Matrix Transform
{
get { return transform.Clone(); }
set { transform=value; }
}
}
如果要通過矩陣乘法進行座標轉換,那麼代碼如下:
e.Graphics.Transform = Graphics.Transform.Multiply(newMatrix(....))
6 變換函數不能解決的問題
GDI+提供了平移、旋轉、縮放等函數供我們調用,來把Page座標系一步步改造成最終的建模座標系在Page中的表現形態。對於絕大多數情況,這些函數足夠了,但是看如下:
無論怎麼平移和旋轉,Page座標系也無法編程上面紅色顯示的座標系。這是因爲Page座標系屬於左手座標系,而紅色所示是右手座標系。在數學上,大多數情況使用的都是右手座標系,模型也是建立在右手座標系上,那麼如何把Page轉化爲右手座標系呢。也就是把y軸反向。答案是通過直接操縱Graphics的變換矩陣。
e.Graphics.Transform = e.Graphics.Transform.Multiply(newMatrix(1, 0, 0, -1, 0, 0));//y反向
上述代碼就實現了y軸反向的目的。
如果你對變換矩陣很瞭解,直接操作矩陣左右乘法,與調用相關的GDI+變換函數功效完全相同,可以實現任意變換。
(後注:Y軸反方向可以通過g.ScaleTransform(1, -1)完成)
以下是原文引用地址:
http://blog.csdn.net/wishfly/article/details/8794536