GDI中的坐标映射问题
GDI中的坐標映射問題
阿里
在我們進行繪圖程序的開發時,不可避免地會遇到坐標映射的問題,而這恰恰是一個很傷腦筋、繞也繞不明白的問題。我就經常為此而一卡就是幾個小時,恨得要命,終于有一天心一橫,豁出一個周末的晚上,啃了所有找得到的資料,特別是那蝌蚪一般的MSDN,發現了相關問題的冰山之一角,不過就這就已經有一種豁然開朗的感覺了,把它寫出來還希望能夠對受到同樣問題困擾的各位看官有一點點幫助,同時也希望編程大俠們不要因為對這樣一個簡單的問題不屑一顧而見笑。
首先要明確的一點是,繪圖語句中使用的坐標始終是邏輯空間的坐標值,而我們最終要繪制的目的地則是物理設備空間(physical device space)。
1.預備知識:GDI中所規定的四種坐標空間(或者叫坐標系)。
1.1 world坐標空間:引入world空間的目的是對圖像進行平移、縮放、剪切等操作,其最大坐標范圍為2^32個單位高,2^32個單位寬,初始狀態時x軸正向向右,y軸正向向上。World坐標空間可以成為邏輯空間。
1.2 page坐標空間:當沒有world空間時,它就是邏輯空間,而且這種情況是最普遍的。最大坐標范圍為2^32個單位高,2^32個單位寬,初始狀態時x軸正向向右,y軸正向向上。
1.3 device空間:設備空間,是坐標變換的常規目的地。最大坐標范圍為2^27個像素高,2^27個像素寬。其特點是x軸正向向右,y軸正向向下,原點在物理設備左上角,而且這些規則我們不能改變。
1.4 physical Device空間:這一空間代表著具體的物理設備,是我們實際能看到的坐標空間,也是圖形繪制的最終目的地,我們繪制的一個大尺寸圖形到底能讓我們看到多少,完全取決于它的大小。它可以是Windows窗口的客戶區,或者是整個桌面,或者是打印機的一頁紙,或者是繪圖儀的一頁紙。
圖1 坐標映射流程
2.從Windows系統的角度來看GDI坐標映射。
首先我們從Windows系統的角度來看坐標映射是如何進行的,或者說來看看,Windows是如何將我們在程序中使用的邏輯空間坐標值轉換成為物理設備空間坐標值的。它通常分成以下3個步驟。
第1步,world空間 → page空間。如果程序員使用SetWorldTransform函數明確定義了world空間向page空間映射的公式,那么windows將進行這種映射,具體規則由SetWorldTransform函數定義,此時的邏輯空間是world空間,。
如果沒有出現SetWorldTransform函數,Windows將不進行world空間到page空間的映射,而直接進行page空間到device空間的映射,此時的邏輯空間是page空間。
事實上world空間是Windows98以后才引入的,我們一般情況下是用不到它的。但是如果我們要將邏輯空間以一種“扭曲”的方式在物理設備上表現出來,world坐標空間是一個非常好的工具。
第2步,page空間 → device空間。這是我們程序員最關心的一個映射步驟,映射規則是:
?
其中,Di表示x或者y方向的設備空間坐標,單位是像素(pixel);
Li表示x或者y方向的page空間坐標,單位是邏輯單位(即自己定義);
L0表示window的原點在page空間中的坐標值,單位是邏輯單位;
WE表示window的寬(高)度,由SetWindowExtEx(W, H)函數確定,單位是邏輯單位。
VE表示viewport的寬(高)度,由SetViewportExtEx(W, H)函數確定,單位是像素。
D0表示viewport的原點在device空間中的坐標值,單位是像素。
看不太明白不要緊,因為我們并不需要操心這個公式,讓Windows去頭疼好了,不過基本的原理我們還是要了解的,這樣才能對坐標映射有更深的了解,這也是我將公式寫出來的原因。
第3步,device空間 → physical device空間。這一映射遵從一對一原則,即device空間的一個像素就是physical device空間的一個像素,并且它們的坐標原點在物理設備的左上角,坐標方向是x軸正向向右,y軸正向向下,記住是向下!。這個映射的規則我們程序員是不能改變的,這也就是所謂的設備無關性。比如說,我們要在一個客戶區窗口(physical device)進行繪圖,我們根本不要管這個客戶區具體在哪里,又是如何顯示的,我們只需把它對應的device空間作為“畫布”,在這個畫布上進行輸出就行了,其它工作完全由Windows自動完成。
3.從程序員的角度來看坐標映射
坐標映射在程序員的眼中就是要根據自己實際問題的要求,構造出一個滿足要求的邏輯空間。所謂的滿足要求就是指每一個我們在程序中使用的點,都能出現在physical device上我們預期的相應位置。由于device空間到physical device空間是一對一的映射,因此,我們完全可以將繪圖目的地看成device空間,所構造出的邏輯空間也只需正確映射到device空間就可以了。
3.1 page空間 → device空間
如果我們不使用world空間,此時的邏輯空間就是page空間。下面來看如何確定它的三個要素:單位刻度值、方向、原點。
首先要使用SetMapMode(int)函數選擇映射模式。這其中有6種事先已經定義好了的模式,可以直接拿來就用,比如MM_HIMETRIC模式表示page空間的單位刻度是0.01毫米,x軸正向向右,y軸正向向上,原點與device空間的原點重合。如果此時程序中有一條值為10的線段,那么在程序員的眼中,這就是一條10×0.01=0.1毫米的線段,不管使用多大分辨率的顯示器它都是這么長,我們甚至可以用尺子在屏幕上量量試試。如果選擇預定義的映射模式,相當于微軟已經為我們構造好了page空間,下面的事我們就都不用做了。
但是很多時候,微軟的東西不一定適合我們,此時就要將映射模式設定為MM_ISOTROPIC或者MM_ANISOTROPIC,使用下面的四個函數定義我們自己的坐標系:
SetWindowExt(int Lwidth, int Lheight) //參數的單位為邏輯單位(Logical),如果參數為負值表示window相應的坐標軸與page空間相反。
SetViewportExt(int Pwidth, int Pheight) //參數的單位為像素(Pixel),如果參數為負值表示viewport相應的坐標軸與device空間相反。
SetWindowOrg(int Lx, int Ly)。
SetViewportOrg(int Px, int Py)。
這四個函數提出了兩個新的概念:window和viewport,它們分別與page空間和device空間對應,但請記住并不是對等。引入它們的目的僅僅是為了確定page空間的單位刻度、方向、原點。
1.x軸的單位刻度=| Pwidth | / | Lwidth |。
這表示x軸上一個邏輯單位等于多少個像素。下面舉例加以解釋。
比如我們先通過GetDeviceCap(LOGPIXELSX)獲得在我們的顯示器上每英寸等于多少個像素,設為p,然后我們將它賦給Pwidth,將Lwidth賦成2,即Pwidth / Lwidth=p / 2。那么,此時page空間x軸上的單位刻度就是p / 2個像素;又由于p個像素是代表一個英寸的,所以此時的page空間x軸上的單位刻度同時也是半個英寸。
請注意這個例子中,雖說viewport的x方向“范圍”是p個像素,但是device空間x軸的“范圍”決不僅僅是p個像素,而是2^27個像素,至于可視的范圍到底是多少,則取決于物理設備空間。
2.x軸的方向:這個好確定,Lwidth與Pwidth同號,則page空間的x軸方向與device空間x軸方向相同,否則相反。
3.原點。這個就有一點麻煩了,我們需將window與viewport進行重疊,包括原點和坐標軸方向,然后才可以確定page空間的原點。下面通過一個例子來加以說明。
例:假設我們通過下面的語句構造了一個page空間:
SetMapeMode(MM_ANISOTROPIC);
SetWindowExt(10, 100);
SetWindowOrg(0, -100);
SetViewportExt(20, 200);
SetViewPortOrg(0,-200);
?
圖2 page空間映射到device空間的例子
(由于100個邏輯單位相當于200個像素,因此我將它們的示意長度畫成一樣。)
從這些語句中我們可以很快確定出page空間的單位刻度(比如y軸上每邏輯單位200 / 100=2個像素),以及y軸的方向與device空間相同(100與200同號),但是page空間的原點在哪里呢?請看:
首先我們分別在page空間中畫出window坐標系、在device空間中畫出viewport坐標系(如圖2的左邊部分)。然后由于例子中的window坐標方向與viewport相反,還需將page空間翻轉(見圖2中間部分)。最后將window與viewport重疊(見圖2右邊部分),使它們的原點和坐標方向都一致。此時我們可以清楚地看到,page空間的原點就對應于device空間的原點,而且方向也和它相同。
通過以上的1、2、3點我們就可以完全確定一個適合我們自己要求的page空間,當我們不要world空間時,它就是邏輯空間。
另外還有一個問題就是要注意MM_ANISOTROPIC與MM_ISOTROPIC的區別。對于前者來說,x方向的單位刻度與y方向的單位刻度可以不同(當然也可以相同),但是后者x方向的單位刻度與y方向的單位刻度一定是相同的,如果通過計算window與viewport范圍的比值得到兩個方向的單位刻度值不同,那么將會以較小的那個為準。
3.2 world空間 → page空間
有時候我們需要從一個傾斜的角度顯示一個圓或者其它什么圖形,但是我們在使用繪圖語句時,心目中仍然要當這個圓正對著我們來考慮問題,因為只有這樣,我們在構造圖形時的思維才不至于混亂,怎么實現呢?就可以通過加上world空間達到這個目的。由于一般很少使用這種映射,我在這里只以一個例子簡單加以說明。
void CSampleView::DrawShearCircle()
{
?????? CClientDC dc(this);
?????? dc.SetMapMode(MM_ANISOTROPIC);? //映射模式設定為各向異性。
?????? //以下語句將page空間最小刻度值設為1mm,原點位于客戶區矩形中心,x正向向右,y正向向上。
?????? dc.SetWindowExt(1, -1);
?????? int PperMMX = dc.GetDeviceCaps(HORZRES) / dc.GetDeviceCaps(HORZSIZE);
?????? int PperMMY = dc.GetDeviceCaps(VERTRES) / dc.GetDeviceCaps(VERTSIZE);
?????? dc.SetViewportExt(PperMMX, PperMMY);
?????? CRect cr;
?????? GetClientRect(&cr);
?????? dc.SetViewportOrg(cr.right/2, cr.bottom/2);
?????? dc.SetWindowOrg(0, 0);
?????? //以下語句設置world空間到page空間的映射規則,將會產生一個y軸的剪切。
SetGraphicsMode(dc.GetSafeHdc(), GM_ADVANCED);?? //一定要首先打開GM_ADVANCED。
?????? XFORM xf;
?????? xf.eM11 = 1.0;
?????? xf.eM12 = 1.0;? //y軸方向的剪切常量為1.0
?????? xf.eM21 = 0.0;? //x軸方向的剪切常量為0.0
?????? xf.eM22 = 1.0;
?????? xf.eDx? = 0.0;
?????? xf.eDy? = 0.0;
?????? SetWorldTransform(dc.GetSafeHdc(), &xf);
??????
?????? dc.Rectangle(-50, 50, 50, -50);? //這個矩形的中心在客戶區中心,長度為100mm。不過由于設置了world空間,盡管從語句上來看是一個正方形,但是實際顯示的卻是一個銳角為45°的菱形。
?????? dc.Ellipse(-50, 50, 50, -50);? //盡管從語句上來看是一個圓,但是實際顯示的卻是一個橢圓。
}
4.結語
以上只是我的一些不成熟的看法,如果有不實之處,還望來信探討:alialili@163.net
總結
以上是生活随笔為你收集整理的GDI中的坐标映射问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WM_PAINT消息小结
- 下一篇: 学习强制删除正在运行的文件