在我們進行繪圖程序的開發時,不可避免地會遇到座標映射的問題,而這恰恰是一個很傷腦筋、繞也繞不明白的問題。我就經常爲此而一卡就是幾個小時,恨得要命,終於有一天心一橫,豁出一個週末的晚上,啃了所有找得到的資料,特別是那蝌蚪一般的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.結語
以上只是我的一些不成熟的看法,如果有不實之處,還望來信探討:[email protected]
program Devcaps1;
uses
Windows,
Messages,
DevcapsConst in 'DevcapsConst.pas';
function WndProc(hWindow: HWND; message, wParam, lParam: LongInt): LRESULT; stdcall;
{$J+}
const
cxChar: Integer = 0;
cyChar: Integer = 0;
cxCaps: Integer = 0;
{$J-}
var
tm: TTextMetricA;
hdc1: HDC;
ps: TPaintStruct;
i, OutList: integer;
szBuffer: array[0..5] of AnsiChar;
begin
Result:= 0;
case message of
WM_CREATE:
begin
hdc1:= GetDC(hWindow);
GetTextMetricsA(hdc1, tm);
ReleaseDC(hWindow, hdc1);
cyChar:= tm.tmHeight+tm.tmExternalLeading;
cxchar:= tm.tmAveCharWidth;
if (tm.tmPitchAndFamily and $1) = 0 then
cxCaps:= tm.tmAveCharWidth
else
cxCaps:= (tm.tmAveCharWidth * 3) div 2;
end;
WM_DESTROY:
begin
PostQuitMessage(0);
end;
WM_PAINT:
begin
hdc1:= BeginPaint(hWindow, ps);
for i := 0 to NUMLINES - 1 do
begin
TextOutA(hdc1, 0, cyChar*i, devcaps[i].szLabel, lstrlenA(devcaps[i].szLabel));
SetTextAlign(hdc1, TA_RIGHT or TA_TOP);
TextOutA(hdc1, 40*cxCaps, cyChar*i, devcaps[i].szDesc, lstrlenA(devcaps[i].szDesc));
OutList:= GetDeviceCaps(hdc1, devcaps[i].iIndex);
TextOutA(hdc1, 40*cxCaps + 22*cxChar, cyChar*i, szBuffer, wvsprintfA(szBuffer, '%5d', @OutList));
SetTextAlign(hdc1, TA_LEFT or TA_TOP);
end;
EndPaint(hWindow, ps);
end
else
begin
Result:= DefWindowProc(hWindow, message, wParam, lParam);
end;
end;
end;
const
szAppName = 'DEVCAPS1';
var
hwnd1: HWND;
wndclass1: TWndClass;
msg1: TMsg;
begin
wndclass1.style:= CS_VREDRAW or CS_HREDRAW;
wndclass1.lpfnWndProc:= @WndProc;
wndclass1.cbClsExtra:= 0;
wndclass1.cbWndExtra:= 0;
wndclass1.hInstance:= HInstance;
wndclass1.hIcon:= LoadIcon(HInstance, IDI_APPLICATION);
wndclass1.hCursor:= LoadCursor(HInstance, IDC_ARROW);
wndclass1.hbrBackground:= GetStockObject(WHITE_BRUSH);
wndclass1.lpszMenuName:= nil;
wndclass1.lpszClassName:= szAppName;
if RegisterClass(wndclass1) = 0 then
begin
MessageBoxA(0, 'This program requires Windows NT!', szAppName, MB_ICONERROR);
exit;
end;
hwnd1:= CreateWindowA(szAppName, 'Device Capabilities', WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, HInstance, nil);
ShowWindow(hwnd1, CmdShow);
UpdateWindow(hwnd1);
while GetMessage(msg1, 0, 0, 0) do
begin
TranslateMessage(msg1);
DispatchMessage(msg1);
end;
end.
unit DevcapsConst;
interface
uses
Windows;
type
TDevcap = record
iIndex: integer;
szLabel: PAnsiChar;
szDesc: PAnsiChar;
end;
const
NUMLINES = 20;
var
devcaps: array[0..NUMLINES-1] of TDevcap = (
(iIndex: HORZSIZE; szLabel: 'HORZSIZE'; szDesc: 'Width in millimeters:'),
(iIndex: VERTSIZE; szLabel: 'VERTSIZE'; szDesc: 'Height in millimeters:'),
(iIndex: HORZRES; szLabel: 'HORZRES'; szDesc: 'Width in pixels:'),
(iIndex: VERTRES; szLabel: 'VERTRES'; szDesc: 'Height in raster lines:'),
(iIndex: BITSPIXEL; szLabel: 'BITSPIXEL'; szDesc: 'Color bits per pixel:'),
(iIndex: PLANES; szLabel: 'PLANES'; szDesc: 'Number of color planes:'),
(iIndex: NUMBRUSHES; szLabel: 'NUMBRUSHES'; szDesc: 'Number of device brushes:'),
(iIndex: NUMPENS; szLabel: 'NUMPENS'; szDesc: 'Number of device pens:'),
(iIndex: NUMMARKERS; szLabel: 'NUMMARKERS'; szDesc: 'Number of device markers:'),
(iIndex: NUMFONTS; szLabel: 'NUMFONTS'; szDesc: 'Number of device fonts:'),
(iIndex: NUMCOLORS; szLabel: 'NUMCOLORS'; szDesc: 'Number of device colors:'),
(iIndex: PDEVICESIZE; szLabel: 'PDEVICESIZE'; szDesc: 'Size of device structure:'),
(iIndex: ASPECTX; szLabel: 'ASPECTX'; szDesc: 'Relative width of pixel:'),
(iIndex: ASPECTY; szLabel: 'ASPECTY'; szDesc: 'Relative height of pixel:'),
(iIndex: ASPECTXY; szLabel: 'ASPECTXY'; szDesc: 'Relative diagonal of pixel:'),
(iIndex: LOGPIXELSX; szLabel: 'LOGPIXELSX'; szDesc: 'Horizontal dots per inch:'),
(iIndex: LOGPIXELSY; szLabel: 'LOGPIXELSY'; szDesc: 'Vertical dots per inch:'),
(iIndex: SIZEPALETTE; szLabel: 'SIZEPALETTE'; szDesc: 'Number of Palette entries:'),
(iIndex: NUMRESERVED; szLabel: 'NUMRESERVED'; szDesc: 'Reserved palette entries:'),
(iIndex: COLORRES; szLabel: 'COLORRES'; szDesc: 'Actual color resolution:')
);
implementation
end.