5. 圖形基礎

Post date: 2012/3/23 上午 05:36:47

5. 圖形基礎

圖形裝置介面(GDI:Graphics Device Interface)是Windows的子系統,它負責在視訊顯示器和印表機上顯示圖形。正如您所認為的那樣,GDI是Windows非常重要的部分。不只您為Windows編寫的應用系統在顯示視覺資訊時使用GDI,就連Windows本身也使用GDI來顯示使用者介面物件,諸如功能表、捲動列、圖示和滑鼠游標。

不幸的是,如果要對GDI進行全面的講述,將需要一整本書-當然不是這本書。在本章中,我只是想向您提供畫線和填入區域的基本知識,這對於理解下面幾章的GDI已經足夠了。在後面幾章中會講述GDI支援的點陣圖、metafile以及格式化文字。

GDI的結構

從程式寫作者的觀點來看,GDI由幾百個函式呼叫和一些相關的資料型態、巨集和結構組成。但是在開始講述這些函式的細節之前,讓我們先從巨觀上瞭解一下GDI的整體結構。

GDI原理

Windows 98和Microsoft Windows NT中的圖形主要由GDI32.DLL動態連結程式庫輸出的函式來處理。在Windows 98中,這個GDI32.DLL實際是利用16位元GDI.EXE動態連結程式庫來執行許多函式。在Windows NT中,GDI.EXE只用於16位元的程式。

這些動態連結程式庫呼叫您安裝的視訊顯示器和任何印表機呼叫驅動程式中的常式。視訊驅動程式存取視訊顯示器的硬體,印表機驅動程式將GDI命令轉換為各種印表機能夠理解的代碼或者命令。顯然,不同的視訊顯示卡和印表機要求不同的裝置驅動程式。

因為PC相容機種上可以連接許多種不同的視訊設備,所以,GDI的主要目的之一是支援與裝置無關的圖形。Windows程式應該能夠毫無困難地在Windows支援的任意一種圖形輸出設備上執行,GDI通過將您的程式和不同輸出設備的特性隔離開來的方法來達到這一目的。

圖形輸出設備分為兩大類:位元映射設備和向量設備。大多數PC的輸出設備是位元映射設備,這意味著它們以圖點構成的陣列來表示圖像,這類設備包括視訊顯示卡、點陣印表機和雷射印表機。向量設備使用線來繪製圖像,通常局限於繪圖機。

許多傳統的電腦圖形程式設計方式都是完全以向量為主的,這意味著使用向量圖形系統的程式與硬體有著一定層次的隔離。輸出設備用圖素表示圖形,但是程式與程式介面之間並不是用圖素進行溝通的。您當然可以使用Windows GDI作為一個高階的向量繪製系統,同時也可以將它用於比較低階的圖素操作。

從這方面來看,Windows GDI和傳統的圖形介面語言之間的關係,就如同C和其他程式設計語言之間的關係一樣。C以它在不同作業系統和環境之間的高度可攜性而聞名,然而C也以允許程式寫作者進行低階系統呼叫而聞名,這些呼叫在其他高階語言中通常是不可能的。正如C有時被認為是一種「高級組合語言」一樣,您可以認為GDI是圖形設備硬體之間的一種高階介面。

您已經看到,Windows內定使用圖素座標系統。大多數傳統的圖形語言使用「虛擬」座標系,其水平和垂直軸的範圍在0到32,767之間。雖然有些圖形語言不讓您使用圖素座標,但是Windows GDI允許您使用兩種座標系統之一(甚至依據實際度量衡的座標系)。您可以使用虛擬座標系以便讓程式獨立於硬體之外,或者也可以使用設備座標系而完全迎合硬體設備提供的環境。

某些程式寫作者認為一旦開始使用操作圖素的程式設計方式,就放棄了裝置無關性。我們在 上一章 看到,這不完全是正確的,其中的訣竅是在與裝置無關的方式中使用圖素。這要求圖形介面語言為程式提供一些方法來確定設備的硬體特徵,並進行適當的調節。例如,在SYSMETS程式中,我們根據標準系統字體字元的圖素大小來確定螢幕上的文字間距,這種方法允許程式針對解析度、文字大小和方向比例各不相同的顯示卡進行相應的調節。您將在本章看到一些用於確定顯示尺寸的其他方法。

早期,許多使用者在單色顯示器上執行Windows。即使是幾年前,筆記本電腦也還只有灰階顯示。為此,GDI的設計保證了您可以在編寫一個程式時不必太擔心色彩問題-也就是說,Windows可以將色彩轉換為灰階顯示。甚至在今天,Windows 98使用的視訊顯示已經具有了不同的色彩能力(16色、256色、「high-Color」以及「true-color」)。雖然,彩色噴墨印表機的成本已經很低了,但是大多數使用者仍然堅持使用黑白印表機。盲目地使用這些設備是可以的,但是您的程式也應該能決定在某種顯示設備上有多少色彩可以使用,從而最佳利用硬體功能。

當然,就如同您編寫C程式時,為了使它在其他電腦上執行而遇到一些微妙的移植性問題一樣,您也可能不小心讓裝置依賴性溜進您的Windows程式,這就是不與硬體完全隔離的代價。您還應該知道Windows GDI的局限。雖然可以在顯示器上到處移動圖形物件,但GDI通常是一個靜態的顯示系統,只有有限的動畫支援。如果需要為遊戲編寫複雜的動畫,就應該研究一下Microsoft DirectX,它提供了您需要的支援。

GDI函式呼叫

組成GDI的幾百個函式呼叫可以分為幾大類:

  • 取得(或者建立)和釋放(或者清除)裝置內容的函式 我們在前面的章節中已經看到過,您在繪圖時需要裝置內容代號。GetDC和RealseDC函式讓您在非WM_PAINT的訊息處理期間來做到這一點,而BeginPaint和EndPaint函式(雖然在技術上它們是USER模組而不是GDI模組的一部分)在進行繪圖的WM_PAINT訊息處理期間使用。我們馬上還會介紹有關裝置內容的其他一些函式。
  • 取得有關裝置內容資訊的函式 再以 第四章中SYSMETS程式 為例,我們使用GetTextMetrics函式來取得有關裝置內容中目前所選字體的尺寸資訊。在本章後面,我們將看到一個取得非常廣泛的裝置內容資訊的 DEVCAPS1程式
  • 繪圖函式 顯然,在所有前提條件都得以滿足之後,這些函式是真正重要的部分。在 上一章 中,我們使用TextOut函式在視窗的顯示區域顯示一些文字。我們將看到,其他GDI函式還可以讓您畫線、填入區域。在 第十四章第十五章 還會看到如何建立點陣圖圖像。
  • 設定和取得裝置內容參數的函式 裝置內容的「屬性」決定有關繪圖函式如何工作的細節。例如,用SetTextColor來指定TextOut(或者其他文字輸出函式)所繪製的文字色彩。在 第四章中SYSMETS程式 中,我們使用SetTextAlign來告訴GDI:TextOut函式中的字串的開始位置應該在字串的右邊而不是內定的左邊。裝置內容的所有屬性都有預設值,取得裝置內容時這些預設值就設定好了。對於所有的Set函式,都有相應的Get函式,以允許您取得目前裝置內容屬性。
  • 使用GDI物件的函式 GDI在這裏變得有點混亂。首先舉一個例子:內定時使用GDI繪製的所有直線都是實線並具有一個標準的寬度。您可能希望繪製更細的直線,或者是由一系列的點或短劃線組成的直線。這種線的寬度和這種線的畫筆樣式不是裝置內容的屬性,而是一個「邏輯畫筆」的特徵。您可以通過在CreatePen、 CreatePenIndirect或ExtCreatePen函式中指定這些特徵來建立一個邏輯畫筆,這些函式傳回一個邏輯畫筆的代號(雖然這些函式被認為是GDI的一部分,但是和大多數GDI函式呼叫不一樣,它們不要求裝置內容的代號)。要使用這個畫筆,就要將畫筆代號選進裝置內容。我們認為,裝置內容中目前選中的畫筆就是裝置內容的一個屬性。這樣,您畫任何線都使用這個畫筆,然後,您可以取消裝置內容中的畫筆選擇,並清除畫筆物件。清除畫筆物件是必要的,因為畫筆定義佔用了分配的記憶體空間。除了畫筆以外,GDI物件還用於建立填入封閉區域的畫刷、字體、點陣圖以及GDI的其他一些方面。

GDI基本圖形

您在螢幕或印表機上顯示的圖形型態本身可以被分為幾類,通常被稱為「基本圖形」,它們是:

  • 直線和曲線 線條是所有向量圖形繪製系統的基礎。GDI支援直線、矩形、橢圓(包括橢圓的子集,也就是我們所說的「圓」)、橢圓圓周上的部分曲線即所謂的「弧」以及貝塞爾曲線(Bezier spline),我們將在本章中分別對它們進行介紹。所有更複雜的曲線可由折線(polyline)代替,折線通過一組非常短的直線來定義一條曲線。線條用裝置內容中選中的目前畫筆繪製。
  • 填入區域 當一系列直線或者曲線封閉了一個區域時,該區域可以使用目前GDI畫刷物件進行填圖。這個畫刷可以是實心色彩、圖案(可以是一系列的水平、垂直或者對角標記)或者是在區域內垂直或者水平重複的點陣圖圖像。
  • 點陣圖 點陣圖是位元的矩形陣列,這些位元對應於顯示設備上的圖素,它們是位元映射圖形的基礎工具。點陣圖通常用於在視訊顯示器或者印表機上顯示複雜(一般都是真實的)圖像。點陣圖還可以用於顯示必須快速繪製的小圖像(諸如圖示、滑鼠游標以及在應用工具條中出現的按鈕等)。GDI支援兩種型態的點陣圖-舊式的(雖然還非常有用)「裝置相關」點陣圖,是GDI物件;和新的(如Windows 3.0的)「裝置無關」點陣圖(或者DIB),可以儲存在磁片檔案中。 第十四章第十五章 討論點陣圖。
  • 文字 文字的數學味道不像電腦圖形的其他方面那樣濃。文字和幾百年的傳統印刷術有關,它被許多印刷工人看作為一門藝術。因此,文字通常不僅是所有的電腦圖形系統中最複雜的部分,而且(如果識字還是社會基本要求的話)也是最重要的部分。用於定義GDI字體物件和取得字體資訊的資料結構是Windows中最龐大的部分之一。從Windows 3.1開始,GDI開始支援TrueType字體,該字體是在填入輪廓線基礎上建立的,這樣的填入輪廓線可由其他GDI函式處理。依據相容性和儲存大小的考慮,Windows 98繼續支援舊式的點陣字體。我會在 第十七章 討論字體。

其他部分

GDI的其他部分無法這麼容易地分類,它們是:

  • 映射模式和變換 雖然內定以圖素為單位進行繪圖,但是您並非局限於此。GDI映射模式允許您以英寸(或者甚至以幾分之一英寸)、毫米或者任何您想使用的單位來繪圖(Windows NT還支援傳統的以三乘三矩陣表示的「座標變換」, 這允許傾斜和旋轉圖形物件。不幸的是,在Windows 98中不支援座標變換)。
  • Metafile Metafile是以二進位形式儲存的GDI命令集合。Metafile主要用於通過剪貼板傳輸向量圖形。 第十八章 會討論metafile。
  • 繪圖區域 繪圖區域是形狀任意的複雜區域,通常定義為較簡單的繪圖區域組合。在GDI內部,繪圖區域除了儲存為最初用來定義繪圖區域的線條組合以外,還以一系列掃描線的形式儲存。您可以將繪圖區域用於繪製輪廓、填入圖形和剪裁。
  • 路徑 路徑是GDI內部儲存的直線和曲線的集合。路徑可以用於繪圖、填入圖形和剪裁,還可以轉換為繪圖區域。
  • 剪裁 繪圖可以限制在顯示區域的某一部分中,這就是所謂的剪裁。剪裁區域是不是矩形都可以,剪裁通常是通過區域或者路徑來定義的。
  • 調色盤 自訂調色盤通常限於顯示256色的顯示器。Windows僅保留這些色彩之中的20種以供系統使用,您可以改變其他236種色彩,以準確顯示按點陣圖形式儲存的真實圖像。 第十六章 會討論調色盤。
  • 列印 雖然本章限於討論視訊顯示,但是您在本章中所學到的全部知識都適用於列印。 第十三章 會討論列印。

裝置內容

在開始繪圖之前,讓我們比 第四章 更精確地討論一下裝置內容。

當您想在一個圖形輸出設備(諸如螢幕或者印表機)上繪圖時,您首先必須獲得一個裝置內容(或者DC)的代號。將代號傳回給程式時,Windows就給了您使用設備的許可權。然後您在GDI函式中將這個代號作為一個參數,向Windows標識您想在其上進行繪圖的設備。

裝置內容中包含許多確定GDI函式如何在設備上工作的目前「屬性」,這些屬性允許傳遞給GDI函式的參數只包含起始座標或者尺寸資訊,而不必包含Windows在設備上顯示物件時需要的所有其他資訊。例如,呼叫TextOut時,您只需要在函式中給出裝置內容代號、起始座標、文字和文字的長度。您不必指定字體、文字顏色、文字後面的背景色彩以及字元間距,因為這些屬性都是裝置內容的一部分。當您想改變這些屬性之一時,您呼叫一個可以改變裝置內容中屬性的函式,以後針對該裝置內容的TextOut呼叫來使用改變後的屬性。

取得裝置內容代號

Windows提供了幾種取得裝置內容代號的方法。如果在處理一個訊息時取得了裝置內容代號,應該在退出視窗函式之前釋放它(或者刪除它)。一旦釋放了代號,它就不再有效了。對於印表機裝置內容代號,規則就沒有這麼嚴格。在 第十三章 會討論列印。

最常用的取得並釋放裝置內容代號的方法是,在處理WM_PAINT訊息時,使用BeginPaint和EndPaint呼叫:

hdc = BeginPaint (hwnd, &ps) ; 其他行程式 EndPaint (hwnd, &ps) ;

變數ps是型態為PAINTSTRUCT的結構,該結構的hdc欄位是BeginPaint傳回的裝置內容代號。 PAINTSTRUCT結構又包含一個名為rcPaint的RECT(矩形)結構,rcPaint定義一個包圍視窗顯示區域無效範圍的矩形。使用從BeginPaint獲得的裝置內容代號,只能在這個區域內繪圖。BeginPaint呼叫使該區域有效。

Windows程式還可以在處理非WM_PAINT訊息時取得裝置內容代號:

hdc = GetDC (hwnd) ; 其他行程式 ReleaseDC (hwnd, hdc) ;

這個裝置內容適用於視窗代號為hwnd的顯示區域。這些呼叫與BeginPaint和EndPaint的組合之間的基本區別是,利用從GetDC傳回的代號可以在整個顯示區域上繪圖。當然, GetDC和ReleaseDC不使顯示區域中任何可能的無效區域變成有效。

Windows程式還可以取得適用於整個視窗(而不僅限於視窗的顯示區域)的裝置內容代號:

hdc = GetWindowDC (hwnd) ; 其他行程式 ReleaseDC (hwnd, hdc) ;

這個裝置內容除了顯示區域之外,還包括視窗的標題列、功能表、捲動列和框架(frame)。GetWindowDC函式很少使用,如果想嘗試用一用它,則必須攔截處理WM_NCPAINT訊息,Windows使用該訊息在視窗的非顯示區域上繪圖。

BeginPaint、GetDC和GetWindowDC獲得的裝置內容都與視訊顯示器上的某個特定視窗相關。取得裝置內容代號的另一個更通用的函式是CreateDC:

hdc = CreateDC (pszDriver, pszDevice, pszOutput, pData) ; 其他行程式 DeleteDC (hdc) ;

例如,您可以通過下面的呼叫來取得整個螢幕的裝置內容代號:

在視窗之外寫入畫面一般是不恰當的,但對於一些不同尋常的應用程式來說,這樣做很方便(您還可通過在呼叫GetDC時使用一個NULL參數,從而取得整個螢幕的裝置內容代號,不過這在文件中已經提到了)。在 第十三章 中,我們將使用CreateDC函式來取得一個印表機裝置內容代號。

有時您只是需要取得關於某裝置內容的一些資訊而並不進行任何繪畫,在這種情況下,您可以使用CreateIC來取得一個「資訊內容」的代號,其參數與CreateDC函式相同,例如:

您不能用這個資訊內容代號往設備上寫東西。

使用點陣圖時,取得一個「記憶體裝置內容」有時是有用的:

hdc = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;

hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;

hdcMem = CreateCompatibleDC (hdc) ; 其他行程式 DeleteDC (hdcMem) ;

您可以將點陣圖選進記憶體裝置內容,然後使用GDI函式在點陣圖上繪畫。我將在 第十四章 討論這些技術。

前面已經提到過,metafile是一些GDI呼叫的集合,以二進位形式編碼。您可以通過取得metafile裝置內容來建立metafile:

hdcMeta = CreateMetaFile (pszFilename) ; 其他行程式 hmf = CloseMetaFile (hdcMeta) ;

在metafile裝置內容有效期間,任何用hdcMeta所做的GDI呼叫都變成metafile的一部分而不會顯示。在呼叫CloseMetaFile之後,裝置內容代號變為無效,函式傳回一個指向metafile(hmf)的代號。我會在 第十八章 討論metafile。

取得裝置內容資訊

一個裝置內容通常是指一個實際顯示設備,如視訊顯示器和印表機。通常,您需要取得有關該設備的資訊,包括顯示器的大小(單位為圖素或者實際長度單位)和色彩顯示能力。您可以通過呼叫GetDeviceCaps(「取得設備功能」)函式來取得這些資訊:

其中,參數iIndex取值為WINGDI.H表頭檔案中定義的29個識別字之一。例如,iIndex為HORZRES時將使GetDeviceCaps傳回設備的寬度(單位為圖素);iIndex為VERTRES時將讓GetDeviceCaps傳回設備的高度(單位為圖素)。如果hdc是印表機裝置內容的代號,則GetDeviceCaps傳回印表機顯示區域的高度和寬度,它們也是以圖素為單位的。

還可以使用GetDeviceCaps來確定設備處理不同型態圖形的能力,這對於視訊顯示器並不很重要,但是對於列印設備卻是非常重要。例如,大多數繪圖機不能畫點陣圖圖像,GetDeviceCaps就可以將這一情況告訴您。

DEVCAPS1程式

程式5-1所示的DEVCAPS1程式顯示了以一個視訊顯示器的裝置內容為參數時,可以從 GetDeviceCaps函式中獲得的部分資訊(該程式的另一個擴充版本 DEVCAPS2 將在第十三章給出,用於取得印表機資訊)。

iValue = GetDeviceCaps (hdc, iIndex) ;

程式5-1 DEVCAPS1 DEVCAPS1.C /*------------------------------------------------------------------------ DEVCAPS1.C -- Device Capabilities Display Program No. 1 (c) Charles Petzold, 1998 ----------------------------------------------------------------------*/ #include <windows.h> #define NUMLINES ((int) (sizeof devcaps / sizeof devcaps [0])) struct { int iIndex ; TCHAR * szLabel ; TCHAR * szDesc ; } devcaps [] = { HORZSIZE, TEXT ("HORZSIZE"),TEXT ("Width in millimeters:"), VERTSIZE, TEXT ("VERTSIZE"),TEXT ("Height in millimeters:"), HORZRES, TEXT ("HORZRES"), TEXT ("Width in pixels:"), VERTRES, TEXT ("VERTRES"), TEXT ("Height in raster lines:"), BITSPIXEL, TEXT ("BITSPIXEL"),TEXT ("Color bits per pixel:"), PLANES, TEXT ("PLANES"), TEXT ("Number of color planes:"), NUMBRUSHES, TEXT ("NUMBRUSHES"), TEXT ("Number of device brushes:"), NUMPENS, TEXT ("NUMPENS"), TEXT ("Number of device pens:"), NUMMARKERS, TEXT ("NUMMARKERS"), TEXT ("Number of device markers:"), NUMFONTS, TEXT ("NUMFONTS"), TEXT ("Number of device fonts:"), NUMCOLORS, TEXT ("NUMCOLORS"), TEXT ("Number of device colors:"), PDEVICESIZE, TEXT ("PDEVICESIZE"), TEXT ("Size of device structure:"), ASPECTX, TEXT ("ASPECTX"), TEXT ("Relative width of pixel:"), ASPECTY, TEXT ("ASPECTY"), TEXT ("Relative height of pixel:"), ASPECTXY, TEXT ("ASPECTXY"), TEXT ("Relative diagonal of pixel:"), LOGPIXELSX, TEXT ("LOGPIXELSX"), TEXT ("Horizontal dots per inch:"), LOGPIXELSY, TEXT ("LOGPIXELSY"), TEXT ("Vertical dots per inch:"), SIZEPALETTE, TEXT ("SIZEPALETTE"), TEXT ("Number of palette entries:"), NUMRESERVED, TEXT ("NUMRESERVED"), TEXT ("Reserved palette entries:"), COLORRES, TEXT ("COLORRES"), TEXT ("Actual color resolution:") } ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("DevCaps1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Device Capabilities"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxChar, cxCaps, cyChar ; TCHAR szBuffer[10] ; HDC hdc ; int i ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) { TextOut ( hdc, 0, cyChar * i, devcaps[i].szLabel, lstrlen (devcaps[i].szLabel)) ; TextOut ( hdc, 14 * cxCaps, cyChar * i, devcaps[i].szDesc, lstrlen (devcaps[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, 14*cxCaps+35*cxChar, cyChar*i, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetDeviceCaps (hdc, devcaps[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

可以看到,這個程式非常類似 第四章的SYSMETS1 。為了保持程式碼的短小,我沒有使用捲動列,因為我知道資訊可以在一個畫面上顯示出來。在256色,640×480的VGA上顯示的結果如圖5-1所示。

裝置的大小

假定要繪製邊長為1英寸的正方形,您(程式寫作者)或Windows(作業系統)需要知道視訊顯示上1英寸對應多少圖素。使用GetDeviceCaps函式能取得有關如視訊顯示器和印表機之類輸出設備的實際顯示大小資訊。

視訊顯示器和印表機是兩個不同的設備。但也許最不明顯的區別是「解析度」與裝置聯繫起來的方式。對於印表機,我們經常用「每英寸的點數(dpi)」表示解析度。例如,大多數雷射印表機有300或600dpi的解析度。然而,視訊顯示器的解析度是以水平和垂直的總圖素數來表示的,例如,1024×768。大多數人不會告訴您他的印表機在一張紙上水平和垂直列印多少圖素或他們的視訊顯示器上每英寸有多少圖素。

在本書中,我用「解析度」來嚴格定義每度量單位(一般為英寸)內的圖素數。我使用「圖素大小」或「圖素尺寸」表示設備水平或垂直顯示的總圖素數。「度量大小」或「度量尺寸」是以英寸或毫米為單位的設備顯示區域的大小。(對於印表機頁面,它不是整個頁面,只是可列印的區域。)圖素大小除以度量大小就得到解析度。

現在Windows使用的大多數視訊顯示器的螢幕都是寬比高多33%。這就表示縱橫比為1.33:1或(一般寫法)4:3。歷史上,該比例可追溯到Thomas Edison製作電影的年代。它一直作為電影的標準縱橫比,直到1953年出現各種型態的寬銀幕投影機。電視機螢幕的縱橫比也是4:3。

然而,Windows應用程式不應假設視訊顯示器具有4:3的縱橫比。人們進行文字處理時希望視訊顯示器與一張紙的長和寬類似。最普通的選擇是把4:3變為3:4顯示,把標準顯示翻轉一下。

如果設備的水平解析度與垂直解析度相等,就稱設備具有「正方形圖素」。現在,Windows普遍使用的視訊顯示器都具有正方形圖素,但也有例外。(應用程式也不應假設視訊顯示器總是具有正方形圖素。)Windows第一次發表時,標準顯示卡卡是IBM Color Graphics Adapter(CGA),它有640×200的圖素大小;Enhanced Graphics Adapter(EGA)有640×350的圖素大小;Hercules Graphics Card有720×348的圖素大小。所有這些顯示卡都使用4:3縱橫比的顯示器,但是水平和垂直圖素數的比值都不是4:3。

執行Windows的使用者很容易確定視訊顯示器的圖素大小。在「控制台」中執行「顯示器」,並選擇「設定」頁面標籤。在標有「桌面區域」的欄位中,可以看到這些圖素尺寸之一:

圖5-1 256色,640×480VGA上的DEVCAPS1顯示

  • 640×480圖素
  • 800×600圖素
  • 1024×768圖素
  • 1280×1024圖素
  • 1600×1200圖素

所有這些都是4:3。(除了1280×1024圖素大小。這不但有些不好,還有些令人反感。所有這些圖素尺寸都認為在4:3的顯示器上會產生正方形的圖素。)

Windows應用程式可以使用SM_CXSCREEN和SM_CYSCREEN參數從GetSystemMetrics得到圖素尺寸。從DEVCAPS1程式中您會注意到,程式可以用HORZRES(水平解析度)和VERTRES參數從GetDeviceCaps中得到同樣的值。這裏「解析度」指的是圖素大小而不是每度量單位的圖素數。

這些是設備大小的簡單部分,現在開始複雜的部分。

前兩個設備能力,HORZSIZE和VERTSIZE,文件中稱為「以毫米計的實際螢幕的寬度」及「以毫米計的實際螢幕的高度」(在/Platform SDK/Graphics和Multimedia Services/GDI/Device Contexts/Device Context Reference/Device Context Functions/GetDeviceCaps中)。這些看起來更像直接的定義。例如,給出視訊顯示卡和顯示器的介面特性,Windows如何真正知道顯示器的大小呢?如果您有台膝上型電腦(它的視訊驅動程式能知道準確的螢幕大小)並且連接了外部顯示器,又是哪種情況呢?如果把視訊投影機連接到電腦上呢?

在Windows的16位元版本中(及在Windows NT中),Windows為HORZSIZE和VERTSIZE使用「標準」的顯示大小。然而,從Windows 95開始,HORZSIZE和VERTSIZE值是從HORZRES、VERTRES、LOGPIXELSX和LOGPIXELSY值中衍生出來的。這是它的工作方式。

當您在「控制台」中使用「顯示器」程式選擇顯示的圖素大小時,也可以選擇系統字體的大小。這個選項的原因是用於640×480顯示的字體在提升到1024×768或更大時字太小,而您可能想要更大的系統字體。這些系統字體大小指「顯示器」程式的「設定」頁面標籤中的「小字體」和「大字體」。

在傳統的排版中,字體的字母大小由「點」表示。1點大約1/72英寸,在電腦排版中1點正好為1/72英寸。

理論上,字體的點值是從字體中最高的字元頂部到例如j、p、q和y等字母下部的字元底部的距離,其中不包括重音符號。例如,在10點的字體中此距離是10/72英寸。根據TEXTMETRIC結構,字體的點值等於tmHeight欄位減去tmInternalLeading欄位,如圖5-2所示(該圖與 上一章的圖4-3 一樣)。

在真正的排版中,字體的點值與字體字母的實際大小並不正好相等。字體的設計者做出的實際字元比點值指示的要大一些或小一些。畢竟,字體設計是一種藝術而不是科學。

TEXTMETRIC結構的tmHeight欄位指出文字的連續行在螢幕或印表機上間隔的方式。這也可以用點來測量。例如,12點的行距指出文字連續行的基準線應該間隔12/72(或1/6)英寸。不應該為10點字體使用10點行距,因為文字的連續行會碰到一起。

10點字體讀起來很舒服。小於10點的字體不益於長時間閱讀。

Windows系統字體-不考慮是大字體還是小字體,也不考慮所選擇的視頻圖素大小-固定假設為10點字體和12點行距。這聽起來很奇怪,如果字體都是10點,為什麼還把它們稱為大字體和小字體呢?

解答是: 當您在「控制台」的「顯示」程式上選擇小字體或大字體時,實際上是選擇了一個假定的視訊顯示解析度,單位是每英寸的點數 。當選擇小字體時,即要Windows假定視訊顯示解析度為每英寸96點。當選擇大字體時,即要Windows假定視訊顯示解析度為每英寸120點。

再看看圖5-2。那是小字體,它依據的顯示解析度為每英寸96點。我說過它是10點字體。10點即是10/72英寸,如果乘以96點,每英寸大概就為13圖素。這即是tmHeight減去tmInternalLeading的值。行距是12點,或12/72英寸,它乘以96點,每英寸就為16圖素。這即是tmHeight的值。

圖5-3顯示大字體。這是依據每英寸120點的解析度。同樣,它是10點字體,10/72乘以120點,每英寸等於16圖素,即是tmHeight減tmInternalLeading的值。12點行距等於20圖素,即是tmHeight的值。(像 第四章 一樣,再次強調所顯示的是實際的度量大小,因此您可以理解它工作的方式。不要在您的程式中對此寫作程式。)

在Windows程式中,您可以使用GetDeviceCaps函式取得使用者在「控制台」的「顯示器」程式中選擇的以每英寸的點數為單位的假定解析度。要得到這些值(如果視訊顯示器不具有正方形圖素,在理論上這些值是不同的),可以使用索引LOGPIXELSX和LOGPIXELSY。LOGPIXELS指邏輯圖素,它的基本意思是「以每英寸的圖素數為單位的非實際解析度」。

用HORZSIZE和VERTSIZE索引從GetDeviceCaps得到的設備能力,在文件上稱為「實際螢幕的寬度,單位毫米」及「實際螢幕的高度,單位毫米」。因為這些值是從HORZRES、VERTRES、LOGPIXELSX和LOGPIXELSY值中衍生出來的,所以它們應該稱為「邏輯寬度」和「邏輯高度」。公式是:

常數25.4用於把英寸轉變為毫米。

這看起來是種不合邏輯的退步。畢竟,視訊顯示器是可以用尺以毫米為單位的大小(至少是近似的)衡量的。但是Windows 98並不關心這個大小。相反,它以使用者選擇的顯示圖素大小和系統字體大小為基礎計算以毫米為單位的顯示大小。更改顯示的圖素大小並根據GetDeviceCaps更改度量大小。這有什麼意義呢?

這非常有意義。假定有一個17英寸的顯示器。實際的顯示大小大約是12英寸乘9英寸。假定在最小要求的640×480圖素大小下執行Windows。這意味著實際的解析度是每英寸53點。10點字體(在紙上便於閱讀)在螢幕上從A的頂部到q的底部只有7個圖素。這樣的字體很難看而且不易讀。(可問問那些在舊的Color Graphics Adapter上執行Windows的人們。)

現在,把您的電腦接上視訊投影機。投影的視訊顯示器是4英尺寬,3英尺高。同樣的640×480圖素大小現在是大約每英寸13點的解析度。在這種條件下試圖顯示10點的字體是很可笑的。

10點字體在視訊顯示器上應是可讀的,因為它在列印時是肯定可讀的。所以10點字體就成為一個重要的參照。當Windows應用程式確保10點螢幕字體為平均大小時,就能夠使用8點字體顯示較小的文字(仍可讀),或用大於10點的字體顯示較大的文字。因而,視頻解析度(以每英寸的點數為單位)由10點字體的圖素大小來確定是很有意義的。

然而,在Windows NT中,用老的方法定義HORZSIZE和VERTSIZE值。這種方法與Windows的16位元版本一致。HORZRES和VERTRES值仍然表示水平和垂直圖素的數值,LOGPIXELSX和LOGPIXELSY仍然與在「控制台」的「顯示器」程式中選擇的字體有關。在Windows 98中,LOGPIXELSX和LOGPIXELSY的典型值是96和120 dpi,這取決於您選擇的是小字體還是大字體。

在Windows NT中的區別是HORZSIZE和VERTSIZE值固定表示標準顯示器大小。對於普通的顯示卡,取得的HORZSIZE和VERTSIZE值分別是320和240毫米。這些值是相同的,與選擇的圖素大小無關。因此,這些值與用HORZRES、VERTRES、LOGPIXELSX和LOGPIXELSY索引從GetDeviceCaps中得到的值不同。然而,可以用前面的公式計算在Windows 98下的HORZSIZE和VERTSIZE值。

如果程式需要實際的視訊顯示大小該怎麼辦?也許最好的解決方法是用對話方塊讓使用者輸入它們。

最後,來自GetDeviceCaps的另三個值與視訊大小有關。ASPECTX、ASPECTY和ASPECTXY值是每一個圖素的相對寬度、高度和對角線大小,四捨五入到整數。對於正方形圖素,ASPECTX和ASPECTY值相同。無論如何,ASPECTXY值應等於ASPECTX與ASPECTY平方和的平方根,就像直角三角形一樣。

關於色彩

如果視訊顯示卡僅顯示黑色圖素和白色圖素,則每個圖素只需要記憶體中的一位元。彩色顯示器中每個圖素需要多個位元。位元數越多,色彩越多,或者更具體地說,可以同時顯示的不同色彩的數目等於2的位元數次方。

「Full-Color」視訊顯示器的解析度是每個圖素24位元-8位元紅色、8位元綠色以及8位元藍色。紅、綠、藍即「色光三原色」。混合這三種基本顏色可以生成許多其他的顏色,您通過放大鏡看顯示幕,就可以看出來。

「High-Color」顯示解析度是每個圖素16位元-5位元紅色、6位元綠色以及5位元藍色。綠色多一位元是因為人眼對綠色更敏感一些。

顯示256種顏色的顯示卡每個圖素需要8位元。然而,這些8位元的值一般由定義實際顏色的調色盤組織的。我會在 第十六章 詳細地討論它們。

最後,顯示16種顏色的顯示卡每個圖素需要4位元。這16種顏色一般固定分為暗的或亮的紅、黑、藍、青、紫、黃、兩種灰色。這16種顏色要回溯到老式的IBM CGA。

祇有在某些怪異的程式中才需要知道視訊顯示卡上的記憶體是如何組織的,但是GetDeviceCaps使程式寫作者可以知道顯示卡的儲存組織以及它能夠表示的色彩數目,下面的呼叫傳回色彩平面的數目:

下面的呼叫傳回每個圖素的色彩位元數:

大多數彩色圖形顯示設備使用多個色彩平面或每圖素有多個色彩位元的設計,但是不能同時一齊使用這兩種方式;換句話說,這兩個呼叫必有一個傳回1。顯示卡能夠表示的色彩數可以用如下公式來計算:

這個值與用NUMCOLORS參數得到的色彩數值可能一樣,也可能不一樣:

我提到過,256色的顯示卡使用色彩調色盤。在那種情況下,以NUMCOLORS為參數時,GetDeviceCaps傳回由Windows保留的色彩數,值為20,剩餘的236種顏色可以由Windows程式用調色盤管理器設定。對於High-Color和True-Color顯示解析度,帶有NUMCOLORS參數的GetDeviceCaps通常傳回-1,這樣就無法得到需要的資訊,因此應該使用前面所示的帶有PLANES和BITSPIXEL值的iColors公式。

在大多數GDI函式呼叫中,使用COLORREF值(只是一個32位元的無正負號長整數)來表示一種色彩。COLORREF值按照紅、綠和藍色的亮度指定了一種顏色,通常叫做「RGB色彩」 。32位元的COLORREF值的設定如圖5-4所示。

注意最前面是標為0的8個位元,並且每種原色都指定為一個8位元的值。理論上,COLORREF可以指定二的二十四次方種或一千六百萬種色彩。

這個無正負號長整數常常稱為一個「RGB色彩」。Windows表頭檔案WINGDI.H提供了幾種使用RGB色彩值的巨集。RGB巨集要求三個參數分別代表紅、綠和藍值,然後將它們組合為一個無正負號長整數:

iPlanes = GetDeviceCaps (hdc, PLANES) ;

iBitsPixel = GetDeviceCaps (hdc, BITSPIXEL) ;

iColors = 1 << (iPlanes * iBitsPixel) ;

iColors = GetDeviceCaps (hdc, NUMCOLORS) ;

圖5-2 小字體和TEXTMETRIC欄位。

圖5-3 大字體和FONTMETRIC欄位

圖5-4 32位COLORREF值

#define RGB(r,g,b) ((COLORREF)(((BYTE)(r) | \ ((WORD)((BYTE)(g)) << 8)) | \ (((DWORD)(BYTE)(b)) << 16)))

注意三個參數的順序是紅、綠和藍。因此,值:

是0x0000FFFF,或黃色(紅色和綠色的合成)。當所有三個參數設定為0時,色彩為黑色;當所有參數設定為255時,色彩為白色。GetRValue、GetGValue和GetBValue巨集從COLORREF值中抽取出原色值。當您在使用傳回RGB色彩值的Windows函式時,這些巨集有時會很方便。

在16色或256色顯示卡上,Windows可以使用「混色」來類比設備能夠顯示的顏色之外的色彩。混色利用了由多種色彩的圖素組成的圖素圖案。可以呼叫GetNearestColor來決定與某一色彩最接近的純色:

裝置內容屬性

前面已經提到過,Windows使用裝置內容來保存控制GDI函式在顯示器上如何操作的「屬性」。例如,在用TextOut函式顯示文字時,程式寫作者不必指定文字的色彩和字體,Windows從裝置內容取得這個資訊。

程式取得一個裝置內容的代號時,Windows用預設值設定所有的屬性(在下一節會看到如何取代這種設定)。表5-1列出了Windows 98支援的裝置內容屬性,程式可以改變或者取得任何一種屬性。

RGB (255, 255, 0)

crPureColor = GetNearestColor (hdc, crColor) ;

表5-1

保存裝置內容

通常,在您呼叫GetDC或BeginPaint時,Windows用預設值建立一個新的裝置內容,您對屬性所做的一切改變在裝置內容用ReleaseDC或EndPaint呼叫釋放時,都會丟失。如果您的程式需要使用非內定的裝置內容屬性,則您必須在每次取得裝置內容代號時初始化裝置內容:

case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; 裝置內容屬性 繪製視窗顯示區域 EndPaint (hwnd, &ps) ; return 0 ;

雖然在通常情況下這種方法已經很令人滿意了,但是您還可能想要在釋放裝置內容之後,仍然保存程式中對裝置內容屬性所做的改變,以便在下一次呼叫GetDC和BeginPaint時它們仍然能夠起作用。為此,可在登錄視窗類別時,將CS_OWNDC旗標納入視窗類別的一部分:

現在,依據這個視窗類別所建立的每個視窗都將擁有自己的裝置內容,它一直存在,直到視窗被刪除。如果使用了CS_OWNDC風格,就只需初始化裝置內容一次,可以在處理WM_CREATE訊息處理期間完成這一操作:

wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC ;

case WM_CREATE: hdc = GetDC (hwnd) ; 初始化裝置內容屬性 ReleaseDC (hwnd, hdc) ;

這些屬性在改變之前一直有效。

CS_OWNDC風格只影響GetDC和BeginPaint獲得的裝置內容,不影響其他函式(如GetWindowDC)獲得的裝置內容。以前不提倡使用CS_OWNDC風格,因為它需要記憶體;現在,在處理大量圖形的Windows NT應用程式中,它可以提高性能。即使用了CS_OWNDC,您仍然應該在退出視窗訊息處理程式之前釋放裝置內容。

某些情況下,您可能想改變某些裝置內容屬性,用改變後的屬性進行繪圖,然後恢復原來的裝置內容。要簡化這一過程,可以通過如下呼叫來保存裝置內容的狀態:

現在,可以改變一些屬性,在想要回到呼叫SaveDC前存在的裝置內容時,呼叫:

您可以在呼叫RestoreDC之前呼叫SaveDC數次。

大多數程式寫作者以不同的方式使用SaveDC和RestoreDC。然而,更像組合語言中的PUSH和POP指令,當您呼叫SaveDC時,不需要保存傳回值:

然後,您可以更改某些屬性並再次呼叫SaveDC。要將裝置內容恢復到一個已經保存的狀態,呼叫:

這就將裝置內容恢復到最近由SaveDC函式保存的狀態中。

畫點和線

第一章 ,我們談論過Windows圖形裝置介面將圖形輸出設備的裝置驅動程式與電腦連在一起的方式。在理論上,只要提供SetPixel和GetPixel函式,就可以使用圖形裝置驅動程式繪製一切東西了。其餘的一切都可以使用GDI模組中實作的更高階的常式來處理。例如,畫線時,只需GDI呼叫SetPixel數次,並適當地調整x和y座標。

在實際情況中,也的確可以僅使用SetPixel和GetPixel函式進行您需要的任何繪製。您也可以在這些函式的基礎上設計出簡潔和構造良好的圖形編程系統。唯一的問題是啟能。如果一個函式通過幾次呼叫才能到達SetPixel函式,那麼它執行起來會非常慢。如果一個圖形系統畫線和進行其他複雜的圖形操作是在裝置驅動程式的層次上,它就會更有效得多,因為裝置驅動程式對完成這些操作的程式碼進行了最佳化。此外,一些顯示卡包含了圖形輔助運算器,它允許視訊硬體自己繪製圖形。

設定圖素

即使Windows GDI包含了SetPixel和GetPixel函式,但很少使用它們。在本書,僅在 第七章的CONNECT程式 中使用了SetPixel函式,僅在 第八章的WHATCLR程式 中使用了GetPixel函式。儘管如此,由它們開始來研究圖形仍是非常方便。

SetPixel函式在指定的x和y座標以特定的顏色設定圖素:

如同在任何繪圖函式中一樣,第一個參數是裝置內容的代號。第二個和第三個參數指明了座標位置。通常要獲得視窗顯示區域的裝置內容,並且x和y相對于該顯示區域的左上角。最後一個參數是COLORREF型態指定了顏色。如果在函式中指定的顏色視訊顯示器不支援,則函式將圖素設定為最接近的純色並從函式傳回該值。

GetPixel函式傳回指定座標處的圖素顏色:

直線

Windows可以畫直線、橢圓線(橢圓圓周上的曲線)和貝塞爾曲線。Windows 98支援的7個畫線函式是:

idSaved = SaveDC (hdc) ;

RestoreDC (hdc, idSaved) ;

SaveDC (hdc) ;

RestoreDC (hdc, -1) ;

SetPixel (hdc, x, y, crColor) ;

crColor = GetPixel (hdc, x, y) ;

  • LineTo 畫直線。
  • Polyline和PolylineTo 畫一系列相連的直線。
  • PolyPolyline 畫多組相連的線。
  • Arc 畫橢圓線。
  • PolyBezier和PolyBezierTo 畫貝塞爾曲線。
      • 另外,Windows NT還支援3種畫線函式:
  • ArcTo和AngleArc 畫橢圓線。
  • PolyDraw 畫一系列相連的線以及貝塞爾曲線。
      • 這三個函式Windows 98不支援。
      • 在本章的後面我將介紹一些既畫線也填入所畫圖形的封閉區域的函式,這些函式是:
  • Rectangle 畫矩形。
  • Ellipse 畫橢圓。
  • RoundRect 畫帶圓角的矩形。
  • Pie 畫橢圓的一部分,使其看起來像一個扇形。
  • Chord 畫橢圓的一部分,以呈弓形。

裝置內容的五個屬性影響著用這些函式所畫線的外觀:目前畫筆的位置(僅用於LineTo、PolylineTo、PolyBezierTo和ArcTo )、畫筆、背景方式、背景色和繪圖模式。

畫一條直線,必須呼叫兩個函式。第一個函式指定了線的開始點,第二個函式指定了線的終點:

MoveToEx實際上不會畫線,它只是設定了裝置內容的「目前位置」屬性。然後LineTo函式從目前的位置到它所指定的點畫一條直線。目前位置只是用於其他幾個GDI函式的開始點。在內定的裝置內容中,目前位置最初設定在點(0,0)。如果在呼叫LineTo之前沒有設定目前位置,那麼它將從顯示區域的左上角開始畫線。

小歷史:

Windows的16位元版本中,用來改變目前位置的函式是MoveTo。該函式只調整三個參數-裝置內容代號、x和y座標。函式通過兩個16位元數拼成的32位元無正負號長整數傳回先前的目前位置。然而,在Windows的32位元版本中,座標是32位元的數值,而C的32位元版本中又沒有定義64位元的整數資料型態,因此這種改變意味著MoveTo在其傳回值中不再指出先前的目前位置。在實際的程式寫作中,由MoveTo傳回的值幾乎從來不用,因此就需要一個新函式,這就是MoveToEx。

MoveToEx的最後一個參數是指向POINT結構的指標。從該函式傳回後,POINT結構的x和y欄位指出了先前的目前位置。如果您不需要這種資訊(通常如此),可以簡單地如上面的例子所示的那樣將最後一個參數設定為NULL。

警告:

儘管Windows 98中的座標值看起來是32位元的,實際上卻只用到了低16位元,座標值實際上被限制在-32,768到32,767之間。在Windows NT中,使用完整的32位元值。

如果您需要目前位置,就可以通過以下呼叫獲得:

其中,pt是POINT結構的。

下面的程式碼從視窗的左上角開始,在顯示區域中畫一個網格,線與線之間相隔100個圖素,其中hwnd是視窗代號,hdc是裝置內容代號,而x和y是整數:

MoveToEx (hdc, xBeg, yBeg, NULL) ; LineTo (hdc, xEnd, yEnd) ;

GetCurrentPositionEx (hdc, &pt) ;

GetClientRect (hwnd, &rect) ; for ( x = 0 ; x < rect.right ; x+= 100) { MoveToEx (hdc, x, 0, NULL) ; LineTo (hdc, x, rect.bottom) ; } for (y = 0 ; y < rect.bottom ; y += 100) { MoveToEx (hdc, 0, y, NULL) ; LineTo (hdc, rect.right, y) ; }

雖然用兩個函式來畫一條直線顯得有些麻煩,但是在希望畫一組相連的直線時,目前畫筆位置屬性又會變得很有用。例如,您可能想定義一個包含5個點(10個值)的陣列,來畫一個矩形的邊界框:

注意,最後一個點與第一個點相同。現在,只需要使用MoveToEx移到第一個點,並對後面的點使用LineTo:

POINT apt[5] = { 100, 100, 200, 100, 200, 200, 100, 200, 100, 100 } ;

MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; for ( i = 1 ; i < 5 ; i++) LineTo (hdc, apt[i].x, apt[i].y) ;

由於LineTo從目前位置畫到(但不包括)LineTo函式中給出的點,所以這段程式碼沒有在任何座標處畫兩次。雖然在顯示器上多輸出幾次不存在問題,但是在繪圖機上或者在其他繪圖方式(下面馬上會講到)下,視覺效果就不太好了。

當您要將陣列中的點連接成線時,使用Polyline函式要簡單得多。下面這條敘述畫出與上面一段程式碼相同的矩形:

最後一個參數是點的數目。我們還可以使用(sizeof (apt) / sizeof (POINT))來表示這個值。Polyline與一個MoveToEx函式後面加幾個LineTo函式的效果相同,但是,Polyline既不使用也不改變目前位置。PolylineTo有些不同,這個函式使用目前位置作為開始點,並將目前位置設定為最後一根線的終點。下面的程式碼畫出與上面所示一樣的矩形:

您可以對幾條線使用Polyline和PolylineTo,這些函式在繪製複雜曲線最有用了。您使用由幾百甚至幾千條線組成的極短線段,把它們連在一起就像一條曲線一樣。例如,畫正弦波就是這樣的,程式5-2所示的SINEWAVE程式顯示了如何做到這一點。

Polyline (hdc, apt, 5) ;

MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; PolylineTo (hdc, apt + 1, 4) ;

程式5-2 SINEWAVE SINEWAVE.C /*------------------------------------------------------------------- SINEWAVE.C -- Sine Wave Using Polyline (c) Charles Petzold, 1998 ---------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #define NUM 1000 #define TWOPI (2 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SineWave") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Sine Wave Using Polyline"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxClient, cyClient ; HDC hdc ; int i ; PAINTSTRUCT ps ; POINT apt [NUM] ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; MoveToEx (hdc, 0, cyClient / 2, NULL) ; LineTo (hdc, cxClient, cyClient / 2) ; for (i = 0 ; i < NUM ; i++) { apt[i].x = i * cxClient / NUM ; apt[i].y = (int) (cyClient / 2 * (1 - sin (TWOPI * i / NUM))) ; } Polyline (hdc, apt, NUM) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

這個程式有一個含有1000個POINT結構的陣列。隨著for迴圈從0增加到999,結構的x成員設定為從0遞增到數值cxClient。結構的y成員設定為一個周期的正弦曲線值,並被放大以填滿顯示區域。整個曲線的繪製僅僅使用了一個Polyline呼叫。因為Polyline函式是在裝置驅動程式層次上實作的,因此它要比呼叫1000次LineTo快得多,結果如圖5-5所示。

邊界框函式

下面我想討論的是Arc函式,它繪製橢圓曲線。然而,如果不先討論一下Ellipse函式,那麼Arc函式將難以理解;而如果不先討論Rectangle函式,那麼Ellipse函式又將難以理解;而如果討論Ellipse和Rectangle函式,那麼我又會討論RoundRect、Chord和Pie函式。

問題在於,Rectangle、Ellipse、RoundRect、Chord和Pie函式嚴格來說不是畫線函式。沒錯,這些函式是在畫線,但它們同時又填入畫刷填入一個封閉區域。這個畫刷內定為白色,因此當您第一次使用這些函式時,您可能不會注意到它們不只是畫線。嚴格地說,這些函式屬於後面「填入區域」的小節,不過,我還是在這裏討論它們。

上面提到的函式有一個共同特性,即它們都是依據一個矩形邊界框來繪圖的。您定義一個包含該物件的框,即「邊界框(bounding box)」;Windows就在這個框內畫出該物件。

這些函式中最簡單的就是畫一個矩形:

點(xLeft, yTop)是矩形的左上角,(xRight, yBottom)是矩形的右下角。用函式Rectangle畫出的圖形如圖5-6所示,矩形的邊總是平行于顯示器的水平和垂直邊。

以前寫過圖形程式的程式寫作者熟悉圖素偏差的問題。有些圖形系統畫出的圖形包含右座標和底座標,而有些則只畫到(而不包含)右座標和底座標。Windows採用後一種方法,不過有一種更簡單的方法來思考這個問題。

考慮下面的函式呼叫:

上面我們提到,Windows在邊界框內畫圖。可以將顯示器想像成一個網格,其中,每個圖素都在一個網格單元內。邊界框畫在網格上,然後在邊界框內畫矩形,下面說明了圖形畫出來時的樣子:

我以前提到過,Rectangle嚴格地說不是畫線函式,GDI也填入封閉區域。然而,因為內定用白色填入區域,因此GDI填入區域並不明顯。

您知道了如何畫矩形,也就知道了如何畫橢圓,因為它們使用的參數都是相同的:

用Ellipse函式畫出的圖形如圖5-7所示(加上了虛線構成的邊界框)。

畫圓角矩形的函式使用與函式Rectangle及Ellipse函式相同的邊界框,還包含另外兩個參數:

用這個函式畫出的圖形如5-8所示。

Windows使用一個小橢圓來畫圓角,這個橢圓的寬為xCornerEllipse,高為yCornerEllipse。可以想像這個小橢圓分為了四個部分,一個象限一個,每個剛好用在矩形的一個角上。 xCornerEllipse和yCornerEllipse的值越大,角就越明顯。如果xCornerEllipse等於xLeft與xRight的差,且yCornerEllipse等於yTop與yBottom的差,那麼RoundRect函式將畫出一個橢圓。

在繪製圖5-8所示的圓角矩形時,用了下面的公式來計算角上橢圓的尺寸。

這是一種簡單的方法,但是結果看起來有點不對勁,因為角的彎曲部分在矩形長的一邊要大些。要矯正這一問題,您可以讓xCornerEllipse與yCornerEllipse的值相等。

Arc、Chord和Pie函式都只要相同的參數:

Rectangle (hdc, xLeft, yTop, xRight, yBottom) ;

Rectangle (hdc, 1, 1, 5, 4) ;

Ellipse (hdc, xLeft, yTop, xRight, yBottom) ;

RoundRect ( hdc, xLeft, yTop, xRight, yBottom, xCornerEllipse, yCornerEllipse) ;

xCornerEllipse = (xRight - xLeft) / 4 ; yCornerEllipse = (yBottom- yTop) / 4 ;

將矩形和顯示區域左上角分開的區域有l個圖素寬。

圖5-6 使用Rectangle函式畫出的圖形

圖5-8 用RoundRect函式畫出的圖形

圖5-7 用Ellipse函式畫出的圖形

圖5-5 SINEWAVE顯示

Arc (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ; Chord (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ; Pie (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ;

用Arc函式畫出的線如圖5-9所示;用Chord和Pie函式畫出的線分別如圖5-10和5-11所示。Windows用一條假想的線將(xStart, yStart)與橢圓的中心連接,從該線與邊界框的交點開始, Windows按反時針方向,沿著橢圓畫一條弧。Windows還用另一條假想的線將(xEnd,yEnd)與橢圓的中心連接,在該線與邊界框的交點處,Windows停止畫弧。

圖5-9 Arc函式畫出的線

圖5-10 Chord函式畫出的線

圖5-11 Pie函式畫出的線

對於Arc函式,這樣就結束了。因為弧只是一條橢圓形的線而已,而不是一個填入區域。對於Chord函式,Windows連接弧線的端點。而對於Pie函式,Windows將弧的兩個端點與橢圓的中心相連接。弦與扇形圖的內部以目前畫刷填入。

您可能不太明白在Arc、Chord和Pie函式中開始和結束位置的用法,為什麼不簡單地在橢圓的周線上指定開始和結束點呢?是的,您可以這麼做,但是您將不得不算出這些點。Windows的方法在不要求這種精確性的條件下,卻完成了相同的工作。

程式5-3 LINEDEMO畫一個矩形、一個橢圓、一個圓角矩形和兩條線段,不過不是按這一順序。程式表明了定義封閉區域的函式實際上對這些區域進行了填入,因為在橢圓後面的線被遮住了,結果如圖5-12中所示。

程式5-3 LINEDEMO LINEDEMO.C /*--------------------------------------------------------- LINEDEMO.C -- Line-Drawing Demonstration Program (c) Charles Petzold, 1998 ----------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("LineDemo") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Line Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; Rectangle (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, cxClient, cyClient) ; MoveToEx (hdc, 0, cyClient, NULL) ; LineTo (hdc, cxClient, 0) ; Ellipse (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; RoundRect (hdc, cxClient / 4, cyClient / 4, 3 * cxClient / 4, 3 * cyClient / 4, cxClient / 4, cyClient / 4) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

圖5-12 LINEDEMO顯示

貝塞爾曲線

「曲尺」這個詞從前指的是一片木頭、橡皮或者金屬,用來在紙上畫曲線。比如說,如果您有一些不同圖點,您想要在它們之間畫一條曲線(內插或者外插),您首先將這些點描在繪圖紙上,然後,將曲尺定在這些點上,並用鉛筆沿著曲尺繞著這些點彎曲的方向畫曲線。

當然,時至今日,曲尺已經數學公式化了。有很多種不同的曲尺公式,它們各有千秋。貝塞爾曲線是電腦程式設計中用得最廣的曲尺公式之一,它是直到最近才加到作業系統層次的圖形支援中的。在六十年代Renault汽車公司進行了由手工設計車體(要用到粘土)到電腦輔助設計的轉變。他們需要一些數學工具,而Pierm Bezier找到了一套公式,最後顯示出這套公式應付這樣的工作非常有用。

此後,二維的貝塞爾曲線成了電腦圖學中最有用的曲線(在直線和橢圓之後)。在PostScript中,所有曲線都用貝塞爾曲線表示-橢圓線用貝塞爾曲線來逼近。貝塞爾曲線也用於定義PostScript字體的字元輪廓(TrueType使用一種更簡單更快速的曲尺公式)。

一條二維的貝塞爾曲線由四個點定義-兩個端點和兩個控制點。曲線的端點在兩個端點上,控制點就好像「磁石」一樣把曲線從兩個端點間的直線處拉走。這一點可以由底下的BEZIER互動交談程式做出最好的展示,如程式5-4所示。

程式5-4 BEZIER BEZIER.C /*----------------------------------------------------------------------- BEZIER.C -- Bezier Splines Demo (c) Charles Petzold, 1998 ------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Bezier") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Bezier Splines"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBezier (HDC hdc, POINT apt[]) { PolyBezier (hdc, apt, 4) ; MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; LineTo (hdc, apt[1].x, apt[1].y) ; MoveToEx (hdc, apt[2].x, apt[2].y, NULL) ; LineTo (hdc, apt[3].x, apt[3].y) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static POINT apt[4] ; HDC hdc ; int cxClient, cyClient ; PAINTSTRUCT ps ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; apt[0].x = cxClient / 4 ; apt[0].y = cyClient / 2 ; apt[1].x = cxClient / 2 ; apt[1].y = cyClient / 4 ; apt[2].x = cxClient / 2 ; apt[2].y = 3 * cyClient / 4 ; apt[3].x = 3 * cxClient / 4 ; apt[3].y = cyClient / 2 ; return 0 ; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MOUSEMOVE: if (wParam & MK_LBUTTON || wParam & MK_RBUTTON) { hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (WHITE_PEN)) ; DrawBezier (hdc, apt) ; if (wParam & MK_LBUTTON) { apt[1].x = LOWORD (lParam) ; apt[1].y = HIWORD (lParam) ; } if (wParam & MK_RBUTTON) { apt[2].x = LOWORD (lParam) ; apt[2].y = HIWORD (lParam) ; } SelectObject (hdc, GetStockObject (BLACK_PEN)) ; DrawBezier (hdc, apt) ; ReleaseDC (hwnd, hdc) ; } return 0 ; case WM_PAINT: InvalidateRect (hwnd, NULL, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; DrawBezier (hdc, apt) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

由於這個程式要用到一些在 第七章 才講的滑鼠處理方式,所以我不在這裏討論它的內部運作(不過,這也是簡單的),而是用這個程式來實驗性地操縱貝塞爾曲線。在這個程式中,兩個頂點設定在顯示區域的上下居中、左右位於1/4和3/4處的位置;兩個控制點可以改變,按住滑鼠左鍵或右鍵並拖動滑鼠可以分別改動兩個控制點之一。圖5-13是一個典型的例子。

除了貝塞爾曲線本身,程式還從第一個控制點向左邊的第一個端點(也叫做開始點)畫一條直線,並從第二個控制點向右邊的端點畫一條直線。

由於下面幾個特點,貝塞爾曲線在電腦輔助設計中非常有用。首先,經過少量練習,就可以把曲線調整到與想要的形狀非常接近。

其次,貝塞爾曲線非常好控制。對於有的曲尺種類來說,曲線不經過任何一個定義該曲線的點。貝塞爾曲線總是由其兩個端點開始和結束的(這是在推導貝塞爾公式時所做的假設之一)。另外,有些形式的曲尺公式有奇異點,在這些點處曲線趨向無窮遠,這在電腦輔助設計中通常是很不合適的。事實上,貝塞爾曲線總是受限於一個四邊形(叫做「凸包」),這個四邊形由端點和控制點連接而成。

第三個特點涉及端點和控制點之間的關係。曲線總是與第一個控制點到起點的直線相切,並保持同一方向;同時,也與第二個控制點到終點的直線相切,並保持同一方向。這是用於推導貝塞爾公式時所做的另外兩個假設。

第四,貝塞爾曲線通常比較具有美感。我知道這是一個主觀評價的問題,不過,並非只有我才這樣想。

在32位元的Windows版本之前,您必須利用Polyline來自己建立貝塞爾曲線,並且還需要知道下面的貝塞爾曲線的參數方程。起點是( x0,y0),終點是( x3,y3),兩個控制點是(x1,y1)和(x2,y2),隨著t的值從0到1的變化,就可以畫出曲線:

在Windows 98中,您不需要知道這些公式。要畫一條或多條連接的貝塞爾曲線,只需呼叫:

兩種情況下,apt都是POINT結構的陣列。對PolyBezier,前四個點(按照順序)給出貝塞爾曲線的起點、第一個控制點、第二個控制點和終點。此後的每一條貝塞爾曲線只需給出三個點,因為後一條貝塞爾曲線的起點就是前一條貝塞爾曲線的終點,如此類推。iCount參數等於1加上您所繪製的這些首尾相接曲線條數的三倍。

PolyBezierTo函式使用目前點作為第一個起點,第一條以及後續的貝塞爾曲線都只需要給出三個點。當函式傳回時,目前點設定為最後一個終點。

一點提示:在畫一系列相連的貝塞爾曲線時,只有當第一條貝塞爾曲線的第二個控制點、第一條貝塞爾曲線的終點(也就是第二條曲線的起點)和第二條貝塞爾曲線的第一個控制點線性相關時,也就是說這三個點在同一條直線上時,曲線在連接點處才是光滑的。

使用現有畫筆(Stock Pens)

當您呼叫這一節中討論的任何畫線函式時,Windows使用裝置內容中目前選中的「畫筆」來畫線。畫筆決定線的色彩、寬度和畫筆樣式,畫筆樣式可以是實線、點劃線或者虛線,內定裝置內容中畫筆為BLACK_PEN。不管映射方式是什麼,這種畫筆都畫出一個圖素寬的黑色實線來。BLACK_PEN是Windows提供的三種現有畫筆之一,其他兩種是WHITE_PEN和NULL_PEN,NULL_PEN什麼都不畫。您也可以自己自訂畫筆。

Windows程式以代號來使用畫筆。 Windows表頭檔案WINDEF.H中包含一個叫做HPEN的型態定義,即畫筆的代號,可以定義這個型態的變數(例如hPen):

呼叫GetStockObject,可以獲得現有畫筆的代號。例如,假設您想使用名為WHITE_PEN的現有畫筆,可以如下取得畫筆的代號:

現在必須將畫筆選進裝置內容:

目前的畫筆是白色。在這個呼叫後,您畫的線將使用WHITE_PEN,直到您將另外一個畫筆選進裝置內容或者釋放裝置內容代號為止。

您也可以不定義hPen變數,而將GetStockObject和SelectObject呼叫合併成一個敘述:

如果想恢復到使用BLACK_PEN的狀態,可以用一個敘述取得這種畫筆的代號,並將其選進裝置內容:

SelectObject的傳回值是此呼叫前裝置內容中的畫筆代號。如果啟動一個新的裝置內容並呼叫

則裝置內容中的目前畫筆將為WHITE_PEN,變數hPen將會是BLACK_PEN的代號。以後通過呼叫

就能夠將BLACK_PEN選進裝置內容。

畫筆的建立、選擇和刪除

儘管使用現有畫筆非常方便,但卻受限於實心的黑畫筆、實心的白畫筆或者沒有畫筆這三種情況。如果想得到更豐富多彩的效果,就必須建立自己的畫筆。

這一過程通常是:使用函式CreatePen或CreatePenIndirect建立一個「邏輯畫筆」,這僅僅是對畫筆的描述。這些函式傳回邏輯畫筆的代號;然後,呼叫SelectObject將畫筆選進裝置內容。現在,就可以使用新的畫筆來畫線了。在任何時候,都只能有一種畫筆選進裝置內容。在釋放裝置內容(或者在選擇了另一種畫筆到裝置內容中)之後,就可以呼叫DeleteObject來刪除所建立的邏輯畫筆了。在刪除後,該畫筆的代號就不再有效了。

邏輯畫筆是一種「GDI物件」,它是您可以建立的六種GDI物件之一,其他五種是畫刷、點陣圖、區域、字體和調色盤。除了調色盤之外,這些物件都是通過SelectObject選進裝置內容的。

在使用畫筆等GDI物件時,應該遵守以下三條規則:

x(t) = (1 - t)3 x0 + 3t (1 - t)2 x1 + 3t2 (1 - t) x2 + t3 x3 y(t) = (1 - t)3 y0 + 3t (1 - t)2 y1 + 3t2 (1 - t) y2 + t3 y3

PolyBezier (hdc, apt, iCount) ;

PolyBezierTo (hdc, apt, iCount) ;

HPEN hPen ;

hPen = GetStockObject (WHITE_PEN) ;

SelectObject (hdc, hPen) ;

SelectObject (hdc, GetStockObject (WHITE_PEN)) ;

SelectObject (hdc, GetStockObject (BLACK_PEN)) ;

hPen = SelectObject (hdc, GetStockobject (WHITE_PEN)) ;

SelectObject (hdc, hPen) ;

圖5-13 BEZIER程式的顯示

  • 最後要刪除自己建立的所有GDI物件。
  • 當GDI物件正在一個有效的裝置內容中使用時,不要刪除它。
  • 不要刪除現有物件。

這些規則當然是有道理的,而且有時這道理還挺微妙的。下面我們將舉些例子來幫助理解這些規則。

CreatePen函式的語法形如:

其中,iPenStyle參數確定畫筆是實線、點線還是虛線,該參數可以是WINGDI.H表頭檔案中定義的以下識別字,圖5-14顯示了每種畫筆產生的畫筆樣式。

對於PS_SOLID、PS_NULL和PS_INSIDEFRAME畫筆樣式,iWidth參數是畫筆的寬度。iWidth值為0則意味著畫筆寬度為一個圖素。現有畫筆是一個圖素寬。如果指定的是點劃線或者虛線式畫筆樣式,同時又指定一個大於1的實際寬度,那麼Windows將使用實線畫筆來代替。

CreatePen的crColor參數是一個COLORREF值,它指定畫筆的顏色。對於除了PS_INSIDEFRAME之外的畫筆樣式,如果將畫筆選入裝置內容中,Windows會將顏色轉換為設備所能表示的最相近的純色。PS_INSIDEFRAME是唯一一種可以使用混色的畫筆樣式,並且只有在寬度大於1的情況下才如此。

在與定義一個填入區域的函式一起使用時,PS_INSIDEFRAME畫筆樣式還有另外一個奇特之處:對於除了PS_INSIDEFRAME以外的所有畫筆樣式來說,如果用來畫邊界框的畫筆寬度大於1個圖素,那麼畫筆將居中對齊在邊界框線上,這樣邊界框線的一部分將位於邊界框之外;而對於PS_INSIDEFRAME畫筆樣式來說,整條邊界框線都畫在邊界框之內。

您也可以通過建立一個型態為LOGPEN(「邏輯畫筆」)的結構,並呼叫CreatePenIndirect來建立畫筆。如果您的程式使用許多能在原始碼中初始化的畫筆,那麼使用這種方法將有效得多。

要使用CreatePenIndirect,首先定義一個LOGPEN型態的結構:

此結構有三個成員:lopnStyle(無正負號整數或UINT)是畫筆樣式,lopnWidth(POINT結構)是按邏輯單位度量的畫筆寬度,lopnColor (COLORREF)是畫筆顏色。Windows只使用lopnWidth結構的x值作為畫筆寬度,而忽略y值。

將結構的位址傳遞給CreatePenIndirect結構就可以建立畫筆了:

注意,CreatePen和CreatePenIndirect函式不需要裝置內容代號作為參數。這些函式建立與裝置內容沒有聯繫的邏輯畫筆。直到呼叫SelectObject之後,畫筆才與裝置內容發生聯繫。因此,可以對不同的設備(如螢幕和印表機)使用相同的邏輯畫筆。

下面是建立、選擇和刪除畫筆的一種方法。假設您的程式使用三種畫筆-一種寬度為1的黑畫筆、一種寬度為3的紅畫筆和一種黑色點式畫筆,您可以先定義三個變數來存放這些畫筆的代號:

在處理WM_CREATE期間,您可以建立這三種畫筆:

hPen = CreatePen (iPenStyle, iWidth, crColor) ;

LOGPEN logpen ;

hPen = CreatePenIndirect (&logpen) ;

static HPEN hPen1, hPen2, hPen3 ;

圖5-14 七種畫筆樣式

hPen1 = CreatePen (PS_SOLID, 1, 0) ; hPen2 = CreatePen (PS_SOLID, 3, RGB (255, 0, 0)) ; hPen3 = CreatePen (PS_DOT, 0, 0) ;

在處理WM_PAINT期間,或者是在擁有一個裝置內容有效代號的任何時間裏,您都可以將這三個畫筆之一選進裝置內容並用它來畫線:

畫線函式

其他畫線函式

在處理WM_DESTROY期間,您可以刪除您建立的三種畫筆:

SelectObject (hdc, hPen2) ;

SelectObject (hdc, hPen1) ;

DeleteObject (hPen1) ; DeleteObject (hPen2) ; DeleteObject (hPen3) ;

這是建立、選擇和刪除畫筆最直接的方法。但是您的程式必須知道執行期間需要哪些邏輯畫筆,為此,您可能想要在每個WM_PAINT訊息處理期間建立畫筆,並在呼叫EndPaint之後刪除它們(您可以在呼叫EndPaint之前刪除它們,但是要小心,不要刪除裝置內容中目前選擇的畫筆)。

您可能還希望隨時建立畫筆,並將CreatePen和SelectObject呼叫組合到同一個敘述中:

現在再開始畫線,您將使用一個紅色虛線畫筆。在畫完紅色虛線之後,可以刪除畫筆。糟了!由於沒有保存畫筆代號,怎麼才能刪除這些畫筆呢?不要緊,請記住,SelectObject將傳回裝置內容中上一次選擇的畫筆代號。所以,您可以通過呼叫SelectObject將BLACK_PEN選進裝置內容,並刪除從SelectObject傳回的值:

下面是另一種方法,在將新建立的畫筆選進裝置內容時,保存SelectObject傳回的畫筆代號:

現在hPen是什麼呢?如果這是在取得裝置內容之後第一次呼叫SelectObject,則hPen是BLACK_PEN物件的代號。現在,可以將hPen選進裝置內容,並刪除所建立的畫筆(第二次SelectObject呼叫傳回的代號),只要一道敘述即可:

如果有一個畫筆的代號,就可以通過呼叫GetObject取得LOGPEN結構各個成員的值:

如果需要目前選進裝置內容的畫筆代號,可以呼叫:

第十七章 將討論另一個建立畫筆的函式ExtCreatePen。

填入空隙

使用點式畫筆和虛線畫筆會產生一個有趣的問題:點和虛線之間的空隙會怎樣呢?您所需要的是什麼?

空隙的著色取決於裝置內容的兩個屬性-背景模式和背景顏色。內定背景模式為OPAQUE,在這種方式下,Windows使用背景色來填入空隙,內定的背景色為白色。這與許多程式在視窗類別中用WHITE_BRUSH來擦除視窗背景的做法是一致的。

您可以通過如下呼叫來改變Windows用來填入空隙的背景色:

與畫筆色彩所使用的crColor參數一樣,Windows將這裏的背景色轉換為純色。可以通過用GetBkColor來取得裝置內容中定義的目前背景色。

通過將背景模式轉換為TRANSPARENT,可以阻止Windows填入空隙:

此後,Windows將忽略背景色,並且不填入空隙,可以通過呼叫GetBkMode來取得目前背景模式(TRANSPARENT或者OPAQUE)。

繪圖方式

裝置內容中定義的繪圖方式也影響顯示器上所畫線的外觀。設想畫這樣一條直線,它的色彩由畫筆色彩和畫線區域原來的色彩共同決定。設想用同一種畫筆在白色表面上畫出黑線而在黑色表面上畫出白線,而且不用知道表面是什麼色彩。這樣的功能對您有用嗎?通過繪圖方式的設定,這些都可以實作。

當Windows使用畫筆來畫線時,它實際上執行畫筆圖素與目標位置處原來圖素之間的某種位元布林運算。圖素間的位元布林運算叫做「位元映射運算」,簡稱為「ROP」。由於畫一條直線只涉及兩種圖素(畫筆和目標),因此這種布林運算又稱為「二元位元映射運算」,簡記為「ROP2」。Windows定義了16種ROP2代碼,表示Windows組合畫筆圖素和目標圖素的方式。在內定裝置內容中,繪圖方式定義為R2_COPYPEN,這意味著Windows只是將畫筆圖素複製到目標圖素,這也是我們通常所熟知的。此外,還有15種ROP2碼。

16種不同的ROP2碼是怎樣得來的呢?為了示範的需要,我們假設使用單色系統,目標色(視窗顯示區域的色彩)為黑色(用0來表示)或者白色(用1來表示),畫筆也可以為黑色或者白色。用黑色或者白色畫筆在黑色或者白色目標上畫圖有四種組合:白筆與白目標、白筆與黑目標、黑筆與白目標、黑筆與黑目標。

畫筆在目標上繪製後會得到什麼呢?一種可能是不管畫筆和目標的色彩,畫出的線總是黑色的,這種繪圖方式由ROP2代碼R2_BLACK表示。另一種可能是只有當畫筆與目標都為黑色時,畫出的結果才是白色,其他情況下畫出的都是黑色。儘管這似乎有些奇怪,Windows還是為這種方式起了一個名字,叫做R2_NOTMERGEPEN。Windows執行目標圖素與畫筆圖素的位元「或」運算,然後翻轉所得色彩。

表5-2顯示了所有16種ROP2繪圖方式,表中指示了畫筆色彩(P)與目標色彩(D)是如何組合而成結果色彩的。在標有「布林操作」的那一欄中,用C語言的表示法給出了目標圖素與畫筆圖素的組合方式。

SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;

DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ;

hPen = SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;

DeleteObject (SelectObject (hdc, hPen)) ;

GetObject (hPen, sizeof (LOGPEN), (LPVOID) &logpen) ;

hPen = GetCurrentObject (hdc, OBJ_PEN) ;

SetBkColor (hdc, crColor) ;

SetBkMode (hdc, TRANSPARENT) ;

表5-2

可以通過以下呼叫在裝置內容中設定新的繪圖模式:

iDrawMode參數是表中「繪圖模式」一欄中給出的值之一。您可以用函式:

來取得目前繪圖方式。裝置內容中的內定設定為R2_COPYPEN,它用畫筆色彩替代目標色彩。在R2_NOTCOPYPEN方式下,若畫筆為黑色,則畫成白色;若畫筆為白色,則畫成黑色。R2_BLACK方式下,不管畫筆和背景色為何種色彩,總是畫成黑色。與此相反,R2_WHITE方式下總是畫成白色。R2_NOP方式就是「不操作」,讓目標保持不變。

現在,我們已經討論了單色系統。然而,大多數系統是彩色的。在彩色系統中,Windows為畫筆和目標圖素的每個顏色位元執行繪圖方式的位元運算,並再次使用上表描述的16種ROP2代碼。R2_NOT繪圖方式總是翻轉目標色彩來決定線的顏色,而不管畫筆的色彩是什麼。例如,在青色目標上的線會變成紫色。R2_NOT方式總是產生可見的畫筆,除非畫筆在中等灰度的背景上繪圖。我將在 第七章的BLOKOUT程式 中展示R2_NOT繪圖方式的使用。

繪製填入區域

現在再更進一步,從畫線到畫圖形。Windows中七個用來畫帶邊緣的填入圖形的函式列於表5-3中。

SetROP2 (hdc, iDrawMode) ;

iDrawMode = GetROP2 (hdc) ;

表5-3

Windows用裝置內容中選擇的目前畫筆來畫圖形的邊界框,邊界框還使用目前背景方式、背景色彩和繪圖方式,這跟Windows畫線時一樣。關於直線的一切也適用於這些圖形的邊界框。

圖形以目前裝置內容中選擇的畫刷來填入。內定情況下,使用現有物件,這意味著圖形內部將畫為白色。Windows定義六種現有畫刷:WHITE_BRUSH、LTGRAY_BRUSH、GRAY_BRUSH、DKGRAY_BRUSH、BLACK_BRUSH和NULL_BRUSH (也叫HOLLOW_BRUSH)。您可以將任何一種現有畫刷選入您的裝置內容中,就和您選擇一種畫筆一樣。Windbws將HBRUSH定義為畫刷的代號,所以可以先定義一個畫刷代號變數:

您可以通過呼叫GetStockObject來取得GRAY_BRUSH的代號:

您可以呼叫SelectObject將它選進裝置內容:

現在,如果您要畫上表中的任一個圖形,則其內部將為灰色。

如果您想畫一個沒有邊界框的圖形,可以將NULL_PEN選進裝置內容:

如果您想畫出圖形的邊界框,但不填入內部,則將NULL_BRUSH選進裝置內容:

您也可以自訂畫刷,就如同您自訂畫筆一樣。我們將馬上談到這個問題。

Polygon函式和多邊形填入方式

我已經討論過了前五個區域填入函式,Polygon是第六個畫帶邊界框的填入圖形的函式,該函式的呼叫與Polyline函式相似:

其中,apt參數是POINT結構的一個陣列,iCount是點的數目。如果該陣列中的最後一個點與第一個點不同,則Windows將會再加一條線,將最後一個點與第一個點連起來(在Polyline函式中,Windows不會這麼做)。PolyPolygon函式如下所示:

該函式繪製多個多邊形。最後一個參數給出了所畫的多邊形的個數。對於每個多邊形,aiCounts陣列給出了多邊形的端點數。apt陣列具有全部多邊形的所有點。除傳回值以外,PolyPolygon在功能上與下面的代碼相同:

HBRUSH hBrush ;

hBrush = GetStockObject (GRAY_BRUSH) ;

SelectObject (hdc, hBrush) ;

SelectObject (hdc, GetStockObject (NULL_PEN)) ;

SelectObject (hdc, GetStockobject (NULL_BRUSH) ;

Polygon (hdc, apt, iCount) ;

PolyPolygon (hdc, apt, aiCounts, iPolyCount) ;

for (i = 0, iAccum = 0 ; i < iPolyCount ; i++) { Polygon (hdc, apt + iAccum, aiCounts[i]) ; iAccum += aiCounts[i] ; }

對於Polygon和PolyPolygon函式,Windows使用定義在裝置內容中的目前畫刷來填入這個帶邊界的區域。至於填入內部的方式,則取決於多邊形填入方式,您可以用SetPolyFillMode函式來設定:

內定情況下,多邊形填入方式是ALTERNATE,但是您可以將它設定為WINDING。兩種方式的區別參見圖5-15所示。

首先,ALTERNATE和WINDING方式之間的區別很容易察覺。對於ALTERNATE方式,您可以設想從一個無窮大的封閉區域內部的點畫線,只有假想的線穿過了奇數條邊界線時,才填入封閉區域。這就是填入了星的角而中心沒被填入的原因。

五角星的例子使得WINDING方式看起來比實際上更簡單一些。在繪製單個的多邊形時, 大多數情況下,WINDING方式會填入所有封閉的區域。但是也有例外。

在WINDING方式下要確定一個封閉區域是否被填入,您仍舊可以設想從那個無窮大的區域畫線。如果假想的線穿過了奇數條邊界線,區域就被填入,這和ALTERNATE方式一樣。如果假想的線穿過了偶數條邊界線,則區域可能被填入也可能不被填入。如果一個方向(相對於假想線)的邊界線數與另一個方向的邊界線數不相等,就填入區域。

例如,考慮圖5-16中的物體。線上的箭頭指出了畫線的方向。兩種方式都會填入三個封閉的L形區域,號碼從1到3。號碼為4和5的兩個小內部區域,在ALTERNATE方式下不會被填入。但是,在WINDING方式下,號碼為5的區域會被填入,因為從區域內必須穿過兩條相同方向的線才能到達圖形外部。號碼為4的區域不會被填入,因為必須穿過兩條方向相反的線。

如果您懷疑Windows沒有這麼聰明,那麼程式5-5 ALTWIND會展示給您看。

SetPolyFillMode (hdc, iMode) ;

圖5-15 用兩種多邊形填入方式畫出的圖:ALTERNATE(左)和WINDING(右)

圖5-16 WINDING方式不能填入所有內部區域的圖形

程式5-5 ALTWIND ALTWIND.C /*------------------------------------------------------------------- ALTWIND.C -- Alternate and Winding Fill Modes (c) Charles Petzold, 1998 -------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("AltWind") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Alternate and Winding Fill Modes"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static POINT aptFigure [10] = {10,70, 50,70, 50,10, 90,10, 90,50, 30,50, 30,90, 70,90, 70,30, 10,30 }; static int cxClient, cyClient ; HDC hdc ; int i ; PAINTSTRUCT ps ; POINT apt[10] ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (GRAY_BRUSH)) ; for (i = 0 ; i < 10 ; i++) { apt[i].x = cxClient * aptFigure[i].x / 200 ; apt[i].y = cyClient * aptFigure[i].y / 100 ; } SetPolyFillMode (hdc, ALTERNATE) ; Polygon (hdc, apt, 10) ; for (i = 0 ; i < 10 ; i++) { apt[i].x += cxClient / 2 ; } SetPolyFillMode (hdc, WINDING) ; Polygon (hdc, apt, 10) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

圖形的座標(劃分為100×100個單位)儲存在aptFigure陣列中。這些座標是依據顯示區域的寬度和高度劃分的。程式顯示圖形兩次,一次使用ALTERNATE填入方式,另一次使用WINDING方式。結果見圖5-17。

用畫刷填入內部

Rectangle、RoundRect、Ellipse、Chord、Pie、Polygon和PolyPolygon圖形的內部是用選進裝置內容的目前畫刷(也稱為「圖樣」)來填入的。畫刷是一個8×8的點陣圖,它水平和垂直地重複使用來填入內部區域。

當Windows用混色的方法來顯示多於可從顯示器上得到的色彩時,實際上是將畫刷用於色彩。在單色系統上,Windows能夠使用黑色和白色圖素的混色建立64種不同的灰色,更精確地說,Windows能夠建立64種不同的單色畫刷。對於純黑色,8×8點陣圖中的所有位元均為0。第一種灰色有一位元為1,第二種灰色有兩位元為1,以此類推,直到8×8點陣圖中所有位元均為1,這就是白色。在16色或256色顯示系統上,混色也是點陣圖,並且可以得到更多的色彩。

Windows還有五個函式,可以讓您建立邏輯畫刷,然後就可使用SelectObject將畫刷選進裝置內容。與邏輯畫筆一樣,邏輯畫刷也是GDI物件。您建立的所有畫刷都必須被刪除,但是當它還在裝置內容中時不能將其刪除。

下面是建立邏輯畫刷的第一個函式:

函式中的Solid並不是指畫刷為純色。在將畫刷選入裝置內容中時,Windows建立一個混色色的點陣圖,並為畫刷使用該點陣圖。

您還可以使用由水平、垂直或者傾斜的線組成的「影線標記(hatch marks)」來建立畫刷,這種風格的畫刷對著色條形圖的內部和在繪圖機上進行繪圖最有用。建立影線畫刷的函式為:

iHatchStyle參數描述影線標記的外觀。圖5-18顯示了六種可用的影線標記風格。

CreateHatchBrush中的crColor參數是影線的色彩。在將畫刷選進裝置內容時,Windows將這種色彩轉換為與之最相近的純色。影線之間的區域根據裝置內容中定義的背景方式和背景色來著色。如果背景方式為OPAQUE,則用背景色(它也被轉換為純色)來填入線之間的空間。在這種情況下,影線和填入色都不能是混色而成的顏色。如果背景方式為TRANSPARENT,則Windows只畫出影線,不填入它們之間的區域。

您也可以使用CreatePatternBrush和CreateDIBPatternBrushPt建立自己的點陣圖畫刷。

建立邏輯畫刷的第五個函式包含其他四個函式:

變數logbrush是一個型態為LOGBRUSH(「邏輯畫刷」)的結構,該結構的三個欄位如表5-4所示,lbStyle欄位的值確定了Windows如何解釋其他兩個欄位的值:

hBrush = CreateSolidBrush (crColor) ;

hBrush = CreateHatchBrush (iHatchStyle, crColor) ;

hBrush = CreateBrushIndirect (&logbrush) ;

圖5-18 六種影線畫刷風格

圖5-17 ALTWIND的顯示

表5-4

前面我們用SelectObject將邏輯畫筆選進裝置內容,用DeleteObject刪除畫筆,用GetObject來取得邏輯畫筆的資訊。對於畫刷,同樣能使用這三個函式。一旦您取得到了畫刷代號,就可以使用SelectObject將該畫刷選進裝置內容:

然後,您可以使用DeleteObject函式刪除所建立的畫刷:

但是,不要刪除目前選進裝置內容的畫刷。

如果您需要取得畫刷的資訊,可以呼叫GetObject:

其中,logbrush是一個型態為LOGBRUSH的結構。

GDI映射方式

到目前為止,所有的程式都是相對於顯示區域的左上角,以圖素為單位繪圖的。這是內定情況,但不是唯一選擇。事實上,「映射方式」是一種幾乎影響任何顯示區域繪圖的裝置內容屬性。另外有四種裝置內容屬性-視窗原點、視埠原點、視窗範圍和視埠範圍-與映射方式密切相關。

大多數GDI繪圖函式需要座標值或大小。例如,下面是TextOut函式:

參數x和y分別表示文字的開始位置。參數x是在水平軸上的位置,參數y是在垂直軸上的位置,通常用(x,y)來表示這個點。

在TextOut中,以及在幾乎所有GDI函式中,這些座標值使用的都是一種「邏輯單位」。Windows必須將邏輯單位轉換為「裝置單位」,即圖素。這種轉換是由映射方式、視窗和視埠的原點以及視窗和視埠的範圍所控制的。映射方式還指示著x軸和y軸的方向(orientation);也就是說,它確定了當您在向顯示器的左或者右移動時x的值是增大還是減小,以及在上下移動時y的值是增大還是減小。

Windows定義了8種映射方式,它們在WINGDI.H中相應的識別字和含義如表5-5所示。

SelectObject (hdc, hBrush) ;

DeleteObject (hBrush) ;

GetObject (hBrush, sizeof (LOGBRUSH), (LPVOID) &logbrush) ;

TextOut (hdc, x, y, psText, iLength) ;

表5-5

METRIC和ENGLISH指一般通行的度量衡系統,點是印刷的測量單位,約等於1/72英寸,但在圖形程式設計中假定為正好1/72英寸。「Twip」等於1/20點,也就是1/1440英寸。「Isotropic」和「anisotropic」是真正的單字,意思是「等方性」(同方向)和「異方性」(不同方向)。

您可以使用下面的敘述來設定映射方式:

其中,iMapMode是8個映射方式識別字之一。您可以通過以下呼叫取得目前的映射方式:

內定映射方式為MM_TEXT。在這種映射方式下,邏輯單位與實際單位相同,這樣我們可以直接以圖素為單位進行操作。在TextOut呼叫中,它看起來像這樣:

文字從距離顯示區域左端8圖素、上端16圖素的位置處開始。

如果映射方式設定為MM_LOENGLISH:

則邏輯單位是百分之一。現在,TextOut呼叫如下:

文字從距離顯示區域左端0.5英寸、上端1英寸的位置處開始。至於y座標前面的負號,隨著我們對映射方式更詳細的討論,將逐漸清楚。其他映射方式允許程式按照毫米、印表機的點大小或者任意單位的座標軸來指定座標。

如果您認為使用圖素進行工作很合適,那麼就不要使用內定的MM_TEXT方式外的任何映射方式。如果需要以英寸或者毫米尺寸顯示圖像,那麼可以從GetDeviceCaps中取得所需要的資訊,自己再進行縮放。其他映射方式都是避免您自己進行縮放的一個方便途徑而已。

雖然您在GDI函式中指定的座標是32位元的值,但是僅有Windows NT能夠處理全32位元。在Windows 98中,座標被限制為16位元,範圍從-32,768到32,767。一些使用座標表示矩形的開始點和結束點的Windows函式也要求矩形的寬和高小於或者等於32,767。

裝置座標和邏輯座標

您也許會問:如果使用MM_LOENGLISH映射方式,是不是將會得到以百分之一英寸為單位的WM_SIZE訊息呢?絕對不會。Windows對所有訊息(如WM_MOVE、WM_SIZE和WM_MOUSEMOVE),對所有非GDI函式,甚至對一些GDI函式,永遠使用裝置座標。可以這樣來考慮:由於映射方式是一種裝置內容屬性,所以,只有對需要裝置內容代號作參數的GDI函式,映射方式才會起作用。GetSystemMetrics不是GDI函式,所以它總是以裝置單位(即圖素)為量度來傳回大小的。儘管GetDeviceCaps是GDI函式,需要一個裝置內容代號作為參數,但是Windows仍然對HORZRES和VERTRES以裝置單位作為傳回值,因為該函式的目的之一就是給程式提供以圖素為單位的設備大小。

不過,從GetTextMetrics呼叫中傳回的TEXTMETRIC結構的值是使用邏輯單位的。如果在進行此呼叫時映射方式為MM_LOENGLISH,則GetTextMetrics將以百分之一英寸為單位提供字元的寬度和高度。在呼叫GetTextMetrics以取得關於字元的寬度和高度資訊時,映射方式必須設定成根據這些資訊輸出文字時所使用的映射方式,這樣就可以簡化工作。

裝置座標系

Windows將GDI函式中指定的邏輯座標映射為裝置座標。在討論以各種不同的映射方式使用邏輯座標系之前,我們先來看一下Windows為視訊顯示器區域定義的不同的裝置座標系。儘管我們大多數時間在視窗的顯示區域內工作,但Windows在不同的時間使用另外兩種裝置座標區域。所有裝置座標系都以圖素為單位,水平軸(即x軸)上的值從左到右遞增,垂直軸(即y軸)上的值從上到下遞增。

當我們使用整個螢幕時,就根據「螢幕座標」進行操作。螢幕的左上角為(0,0)點,螢幕座標用在WM_MOVE訊息(對於非子視窗)以及下列Windows函式中:CreateWindow和MoveWindow(都是對於非子視窗)、GetMessagePos、GetCursorPos、SetCursorPos、GetWindowRect以及WindowFromPoint(這不是全部函式的列表)。它們或者是與視窗無關的函式(如兩個游標函式),或者是必須相對於某個螢幕點來移動(或者尋找)視窗的函式。如果以DISPLAY為參數呼叫CreateDC,以取得整個螢幕的裝置內容,則內定情況下GDI呼叫中指定的邏輯座標將被映射為螢幕座標。

「全視窗座標」 以程式的整個視窗為基準,如標題列、功能表、捲動列和視窗框都包括在內。而對於普通視窗,點(0,0)是縮放邊框的左上角。全視窗座標在Windows中極少使用,但是如果用GetWindowDC取得裝置內容,GDI函式中的邏輯座標就會轉換為顯示區域座標。

第三種座標系是我們最常使用的「顯示區域座標系」。點(0,0)是顯示區域的左上角。當使用GetDC或BeginPaint取得裝置內容時,GDI函式中的邏輯座標就會內定轉換為顯示區域座標。

用函式ClientToScreen和ScreenToClient可以將顯示區域座標轉換為螢幕座標,或者反過來,將螢幕座標轉換為顯示區域座標。也可以使用GetWindowRect函式取得螢幕座標下的整個視窗的位置和大小。這三個函式為一種裝置座標轉換為另一種提供了足夠的資訊。

視埠和視窗

映射方式定義了Windows如何將GDI函式中指定的邏輯座標映射為裝置座標,這裏的裝置座標系取決於您用哪個函式來取得裝置內容。要繼續討論映射方式,我們需要一些術語:映射方式用於定義從「視窗」(邏輯座標)到「視埠」(裝置座標)的映射。

「視窗」和「視埠」這兩個詞用得並不恰當。在其他圖形介面語言中,視埠通常包含有剪裁區域的意思,並且,我們已經用視窗來指程式在螢幕上佔據的區域。在這裏的討論中,我們必須把關於這些詞的先入之見丟到一邊。

「視埠」是依據裝置座標(圖素)的。通常,視埠和顯示區域相同,但是,如果您已經用GetWindowDC或CreateDC取得了一個裝置內容,則視埠也可以是指整視窗座標或者螢幕座標。點(0,0)是顯示區域(或者整個視窗或螢幕)的左上角,x的值向右增加,y的值向下增加。

「視窗」是依據邏輯座標的,邏輯座標可以是圖素、毫米、英寸或者您想要的任何其他單位。您在GDI繪圖函式中指定邏輯視窗座標。

但是在真正的意義上,視埠和視窗僅是數學上的概念。對於所有的映射方式,Windows都用下面兩個公式來將視窗(邏輯)座標轉化為視埠(設備)座標:

其中,(xWindow,yWindow)是待轉換的邏輯點,(xViewport,yViewport)是轉換後的裝置座標點,一般情形下差不多就是顯示區域座標了。

這兩個公式使用了分別指定視窗和視埠「原點」的點:(xWinOrg,yWinOrg)是邏輯座標的視窗原點;(xViewOrg,yViewOrg)是裝置座標的視埠原點。在內定的裝置內容中,這兩個點均被設定為(0,0),但是它們可以改變。此公式意味著,邏輯點(xWinOrg,yWinOrg)總被映射為裝置點(xViewOrg,yViewOrg)。如果視窗和視埠的原點是預設值(0,0),則公式簡化為:

此公式還使用了兩點來指定「範圍」:(xWinExt,yWinExt)是邏輯座標的視窗範圍;(xViewExt,yViewExt)是裝置座標的視窗範圍。在多數映射方式中,範圍是映射方式所隱含的,不能夠改變。每個範圍自身沒有什麼意義,但是視埠範圍與視窗範圍的比例是邏輯單位轉換為裝置單位的換算因數。

例如,當您設定MM_LOENGLISH映射方式時,Windows將xViewExt設定為某個圖素數而將xWinExt設定為xViewExt圖素佔據的一英寸內有幾百圖素的長度。比值給出了一英寸內有幾百個圖素的數值。為了提高轉換效能,換算因數表示為整數比而不是浮點數。

範圍可以為負,也就是說,邏輯x軸上的值不一定非得在向右時增加;邏輯y軸上的值不一定非得在向下時增加。

Windows也能將視埠(設備)座標轉換為視窗(邏輯)座標:

Windows提供了兩個函式來讓您將裝置點轉換為邏輯點以及將邏輯點轉換為裝置點。下面的函式將裝置點轉換為邏輯點:

其中,pPoints是一個指向POINT結構陣列的指標,而iNumber是要轉換的點的個數。您會發現這個函式對於將GetClientRect(它總是使用裝置單位)取得的顯示區域大小轉換為邏輯座標很有用:

下面的函式將邏輯點轉換為裝置點:

處理MM_TEXT

對於MM_TEXT映射方式,內定的原點和範圍如下所示:

視窗原點: (0, 0) 可以改變

視埠原點: (0, 0) 可以改變

視窗範圍: (1, 1) 不可改變

視埠範圍: (1, 1) 不可改變

視埠範圍與視窗範圍的比例為1,所以不用在邏輯座標與裝置座標之間進行縮放。上面所給出的公式可以簡化為:

這種映射方式稱為「文字」映射方式,不是因為它對於文字最適合,而是由於軸的方向。我們讀文字是從左至右,從上至下的,而MM_TEXT以同樣的方向定義軸上值的增長方向:

Windows提供了函式SetViewportOrgEx和SetWindowOrgEx,用來改變視埠和視窗的原點,這些函式都具有改變軸的效果,以致(0,0)不再指左上角。一般來說,您會使用SetViewportOrgEx或SetWindowOrgEx之一,但不會同時使用二者。

我們來看一看這些函式有何效果:如果將視埠原點改變為(xViewOrg,yViewOrg),則邏輯點(0.0)就會映射為裝置點(xViewOrg,yViewOrg)。如果將視窗原點改變為(xWinOrg,yWinOrg),則邏輯點(xWinOrg,yWinOrg)將會映射為裝置點(0,0),即左上角。不管對視窗和視埠原點作什麼改變,裝置點(0,0)始終是顯示區域的左上角。

例如,假設顯示區域為cxClient個圖素寬和cyClient個圖素高。如果想將邏輯點(0,0)定義為顯示區域的中心,可進行如下呼叫:

SetViewportOrgEx的參數總是使用裝置單位。現在,邏輯點(0,0)將映射為裝置點(cxClient/2,cyClient/2),而顯示區域的座標系變成如下形狀:

邏輯x軸的範圍從-cxClient/2到+cxClient/2,邏輯y軸的範圍從-cyClient/2到+cyClient/2,顯示區域的右下角為邏輯點 (cxClient/2,cyClient/2)。如果您想從顯示區域的左上角開始顯示文字。則需要使用負座標:

用下面的SetWindowOrgEx敘述可以獲得與上面使用SetViewportOrgEx同樣的效果:

SetWindowOrgEx的參數總是使用邏輯單位。在這個呼叫之後,邏輯點(-cxClient / 2,-cyClient / 2)映射為裝置點(0,0),即顯示區域的左上角。

您不會將這兩個函式一起用,除非您知道這麼做的結果:

這意味著邏輯點(-cxClient/2,-cyClient/2)將映射為裝置點(cxClient/2, cyClient/2),結果是如下所示的座標系:

您可以使用下面兩個函式取得目前視埠和視窗的原點:

其中pt是POINT結構。由GetViewportOrgEx傳回的值是裝置座標,而由GetWindowOrgEx傳回的值是邏輯座標。

您可能想改變視埠或者視窗的原點,以改變視窗顯示區域內的顯示輸出-例如,回應使用者在捲動列內的輸入。但是,改變視埠和視窗原點並不能立即改變顯示輸出,而必須在改變原點之後更新輸出。例如,在第四章的SYSMETS2程式中,我們使用了iVscrollPos值(垂直捲動列的目前位置)來調整顯示輸出的y座標:

SetMapMode (hdc, iMapMode) ;

iMapMode = GetMapMode (hdc) ;

TextOut (hdc, 8, 16, TEXT ("Hello"), 5) ;

SetMapMode (hdc, MM_LOENGLISH) ;

TextOut (hdc, 50, -100, TEXT ("Hello"), 5) ;

DPtoLP (hdc, pPoints, iNumber) ;

GetClientRect (hwnd, &rect) ; DPtoLP (hdc, (PPOINT) &rect, 2) ;

LPtoDP (hdc, pPoints, iNumber) ;

SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;

TextOut (hdc, -cxClient / 2, -cyClient / 2, "Hello", 5) ;

SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL) ;

SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL) ;

GetViewportOrgEx (hdc, &pt) ; GetWindowOrgEx (hdc, &pt) ;

case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) { y = cyChar * (i - iVscrollPos) ; // 顯示文字 } EndPaint (hwnd, &ps) ; return 0 ;

我們可以使用SetWindowOrgEx獲得同樣的效果:

case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetWindowOrgEx (hdc, 0, cyChar * iVscrollPos) ; for (i = 0 ; i < NUMLINES ; i++) { y = cyChar * i ; // 顯示文字 } EndPaint (hwnd, &ps) ; return 0 ;

現在,TextOut函式的y座標的計算不需要iVscrollPos的值。這意味著您可以將文字輸出函式放到一個常式中,不用將iVscrollPos值傳給該常式,因為我們是通過改變視窗原點來調整文字顯示的。

如果您有使用直角座標系(即笛卡爾座標系)的經驗,那麼將邏輯點(0,0)移到顯示區域的中央(像我們上面所說的那樣)的確值得考慮。但是,對於MM_TEXT映射方式來說,還存在著一個小小的問題:笛卡爾座標系中,y值是隨著上移而增加的,而MM_TEXT定義為下移時y值增加。從這一點來看,MM_TEXT有點古怪,而下面這五種映射方式都使用通常的增值方法。

「度量」映射方式

Windows包含五種以實際尺寸來表示邏輯座標的映射方式。由於x軸和y軸的邏輯座標映射為相同的實際單位,這些映射方式能使您畫出不變形的圓和矩形。

這五種「度量」映射方式在表5-6中列出,按照從低精度到高精度的順序排列。右邊的兩列分別給出了以英寸和毫米為單位時邏輯單位的大小,以便比較。

表5-6

內定視窗及視埠的原點和範圍如下所示:

視窗原點: (0, 0) 可以改變

視埠原點: (0, 0) 可以改變

視窗範圍: (1, 1) 不可改變

視埠範圍: (1, 1) 不可改變

問號表示視窗和視埠的範圍依賴於映射方式和設備的解析度。前面已經提到過,這些範圍本身並不重要,但是表示比例時就必須知道。下面是視窗座標到視埠座標的轉換公式:

例如,對於MM_LOENGLISH,Windows計算的範圍如下:

Windows使用這些來自GetDeviceCaps的有用資訊設定範圍。只是在Windows 98和Windows NT之間有一點差別。

首先,來看看Windows 98是如何做的:假設您使用「控制台」的「顯示」程式選擇了96 dpi的系統字體。GetDeviceCaps對於LOGPIXELSX和LOGPIXELSY索引都將傳回值96。Windows為視埠範圍使用這些值並以表5-7的方式設定視埠和視窗的範圍。

表5-7

這樣,對MM_LOENGLISH來說,96除以100的比值是0.01英寸中的圖素數。對MM_LOMETRIC來說,96除以254的比值是0.1毫米中的圖素數。

Windows NT使用不同的方法設定視埠和視窗的範圍(與早期16位元版本的Windows一致的方法)。視埠範圍依據螢幕的圖素尺寸。可以使用HORZRES和VERTRES索引從GetDeviceCaps取得這種資訊。視窗範圍依據假定的顯示大小,它是您使用HORZSIZE和VERTSIZE索引時由GetDeviceCaps傳回的。我在前面提到過,這些值一般是320和240毫米。如果您將顯示器的圖素尺寸設定為1024×768,則表5-8就是Windows NT報告的視埠和視窗範圍的值。

表5-8

這些視窗範圍表示包含顯示器全部寬度和高度的邏輯單位元數值。320毫米寬的螢幕也為1260 MM_LOENGLISH單位或12.6英寸(320除以25.4毫米/英寸)。

範圍中,y前面的負號表示改變了軸的方向。對於這五種映射方式,y值隨上升而增加,然而注意內定的視窗和視埠原點均為(0,0)。這個事實有一個有趣的結果。當一開始改變為五種映射方式之一時,座標系如下:

要想在顯示區域顯示任何東西,必須使用負的y值。例如下面的程式碼:

將把文字顯示在距離顯示區域左邊和上邊各一英寸的地方。

為了使自己保持頭腦清醒,您可能想避免這樣做。一種解決辦法是將邏輯的(0,0)點設為顯示區域的左下角,您可以通過呼叫SetViewportOrgEx來完成(假設cyClient是以圖素為單位的顯示區域的高度):

此時的座標系如下:

這是直角座標系的右上象限。

另一種方法是將邏輯(0,0)點設為顯示區域的中心:

此時的座標系如下所示:

現在,我們有了一個真正的4象限笛卡爾座標系,在x軸和y軸上有相等的按英寸、毫米或twip計算的邏輯單位。

您還可以使用SetWindowOrgEx函式來改變邏輯(0,0)點,但是這稍微困難一些,因為SetWindowOrgEx的參數必須使用邏輯單位,先要將(cxClient,cyClient)用DPtoLP函式轉換為邏輯座標。假設變數pt是型態為POINT的結構,下面的代碼將邏輯(0,0)點改變到顯示區域的中央:

SetMapMode (hdc, MM_LOENGLISH) ; TextOut (hdc, 100, -100, "Hello", 5) ;

SetViewportOrgEx (hdc, 0, cyClient, NULL) ;

SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;

pt.x = cxClient ; pt.y = cyClient ; DptoLP (hdc, &pt, 1) ; SetWindowOrgEx (hdc, -pt.x / 2, -pt.y / 2, NULL) ;

「自行決定」的映射方式

剩下的兩種映射方式為MM_ISOTROPIC和MM_ANISOTROPIC。只有這兩種映射方式可以讓您改變視埠和視窗範圍,也就是說可以改變Windows用來轉換邏輯和裝置座標的換算因數。「isotropic」的意思是「同方向性」;「anisotropic」的意思是「異方向性」。與上面所討論的度量映射方式相似,MM_ISOTROPIC使用相同的軸,x軸上的邏輯單位與y軸上的邏輯單位的實際尺寸相等。這對您建立縱橫比與顯示比無關的圖像是有幫助的。

MM_ISOTROPIC與度量映射方式之間的區別是,使用MM_ISOTROPIC,您可以控制邏輯單位的實際尺寸。如果願意,您可以根據顯示區域的大小來調整邏輯單位的實際尺寸,從而使所畫的圖像總是包含在顯示區域內,並相應地放大或縮小。例如, 第八章的兩個時鐘程式 就是方向同性的例子。在您改變視窗大小時,時鐘也相應地調整。

Windows程式完全可以通過調整視窗和視埠範圍來處理圖像大小的變化。因此,不管視窗尺寸怎樣變,程式都可以在繪圖函式中使用相同的邏輯單位。

有時候MM_TEXT和度量映射方式稱為「完全侷限性」映射方式,這就是說,您不能改變視窗和視埠的範圍以及Windows將邏輯座標換算為裝置座標的方法。MM_ISOTROPIC是一種「半侷限性」的映射方式,Windows允許您改變視窗和視埠範圍,但只是調整它們,以便x和y邏輯單位代表同樣的實際尺寸。MM_ANISOTROPIC映射方式是「非侷限性」的,您可以改變視窗和視埠範圍,但是Windows不調整這些值。

MM_ISOTROPIC映射方式

如果想要在使用任意的軸時都保證兩個軸上的邏輯單位相同,則MM_ISOTROPIC映射方式就是理想的映射方式。這時,具有相同邏輯寬度和高度的矩形顯示為正方形,具有相同邏輯寬度和高度的橢圓顯示為圓。

當您剛開始將映射方式設定為MM_ISOTROPIC時,Windows使用與MM_LOMETRIC同樣的視窗和視埠範圍(但是,不要對此有所依賴)。區別在於,您現在可以呼叫SetWindowExtEx和SetViewportExtEx來根據自己的偏好改變範圍了,然後,Windows將調整範圍的值,以便兩條軸上的邏輯單位有相同的實際距離。

一般說來,您可以用所期望的邏輯視窗的邏輯尺寸作為SetWindowExtEx的參數,用顯示區域的實際寬和高作為SetViewportExtEx的參數。Windows在調整這些範圍時,必須讓邏輯視窗適應實際視窗,這就有可能導致顯示區域的一段落到了邏輯視窗的外面。必須在呼叫SetViewportExtEx之前呼叫SetWindowExtEx,以便最有效地使用顯示區域中的空間。

例如,假設您想要一個「傳統的」單象限虛擬座標系,其中(0,0)在顯示區域的左下角,寬度和高度的範圍都是從0到32,767,並且希望x和y軸的單位具有同樣的實際尺寸。以下就是所需的程式:

SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767, NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetViewportOrgEx (hdc, 0, cyClient, NULL) ;

如果其後用GetWindowExtEx和GetViewportExtEx函式獲得了視窗和視埠的範圍,可以發現,它們並不是先前指定的值。Windows將根據顯示設備的縱橫比來調整範圍,以便兩條軸上的邏輯單位表示相同的實際尺寸。

如果顯示區域的寬度大於高度(以實際尺寸為準),Windows將調整x的範圍,以便邏輯視窗比顯示區域視埠窄。這樣,邏輯視窗將放置在顯示區域的左邊:

Windows 98不允許在顯示區域的右邊超越x軸的範圍之外顯示任何東西,因為這需要一個大於16位元所能表示的座標。Windows NT使用全32位元座標,您可以在超出右邊顯示一些東西。

如果顯示區域的高度大於寬度(以實際尺寸為準),那麼Windows將調整y的範圍。這樣,邏輯視窗將放置在顯示區域的下邊:

Windows 98不允許在顯示區域的頂部顯示任何東西。

如果您希望邏輯視窗總是放在顯示區域的左上部,那麼將前面給出的程式碼改為:

SetMapMode (MM_ISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767, NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetWindowOrgEx (hdc, 0, 32767, NULL) ;

在呼叫SetWindowOrgEx中,我們要求將邏輯點(0, 32767)映射為裝置點(0,0)。現在,如果顯示區域的高大於寬,則座標系將安排為:

對於時鐘程式,您也許想要使用一個四象限的笛卡爾座標系,四個方向的座標尺度可以任意指定,(0,0) 必須居於顯示區域的中央。如果您想要每條軸的範圍從0到1000,則可以使用以下程式碼:

SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;

如果顯示區域的寬度大於高度,則邏輯座標系形如:

如果顯示區域的高度大於寬度,那麼邏輯座標也會居中:

記住,視窗或者視埠範圍並不意味著要進行剪裁。在呼叫GDI函式時,您仍然對以隨便地使用小於-1000和大於1000的x和y值。根據顯示區域的外形,這些點可能看得見,也可能看不見。

在MM_ISOTROPIC映射方式下,可以使邏輯單位大於圖素。例如,假設您想要一種映射方式,使點(0,0)顯示在螢幕的左上角,y的值向下增長(和MM_TEXT相似),但是邏輯座標單位為1/16英寸。以下是一種方法:

SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 16, 16, NULL) ; SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX), GetDeviceCaps (hdc, LOGPIXELSY), NULL) ;

SetWindowExtEx函式的參數指出了每一英寸中邏輯單位數。SetViewportExtEx函式的參數指出了每一英寸中實際單位數(圖素)。

然而,這種方法與Windows NT中的度量映射方式不一致。這些映射方式使用顯示器的圖素大小和公制大小。要與度量映射方式保持一致,可以這樣做:

SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 160 * GetDeviceCaps (hdc, HORZSIZE) / 254, 160 * GetDeviceCaps (hdc, VERTSIZE) / 254, NULL) ; SetViewportExtEx (hdc, GetDeviceCaps (hdc, HORZRES), GetDeviceCaps (hdc, VERTRES), NULL) ;

在這個程式碼中,視埠範圍設定為按圖素計算的整個螢幕的大小,視窗範圍則必須設定為以1/16英寸為單位的整個螢幕的大小。GetDeviceCaps以HORZRES和VERTRES為參數,傳回以毫米為單位的裝置尺寸。如果我們使用浮點數,將把毫米數除以25.4,轉換為英寸,然後,再乘以16以轉換為l/16英寸。但是,由於我們使用的是整數,所以先乘以160,再除以254。

當然,這種座標系會使邏輯單位大於實際單位。在設備上輸出的所有東西都將映射為按1/16英寸增量的座標值。當然,這樣就不能畫兩條間隔l/32英寸的水平直線,因為這樣將需要小數邏輯座標。

MM_ANISOTROPIC:根據需要放縮圖像

在MM_ISOTROPIC映射方式下設定視窗和視埠範圍時,Windows會調整範圍,以便兩條軸上的邏輯單位具有相同的實際尺度。在MM_ANISOTROPIC映射方式下,Windows不對您所設定的值進行調整,這就是說,MM_ANISOTROPIC不需要維持正確的縱橫比。

使用MM_ANISOTROPIC的一種方法是對顯示區域使用任意座標,就像我們對MM_ISOTROPIC所做的一樣。下面的程式碼將點(0,0)設定為顯示區域的左下角,x軸和y軸都從0到32,767:

SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767, NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetViewportOrgEx (hdc, 0, cyClient, NULL) ;

在MM_ISOTROPIC方式下,相似的程式碼導致顯示區域的一部分在軸的範圍之外。但是對於MM_ANISOTROPIC,不論其尺度多大,顯示區域的右上角總是(32767, 32767)。如果顯示區域不是正方形的,則邏輯x和y的單位具有不同的實際尺度。

前一節在MM_ISOTROPIC映射方式下,我們討論了在顯示區域中畫一個類似時鐘的圖像,x和y軸的範圍都是從-1000到+1000。對於MM_ANISOTROPIC,也可以寫出類似的程式:

SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;

與MM_ANISOTROPIC方式不同的是,這個時鐘一般是橢圓形的,而不是圓形的。

另一種使用MM_ANISOTROPIC的方法是將x和y軸的單位固定,但其值不相等。例如,如果有一個隻顯示文字的程式,您可能想根據單個字元的高度和寬度設定一種粗刻度的座標:

SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 1, 1, NULL) ; SetViewportExtEx (hdc, cxChar, cyChar, NULL) ;

當然,這裏假設cxChar和cyChar分別是那種字體的字元寬度和高度。現在,您可以按字元行和列指定座標。下面的敘述在距離顯示區域左邊三個字元,上邊二個字元處顯示文字:

如果您使用固定大小的字體時會更加方便,就像下面的WHATSIZE程式所示的那樣。

當您第一次設定MM_ANISOTROPIC映射方式時,它總是繼承前面所設定的映射方式的範圍,這會很方便。可以認為MM_ANISOTROPIC不「鎖定」範圍;也就是說,它允許您任意改變視窗範圍。例如,假設您想用MM_LOENGLISH映射方式,因為希望邏輯單位為0.01英寸,但您不希望y軸的值向上增加,喜歡如MM_TEXT那樣的方向,即y軸的值向下增加,可以使用如下的代碼:

其他行程式

TextOut (hdc, 3, 2, TEXT ("Hello"), 5) ;

SIZE size ;

SetMapMode (hdc, MM_LOENGLISH) ; SetMapMode (hdc, MM_ANISOTROPIC) ; GetViewportExtEx (hdc, &size) ; SetViewportExtEx (hdc, size.cx, -size.cy, NULL) ;

我們首先將映射方式設定為MM_LOENGLISH,然後,通過將映射方式設定為MM_ANISOTROPIC讓範圍可以自由改變。GetViewportExtEx取得視埠範圍並放到一個SIZE結構中,然後,我們使用範圍來呼叫SetViewportExtEx,只是要將y範圍取反。

WHATSIZE程式

Windows的小歷史:第一篇如何寫作Windows程式的介紹文章出現在《Microsoft Systems Journal》1986年12月號上。在那篇文章中,範例程式叫做WSZ(「what size:什麼尺寸」),它以圖素、英寸和毫米為單位顯示了顯示區域的大小。那個程式的更簡易版本是WHATSIZE,如程式5-6所示。程式顯示了以五種度量映射方式顯示的視窗顯示區域的大小。

程式5-6 WHATSIZE WHATSIZE.C /*------------------------------------------------------------ WHATSIZE.C -- What Size is the Window? (c) Charles Petzold, 1998 ----------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("WhatSize") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor= LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("What Size is the Window?"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void Show (HWND hwnd, HDC hdc, int xText, int yText, int iMapMode, TCHAR * szMapMode) { TCHAR szBuffer [60] ; RECT rect ; SaveDC (hdc) ; SetMapMode (hdc, iMapMode) ; GetClientRect (hwnd, &rect) ; DPtoLP (hdc, (PPOINT) &rect, 2) ; RestoreDC (hdc, -1) ; TextOut ( hdc, xText, yText, szBuffer, wsprintf (szBuffer, TEXT ("%-20s %7d %7d %7d %7d"), szMapMode, rect.left, rect.right, rect.top, rect.bottom)) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static TCHAR szHeading [] = TEXT ("Mapping Mode Left Right Top Bottom") ; static TCHAR szUndLine [] = TEXT ("------------ ---- ----- --- ------") ; static int cxChar, cyChar ; HDC hdc ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 1, 1, NULL) ; SetViewportExtEx (hdc, cxChar, cyChar, NULL) ; TextOut (hdc, 1, 1, szHeading, lstrlen (szHeading)) ; TextOut (hdc, 1, 2, szUndLine, lstrlen (szUndLine)) ; Show (hwnd, hdc, 1, 3, MM_TEXT, TEXT ("TEXT (pixels)")) ; Show (hwnd, hdc, 1, 4, MM_LOMETRIC, TEXT ("LOMETRIC (.1 mm)")) ; Show (hwnd, hdc, 1, 5, MM_HIMETRIC, TEXT ("HIMETRIC (.01 mm)")) ; Show (hwnd, hdc, 1, 6, MM_LOENGLISH, TEXT ("LOENGLISH (.01 in)")) ; Show (hwnd, hdc, 1, 7, MM_HIENGLISH,TEXT ("HIENGLISH (.001 in)")) ; Show (hwnd, hdc, 1, 8, MM_TWIPS, EXT ("TWIPS (1/1440 in)")) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

為了便於用TextOut函式顯示資訊,WHATSIZE使用了一種固定間距的字體。下面一條簡單的敘述就可以切換為固定間距的字體(在Windows 3.0中它是優先使用的):

有兩個同樣的函式用於選取畫筆和畫刷。像前面提到的,WHATSIZE也使用MM_ANISTROPIC映射方式將邏輯單位設定為字元大小。

當WHATSIZE需要取得六種映射方式之一的顯示區域的大小時,它保存目前的裝置內容,設定一種新的映射方式,取得顯示區域座標,將它們轉換為邏輯座標,然後在顯示資訊之前,恢復原映射方式。底下這些程式碼在WHATSIZE的Show函式裏:

SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

SaveDC (hdc) ; SetMapMode (hdc, iMapMode) ; GetClientRect (hwnd, &rect) ; DptoLP (hdc, (PPOINT) &rect, 2) ; RestoreDC (hdc, -1) ;

圖5-19顯示了WHATSIZE的典型輸出。

矩形、區域和剪裁

Windows包含了幾種使用RECT(矩形)結構和「區域」的繪圖函式。區域就是螢幕上的一塊地方,它是矩形、多邊形和橢圓的組合。

矩形函式

下面三個繪圖函式需要一個指向矩形結構的指標:

圖5-19 典型的WHATSIZE顯示

FillRect (hdc, &rect, hBrush) ; FrameRect (hdc, &rect, hBrush) ; InvertRect (hdc, &rect) ;

在這些函式中,rect參數是一個RECT型態的結構,它包含有4個欄位:left、top、right和bottom。這個結構中的座標被當作邏輯座標。

FillRect用指定畫刷來填入矩形(直到但不包含right和bottom座標),該函式不需要先將畫刷選進裝置內容。

FrameRect使用畫刷畫矩形框,但是不填入矩形。使用畫刷畫矩形看起來有點奇怪,因為對於我們所介紹過的函式(如Rectangle),其邊線都是用目前畫筆繪製的。FrameRect允許使用者畫一個不一定為純色的矩形框。該邊界框為一個邏輯單位元寬。如果邏輯單位大於裝置單位,則邊界框將會為2個圖素寬或者更寬。

InvertRect將矩形中所有圖素翻轉,1轉換成0,0轉換為1,該函式將白色區域轉變成黑色,黑色區域轉變為白色,綠色區域轉變成洋紅色。

Windows還提供了9個函式,使您可以更容易、更清楚地操作RECT結構。例如,要將RECT結構的四個欄位設定為特定值,通常使用如下的程式段:

rect.left = xLeft ; rect.top = xTop ; rect.right = xRight ; rect.bottom = xBottom ;

但是,通過呼叫SetRect函式,只需要一道敘述就可以得到同樣的結果:

在您想要做以下事情之一時,可以很方便地選用其他8個函式:

SetRect (&rect, xLeft, yTop, xRight, yBottom) ;

  • 將矩形沿x軸和y軸移動幾個單元:
      • OffsetRect (&rect, x, y) ;
  • 增減矩形的尺寸:
      • InflateRect (&rect, x, y) ;
  • 矩形各欄位設定為0:
      • SetRectEmpty (&rect) ;
  • 將矩形複製給另一個矩形:
      • CopyRect (&DestRect, &SrcRect) ;
  • 取得兩個矩形的交集:
      • IntersectRect (&DestRect, &SrcRect1, &SrcRect2) ;
  • 取得兩個矩形的聯集:
      • UnionRect (&DestRect, &SrcRect1, &SrcRect2) ;
  • 確定矩形是否為空:
      • bEmpty = IsRectEmpty (&rect) ;
  • 確定點是否在矩形內:
      • bInRect = PtInRect (&rect, point) ;

大多數情況下,與這些函式相同作用的程式碼很簡單。例如,您可以用下列敘述來替代CopyRect函式呼叫:

隨機矩形

在圖形系統中,有這麼一個「永遠」有人執行的有趣程式,它簡單地使用隨機的大小和色彩繪製一系列矩形。您可以在Windows中建立一個這樣的程式,但是它並不像乍看起來那樣容易編寫。我希望您能認識到,您不能簡單地在WM_PAINT訊息中使用一個while(TRUE)迴圈。當然,它能夠執行,但是程式將停止對其他訊息的處理,同時,這個程式不能中止或者最小化。

一種可以接受的方法是設定一個Windows計時器,給視窗程序發送WM_TIMER訊息(我將在 第八章 中討論計時器)。對於每條WM_TIMER訊息,您使用GetDC取得一個裝置內容,畫一個隨機的矩形,然後用ReleaseDC釋放裝置內容。但是這樣又降低了程式的趣昧性,因為程式不能盡可能快地畫隨機矩形,它必須等待WM_TIMER訊息,而這又依賴於系統時鐘的解析度。

在Windows中一定有很多「閒置時間」,在這個時間內,所有訊息佇列為空,Windows只停在一個小迴圈中等待鍵盤或者滑鼠輸入。我們能否在閒置時間內獲得控制,繪製矩形,並且只在有訊息加入程式的訊息佇列之後才釋放控制呢?這就是PeekMessage函式的目的之一。下面是PeekMessage呼叫的一個例子:

前面的四個參數(一個指向MSG結構的指標、一個視窗代號、兩個值指示訊息範圍)與GetMessage的參數相同。將第二、三、四個參數設定為NULL或0時,表明我們想讓PeekMessage傳回程式中所有視窗的所有訊息。如果要將訊息從訊息佇列中刪除,則將PeekMessage的最後一個參數設定為PM_REMOVE。如果您不希望刪除訊息,那麼您可以將這個參數設定為PM_NOREMOVE。這就是為什麼Peek_Message是「偷看」而不是「取得」的原因,它使得程式可以檢查程式的佇列中的下一個訊息,而不實際刪除它。

GetMessage不將控制傳回給程式,直到從程式的訊息佇列中取得訊息,但是PeekMessage總是立刻傳回,而不論一個訊息是否出現。當訊息佇列中有一個訊息時,PeekMessage的傳回值為TRUE(非0),並且將按通常方式處理訊息。當佇列中沒有訊息時,PeekMessage傳回FALSE(0)。

這使得我們可以改寫普通的訊息迴圈。我們可以將如下所示的迴圈:

DestRect = SrcRect ;

PeekMessage (&msg, NULL, 0, 0, PM_REMOVE) ;

while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ;

替換為下面的迴圈:

while (TRUE) { if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) break ; TranslateMessage (&msg) ; DispatchMessage (&msg) ; } else { // 完成某些工作的其他行程式 } } return msg.wParam ;

注意,WM_QUIT訊息被另外挑出來檢查。在普通的訊息迴圈中您不必這麼作,因為如果GetMessage接收到一個WM_QUIT訊息,它將傳回0,但是PeekMessage用它的傳回值來指示是否得到一個訊息,所以需要對WM_QUIT進行檢查。

如果PeekMessage的傳回值為TRUE,則訊息按通常方式進行處理。如果傳回值為FALSE,則在將控制傳回給Windows之前,還可以作一點工作(如顯示另一個隨機矩形)。

(儘管Windows文件上說,您不能用PeekMessage從訊息佇列中刪除WM_PAINT訊息,但是這並不是什麼大不了的問題。畢竟,GetMessage並不從訊息佇列中刪除WM_PAINT訊息。從佇列中刪除WM_PAINT訊息的唯一方法是令視窗顯示區域的失效區域變得有效,這可以用ValidateRect和ValidateRgn或者BeginPaint和EndPaint對來完成。如果您在使用PeekMessage從佇列中取出WM_PAINT訊息後,同平常一樣處理它,那麼就不會有問題了。所不能作的是使用如下所示的程式碼來清除訊息佇列中的所有訊息:

這行敘述從訊息佇列中刪除WM_PAINT之外的所有訊息。如果佇列中有一個WM_PAINT訊息,程式就會永遠地陷在while迴圈中。)

PeekMessage在Windows的早期版本中比在Windows 98中要重要得多。這是因為Windows的16位元版本使用的是非優先權式的多工(我將在 第二十章 中討論這一點)。Windows的Terminal程式在從通訊埠接收輸入後,使用一個PeekMessage迴圈。列印管理器程式使用這個技術來進行列印,其他的Windows列印應用程式通常都會使用一個PeekMessage迴圈。在Windows 98優先權式的多工環境下,程式可以建立多個執行緒,我們將 第二十章 看到這一點。

不管怎樣,有了PeekMessage函式,我們就可以編寫一個不停地顯示隨機矩形的程式。這個RANDRECT如程式5-7中所示。

while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) ;

程式5-7 RANDRECT RANDRECT.C /*---------------------------------------------------------------------- RANDRECT.C -- Displays Random Rectangles (c) Charles Petzold, 1998 -----------------------------------------------------------------------*/ #include <windows.h> #include <stdlib.h> // for the rand function LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; void DrawRectangle (HWND) ; int cxClient, cyClient ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("RandRect") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Random Rectangles"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (TRUE) { if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) break ; TranslateMessage (&msg) ; DispatchMessage (&msg) ; } else DrawRectangle (hwnd) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { switch (iMsg) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ; } void DrawRectangle (HWND hwnd) { HBRUSH hBrush ; HDC hdc ; RECT rect ; if (cxClient == 0 || cyClient == 0) return ; SetRect (&rect, rand () % cxClient, rand () % cyClient, rand () % cxClient, rand () % cyClient) ; hBrush = CreateSolidBrush ( RGB (rand () % 256, rand () % 256, rand () % 256)) ; hdc = GetDC (hwnd) ; FillRect (hdc, &rect, hBrush) ; ReleaseDC (hwnd, hdc) ; DeleteObject (hBrush) ; }

這個程式在現在的電腦上執行得非常快,看起來都不像是一系列隨機矩形了。程式使用我在上面討論過的SetRect和FillRect函式,根據由C的rand函式得到的亂數決定矩形座標和實心畫刷的色彩。我將在 第二十章 中提供這個程式的多執行緒版本。

建立和繪製剪裁區域

剪裁區域是對顯示器上一個範圍的描述,這個範圍是矩形、多邊形和橢圓的組合。剪裁區域可以用於繪製和剪裁,通過將剪裁區域選進裝置內容,就可以用剪裁區域來進行剪裁(就是說,將可以繪圖的範圍限制為顯示區域的一部分)。與畫筆、畫刷和點陣圖一樣,剪裁區域是GDI物件,您應該呼叫DeleteObject來刪除您所建立的剪裁區域。

當您建立一個剪裁區域時,Windows傳回一個該剪裁區域的代號,型態為HRGN。最簡單的剪裁區域是矩形,有兩種建立矩形的方法:

或者

您也可以建立橢圓剪裁區域:

或者

CreateRoundRectRgn建立圓角的矩形剪裁區域。

建立多邊形剪裁區域的函式類似於Polygon函式:

point參數是一個POINT型態的結構陣列,iCount是點的數目,iPolyFillMode是ALTERNATE或者WINDING。您還可以用CreatePolyPolygonRgn來建立多個多邊形剪裁區域。

那麼,您會問,剪裁區域究竟有什麼特別之處?下面這個函式才真正顯示出了剪裁區域的作用:

這一函式將兩個剪裁區域(hSrcRgn1和hSrcRgn2)組合起來並用代號hDestRgn指向組合成的剪裁區域。這三個剪裁區域代號都必須是有效的,但是hDestRgn原來所指向的剪裁區域被破壞掉了(當您使用這個函式時,您可能要讓hDestRgn在初始時指向一個小的矩形剪裁區域)。

iCombine參數說明hSrcRgn1和hSrcRgn2如何組合,見表5-9。

hRgn = CreateRectRgn (xLeft, yTop, xRight, yBottom) ;

hRgn = CreateRectRgnIndirect (&rect) ;

hRgn = CreateEllipticRgn (xLeft, yTop, xRight, yBottom) ;

hRgn = CreateEllipticRgnIndirect (&rect) ;

hRgn = CreatePolygonRgn (&point, iCount, iPolyFillMode) ;

iRgnType = CombineRgn (hDestRgn, hSrcRgn1, hSrcRgn2, iCombine) ;

表5-9

從CombineRgn傳回的iRgnType值是下列之一:NULLREGION,表示得到一個空剪裁區域;SIMPLEREGION,表示得到一個簡單的矩形、橢圓或者多邊形;COMPLEXREGION,表示多個矩形、橢圓或多邊形的組合;ERROR,表示出錯了。

剪裁區域的代號可以用於四個繪圖函式:

FillRgn (hdc, hRgn, hBrush) ; FrameRgn (hdc, hRgn, hBrush, xFrame, yFrame) ; InvertRgn (hdc, hRgn) ; PaintRgn (hdc, hRgn) ;

FillRgn、FrameRgn和InvertRgn類似於FillRect、FrameRect和InvertRect。FrameRgn的xFrame和yFrame參數是畫在區域周圍的邊框的寬度和高度。PaintRgn函式用裝置內容中目前畫刷填入所指定的區域。所有這些函式都假定區域是用邏輯座標定義的。

在您用完一個區域後,可以像刪除其他GDI物件那樣刪除它:

矩形與區域的剪裁

區域也在剪裁中扮演了一個角色。InvalidateRect函式使顯示的一個矩形區域失效,並產生一個WM_PAINT訊息。例如,您可以使用InvalidateRect函式來清除顯示區域並產生一個WM_PAINT訊息:

您可以通過呼叫GetUpdateRect來取得失效矩形的座標,並且可以使用ValidateRect函式使顯示區域的矩形有效。當您接收到一個WM_PAINT訊息時,無效矩形的座標可以從PAINTSTRUCT結構中得到,該結構是用BeginPaint函式填入的。這個無效矩形還定義了一個「剪裁區域」,您不能在剪裁區域外繪圖。

Windows有兩個作用於剪裁區域而不是矩形的函式,它們類似於InvalidateRect和ValidateRect:

當您接收到一個由無效區域引起的WM_PAINT訊息時,剪裁區域不一定是矩形。

您可以使用以下兩個函式之一:

通過將一個剪裁區域選進裝置內容來建立自己的剪裁區域,這個剪裁區域使用裝置座標。

GDI為剪裁區域建立一份副本,所以在將它選進裝置內容之後,使用者可以刪除它。Windows還提供了幾個對剪裁區域進行操作的函式,如ExcludeClipRect用於將一個矩形從剪裁區域裏排除掉,IntersectClipRect用於建立一個新的剪裁區域,它是前一個剪裁區域與一個矩形的交,OffsetClipRgn用於將剪裁區域移動到顯示區域的另一部分。

CLOVER程式

CLOVER程式用四個橢圓組成一個剪裁區域,將這個剪裁區域選進裝置內容中,然後畫出從視窗顯示區域的中心出發的一系列直線,這些直線只出現在剪裁區域所限定的範圍,結果顯示如圖5-20所示。

要用常規的方法畫出這個圖形,就必須根據橢圓的邊線公式計算出每條直線的端點。利用複雜的剪裁區域,可以直接畫出這些線條,而讓Windows確定其端點。CLOVER如程式5-8所示。

DeleteObject (hRgn) ;

InvalidateRect (hwnd, NULL, TRUE) ;

InvalidateRgn (hwnd, hRgn, bErase) ;

ValidateRgn (hwnd, hRgn) ;

SelectObject (hdc, hRgn) ;

SelectClipRgn (hdc, hRgn) ;

圖5-20 CLOVER利用複雜的剪裁區域畫出的圖像

程式5-8 CLOVER CLOVER.C /*-------------------------------------------------------------------------- CLOVER.C -- Clover Drawing Program Using Regions (c) Charles Petzold, 1998 ----------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #define TWO_PI (2.0 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Clover") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Draw a Clover"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) { static HRGN hRgnClip ; static int cxClient, cyClient ; double fAngle, fRadius ; HCURSOR hCursor ; HDC hdc ; HRGN hRgnTemp[6] ; int i ; PAINTSTRUCT ps ; switch (iMsg) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; hCursor = SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; if (hRgnClip) DeleteObject (hRgnClip) ; hRgnTemp[0] = CreateEllipticRgn (0, cyClient / 3, cxClient / 2, 2 * cyClient / 3) ; hRgnTemp[1] = CreateEllipticRgn (cxClient / 2, cyClient / 3, cxClient, 2 * cyClient / 3) ; hRgnTemp[2] = CreateEllipticRgn (cxClient / 3, 0, 2 * cxClient / 3, cyClient / 2) ; hRgnTemp[3] = CreateEllipticRgn (cxClient / 3, cyClient / 2, 2 * cxClient / 3, cyClient) ; hRgnTemp[4] = CreateRectRgn (0, 0, 1, 1) ; hRgnTemp[5] = CreateRectRgn (0, 0, 1, 1) ; hRgnClip = CreateRectRgn (0, 0, 1, 1) ; CombineRgn (hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR) ; CombineRgn (hRgnTemp[5], hRgnTemp[2], hRgnTemp[3], RGN_OR) ; CombineRgn (hRgnClip, hRgnTemp[4], hRgnTemp[5], RGN_XOR) ; for (i = 0 ; i < 6 ; i++) DeleteObject (hRgnTemp[i]) ; SetCursor (hCursor) ; ShowCursor (FALSE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SelectClipRgn (hdc, hRgnClip) ; fRadius = _hypot (cxClient / 2.0, cyClient / 2.0) ; for (fAngle = 0.0 ; fAngle < TWO_PI ; fAngle += TWO_PI / 360) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, (int) ( fRadius * cos (fAngle) + 0.5), (int) (-fRadius * sin (fAngle) + 0.5)) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteObject (hRgnClip) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ; }

由於剪裁區域總是使用裝置座標,CLOVER程式必須在每次接收到WM_SIZE訊息時重新建立剪裁區域。幾年前,這可能需要幾秒鐘。現在的快速機器在一瞬間就可以畫出來。

CLOVER從建立四個橢圓剪裁區域開始,這四個橢圓存放在hRgnTemp陣列的頭四個元素中,然後建立三個「空」剪裁區域:

hRgnTemp [4] = CreateRectRgn (0, 0, 1, 1) ; hRgnTemp [5] = CreateRectRgn (0, 0, 1, 1) ; hRgnClip = CreateRectRgn (0, 0, 1, 1) ;

顯示區域左右的兩個橢圓區域組合起來:

同樣,顯示區域上下兩個橢圓區域組合起來:

最後,兩個組合後的區域再組合到hRgnClip中:

RGN_XOR識別字用於從結果區域中排除重疊部分。最後,刪除6個臨時區域:

與畫出的圖形比起來,WM_PAINT的處理很簡單。視埠原點設定為顯示區域的中心(使畫直線更容易一些),在WM_SIZE訊息處理期間建立的區域選擇為裝置內容的剪裁區域:

現在,剩下的就是畫直線了,共360條,每隔一度畫一條。每條線的長度為變數fRadius,這是從中心到顯示區域的角落的距離:

CombineRgn (hRgnTemp [4], hRgnTemp [0], hRgnTemp [1], RGN_OR) ;

CombineRgn (hRgnTemp [5], hRgnTemp [2], hRgnTemp [3], RGN_OR) ;

CombineRgn (hRgnClip, hRgnTemp [4], hRgnTemp [5], RGN_XOR) ;

for (i = 0 ; i < 6 ; i++) DeleteObject (hRgnTemp [i]) ;

SetViewportOrg (hdc, xClient / 2, yClient / 2) ; SelectClipRgn (hdc, hRgnClip) ;

fRadius = hypot (xClient / 2.0, yClient / 2.0) ; for (fAngle = 0.0 ; fAngle < TWO_PI ; fAngle += TWO_PI / 360) { MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, (int) ( fRadius * cos (fAngle) + 0.5), (int) (-fRadius * sin (fAngle) + 0.5)) ; }

在處理WM_DESTROY訊息時,刪除該剪裁區域:

這不是本書關於圖形程式設計的最後內容。 第十三章 討論列印, 第十四章十五章 討論點陣圖, 第十七章 討論文字和字體, 第十八章 討論metafile。

DeleteObject (hRgnClip) ;