Chapter 05 绘图基础
整理了大半個月,終于把Chapter 05整理好,希望能夠對自己以及網友有所幫助。本章我們將一起學習繪圖基礎,本章節會學習到GDI基礎、繪制線條和填充區域的基礎知識。Windows子系統負責在稱為圖形設備接口(Graphics Device Interface,GDI)的視頻顯示器和打印機上顯示圖形,GDI的重要性不僅體現在Windows上顯示信息的應用程序時要使用GDI,Windows本身也會使用GDI顯示用戶界面的項目,比如菜單、滾動條、圖標和鼠標指針。
5.1??? GDI的結構
5.1.1 GDI原理
在Windows NT中,圖形顯示主要由GDI32.DLL中導出的函數處理,該動態鏈接庫會調用你安裝的視頻顯示器和打印機的設備驅動程序中的一些函數。視頻驅動程序會直接訪問視頻顯示器的硬件,而打印機驅動程序則將GDI命令轉化為各種打印機所能理解的代碼或者命令,因此不同的顯示適配器和打印機需要使用不同的設備驅動程序。GDI提供了一種特殊的機制來徹底隔離應用程序和不同輸出設備的特性,以提供與設備無關的圖形。
5.1.2 GDI函數調用
GDI包含有幾百個函數,可以分成如下幾類:
?獲取(或建立)和釋放(或銷毀)設備環境的函數:繪制時,需要使用一個設備環境句柄。BeginPaint和EndPaint函數(盡管從技術上來說,它們屬于USER模塊,而不是GDI模塊)允許你在處理WM_PAINT消息時做到這一點。在處理其他消息時,可以通過GetDC和ReleaseDC函數來達到相同的目的。
?獲取設備環境信息的函數:例如GetTextMetrics函數可以用來獲取當前被選入設備環境的字體的尺寸信息。GetSystemMetrics函數可以用來獲取顯示設備和系統配置的尺寸信息(以像素為單位)。
?繪圖函數:一旦繪圖準備工作就緒,繪圖函數才會發揮它的作用。例如TextOut函數在窗口的客戶區顯示,以及即將介紹的繪制線條和填充區域的GDI函數等等。
?設置和獲取設備環境屬性的函數:設備環境的屬性確定會凸函數繪圖時的各種細節。例如,可以使用SetTextColor函數來指定TextOut繪制的文本的顏色。所有的設備環境的屬性都有一個默認值,這個默認值在獲取設備環境時候已經被設置好了,對所有的以Set開頭的函數,都有相應的一個以Get開頭的函數用戶獲取當前設備環境的屬性。
使用GDI“對象”的函數: GDI對象包括畫筆、畫刷、字體、區域、位圖、調色板以及其他GDI對象。包含各種針對GDI對象的操作函數,例如使用CreatePen、CreatePenIndirect或者ExtCreatePen函數來創建邏輯畫筆,使用SelectObject函數將邏輯畫筆選入某設備環境中。
5.1.3 GDI的基本圖形
在屏幕上或者打印機顯示的圖形類型可以分為下面幾類,被稱為“基本圖形”。
線條和曲線 線條是任何矢量圖形繪制系統的基礎。GDI支持直線、矩形、橢圓和貝塞爾曲線。GDI使用當前選入設備環境的畫筆繪制線條。
可悲填充的封閉區域 當一系列的線條或者曲線構成一個封閉區域時,你可以使用當前GDI的畫刷對象來填充這個區域。
位圖 位圖是一個二維的位數組,每一個元素都對應顯示設備上的一個像素。位圖是光柵圖形的基礎。位圖通常位于在視頻顯示器或者打印機上顯示復雜(通常是真實世界)的圖像。位圖也通常用于顯示必須要快速繪制的小圖像,例如圖標、鼠標指針以及出現在應用程序工具欄里的按鈕。GDI支持兩種類型的位圖:舊式的設備相關位圖和新式的設備無關位圖(DIB,從Windows3.0起)。DIB可以存放在磁盤文件中。
文本 文本通常是任何計算機圖形系統中最復雜的部分,而且也是最重要的部分。在所有Windows的數據結構中,用于定義GDI字體對象和獲取字體信息的數據結構式最龐大的。GDI從Windows3.1開始支持TrueType字體,這種字體是以填充的輪廓線為基礎的,某些GDI函數可以操控這些輪廓線。
5.1.4 其他
GDI的其他方面就不太容易分類,具體如下:
映射模式(mapping mode)和轉換(transform):盡管在默認時是以像素為單位進行繪制的,但是并不是別無選擇。GDI的映射模式允許以英寸(甚至幾分之一英寸)、毫米或者其他你所想要的任何單位進行繪制。除此之外,Windows NT支持傳統的世界坐標轉換,適用于傾斜和旋轉圖形對象。
圖元文件(metafile): 一個圖元文件是以二進制形式存儲的GDI命令的集合。圖元文件主要用于通過剪貼板轉換矢量圖形繪制的表現形式。
區域(region):區域是一個任意形狀的封閉圖形,通常可以表示為由一系列簡單區域進行布爾運算后得到的結果。在GDI內部,可以使用一個從已知區域出發的一系列掃描線來頂一個復雜的區域。可以使用區域進行輪廓繪制、填充或者剪裁。
路徑(path):路徑是存儲在GDI內部的直線和曲線的集合。可以用于繪制、填充和剪裁。路徑還可以轉換為區域。
剪裁(clipping):當繪圖被限制在客戶區的一個特定的空間位置時,就發生了剪裁。那個特定的空間位置可以是矩形或者非矩形,它通常被指定為一個區域或者一個路徑。
調色板(palettes):僅在支持256種顏色時,才能使用自定義的調色板。Windows僅保留中的20種色彩以供系統使用。你可以改變其他236種色彩,這樣就可以準確顯示按位圖形式存儲的真實圖像。
打印(printing):盡管本章只討論視頻顯示器,但在本章學到的所有知識幾乎都可以應用于打印機。將在Chapter 13討論打印機。
5.2 設備環境
開始繪圖之前,首先讓我們在第4章的基礎上更嚴謹地討論一下設備環境。
如果希望在圖形設輸出設備上繪制圖形,必須首先獲取設備環境(即DC)的句柄。當Windows把這個句柄交給你的程序,Windows同時也就給予你使用這個設備的權限。接著,在GDI函數中將這個句柄作為一個參數,告訴Windows在哪個設備上進行繪圖。設備環境包含許多決定GDI函數如何工作的屬性,這些屬性使得GDI函數只需要提供少量的參數,而不需要提供Windows在設備上顯示對象時需要的所有消息。
5.2.1 獲取設備環境句柄
獲取和釋放設備環境句柄最常用的方法是在處理WM_PAINT消息時使用BeginPaint函數和EndPaint函數:
hdc= BeginPaint(hwnd, &ps);[Otherprogram lines] EndPaint(hwnd,&ps);其中,變量ps是一個類型為PAINTSTRUCT的結構。這個結構中的字段hdc和BeginPaint函數返回的設備環境句柄的值相同。PAINTSTRUCT結構還包含一個名為rcPaint的矩形結構,該結構定義了一個包圍窗口客戶區無效范圍的矩形。使用從BeginPaint函數獲取的設備環境句柄,就只能在這個矩形區域內繪圖。調用EndPaint函數將使這個區域有效。
設備環境句柄還可以在處理非WM_PAINT消息時由Windows程序獲取:
hdc= GetDC(hwnd);[Other program lines]ReleaseDC(hwnd,hdc);其中,設備環境指的是窗口句柄為hwnd的窗口客戶區。調用這些函數和使用BeginPaint、EndPaint函數組合的主要差別是從GetDC函數返回的句柄可以在整個客戶區內繪制。并且GetDC和ReleaseDC函數并不使任何客戶區的無效區域變為很有效。
Windows程序還可以獲得用于整個窗口的,而不僅僅是窗口客戶區的設備環境句柄:
hdc= GetWindowDC(hwnd);[Otherprogram lines] ReleaseDC(hwnd,hdc);這里的設備環境除了客戶區,還包括窗口標題欄、菜單、滾動條和客戶區的外框。應用程序很少使用GetWindowDC函數。如果你想嘗試使用它,則還應當捕獲WM_NCPAINT(nonclient paint, 非客戶區繪制)消息,Windows使用這個消息在窗口的非客戶區繪圖。
調用BeginPaint、GetDC和GetWindowDC函數可以獲得在視頻顯示器上與一個特定的窗口相關聯的設備環境。還有一個更通用的用于獲取設備環境句柄的函數是CreateDC:
hdc= CreateDC(pszDriver,pszDevice,pszOutput,pData);[Otherprogram lines] DeleteDC(hdc);例如,你可以通過調用下面的函數獲取當前整個屏幕的設備環境句柄: ? ? ?
hdc= CreateDC(TEXT(“DISPLAY”), NULL, NULL, NULL);在窗口外輸出文字或者圖像不是很好,但是對于一些特殊的應用還是很有用的。(雖然在官方文檔中,并沒有提到這種方法,但是你還是可以通過在調用GetDC時使用一個NULL參數來得到整個屏幕的設備環境。)
有時候,僅需要獲取一些關于設備環境的信息,而不需要在上面繪制任何東西。在這些情況下,可以調用CreateIC函數獲取一個“信息上下文”(Information Context)句柄。這個函數的參數和CreateDC函數的參數相同。例如: ? ?
hdc= CreateIC(TEXT(“DISPLAY”), NULL, NULL, NULL);但是,往設備上寫東西時,不能使用信息上下文句柄。
處理文圖時,有時可能會用到一個“內存設備環境”:
hdcMem =CreateCompatibleDC(hdc);[Other program lines] DeleteDC(hdcMem);可以把一個位圖選入內存設備環境,并且調用GDI函數繪制這個位圖。將在Chapter14章節介紹這些技術。
????? 如前所述,圖元文件是以二進制形式編碼的GDI函數調用的集合。它可以通過獲取一個圖元文件的設備環境來創建: ? ? ?
hdcMeta= CreateMetaFile(pszFileName);[Otherprogram lines] hmf= ClosemetaFile(hdcMeta);在圖元文件設備環境有效時,使用hdcMeta所做的任何GDI調用都不會被顯示出來,它們都會變成圖元文件的一部分。當你調用CloseMetaFile時,圖元文件設備環境句柄變為無效,該函數返回一個圖元文件句柄(hmf)。我們將在Chapter 18討論圖元文件。
5.2.2 獲取設備環境的信息
????? 設備環境通常指的是物理的顯示設備,如視頻顯示器,或者打印機。經常需要獲取這些設備的某些信息,包括顯示器的大小(以像素或者物理尺寸的方式)和它的色彩能力。這些信息可以通過調用GetDeviceCaps(意思為獲取設備的能力)函數來獲取:
iValue= GetDeviceCaps(hdc, iIndex);其中,參數iIndex是定義在WINGDI.H頭文件中的29個標識符之一。例如,當iIndex的值為HORZRES時,GetDeviceCaps函數以像素為單位返回設備的寬度;使用VERTRES參數值會以像素為單位返回設備的高度。如果hdc是一個屏幕設備環境的句柄,這里獲取的信息和從GetSystemMetrics函數獲取的信息是一樣的。如果hdc是一個打印機設備環境,那么GetDeviceCaps將以像素為單位返回打印機顯示區域的高度和寬度。
????? 還可以使用GetDeviceCaps函數來確定設備處理各種類型圖形的能力,通常這對于視頻顯示器并不重要,但是對于打印機卻非常重要。例如,大多數的繪圖儀不能繪制位圖圖像,通過調用GetDeviceCaps函數可以讓你提前知道這一情況。
5.2.3 DEVCAPS1程序
DEVCAPS1程序顯示了在使用視頻顯示器設備環境時,可從GetDeviceCaps函數得到的部分(并不是全部)信息。代碼如下所示:
*---------------------------------------------------------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) ; }
5.2.4 設備的尺寸
視頻顯示器和打印機是兩種非常不同的設備,但是最不明顯的區別或許是“分辨率”和設備聯系起來的方式。使用打印機時,分辨率通常用每英寸的點數表示。例如有的激光打印機分辨率是每英寸300點或者600點。但是,視頻顯示器的分辨率卻是以水平和垂直方向上顯示的總的像素數給出的,如1024768。
在《Windows程序設計》這本書中,“分辨率“被嚴格定義為每度量單位(通常是英寸)中含有的像素數。我們將使用“像素尺寸(pixel size)”或者“像素規模(pixel dimension)”來表示設備在水平方向和垂直方向上顯示的總的像素數。“度量尺寸”(metrical size)和“度量規模”(metrical dimension)是以每英寸或者毫米為單位的設備的客戶區域的大小。
Windows應用程序可以通過在調用GetSystemMetrics函數時使用SM_CXSCREEN和SM_CYSCREEN參數來獲取顯示器的像素規模。從DEVCAPS1程序可以看出來,一個程序可以在調用GetDeviceCaps函數時使用HORZRES(“水平分辨率”)和VERTRES(“垂直分辨率”)來獲取相同的值。在HORZRES中“分辨率”指的是像素尺寸,而不是每度量單位的像素數。
前兩個設備能力HORZSIZE和VERTSIZE,官方文檔中稱為“以毫米計的物理屏幕寬度”和“以毫米計的物理屏幕高度”。
在計算機排版中,1點(point size亦稱磅值)通常假定正好是1/72英寸。用TEXTMETRIC結構中的術語來說,字號(字體的大小)等于tmHeight減去tmInternalLeading。tmHeight字段表示文本的相鄰行在屏幕上或者打印機上間隔有多大。
在Windows 98中,Windows是通過用戶選擇的顯示器像素規模和用戶為系統字體大小選擇的分辨率來計算顯示器的尺寸的。
在Windows NT中,HORZRES和VERTRES的值仍表示水平方向上和垂直方向上像素的數目,并且LOGPIXELX和LOGPIXELY仍和你在控制面板中的【顯示】程序中設置視頻分辨率時選擇的字體相關。與Windows 98不同之處是:HORZSIZE和VERTSIZE的值是固定的,用來表示標準顯示器的尺寸。對一般的適配器,你獲取的HORZSIZE和VERTSIZE的值分別是320毫米和240毫米。這些值是相同的,不管你選擇什么樣的像素規模。
如果程序需要視頻顯示器的實際物理尺寸,最好的解決辦法是提供一個對話框來實際要求用戶輸入它們。
最后,另外三個從GetDeviceCaps函數獲得的值是與視頻尺寸相關的。這三個值分別是ASPECTX、ASPECTY和ASPECTXY,它們分別表示每個像素點相對的寬度、高度和對角線長度,并且四舍五入到整數。
5.2.5 色彩ABC
只能顯示黑色像素和白色像素的視頻顯示器要顯示每個像素只需要一位的內存。彩色顯示器的每個像素卻需要多個位的內存。位數越多,可表示的色彩越多;更精確一點,2的位數次方就是它可以表示的不同色彩數目。
真彩(true color)視頻顯示器有每像素24位的分辨率(8位表示紅色、8位表示綠色和8位表示藍色)。紅、綠和藍為“三原色”。許多其他的顏色可以由這三種顏色混合。
高彩(high color)顯示器有每像素16位的分辨率,通常5位表示紅色、6位表示綠色和5位表示藍色。
一個顯示256色的視頻適配器每像素8位的內存,然而,這8位的值通常是一個索引值,它指向一個調色板中定義的某種實際顏色。我們將在Chapter16章節中詳細地介紹。
最后,16色的視頻板卡每個像素需要4位的內存。這16種顏色通常固定為暗或者亮的紅、綠、藍、青、紫、黃、兩種灰色、黑和白。
雖然只有在一些奇怪的程序中才有必要知道視頻適配器板卡上的內存組織形式,但是調用GetDeviceCaps函數總可以幫助你確定這些信息。各個像素的多個色彩位可以在顯卡內存中以順序方式存儲,也可以不同的色彩位被放在內存的不同平面(plane)上。色彩平面的數目可以由下面的函數調用獲得: ? ??
iPlanes= GetDeviceCaps(hdc, PLANES);每個像素的顏色位數可以由下面的調用獲得:
iBitsPixel= GetDeviceCaps(hdc, BITSPIXEL);前面兩個函數的返回值肯定有一個為1。視頻適配器支持的色彩數可以由下面的公式來計算:
iColors= 1 << (iPlanes * iBitsPixel);這個值和通過使用NUMCOLORS參數獲取的色彩數值可能一樣,也可能不一樣:
iColors= GetDeviceCaps(hdc,NUMCOLORS);256色的視頻適配器會使用顏色調色板。在這種情況下,使用NUMCOLORS參數的GetDeviceCaps函數會返回Windows保留的色彩數,其值為20.Windows程序使用調色板管理器設定剩余的236種色彩。對高彩和真彩視頻適配器,使用NUMCOLORS參數的GetDeviceCaps函數通常返回-1,所以它對于確定色彩數來說不是一個可靠的函數。因此,應該使用PLANES和BITSPIXEL值按前面給出的iColors的公式來計算色彩數。
在調用大多數GDI函數時,使用COLORREF值(是一個32位的無符號長整型)來表示一個特定的顏色。COLORREF值按照紅、綠、藍的順序指定一種顏色,通常稱為“RGB色彩”。32位COLORREF值由如下四部分組成(從最高字節到最小字節順序):最高字節的8位由0組成,其后字節的8位表示藍色,藍色字節之后的8位表示綠色,最低字節的8位表示紅色。
Windows頭文件WINGDI.H中有幾個用于RGB色彩值的宏,RGB宏帶有三個參數,分別表示紅、綠和藍,并且把它們組合成一個無符號長整型:
#defineRGB(r, g, b) ((COLORREF)(((BYTE)(r) | \ ((WORD)((BYTE)(g))<<8)) | \ (((DWORD)(BYTE)(b))<< 16)))注意,這三個參數的書序是紅、綠、藍。當r和g都為255,且b為0時,表示黃色。
當r、g、b都為0時,表示黑色。當r、g、b都為255時,表示白色。GetRValue、GetGValue、GetBValue宏從COLORREF值中提取RGB的原色值。
在16色或者256色的視頻適配器上,Windows能夠使用“抖動”來仿真,使設備能夠顯示更多的色彩。抖動就是使用不同色彩的相鄰像素形成一個小圖案。可以通過GetNearestColor函數確定于某種特殊的顏色值最接近的非合成顏色:?
crPureColor= GetNearestColor(hdc, crColor);5.2.6 設備環境屬性
由前所述,Windows在設備環境中存儲著一些“屬性”,這些屬性控制GDI函數在顯示器上的操作方式。當一個程序獲取一個設備環境句柄時,Windows設置所有的屬性為默認值。下面將介紹部分設備環境屬性、其默認值以及改變或者獲取其值的函數。
| 設備環境屬性 | 默認值 | 修改其值的函數 | 獲取其值的函數 |
| Mapping Mode | MM_TEXT | SetMapMode | GetMapMode |
| Window Origin | (0,0) | SetWindowOrgEx OffsetWindowOrgEx | GetWindowOrgEx |
| Viewport Origin | (0,0) | SetViewportOrgEx OffsetViewportOrgEx | GetViewportOrgEx |
| Window Extents | (1,1) | SetWindowExtEx SetMapMode ScaleWindowExtEx | GetWindowExtEx |
| Viewport Extents | (1,1) | SetViewportExtEx SetMapMode ScaleViewportExtEx | GetViewportExtEx |
| Pen | BLACK_PEN | SelectObject | SelectObject |
| Brush | WHITE_BRUSH | SelectObject | SelectObject |
| Font | SYSTEM_FONT | SelectObject | SelectObject |
| Bitmap | None | SelectObject | SelectObject |
| Current Position | (0,0) | MoveToEx LineTo PolyLineTo PolyBezierTo | GetCurrentPositionEx |
| Background Mode | OPAQUE | SetBkMode | GetBkMode |
| Background Color | White | SetBkColor | GetBkColor |
| Text Color | Black | SetTextColor | GetTextColor |
| Drawing Mode | R2_COPYPEN | SetROP2 | GetROP2 |
| Stretching Mode | BLACKONWHITE | SetStretchBltMode | GetStretchBltMode |
| Polygon Fill Mode | ALTERNATE | SetPolyFillMode | GetPolyFillMode |
| Intercharacter Spacing | 0 | SetTextCharacterExtra | GetTextCharacterExtra |
| Brush Origin | (0,0) | SetBrushOrgEx | GetBrushOrgEx |
| Clipping Region | None | SelectObject SelectClipRgn IntersectClipRgn OffsetClipRgn ExcludeClipRect SelectClipPath | GetClipBox |
5.2.7 保存設備環境
通常,當調用GetDC或者BeginPaint函數時,Windows會返回一個設備環境,它的所有屬性都被設定為默認值。當設備環境調用ReleaseDC或者EndPaint函數時,對屬性所做的任何改變都會丟失。如果程序需要使用非默認的設備環境屬性,則必須在每次獲取一個新的設備環境句柄時初始化這個設備環境: ? ? ?
caseWM_PAINT:hdc= BeginPaint(hwnd,&ps);[initializedevice context attributes][pointclient area of window]EndPaint(hwnd,&ps);return0;? ? ? 盡管這種方法通常令人滿意,但是你可能喜歡在釋放設備環境屬性時保存對屬性做的修改,以便在下次調用GetDC或者BeginPaint函數時,這些屬性仍然有效。為此,在注冊窗口類時將CS_OWNDC標志作為窗口類樣式的一部分即可:
wndclass.style= CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
現在,每個基于這個窗口類創建的窗口都有它的私有的設備環境,當窗口被銷毀時,這個設備環境繼續存在。使用CS_OWNDC樣式時,只需要初始化設備環境屬性一次,例如,在處理WM_CREATE消息期間:?
case WM_CREATE:hdc= GetDC(hwnd);[initializedevice context attributes]ReleaseDC(hwnd,hdc);
在再次改變這些屬性值之前,它們會一直有效。
????? CS_OWNDC樣式僅影響通過GetDC和BeginPaint函數獲得的設備屬性,通過其他函數(如GetWindowDC函數)獲取的設備環境并不受影響。以前,CS_OWNDC樣式不提倡使用,因為它需要一定的內存開銷。現在,在處理大型圖形的Windows NT程序中,它可以明確改善性能。但是即使使用了CS_OWNDC樣式,設備環境句柄在退出窗口過程前也應該被釋放。
????? 在一些情況下,可能想改變某些設備環境屬性,然后使用變更后的屬性進行繪制,接著再恢復原來的設備屬性。為了簡化這個過程,可以調用下面的函數保存設備環境的狀態: ? ??
idSaved= SaveDC(hdc);現在,可以改變一些屬性。而調用下面的函數則可以返回調用SaveDC函數之前存在的設備環境:
ReleaseDC(hdc,idSaved);可以在調用RestoreDC之前,多次調用SaveDC。
????? 不過,大部分程序員會以一種不同的方式使用SaveDC和RestoreDC函數,這種方式非常類似于匯編語言中的PUSH和POP指令。調用SaveDC函數時,返回值可以不必保存: ? ??
SaveDC(hdc);然后改變一些屬性,并再次調用SaveDC函數,而為了將設備環境恢復到已保存的狀態,則調用下面的函數:
RestoreDC(hdc,-1);這會使設備環境恢復到最近一次由SaveDC函數保存的狀態。
5.3 點和線的繪制
5.3.1 設定像素
SetPixel函數將坐標為x和y的像素點設定為某個特定的顏色:?
COLORREFSetPixel( HDC hdc, // handle to DCintX, // x-coordinate of pixelintY, // y-coordinate ofpixel COLORREF crColor // pixel color );? ? ? 第一個參數hdc,表示設備環境句柄;
????? 第二個、三個參數X和Y,分別表示特定像素點的邏輯水平坐標和垂直坐標;
? ? ? 第四個參數crColor,表示特定顏色,是COLORREF類型,可以用RGB宏來設定。
? 當函數執行成功時,返回一個COLORREF類型值,表示設定的顏色值。該值可能與crColor相同,也可能與crColor相同(當要設定的crColor值不能在當前設備環境中找到)。當函數執行失敗時,返回值為1。
GetPixel函數返回指定坐標位置的像素的顏色:
COLORREF GetPixel( HDChdc, // handle to DCint nXPos, // x-coordinate of pixel int nYPos // y-coordinate ofpixel);第一個參數hdc,表示設備環境句柄;
第二個、三個參數nXPos和nYPos,分別表示特定像素點的邏輯水平坐標和垂直坐標;
函數執行成功時返回值為COLORREF類型,為指定的像素點的顏色,要求該像素點必須在當前剪輯區域范圍內。函數執行失敗后返回值為CLR_INVALID。
5.3.2 直線
????? Windows可以繪制直線、橢圓弧線(橢圓圓周上的曲線)和貝塞爾樣條曲線。Windows支持7中畫線函數:
- LineTo,畫直線
- PolyLine和PolyLineTo,畫一條由多條首尾相連的直線構成的折線
- PolyPolyLine,畫多條折線
- Arc,畫橢圓弧線
- PolyBezier和PolyBezierTo,畫貝塞爾樣條曲線
- ArcTo和AngleArc,畫橢圓弧線
- PloyDraw,畫多條貝塞爾樣條曲線或者一條由多條首尾相連直線構成的折線
為了畫一條直線,需要首先指定直線的起點,然后指定直線的終點:? MoveToEx(hdc, xBeg, yBeg, NULL); LineTo(hdc, xEnd, yEnd); 當調用LineTo函數,當前位置為直線的終點。在默認的設備環境中,當前位置為(0,0),可以使用GetCurrentPositionEx函數獲取當前位置。
? ? ? 為了畫一條由多條直線構成的折線,可以使用PolyLine和PolyLineTo函數,這樣可以不需要多次調用LineTo函數。這兩個函數聲明如下:
BOOLPolyline( HDC hdc, // handle to device contextCONST POINT *lppt, // array of endpointsint cPoints // number of points in array); 第一個參數hdc,表示設備環境句柄;
? ? ? 第二個參數lppt,表示點數組指針,由多條直線的起始點依次構成;
? ? ? 第三個參數cPoints,表示點數組中點的個數;
? ? ? 當函數執行成功后,返回值為非零值,否則返回零值。該函數不使用或者改變當前位置。 ? ? ? BOOLPolylineTo( HDC hdc, // handle to device contextCONST POINT *lppt, // array of pointsDWORD cCount // number of points in array);
? ? ? 該函數與PolyLine函數功能相同,除了PolyLineTo函數需要使用當前位置,并畫線結束后,將終點作為當前位置。
5.3.3 邊框繪制函數
- 繪制矩形函數Rectange聲明如下:?
第二個、三個參數nLeftRect和nTopRect,分別表示矩形左上角坐標的水平位置和垂直位置;
第四個、五個參數nRightRect和nBottomRect,分別表示矩形的右下角坐標的水平位置和垂直位置;
? ? ? ? ? ? ? ? ? ? ? ?Rectangle函數不僅繪制了矩形,而且還用當前畫刷填充了一個矩形封閉區域。
- 繪制橢圓函數Ellipse聲明如下: ? ? ? ? ??
? ? ? ? ? ? ? ? Ellipse函數參數與Rectangle函數參數一致。
- 繪制圓角矩形函數RoundRect聲明如下: ? ? ? ? ??
RoundRect函數前5個參數與Rectangle函數參數一致,第6個、7個參數nWidth和nHeight分別表示圓角橢圓的寬度和高度。
- 繪制橢圓弧線函數Arc聲明如下:
? ? ? ? ?Arc函數前5個參數與Rectangle函數參數相同,第6個、7個參數nXStartArc和nYStartArc構成的坐標(nXStartArc, nYStartArc),該坐標與橢圓中心點的連線與橢圓的交點坐標,是橢圓弧線開始點的坐標。第8個、9個參數nXEndArc和nYEndArc構成的坐標(nXEndArc, nYEndArc),該坐標與橢圓中心點的連線與橢圓的交點坐標,是橢圓弧線結束點的坐標。
- 繪制橢圓弦函數Chord聲明如下:
Chord函數前5個參數與Rectangle函數參數相同,第6個、7個參數nXRadial1和nYRadial1構成的坐標(nXRadial1, nYRadial1),該坐標與橢圓中心連線的交點POS1。第8個、9個參數nXRadial2和nYRadial2構成的坐標(nXRadial2, nYRadial2),該坐標與橢圓中心連線的交點POS2。POS1點和POS2點連線,與兩點之間的弧線(逆時針方向)一起構成了封閉的橢圓弦。
- 繪制橢圓餅函數Pie聲明如下:
? ? ? ? ? ?Pie函數前5個參數與Rectangle函數參數相同,第6個、7個參數nXRadial1和nYRadial1構成的坐標(nXRadial1, nYRadial1),該坐標與橢圓中心連線的交點POS1。第8個、9個參數nXRadial2和nYRadial2構成的坐標(nXRadial2, nYRadial2),該坐標與橢圓中心連線的交點POS2。POS1點和橢圓中心的連線、POS2點和橢圓中心的連線,與兩點之間的弧線(逆時針方向)一起構成了封閉的橢圓餅。
5.3.4 貝塞爾樣條曲線
????? 一條貝塞爾樣條曲線使用四個點定義:兩個端點和兩個控點。兩個端點表示曲線的起點和終點。控點好像“磁鐵”一樣把曲線從兩個端點間的直線處吸彎。
????? 繪制一條或者多條貝塞爾樣條曲線的函數PolyBezier和PolyBezierTo,函數聲明分別如下: ? ?
BOOLPolyBezier( HDC hdc, // handle to device contextCONST POINT* lppt, // endpoints and control pointsDWORD cPoints // count of endpoints and controlpoints);和 ??
BOOL PolyBezierTo( HDC hdc, // handle to device contextCONST POINT *lppt, // endpoints and control pointsDWORD cCount // count of endpoints and controlpoints);第一個參數hdc,表示設備環境句柄;
第二個參數lppt,表示終點和控點組成的POINT數組;
第三個參數cPoints/cCount,表示終點和控點的點個數;
其中前4個點分別表示第一條貝塞爾樣條曲線的起點、第一個控點、第二個控點和終點,隨后的每一條貝塞爾樣條曲線則只需要給出三個點,因為前一條貝塞爾樣條曲線的終點就是后一條貝塞爾樣條曲線的起點,如此類推,cPoints/cCount等于所繪制的貝塞爾樣條曲線個數的3倍加1。
5.3.5 使用現有畫筆
畫筆決定了線條的顏色、寬度和樣式,樣式可以是實線、點線或者虛線。Windows提供三種“備用畫筆”:BLACK_PEN,WHITE_PEN和NULL_PEN。畫筆的默認設備環境是BLACK_PEN。NULL_PEN表示不繪制任何圖形的畫筆。亦可以創建自己的畫筆。
????? 在Windows程序中,使用句柄來操作畫筆。畫筆句柄的類型為HPEN,可以用該類型聲明一個畫筆變量。可以調用GetStockObject函數獲取備用畫筆的句柄,如 ? ? ?
HPENhPen = GetStockObject(WHITE_PEN);現在必須把該畫筆選入設備環境中:
SelectObject(hdc,hPen);? ??5.3.6 創建、選擇和刪除畫筆
????? 盡管使用備用對象中的畫筆非常方便,但是只能使用實心的黑色畫筆、實心的白色畫筆或者沒有畫筆三種情況。如果想獲得更豐富的效果,則必須創建自己的畫筆。
????? 創建畫筆的一般過程:
- 首先,調用CreatePen或者CreatePenIndirect函數創建一個“邏輯畫筆”對象,該函數會返回該畫筆對應的句柄。
CreatePen函數聲明如下:
第一個參數fnPenStyle,表示畫筆的樣式,包括實心(PS_SOLID)、虛線(PS_DASH)、點線(PS_DOT)、虛點(PS_DASHDOT)、虛點點式(PS_DASHDOTDOT)、不可見式(PS_NULL)、內部框架式(PS_INSIDEFRAME);
第二個參數nWidth,表示畫筆的寬度,備用畫筆的寬度總是1個像素寬。如果指定使用虛線或者點線樣式,同時把畫筆的寬度設定為大于1個像素,那么Windows使用實心的畫筆來代替;
第三個參數crColor,表示顏色,是COLORREF類型。
函數執行成功后,返回畫筆的句柄,否則返回NULL。
HPEN CreatePenIndirect( CONST LOGPEN *lplgpn);參數lplgpn,指向LOGPEN的指針。LOGPEN結構聲明如下:
typedefstruct tagLOGPEN {UINT lopnStyle;POINT lopnWidth;COLORREF lopnColor; } LOGPEN, *PLOGPEN;其中lopnStyle、lopnWidth、lopnColor與CreatePen的三個參數一一對應,表示相同的意義。
- 其次,調用SelectObject將創建的畫筆句柄選入設備環境中,接著可以使用該畫筆繪制線條。
- ?最后,在釋放環境句柄之后,調用DeleteObject函數刪除創建的邏輯畫筆。此后,畫筆的句柄將不再有效。
邏輯畫筆是一個“GDI對象”,一個程序可以創建6個GDI對象,其他5個分別是畫刷、位圖、區域、字體和調色板。除了調色板之外,所有這些對象都可以使用SelectObject函數選入設備環境。
下面三條規則控制畫筆等GDI對象的使用:
- ?最終應該刪除你所創建的所有GDI對象
- 當GDI對象被選入一個有效的設備環境時,不要刪除它
- 不要刪除備用對象。
可以通過GetObject函數獲取指定畫筆句柄的特點:
GetObject(hPen, sizeof(LOGPEN),(LPVOID)&logPen);如果需要獲取當前被選入設備環境的畫筆句柄,則調用:?
hPen = GetCurrentObject(hdc, OBJ_PEN);5.3.7 填充空隙
????? 使用點式畫筆和虛線畫筆會帶來這樣一個問題:點和虛線之間的空隙是什么顏色呢?空隙的顏色是由設備環境的兩個屬性(背景模式和背景顏色)決定的。默認的背景模式是OPAQUE(不透明),這意味著Windows使用背景顏色來填充空隙,背景顏色在默認時是白色。
????? 改變背景顏色函數是:
SetBkColor(hdc, crColor);? ? ? 獲取背景顏色函數是:
GetBkColor(hdc);? ? ? 改變背景模式函數是:
SetBkMode(hdc,iBkMode);? ? ? 獲取背景模式函數是:
GetBkMode(hdc);5.3.8 繪圖模式
??? 當Windows使用一個畫筆繪制直線時,它實際上是將畫筆的像素顏色和目標顯示表面的像素顏色進行布爾運算。對像素顏色執行一個按位布爾運算稱為“光柵操作”(raster operation, ROP),簡稱“ROP”,因為繪制一條直線只涉及兩種像素顏色(即畫筆和目標),這里的布爾運算就被稱為“二元光柵操作”,或者“ROP2”。Windows定義了16中ROP2運算碼,每一個都表示Windows組合畫筆像素色和目標像素色的一種方法。在默認的設備環境中,繪圖模式是R2_COPYPEN,表示Windows只是簡單地將畫筆像素的顏色復制到目標像素色上。詳細16中繪圖模式詳見MSDN手冊。
????? 設置繪圖模式函數: ? ? ?
SetROP2(hdc, iDrawMode);? ? ? 獲取繪圖模式函數:
GetROP2(hdc);5.4 繪制填充區域
????? Windows用于繪制帶有邊框的填充區域的7個函數如下表示:??
| 函數名稱 | 圖形 |
| Rectangle | 直角矩形 |
| Ellipse | 橢圓 |
| RoundRect | 圓角矩形 |
| Chord | 一個弓形,由橢圓圓周上的弧和一根弦組成 |
| Pie | 橢圓上的一個扇形 |
| Polygon | 多邊形 |
| PolyPolygon | 多個多邊形 |
?
????? Windows使用當前被選入設備環境的畫筆來繪制圖形的邊框線。邊框線使用當前的背景模式、背景顏色和繪圖模式。這與Windows繪制線條一樣。
????? Windows使用當前被選入設備環境的畫刷來填充圖形。在默認情況下,使用的是備用對象WHITE_BRUSH。Windows定義了6種備用畫刷:WHITE_BRUSH、LTGRAY_BRUSH、GRAY_BRUSH、DRGRAY_BRUSH、BLACK_BRUSH和NULL_BRUSH(又稱為HOLLOW_BRUSH)。
????? Windows定義畫刷的句柄為HBRUSH類型,可用該類型定義一個畫刷句柄變量: ? ? ?
HBRUSHhBrush;? ? ? 和選擇備用畫筆到設備環境中一樣,選擇備用畫刷到設備環境使用GetStockObjet函數來實現。
hBrush= GetStockObject(GRAY_BRUSH);5.4.1 Polygon函數和多邊形填充模式
繪制多邊形函數Polygon聲明如下: ?
BOOLPolygon( HDC hdc, // handle to DCCONST POINT *lpPoints, // polygon verticesint nCount // count of polygon vertices);第一個參數hdc,表示設備環境句柄;
第二個參數lpPoints,是一個POINT結構的數組指針,包含多邊形的頂點;
第三個參數nCount,表示點的個數。
如果數組中最后一點和第一個點不同,則Windows會再加一條線連接最后一個點與第一個點。(在PolyLine函數中不會這么做。)。
繪制多個多邊形函數PolyPolygon聲明如下:?
BOOLPolyPolygon( HDC hdc, // handle to DCCONST POINT *lpPoints, // array of verticesCONST INT*lpPolyCounts, // array of count ofverticesint nCount // count of polygons);第一個參數hdc,表示設備環境句柄;
第二個參數lpPoints,是一個POINTS結構的數組指針,包含多邊形的頂點;
第三個參數lpPolyCounts,表示多邊形頂點個數的數組;
第四個參數nCount,表示多邊形的個數;
? ? ? ? ? ? 對于Polygon和PolyPolygon函數,Windows都使用設備環境中的當前畫刷來填充區域。至于內部是如何填充的,要取決于多邊形的填充模式,可以調用SetPolyFillMode函數來設置:SetPolyFillMode(hdc,iMode);
填充模式包括ALTERNATE(交替)和WINDING(螺旋)兩種模式。對于ALTERNATE(交替)模式,我們可以想象從一個封閉區域紅的一點向無窮遠處畫一條射線。只有該射線穿越奇數條邊框線時,封閉區域才會被填充。對于WINDING(螺旋)模式,我們可以設想從區域內的一個點畫一條伸向無窮遠的射線。如果射線穿越過奇數條邊框線,則區域被填充,這和ALTERNATE模式相同。如果射線穿越過偶數條邊框線,情況比較復雜,還要考慮到邊框線的繪制方向:在被穿越的偶數條邊框線中,不同的邊框線(相對于射線的方向)的數目如果相同,則區域不會被填充;不同方向的邊框線(相對于射線的方向)的數目如果不相等,則區域會被填充。
5.4.2 用畫刷填充內部
Rectangle、RoundRect、Ellipse、Chord、Pie、Polygon和PolyPolygon函數繪制的圖形內部會使用當前設備環境的畫刷(有時也稱為圖案(pattern))來填充。畫刷是一個像素的很小的位圖,Windows在水平方向上和垂直方向上重復地使用它來填充一個區域。
當Windows使用抖動技術顯示比通常顯示器可用顏色更多的色彩時,它實際上是使用畫刷來顯示顏色的。在單色系統中,Windows可以通過混合黑色和白色像素來建立64種不同的灰色色調。更確切地說,Windows可以建立64個不同的畫刷。對純黑色,像素的位圖的每一個像素的值都是0.如果64位中只有一位是1(代表白色),就對應第一個灰色色調。如果2位是白色對應第二個灰色色調,如此類推,直到所有的的位圖位都是1,對應純白色。對于16色或者256色的視頻系統,畫刷也是由抖動實現的,這樣Windows可以顯示的顏色比通常顏色更多。
Windows允許使用5種函數來建立邏輯畫刷。調用SelectObject函數將畫刷選入設備環境,調用DeleteObject函數刪除已建立的畫刷。如果它被選入了設備環境中,則不要刪除它。
5種創建邏輯畫刷的函數如下所示:
- CreateSolidBrush函數:聲明HBRUSHCreateSolidBrush(? COLORREF crColor?? // brush color value);參數clColor表示畫刷顏色。當函數執行成功后,返回畫刷句柄,否則返回NULL
- CreateHatchBrush函數,用于創建由水平、垂直或者對角線組成的“陰影線標記”(hatch mark)畫刷,函數聲明:
第一個參數fnStyle,表示陰影線類型,包括HS_BDIAGONAL、HS_CROSS、HS_DIAGCROSS、HS_FDIAGONAL、HS_HORIZONTAL和HS_VERTICAL六種類型,如下圖所示:
第二個參數clrref,表示陰影線的顏色。函數執行成功后,返回邏輯畫刷句柄,否則返回NULL。
- CreatePatternBrush函數使用指定位圖作為邏輯畫刷,聲明:
參數hbmp,表示位圖句柄,該位圖可以是設備相關位圖,也可以是設備無關位圖。函數執行成功后,返回邏輯畫刷句柄,否則返回NULL。
- CreateDIBPatternBrushPt函數使用設備無關位圖作為邏輯畫刷,聲明:
? ? ? ? ? ?參數詳見MSDN文檔。
- CreateBrushIndirect函數,該函數包含了其他4個函數的功能,聲明:
參數lplb,表示LOGBRUSH結構的各種屬性。LOGBRUSH包含三個字段,lbStyle字段的值決定著Windows如惡化解釋其他的兩個字段。
????? 調用GetObject函數獲取畫刷的信息,如下形式: ? ? ??
GetObject(hBrush,sizeof(LOGBRUSH), (LPVOID)&logbrush);? ? ? Logbrush變量是LOGBRUSH類型的。
5.5 GDI映射模式
????? 到目前為止,所有的范例程序都是相對于客戶區左上角坐標并以像素為單位來繪制的。這是默認的情況,但并非是唯一的選擇。設備環境中的“映射模式”屬性,能影響幾乎所有在客戶區繪制的圖形。和映射模式緊密相關的還有4個其他的設備屬性,分別為窗口原點(windoworigin)、視口原點(viewportorigin)、窗口范圍(windowextents)、視口范圍(viewportextents)。
????? 在幾乎所有的GDI函數中,輸入的坐標值都是“邏輯單位”(logic unit)。Windows必須要將邏輯單位轉換為“設備單位”(device unit),也就是像素。這些轉換都是由映射模式、窗口原點、視口原點、窗口范圍和視口范圍共同控制的。映射模式同樣暗含著x軸和y軸的方向,也就是說,它決定了隨著向顯示區域的左邊移動,x值是增加還是減少;隨著向顯示區域的上邊移動,y值是增加還是減少。
????? Windows定義了8種映射模式,具體描述如下表所示:
| 映射模式 | 邏輯單位 | 值增加的方向 | |
| x軸 | Y軸 | ||
| MM_TEXT | 像素 | 右 | 下 |
| MM_LOMETRIC | 0.1mm | 右 | 上 |
| MM_HIMETRIC | 0.01mm | 右 | 上 |
| MM_LOENGLISH | 0.01in | 右 | 上 |
| MM_HIENGLISH | 0.001in | 右 | 上 |
| MM_TWIPS | 1/1440in | 右 | 上 |
| MM_ISOTROPIC | 任意(x=y) | 可選 | 可選 |
| MM_ANISOTROPIC | 任意(x!=y) | 可選 | 可選 |
?
????? 映射模式設置可以由下面的函數實現: ? ? ?
SetMapMode(hdc,iMapMode);? ? ? 當函數執行成功后,返回值為之前的映射模式,否則返回0。
????? 映射模式獲取可以由下面的函數實現: ? ? ?
iMapMode= GetMapMode(hdc);5.5.1 設備坐標和邏輯坐標
????? 我們或許會有這樣的疑問:如果使用MM_LOENGLISH映射模式,是否會得到以百分之一英寸來度量的WM_SIZE消息?絕對不是。Windows對所有消息(例如WM_SIZE、WM_MOVE和WM_MOUSEMOVE),所有非GDI函數,甚至一些GDI函數,都繼續使用設備坐標。映射模式是設備環境的一種屬性,只有當使用以設備環境句柄作為參數的GDI函數,映射模式才會生效。GetSystemMetrics是一個非GDI函數,因此它將繼續以設備單位的形式,也就是像素為單位返回。盡管GetDeviceCaps是一個需要設備環境句柄的GDI函數,但是Windows繼續為HORZRES和VERTRES索引返回設備單位。因為這個函數的目的之一就是以像素為單位向程序返回設備的尺寸。
????? 然而,通過調用GetTextMetrics函數獲取的TEXTMETRIC結構中的值是以邏輯單位形式給出的。如果調用這個函數時,映射模式是MM_LOENGLISH,GetTextMetrics函數就以百分之一英寸為單位返回字符的高度和寬度。為了簡化工作,當調用GetTextMetrics函數獲取字符的高度和寬度信息時,映射模式應當和基于這些尺寸繪制文本時使用的映射模式相同。
5.5.2 設備坐標系統
Windows會把在GDI函數中指定的邏輯坐標轉換為設備坐標。在我們討論用于各種映射模式的邏輯坐標系統之前,首先介紹一下Windows為視頻顯示器定義的不同的設備坐標系統。盡管在大多數情況下我們都是在我們窗口的客戶區內工作,但是在有些情況下,Windows還使用另外兩種設備坐標系統。在所有的設備坐標系統中,單位都是以像素形式表示的。水平方向上x值從左向右增加,垂直方向上y值從上往下增加。
當我們使用整個屏幕時,我們是以“屏幕坐標”(screen coordinate)的形式工作的。屏幕左上角是點(0,0)。屏幕坐標用于WM_MOVE消息(對非子窗口)和下列的Windows函數中:CreateWindow和MoveWindows(對非子窗口)、GetMeesagePos、GetCursorPos、SetCursorPos、GetWindowRect和WindowFromPoint。(這并不一個完整的清單)。這些函數一般分為兩類:一類是與窗口無關的函數(例如兩個和鼠標指針相關的函數),還有一類是必須根據屏幕上的點移動或者尋找窗口的函數。如果使用帶有“DISPLAY”參數的CreateDC函數來獲取整個屏幕的設備環境,那么在GDI調用中,邏輯坐標值將被默認映射屏幕坐標。
“全窗口”坐標指的是一個應用程序的整個應用窗口,包括標題欄、菜單、滾動條和邊框。對于一個普通的應用窗口,,點(0,0)是邊框的左上角。全窗口坐標在Windows中很少用,但是如果設備環境是從GetWindowDC函數獲取的,則在GDI函數調用中,邏輯坐標會被默認映射為全窗口坐標。
第三種設備坐標系統是“客戶區坐標”。點(0,0)是客戶區左上角。調用GetDC或者BeginPaint函數獲取設備環境時,在GDI函數中的邏輯坐標將被默認轉換為客戶區坐標。
可以使用ClientToScreen函數將客戶區坐標轉換到屏幕坐標,反之亦然,可以調用ScreenToClient函數把屏幕坐標轉換為客戶區坐標。也可以調用GetWindowRect函數以屏幕坐標的形式獲取整個窗口的位置和大小。這三個函數為把任何一種設備坐標鉆換為另一種設備坐標提供了足夠的信息。
5.5.3 視口和窗口
????? 映射模式定義了Windows如何將GDI函數中指定的邏輯坐標映射到設備坐標。這里的設備坐標系統取決于獲取設備環境所用的函數。為了繼續討論映射模式,我們需要定義一些術語。映射模式被定義為從“窗口”(windows)(邏輯坐標)到“視口”(viewport)(設備坐標)的映射。
????? 使用這兩個術語是很不正確的,因為他們在其他情況下還有其他意思。在其他的一些圖形界面系統中,視口常常含有“剪切區域”的意思。在Windows中,“窗口”還有一個非常具體的意思,就是描述一個程序占用屏幕的區域。在本章的討論中,我們必須先把這些屬于的陳見放到一邊。
????? 視口是以設備坐標(像素)的形式指定的。大多數情況下,視口與客戶區相同,但是如果從GetWindowDC或者CreateDC函數獲取了設備環境,視口也可以是全窗口坐標或者是屏幕坐標。點(0,0)是客戶區(或者全窗口,或者屏幕)的左上角。X值向右增加,y值向下增加。
????? 窗口時以邏輯坐標的形式指定的,可能是像素、毫米、英寸或者其他任何單位。可以在GDI繪圖函數中指定想用的邏輯窗口坐標。
????? 對于所有的映射模式,Windows使用下面公式將窗口(邏輯)坐標轉換為視口(設備)坐標:
?????
其中(xWindow,yWindow)是一個待轉換的邏輯點坐標,(xViewport,yViewport)是一個待轉換的設備點坐標,大多數情況下是客戶區坐標。點(xWinOrg, yWinOrg)是在邏輯坐標系下的窗口的原點,點(xViewOrg, yViewOrg)是在設備坐標系下視口的原點。默認情況下窗口原點和視口原點都是(0,0),可以改變它們。點(xWinExt,yWinExt)是在邏輯坐標系下的窗口范圍,點(xViewExt,yViewExt)是在設備坐標系下的視口范圍。在特定映射模式下,可以改變窗口范圍和視口范圍。
????? 窗口范圍和視口范圍可以是負值,表示邏輯x軸的值不是向右增加,邏輯y軸的值不是向下增加。
????? Windows提供了兩個函數來讓你在程序中在設備點和邏輯點之間轉換,其中將設備點轉換為邏輯點的函數: ? ??
DPtoLP(hdc,pPoints, iNumber);其中變量pPoints是一個指針,它指向一個POINT結構的數組,iNumber是待轉換的點的個數。例如,我們會發現這個函數對于把從GetClientRect函數(它總以設備單位的形式)獲取的客戶區大小轉換到邏輯坐標非常有用: ? ??
GetClientRect(hwnd,&rect); DLtoLP(hdc,(PPOINT)&rect, 2);將邏輯點轉換為設備點的函數:
LPtoDP(hdc,pPoints, iNumber);獲取窗口原點坐標的函數:
GetWindowOrgEx(hdc,&pt);設置窗口原點坐標的函數:?
SetWindowOrgEx(hdc,x, y, lppoint);獲取窗口范圍的函數:
GetWindowExtEx(hdc,lpsize);在部分映射模式下,可以設置窗口范圍,該函數:
SetWindowExtEx(hdc,xExtent, yExtent, lpSize);獲取視口坐標原點的函數:?
GetViewportOrgEx(hdc,&pt);設置視口坐標原點的函數: ? ? ? ? ? ??
SetViewportOrgEx(hdc,x, y, lppoint); 獲取視口范圍的函數: GetViewportExtEx(hdc,lpsize);在某些映射模式下,可以設置視口范圍,該函數:?
SetViewportExtEx(hdc,xExtent, yExtent, lpSize);5.5.4 使用MM_TEXT
????? 在MM_TEXT映射模式下,默認的原點和范圍顯示如下:
????? 窗口原點:(0,0)?? ,可以改變
????? 視口原點:(0,0)?? ,可以改變
????? 窗口范圍:(1,1),不可以改變
????? 視口范圍:(1,1),不可以改變
????? 猶豫視口范圍和窗口范圍的比例都是1,因此從邏輯坐標到視口坐標的轉換公式可以簡化為
????? xViewport= xWindow – xWinOrg + xViewOrg
????? yViewport= yWindow – yWinOrg + yViewOrg
5.5.5 度量映射模式
????? Windows包含5種映射模式,它們分別表示將邏輯坐標轉換為物理坐標的不同方式。因為在x軸上y軸上的邏輯坐標都被映射到相同的物理度量單位。5種映射模式的度量大小,具體如下表所示:
| 映射模式 | 邏輯單位 | 英寸(in.) | 毫米(mm) |
| MM_LOENGLISH | 0.01 in | 0.01 | 0.254 |
| MM_LOMETRIC | 0.1mm | 0.0394 | 0.1 |
| MM_HIENGLISH | 0.001in | 0.001 | 0.0254 |
| MM_TWIPS | 1/1440in | 0.000694 | 0.0176 |
| MM_HIMETRIC | 0.01mm | 0.000394 | 0.01 |
在默認情況下,度量映射模式的窗口和視口的原點及范圍如下:
窗口原點:(0,0),可以改變
視口原點:(0,0),可以改變
窗口范圍:(?, ?), 不可改變
視口范圍:(?, ?), 不可改變
這里的問號,表示窗口和視口的范圍取決于映射模式和設備分辨率。
在WindowsNT中,視口的范圍是基于屏幕上像素的尺寸的,這個信息是使用HORZRES和VERTRES參數從GetDeviceCaps函數獲取的。窗口的范圍是基于假定的顯示尺寸,該顯示顯示是在使用HORZSIZE和VERTSIZE參數時調用GetDeviceCaps函數返回的。正如前面提到的,這些值通常是320mm和240mm。如果設置的顯示器的像素是,則視口范圍和窗口范圍如下表所示:
| 映射模式 | 視口范圍(x,y) | 窗口范圍(x,y) |
| MM_LOMETRIC | (1024, -768) | (3200, 2400) |
| MM_HIMETRIC | (1024, -768) | (32000, 24000) |
| MM_LOENGLISH | (1024, -768) | (1260, 945) |
| MM_HIENGLISH | (1024, -768) | (12598, 9449) |
| MM_TWIPS | (1024, -768) | (18142, 13606) |
y前的符號表示y軸方向的改變。對于這5種映射模式,y值隨著設備上移增加。
5.5.6 自定義的映射模式
????? 剩余的兩種映射模式分別稱為MM_ISOTROPIC和MM_ANISOTROPIC。只有在這兩種映射模式下,Windows才允許你改變視口和窗口的范圍,也就意味著你能夠改變Windows用于轉換邏輯坐標和設備坐標的換算因子。Isotropic的意思是”各向同性;anisotropic是“各向異性”。和前面的度量映射模式類似,MM_ISOTROPIC會同比例地縮放兩個坐標軸,x軸上的邏輯單位和y軸上的邏輯單位表示的物理尺寸時相同的。對于建立寬高比與顯示設備無關的圖像,這是有幫助的。
????? MM_ISOTROPIC和其他度量映射模式的區別是在使用MM_ISOTROPIC映射模式時,可以控制邏輯單位的物理尺寸。如果需要的話,可以依據客戶區來調整邏輯單位的大小。這樣會使你繪制的圖像總是包含在客戶區內,并相應的放大或者縮小。
????? Windows程序能夠通過調整窗口和視口的范圍來處理圖形大小的變化,這樣一來,程序代碼能夠在繪圖函數中使用相同的邏輯單位,而不用去管窗口的大小。
????? 有時候,MM_TEXT映射模式和度量映射模式也被稱為“完全受限”的映射模式。這意味著不能改變窗口和視口的范圍。MM_ISOTROPIC映射模式是一種“半受限”映射模式。Windows允許改變窗口和視口的范圍,但是Windows會調整它們的值了,這是為了讓x和y邏輯單位表示相同的物理尺寸。MM_ANISOTROPIC映射模式是“不受限”的,你可以改變窗口和視口的范圍,并且Windows不會相應的調整它們的值。
- ? ? ? MM_ISOTROPIC
????? 當第一次設置映射模式為MM_ISOTROPIC時,Windows使用與MM_LOMETRIC映射模式相同的窗口和視口范圍。有一個差別是現在可以根據自己的喜好調用SetWindowExtEx和SetViewportExtEx函數來改變范圍。接著,Windows將調整范圍以使得兩個軸上的邏輯單位表示相同的物理距離。
????? 一般說來,調用SetWindowExtEx函數時,要把參數設定為期望得到的邏輯窗口的邏輯大小,而在調用SetViewportExtEx函數時,則把參數設定為客戶區的實際寬度和高度。當Windows調整這些范圍時,它必須讓邏輯窗口可以容納在對應的物理視口之內,這就可能導致一部分的客戶區落在邏輯窗口外邊。應當在調用SetViewportExtEx之前先調用SetWindowExtEx來最有效地使用客戶區的空間。
- ? ? ? MM_ANISOTROPIC
????? 在MM_ANISOTROPIC映射模式下,Windows不對設置的視口和窗口范圍做任何的調整。這也就意味著MM_ANISOTROPIC并不需要保持正確的高寬比。
????? 使用MM_ANISOTROPIC的一種方式是在客戶區使用任意的坐標方式,就像我們在MM_ISOTROPIC映射模式下所做的一樣。區別是MM_ANISOTROPIC模式下不會調整視口和窗口范圍。
????? 使用MM_ANISOTROPIC映射模式的另一種方式是把x和y單位設置為固定值,但是值并不相等。
5.6 矩形、區域和剪裁
????? Windows還有其他幾個使用RECT(矩形)結構和區域的繪圖函數。一個區域指的是屏幕上的一塊空間,它由矩形、多邊形和橢圓組合而成。
5.6.1 處理矩形
- FillRect函數:使用指定的畫刷填充矩形(到達但不包括右下坐標)。方式:
這個函數不需要把畫刷提前選入設備環境中。
- FrameRect函數:使用畫刷繪制一個矩形框,但是它并不填充矩形。方式:
- InvertRect函數:翻轉矩形內所有的像素,將1變為0,將0變為1。方式:
- SetRect函數:設置RECT結構的4個字段。方式:
- OffsetRect函數:將矩形沿x軸和y軸移動幾個單位。方式:Offset(&rect, x, y);
- ?InflateRect函數:增大或者減小矩形的尺寸。方式:InflateRect(&rect, x, y);
- ?SetRectEmpty函數:設置矩形結構的個字段為0:。方式:SetRectEmpty(&rect);
- CopyRect函數:將一個矩形結構復制到另一個矩形結構。方式:CopyRect(&DestRect, &SrcRect);
- IntersectRect函數:獲取兩個矩形的交集。方式:IntersetRect(&DestRect, &SrcRect1, &SrcRect2);
- UnionRect函數:獲取兩個矩形的并集。方式:UnitRect(&DestRect, &SrcRect1, &SrcRect2);
- IsRectEmpty函數:判斷矩形是否為空。方式:bEmpty = IsRectEmpty(&rect);
- PtInRect函數:判斷點是否在矩形內部。方式:bInRect = PtInRect(&rect, point);
- =函數:復制矩形結構字段。方式:DestRect= SrcRect;
5.6.2 隨機矩形
????? 在任何一個圖形系統中,總存在這樣一個有趣的程序,即簡單地使用隨機的尺寸和顏色不停地繪制一系列的圖像,例如隨機大小和顏色的矩形。在Windows中可以創建這樣的一個程序:但是這不是想象中的那樣容易。我們需要意識到,不能在處理WM_PAINT消息中簡單地使用while(TRUE)循環。當然,這樣做會很有效,但是這樣做的結果是,程序將停止對其他消息的處理,而且程序不能退出或者最小化。
????? 一種可接受的方式是設置一個向你的窗口函數發送WM_TIMER消息的Windows計時器。對每個WM_TIMER消息,可以調用GetDC函數獲取設備環境,然后繪制一個隨機矩形。接著調用ReleaseDC函數釋放設備環境。但是那樣做又使程序失去一些趣味性,因為程序不能很快地繪制隨機矩形。必須等待每個WM_TIMER消息,那樣會依賴系統時鐘的精度。
????? 在Windows中有很多的“空閑時間”,在這期間所有的消息隊列都是空的。Windows就在等待鍵盤或者鼠標的輸入。那么能否在空閑期間從某種程度上獲取控制并繪制隨機矩形,而一旦有效加載到程序的消息隊列,就釋放控制呢?這正是PeekMessage函數的“用武之地”。下面是PeekMessage函數調用的一個例子:
PeekMessage(&msg,NULL, 0, 0, PM_REMOVE);函數的前4個參數與GetMessage函數相同。最后一個參數PM_REMOVE,表示刪除消息隊列中的消息。如果不想刪除,則設置最后一個參數為PM_NOREMOVE。
????? GetMessage函數并不把控制權交給程序,除非它從程序的消息隊列中獲得了消息。但是PeekMessage函數卻總是立即返回,不管消息是否出現。當一個消息在程序的消息隊列中時,PeekMessage函數的返回值是TRUE,而消息則像正常處理一樣處理,當隊列中沒有消息時,PeekMessage返回FASLE。
????? 這允許我們替換正常的消息循環,正常的消息循環如下所示:
while(GetMessage(&msg, NULL, 0, 0)) {TranslateMessage(&msg);DispatchMessage(&msg); }????? 替換后的消息循環如下:
while (TRUE) { if ( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {if ( msg.message == WM_QUIT){ break; } TranslateMessage(&msg); DispatchMessage(&msg); } else {[other program lines to do some work] } } return msg.wParam;注意:GetMessage函數檢測到WM_QUIT消息時,返回值為FALSE,這與PeekMessage函數不一樣。
? ????? 如果PeekMessage函數返回TRUE,那么消息會正常執行。如果返回FALSE,那么程序可以在返回給Windows控制之前做些事情(如顯示一個隨機矩形)。
參考如上方法,我們實現一個顯示隨機矩形的程序,程序代碼如下所示:
/*------------------------------------------RANDRECT.C -- Displays Random Rectangles(c) Charles Petzold, 1998------------------------------------------*/#include <windows.h> #include <stdlib.h> // for the rand functionLRESULT 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) ;}elseDrawRectangle (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) ; }
5.6.3 建立和繪制區域
????? 一個區域是對顯示器一塊區間的描述,這個空間可以是矩形、多邊形和橢圓的組合。可以使用區域進行繪圖或者剪裁。將區域選入設備環境,就可以使用這個區域來剪裁(也就是說,將繪制動作限制在客戶區的一個特定部分)。同畫筆和畫刷一樣,區域也是GDI對象,應當通過調用DeleteObject函數來刪除所有建立的區域。
????? 當建立一個區域時,Windows會返回一個類型為HRGN的區域句柄。最簡單的區域是一個矩形區域。可以用下面兩種方法建立一個矩形區域: ? ? ?
hRgn= CreateRectRgn(xLeft, yTop, xRight, yBottom);或者 ? ? ? ? ? ?
hRgn= CreateRectRngIndirect(&rect);建立橢圓區域的兩種方法:
hRgn= CreateEllipticRgn(xLeft, yTop, xRight, yBottom);或者
hRgn= CreateEllipticRgnIndirect(&rect);創建圓角矩形可以通過CreateRoundRectRgn函數來實現。
創建一個多邊形區域的函數:
hRgn= CreatePolygonRgn(&point, iCount, iPolyFillMode);同理,可以通過CreatePolyPolygonRgn函數創建多個多邊形區域。
CombineRgn函數可以把兩個區域按照某種組合方式,生成一個新的區域,如下:?
iRgnType= CombineRgn(hDestRgn, hSrcRgn1, hSrcRgn2, iCombine);區域組合方式如下表所示:
| iCombine值 | 新的區域 |
| RGN_AND | 兩個源區域的公共部分 |
| RGN_OR | 兩個源區域的全部 |
| RGN_XOR | 兩個源區域的全部,但除去公共部分 |
| RGN_DIFF | hSrcRgn1不在hSrcRgn2中的部分 |
| RGN_COPY | hSrcRgn1的全部(忽略hSrcRgn2) |
?????
?
?
?
?
?
返回值iRgnType為如下值之一:
- NULLREGION,指的是一個空的區域;
- SIMPLEREGION,指的是一個簡單的矩形、橢圓或者多邊形;
- COMPLEXREGION,指的是矩形、橢圓或者多邊形的組合;
- ERROR,指的是有錯誤發生。
當區域建立后,可以用如下四個操作區域的函數:
- FillRgn函數:用指定畫刷填充指定區域。方式:FillRgn(hdc,hRgn, hBrush);
- FrameRgn函數:用指定畫刷在區域周圍繪制邊框。方式:FrameRgn(hdc,hRgn, hBrush, xFrame, yFrame);其中參數xFrame和yFrame表示繪制邊框的邏輯寬度和高度。
- PaintRgn函數:使用當前被選入設備環境的畫刷來填充區域。方式:PaintRgn(hdc, hRgn);
當使用完一個區域后,使用DeleteObject函數刪除該區域。
5.6.4 矩形與區域的剪裁
????? 區域在剪裁中扮演著重要角色。InvalidateRect函數使顯示的矩形區域無效,并產生一個WM_PAINT消息。GetUpdateRect函數可以獲取無效矩形的坐標,并且使用ValidateRect函數使客戶區的矩形有效。當接收到一個WM_PAINT消息時,PAINTSTRUCT結構中的無效矩形的坐標是可以利用的。這個結構是通過BeginPaint函數填充的。這個無效矩形也定義了一個“剪裁區域”,不能在剪裁區域之外繪圖。
Windows有兩個類似InvalidateRect和ValidateRect的函數,用于處理矩形而不是矩形:
InvalidateRgn(hdc, hRgn, bErase);和?
ValidateRgn(hdc, hRgn);當接收一條由無效區域產生的WM_PAINT消息時,剪裁區域在形狀上不一定是矩形。
可以通過將一個區域選入設備環境來創建你自己的裁剪區域,將區域選入設備環境可以使用
SeletObject(hdc,hRgn);或者?
SelectClipRgn(hdc,hRgn);? ? ? GDI為剪裁區域做了一個副本,因此當把區域對象選入到設備環境后,可以刪除它。Windows還包括幾個操作這個剪裁區域的函數,例如ExcludeClipRect函數用來從剪裁區域中去除一個矩形;IntersectClipRgn函數用來建立一個新的剪裁區域,這個新的剪裁區域是先前的剪裁區域和某個矩形的交集;OffsetClipRgn函數用來把一個剪裁區域移動到客戶區的另一部分。
5.7 總結
????? 這次,我們花了大篇幅的時間和文字來介紹繪圖基礎內容,雖然看是冗余,但是這將為后續的繪圖操作打下良好的基礎。之后我們將在Chapter 13介紹打印機、Chapter 14/15介紹位圖,Chapter 17介紹文本和字體、Chapter 18介紹圖元文件。好了,我們下次將學習鍵盤操作。
?
總結
以上是生活随笔為你收集整理的Chapter 05 绘图基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 快速排序2
- 下一篇: 17 张程序员壁纸(使用频率很高)