本文轉自:http://anony3721.blog.163.com/blog/static/511974201132210856781/
視窗: 邏輯環境中的一小部分,是一個矩形框;全局座標系是邏輯座標,SetWindowOrg(X,Y )設定視窗的邏輯座標點(X,Y)映射為的裝置環境的裝置點(0,0)。
裝置環境:顯示器、印表機等等。座標係為裝置座標系,正Y軸向下,正X軸向右,原點在左上方,固定不變,不可修改!其X、Y的負半軸為虛設,無法顯示或無法列印圖形。
視口: 裝置環境中的一部分,一個矩形框;座標系同裝置環境。SetViewportOrg(X,Y )設定視口矩形框的座標原點,預設值為裝置座標原點。視口是視窗的按比例的映像(投影)。
1. 視窗和視口以及裝置環境出現的緣起
視窗和視口實際上描述的是同一個客觀情境,它們之間的區別僅僅在於兩者座標系的單位不同。裝置環境就是顯示裝置。視窗和視口中的情境必須通過映射模式正確的顯示到裝置環境中。 預設情況下,Windows GDI 繪圖中的視窗座標原點、視口座標原點以及裝置點重合在一起都是在裝置點(0,0)處。Windows GDI的裝置環境原點是位於螢幕的左上方,裝置點始終位於(0,0)點,x軸向右,y軸向上;OpenGL使用的視窗座標和WindowsGDI使用的視窗座標是不一樣的。OpengL預設世界視窗座標原點位於螢幕中心,x軸向右,y軸向上。視口原點在螢幕左下角。
視口與視窗表徵的是同一個實際幾何物體,所以有視窗必定有視口,有視口必定有視窗。視窗或視口可以理解為用眼睛看到的物體實體例如一個球,裝置環境顯示地區(顯示屏)可以理解為由於電腦螢幕長寬比不同,實際物體在電腦螢幕上看到的最終顯示結果,例如顯示屏中的球看起來變成一個橢球。幾乎在所有的GDI函數中使用的座標值都是採用的邏輯單位,也就是物體實際多大就是多大。Windows必須將邏輯單位轉換為“裝置單位”---像素,也就是物體在裝置環境(顯示屏或印表機)顯示後看起來應該多大。這種轉換是由映射方式、視窗和視口的原點以及視窗和視口的範圍所控制的。
通過不同的映射方式可以在邏輯單位下改變視窗原點的位置而將物體現實在裝置環境中,也可以在以像素為單位的視口座標系下移動視口將物體顯示在顯示屏上,不管對視窗和視口原點作什麼改變,裝置點(0,0)始終是客戶區的左上方,兩者都是為了讓物體在顯示屏上現實。調整視窗原點和調整視口原點可以達到相同的效果。
視口是與裝置相關的一個矩形地區,座標單位是與裝置相關的,直觀的視口原點座標的移動就是dc的移動。視窗的座標是邏輯座標,與裝置無關。視窗座標的原點與視口座標的原點始終對應於同一點。對於同一個圖形,用視窗座標系統表達的該地區的長和寬與視口的座標系統表達的長和寬是不同的,因單位不同。二者就定義了這兩個座標系統的比例關係。程式作圖時,使用的座標總是是視窗座標,而實際的顯示或輸出裝置卻各有自己的座標。例如,有的印表機裝置水平和垂直解析度不同,其象素實際上是長方形。程式編寫畫一個圓,若不經任何座標轉換,在印表機上輸出的就是個橢圓。
在MFC 中ONDRAW 之前已經調用了ONPREPAREDC 函數為你做好映射模式。預設情況下,其映射模式為 MM_TEXT模式,即1:1模式。 要改變預設映射模式應重載OnPrepareDC重新設定自己的映射模式。為了提高繪圖精度,經常需要改變映射模式。
2. 視窗和視口理解要點
視窗與視口一向是初學者比較難以理解的痛點,本人以前也是糊裡糊塗的,不過最近有時間去深入研究之後,才徹底弄明白,擺脫了以前很多錯誤的觀念。弄清楚了這些才會更好的使用不同的座標影射模式,更靈活的為自己的繪圖帶來便利:
首先要清楚視窗和視口的座標原點始終是同一個點,視窗和視口中的內容是同樣的內容。裝置座標(顯示器)中則會根據視口或視窗原點的改變而顯示出不同的內容。在MM_TEXT映射模式下全局座標系第三象限的內容就是視窗座標系下第一象限的內容,所以有SetViewportOrg(x, y)與SetWindowOrg(-x,-y)具有相同的效果。這個很難理解但是這是事實。“裝置環境”如顯示屏始終是只有左手座標系xoy第一象限的地區。開始時視窗座標原點、視口座標原點以及裝置點重合在一起都是在客戶區的左上方為了將物體顯示在現實屏上,通過SetWindowOrg(x,y)將"畫布"(全局座標系原點)向螢幕的左邊移動x個單位,向螢幕的上方移動y個單位。SetViewportOrg(x,y)是將"畫筆"(視口原點)向螢幕的右邊移動x個單位,向螢幕的下方移動y個單位。
現將其關鍵點歸納如下:
1、 視口與顯示裝置有關,視口等同客戶區,使用裝置座標。視口是和視窗等同的一塊矩形地區,它的x軸向右和y軸向下。
2、 視窗與顯示裝置無關,視窗與視口為同一地區,但使用邏輯座標,它的x軸向右,y軸向上。
3、 視窗與視口使用不同的座標系,但是兩套座標系的原點始終為同一點。但該點座標(不管是視口座標原點還是視窗座標原點)不一定為(0,0)。視窗就是視口,去吧僅僅在於兩者建立的座標系不同,移動視窗的同時就是移動視口,反之亦然。
4、 視口原點的位置(就是畫筆dc的初始位置)僅僅由SetViewportOrgEx (x,y) 函數來移動。(x,y)是相對於客戶區左上方的裝置座標,即像素),而SetWindowOrg(X,Y )設定視窗的邏輯座標點(X,Y)映射為的裝置環境的裝置點(0,0)
It's easy to get SetViewportOrg and SetWindowOrg confused, but the distinction between them is actually quite clear. Changing the viewport origin to (x,y) with SetViewportOrg tells Windows to map the logical point (0,0) to the device point (x,y). Changing
the window origin to (x,y) with SetWindowOrg does essentially the reverse, telling Windows to map the logical point (x,y) to the device point (0,0)—the upper left corner of the display surface. In the MM_TEXT mapping mode, the only real difference between
the two functions is the signs of x and y. In other mapping modes, there's more to it than that because SetViewportOrg deals in device coordinates and SetWindowOrg deals in logical coordinates.
<<Programing windows with MFC>>中這樣來描述
dc始終開始從全局座標系的原點開始畫,畫筆的原點就是畫布的原點就是全局座標系的原點。在MM_TEXT下通過SetViewportOrg(X,Y)和SetWindowOrg(-X,-Y)將全局座標系的原點移動到裝置環境座標系下的(X,Y)點。SetWindowOrg(-X,-Y)指裝置點(0,0)處對應dc繪圖的邏輯點(-X,-Y),邏輯點(0,0)在MM_TEXT映射模式下位於裝置座標系的(X,Y)點。
1) 移動視口原點好比移動畫筆dc,如果將視口原點設定為(xViewOrg,yViewOrg),相當於dc畫筆的移動,則邏輯點(0,0)就會被映射為裝置點(xViewOrg,yViewOrg),初始dc繪圖在邏輯點(0,0)下筆,圖形將以客戶區(裝置點)的(xViewOrg,yViewOrg)為中心進行繪製。在MM_TEXT映射模式下SetViewportOrg(150,100);dc繪圖座標原點從(0,0)向右下方移動到(150,100)。可以看出SetViewportOrg()函數可以更改裝置上下文dc的座標原點。
void CDemoView::OnPaint()
{
CPaintDC dc(this); // device context for painting
dc.SetViewportOrg(150, 100);
CPen PenBlue;
PenBlue.CreatePen(PS_SOLID,1,RGB(0,12,255));
dc.SelectObject(&PenBlue);
dc.Ellipse(-100,-100,100,100);
}
2) 移動視窗原點好比畫布的移動,如果將視窗原點改變為(xWinOrg,yWinOrg),則邏輯點(xWinOrg,yWinOrg)將會被映射為裝置點(0,0),SetWindowOrg(150,100);邏輯點(150,100)對應於裝置點(0,0);不管對視窗和視口原點作什麼改變,應該銘記第一裝置點(0,0)始終是客戶區的左上方,第二視窗和視口原點是同一個點。
void CDemoView::OnPaint()
{
CPaintDC dc(this); // device context for painting
dc.SetWindowOrg(-150,-100);
CPen PenBlue;
PenBlue.CreatePen(PS_SOLID,1,RGB(0,12,255));
dc.SelectObject(&PenBlue);
dc.Ellipse(-100,-100,100,100);
//OnDraw(&dc);
}
5、 理解視窗與視口的座標轉換公式:
Xviewport=(Xwindow-Xwinorg)* Xviewext / Xwinext + Xvieworg;
Yviewport=(Ywindow-Xwinorg)* Yviewext / Ywinext + Yvieworg;
此公式初看上去不好理解,變形如下:
(Xviewport-Xvieworg)/(Xwindow-Xwinorg)= Xviewext / Xwinext;
(Yviewport-Yvieworg)/(Ywindow-Xwinorg)= Yviewext / Ywinext;
如此就很好理解了:邏輯座標單位與裝置座標單位的比,即比例因素。
6、 對於定義的用戶端區域大於程式所建立的視窗時,就需要有捲軸來滾動顯示,才能顯示完整的用戶端區域。
7、 處理滾動視窗:假設未滾動視窗時,客戶區左上方對應視窗和視口的原點座標,且預設均為(0,0), 如果滾動視窗時,水平方向向右滾動了X個像素,垂直方向向下滾動了Y個像素,則應該認為用戶端區域的左上方為視窗原點(同時也是視口原點)一起滾動,並且視窗原點(同時也是視口的原點)的座標始終為(0,0)不變,變的只是視窗原點在裝置環境座標系中的位置。當前看到的情境就是裝置環境(顯示屏)中顯示的內容,裝置環境的座標系永遠不會移動。通過SetViewportOrgEx
(X,Y)實現將全局座標系的原點移到顯示屏的(X,Y)位置。這兩種移動有個相同的結果就是視窗座標的原點與視口座標的原點始終對應於同一點(全局座標系的原點),而新的全局座標系的原點位置恰好是裝置環境中的(X,Y)。通過調試MFC中CSrollView類函數發現,它就是通過SetViewportOrgEx
(X,Y)函數移動全局座標系的原點從而實現“視窗的滾動”。
8、 座標原點(不論是視口還是視窗)不等於座標零點即裝置點(0,0)始終是客戶區的左上方(必需明白)。
9、 視口的座標原點可以任意移動,但其零點始終在客戶區左上方。
10、視窗原點可任意移動,視窗類別似於單張拍攝,僅僅是當前看到的景物。視口類似於電影菲林,記錄了以前到現在的所有資訊。
為了能夠在電腦中更好的描述真實的世界,必須設定一種與真實世界相符的邏輯座標系統。當然Windows為我們提供了裝置座標與邏輯座標進行映射的介面。
Windows提供的座標系統映射的介面有:
SetMapMode(int nMapMode) 設定映射模式,Windows提供了8種映射模式
SetWindowOrg(int x, int y) / SetWindowOrg(POINT point) 設定與視口座標原點相對應的視窗座標原點,windows要求視口的座標原點必須與視窗的座標原點相對應,x, y和point的單位為邏輯單位
SetViewportOrg(int x, int y) / SetViewportOrg(POINT point) 設定與視窗座標原點相對應的視口座標原點,windows要求視口的座標原點必須與視窗的座標原點相對應,x, y和point的單位為裝置單位,即像素
SetWindowExt(int cx, int cy) / SetWindowExt(SIZE size) 設定邏輯座標系統中視窗的大小範圍,cx, cy和size的單位為邏輯單位
SetViewportExt(int cx, int cy) / SetViewportExt(SIZE size) 設定裝置座標系統中視口的大小範圍,cx, cy和size的單位為裝置單位,即像素
3.孫鑫老師關於圖形錯位的說明
當我們在視窗中點擊滑鼠左鍵的時候,得到的是裝置座標(680,390),在MM_TEXT的映射模式下,邏輯座標和裝置座標是相等的,所以我們利用集合類儲存的這個點的座標是以象素為單位,座標值為(680,390)。在調用OnDraw函數前,在OnPaint函數中調用了OnPrepareDC函數,OnPrepareDC函數內調用SetViewportOrgEx ()調整了顯示內容相關的屬性,將視口的原點設定為了(0,-150),這樣的話,視窗的原點,也就是邏輯座標(0,0)將被映射為裝置座標(0,-150),在畫線的時候,因為GDI的函數使用的是邏輯座標,而圖形在顯示的時候,Windows需要將邏輯座標轉化為裝置座標,因此,原先儲存的座標點(680,390)(在GDI函數中,作為邏輯座標使用),根據轉換公式
xViewport = xWindow-xWinOrg+xViewOrg
和yViewport = yWindow-yWinOrg+yViewOrg,
得到裝置點的x座標為680-0+0=680,裝置點的y座標為390-0+(-150)=240,於是我們看到圖形在原先顯示地方的上方出現了。
關於解決方案的說明
首先我們在繪製圖形之後,在儲存座標點之前,調用OnPrepareDC函數,調整顯示內容相關的屬性,將視口的原點設定為(0,-150),這樣的話,視窗的原點,也就是邏輯座標(0,0)將被映射為裝置座標(0,-150),然後我們調用DPtoLP函數將裝置座標(680,390)轉換為邏輯座標,根據裝置座標轉換為邏輯座標的公式:
xWindow = xViewport-xViewOrg+xWinOrg,
yWindow = yViewport-yViewOrg+yWinOrg,得到邏輯點的x座標為680-0+0=680,y座標為390-(-150)+0=540,將邏輯座標(680,540)儲存起來,在視窗重繪時,會先調用OnPrepareDC函數,調整顯示內容相關的屬性,將視口的原點設定為了(0,-150),然後GDI函數用邏輯座標點(680,540)繪製圖形,被Windows轉換為裝置座標點(680,390),和原先顯示圖形時的裝置點是一樣的,當然圖形就還在原先的地方顯示出來。
從Windows的滑鼠訊息可以獲得滑鼠指標的當前座標值(point.x和point.y)此座標值是裝置座標。
很多MFC庫函數尤其是CRect的成員函數只能工作在裝置座標下。
還有我們有時需要利用物理座標,物理座標的概念就是現實世界的實際尺寸。
裝置座標-邏輯座標-物理座標之間如何進行轉換便成為我們要考慮的一個問題,物理座標和邏輯座標是完全要我們自己來做的,但WINDOWS提供了函數來協助我們轉換邏輯座標和裝置座標。
CDC的LPtoDP函數可以將邏輯座標轉換成裝置座標
CDC的DPtoLP函數可以將裝置座標轉換成邏輯座標
下面列出我們應該在什麼時候使用什麼樣的座標系一定要記住:
◎CDC的所有成員函數都以邏輯座標為參數
◎CWnd的所有成員函數都以裝置座標為參數
◎地區的定義採用裝置座標
◎所有的選中測試操作應考慮使用裝置座標。
◎需要長時間使用的值用邏輯座標或物理座標來儲存。因裝置座標會因視窗的滾動變化而改變。