GDI+中的座標系

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

http://blog.csdn.net/smstong/article/details/6405482

發佈了12 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章