GDI中的座標映射問題

  WindowsGDI支持兩種座標系,即邏輯座標系和物理設備座標系。必須明確邏輯座標系對應於平時所說的窗口(Window),而設備座標系纔對應視口 Viewport)。Ondraw中的CDC類所有的繪圖方法都是基於邏輯座標而言的,GDI通過映射模式將邏輯窗口中的圖形輸出到物理設備上來的。
由邏輯窗口到物理視口的映射過程我們可以這樣理解:我們在窗口座標系(一般是X軸向右遞增,Y軸向上遞增,即與笛卡爾座標系一致,而座標單位是用戶自定義的)上繪圖,然後要在視口座標系(一般座標原點在屏幕視圖的左上角,X軸向右遞增,Y軸向下爲正方向)下顯示,座標映射方式則規定了窗口和視口的範圍,各軸的相對方向、原點或縮放比例等。

在我們進行繪圖程序的開發時,不可避免地會遇到座標映射的問題,而這恰恰是一個很傷腦筋、繞也繞不明白的問題。我就經常爲此而一卡就是幾個小時,恨得要命,終於有一天心一橫,豁出一個週末的晚上,啃了所有找得到的資料,特別是那蝌蚪一般的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窗口的客戶區,或者是整個桌面,或者是打印機的一頁紙,或者是繪圖儀的一頁紙。

座標映射流程

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空間。下面來看如何確定它的三個要素:單位刻度值、方向、原點。

首先要使用SetMapModeint)函數選擇映射模式。這其中有6種事先已經定義好了的模式,可以直接拿來就用,比如MM_HIMETRIC模式表示page空間的單位刻度是0.01毫米,x軸正向向右,y軸正向向上,原點與device空間的原點重合。如果此時程序中有一條值爲10的線段,那麼在程序員的眼中,這就是一條10×0.010.1毫米的線段,不管使用多大分辨率的顯示器它都是這麼長,我們甚至可以用尺子在屏幕上量量試試。如果選擇預定義的映射模式,相當於微軟已經爲我們構造好了page空間,下面的事我們就都不用做了。

但是很多時候,微軟的東西不一定適合我們,此時就要將映射模式設定爲MM_ISOTROPIC或者MM_ANISOTROPIC,使用下面的四個函數定義我們自己的座標系:

SetWindowExtint Lwidth, int Lheight//參數的單位爲邏輯單位(Logical),如果參數爲負值表示window相應的座標軸與page空間相反。

SetViewportExtint Pwidth, int Pheight//參數的單位爲像素(Pixel),如果參數爲負值表示viewport相應的座標軸與device空間相反。

SetWindowOrgint Lx, int Ly)。

SetViewportOrgint Px, int Py)。

這四個函數提出了兩個新的概念:windowviewport,它們分別與page空間和device空間對應,但請記住並不是對等。引入它們的目的僅僅是爲了確定page空間的單位刻度、方向、原點。

1x軸的單位刻度=| Pwidth | / | Lwidth |

這表示x軸上一個邏輯單位等於多少個像素。下面舉例加以解釋。

比如我們先通過GetDeviceCapLOGPIXELSX)獲得在我們的顯示器上每英寸等於多少個像素,設爲p,然後我們將它賦給Pwidth,將Lwidth賦成2,即Pwidth / Lwidthp / 2。那麼,此時page空間x軸上的單位刻度就是p / 2個像素;又由於p個像素是代表一個英寸的,所以此時的page空間x軸上的單位刻度同時也是半個英寸。

請注意這個例子中,雖說viewportx方向“範圍”是p個像素,但是device空間x軸的“範圍”決不僅僅是p個像素,而是2^27個像素,至於可視的範圍到底是多少,則取決於物理設備空間。

2x軸的方向:這個好確定,LwidthPwidth同號,則page空間的x軸方向與device空間x軸方向相同,否則相反。

3.原點。這個就有一點麻煩了,我們需將windowviewport進行重疊,包括原點和座標軸方向,然後纔可以確定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 / 1002個像素),以及y軸的方向與device空間相同(100200同號),但是page空間的原點在哪裏呢?請看:

首先我們分別在page空間中畫出window座標系、在device空間中畫出viewport座標系(如圖2的左邊部分)。然後由於例子中的window座標方向與viewport相反,還需將page空間翻轉(見圖2中間部分)。最後將windowviewport重疊(見圖2右邊部分),使它們的原點和座標方向都一致。此時我們可以清楚地看到,page空間的原點就對應於device空間的原點,而且方向也和它相同。

通過以上的123點我們就可以完全確定一個適合我們自己要求的page空間,當我們不要world空間時,它就是邏輯空間。

另外還有一個問題就是要注意MM_ANISOTROPICMM_ISOTROPIC的區別。對於前者來說,x方向的單位刻度與y方向的單位刻度可以不同(當然也可以相同),但是後者x方向的單位刻度與y方向的單位刻度一定是相同的,如果通過計算windowviewport範圍的比值得到兩個方向的單位刻度值不同,那麼將會以較小的那個爲準。

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]

posted @ 2009-04-22 16:38 zwz_good 閱讀(47) | 評論(0) | 編輯

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.

posted @ 2009-04-22 15:55 zwz_good 閱讀(13) | 評論(0) | 編輯

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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章