-----路過的朋友,若發現錯誤或有好的建議,歡迎在下面留言,謝謝!-----
之前講了很多GDI和裝置描述表的內容,想必大家對這麼多枯燥晦澀的東西早就感到厭倦了吧。為了激發一下大家繼續學習Windows的興趣,這回就給大家展示一些“有趣”的東西吧。
畫點(寫像素)
先前總是說GDI很重要,功能很強大,快把它捧得天花亂墜了,讓人聽著總感覺有點“懸”。現在我們就從繪製圖形中最簡單的畫點來開始具體地使用一下它吧。
說到畫點就不得不提一下SetPixel和GetPixel兩個函數。(讀者可以試著從名字來猜一下它們的功能,API的很多函數都可以從名字大概地推測出它們的用途)
SetPixel函數在指定的x和y座標以特定的顏色設定像素:
SetPixel (hdc, x, y, crColor) ;
如同在任何繪圖函數中一樣,第一個參數是裝置描述表的控制代碼。第二個和第三個參數指明了座標位置。通常要獲得視窗客戶區的裝置描述表,並且x和y相對於該客戶區的左上方。最後一個參數是COLORREF類型①,用於指定顏色。如果在函數中指定的顏色在顯示器不能被不支援,則函數將像素設定為最接近的純色並從函數返回該值。
GetPixel函數返回指定座標處的像素顏色:
COLORREF crColor = GetPixel (hdc, x, y) ;
理論上,有了這兩個函數我們就可以繪製一切圖形了。任何圖形不都是由點組成的嗎?但實際情況中如果要繪製一條直線或一個矩形等“複雜”一點的圖形,我們會用專門的函數。(當然你也可以使用SetPixel函數來完成,只要你不嫌麻煩,並不斷地調整x和y座標)顯然專門的函數要比數次調用SetPixel函數要簡便得多,而且效能更佳。因此,Windows GDI雖然包含了SetPixel和GetPixel兩個函數,但我們平時繪圖很少使用他們。
那我們接下來看一下常用的“專門的函數”吧。
與畫直線相關的
畫一條直線,必須調用兩個函數。第一個函數指定了線的開始點,第二個函數指定了線的終點:
MoveToEx (hdc, xBeg, yBeg, NULL) ;
LineTo (hdc, xEnd, yEnd) ;
MoveToEx實際上不會畫線,它只是設定了裝置描述表的“當前位置”屬性。MoveToEx的最後一個參數是指向POINT結構②的指標。從該函數返回後,POINT結構的x和y欄位指出了先前的當前位置。如果你不需要這種資訊(通常如此),可以簡單地像上面的例子那樣將最後一個參數設定為NULL。
LineTo函數才負責從當前的位置到它所指定的點畫一條直線。在預設的裝置描述表中,當前位置最初設定在點(0,0)。如果在調用LineTo之前沒有設定當前位置,那麼它將從客戶區的左上方開始畫線。③
我們來個例子吧:嘗試畫一個矩形。
我們可以先定義一個數組
POINT apt[5] = { 100, 100, 200, 100, 200, 200, 100, 200, 100, 100 } ;
注意,最後一個點與第一個點相同。現在,只需要使用MoveToEx函數把當前位置移到第一個點,並對後面的點使用LineTo:
MoveToEx(hdc, apt[0].x, apt[0].y, NULL) ;
for (i = 1 ; i < 5 ; i++)
LineTo (hdc, apt[i].x, apt[i].y) ;
你是否覺得有些奇怪呢,只調用一次MoveToEx函數把當前位置設定在(100,100),後面四次調用LineTo函數畫線,似乎是畫了三條以(100,100)為起點的射線呀(第四次畫線的起點與終點相同)。代碼是對的,確實是畫了一個矩形。只是關於LineTo函數有一點我們沒有講:LineTo函數每次畫完線後會不斷更新當前位置為線的終點。
當你要將數組中的點串連成線時,使用Polyline函數會簡單得多。下面這條語句畫出與上面一段代碼相同的矩形:
Polyline (hdc, apt, 5) ;
最後一個參數是點的數目。我們還可以使用(sizeof (apt) / sizeof (POINT))來表示這個值。Polyline與一個MoveToEx函數後面加幾個LineTo函數的效果相同,但是,Polyline既不使用也不改變當前位置。而PolylineTo有些不同,這個函數使用當前位置作為開始點,並將當前位置設定為最後一根線的終點。下面的程式碼畫出與上面所示一樣的矩形:
MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ;
PolylineTo (hdc, apt + 1, 4) ;//apt+1指向apt[1]結構項
你可以對幾條線使用Polyline和PolylineTo,這些函數在繪製複雜曲線最有用了。你使用由幾百甚至幾千條極短線段,把它們恰當的連在一起來繪製曲線,例如正弦曲線等。
與畫矩形相關的
我們先來認識一下繪製矩形的函數:
Rectangle (hdc, xLeft, yTop, xRight, yBottom) ;
點(xLeft, yTop)是矩形的左上方,(xRight, yBottom)是矩形的右下角。用函數Rectangle畫出的圖形,矩形的邊總是平行於顯示器的水平和垂直邊。
你知道了如何畫矩形,也就知道了如何畫橢圓,因為它們使用的參數都是相同的:
Ellipse (hdc, xLeft, yTop, xRight, yBottom) ;
看圖之後也許你就會明白了吧,在Windows裡橢圓是通過給定與它相切的矩形框來間接確定的。
大家再來瞧一瞧畫圓角的矩形的函數吧:
RoundRect (hdc, xLeft, yTop, xRight, yBottom,xCornerEllipse, yCornerEllipse) ;
沒錯,你又會發現此函數使用與函數Rectangle及Ellipse相同的邊界框。Windows使用一個小橢圓來畫圓角,這個橢圓的寬為xCornerEllipse,高為yCornerEllipse。xCornerEllipse和yCornerEllipse的值越大,角就越明顯。如果xCornerEllipse等於xRight與xLeft的差,且yCornerEllipse等於yTop與yBottom的差,那麼RoundRect函數將畫出一個橢圓。這是一種簡單的方法,但是結果看起來有點不對勁,因為角的彎曲部分在矩形長的一邊要大些。要矯正這一問題,你可以讓xCornerEllipse與yCornerEllipse的值相等。
最後,看到下面一句話你可能會吃驚:其實還有三個函數與上面三個函數使用相同的邊界框。
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函數用來畫橢圓線。Windows用一條假想的線將(xStart, yStart)與橢圓的中心串連,從該線與邊界框的交點開始,Windows按逆時針方向,沿著橢圓畫一條弧。Windows還用另一條假想的線將(xEnd,yEnd)與橢圓的中心串連,在該線與邊界框的交點處,Windows停止畫弧。
Chord函數用相同的方法用來畫橢圓弓形。
而Pie函數則用相同的方法用來畫橢圓扇形。
你可能不太明白在Arc、Chord和Pie函數中開始和結束位置的用法,為什麼不簡單地在橢圓的周線線指定開始和結束點呢?是的,你可以這麼做,但是你將不得不算出這些點。Windows的方法在不要求這種精確性的條件下,卻完成了相同的工作。
可能會有些可愛的小盆友問如果要畫正方形、圓、圓角正方形……腫麼辦?
把上面的參數改一下不就可以了麼。
畫“彩圖”
很不幸,如果你只用上面的函數繪圖,你會發現最終顯示在螢幕上的圖形都是一樣粗細的黑色線條,這是系統預設的。你可能會覺得很單調,還好我們有辦法解決這個問題。微軟為我們提供了“畫筆”。它可以決定繪圖線條的色彩、寬度和線型。
使用現有畫筆(Windows預定義的一些常用的畫筆)
當你調用本回任一個畫線函數時,Windows使用裝置描述表中當前選中的“畫筆”來畫線。預設裝置描述表中的畫筆為BLACK_PEN。這種畫筆只畫出一個像素寬的黑色實線。另外兩種Windows提供的現有畫筆是WHITE_PEN和NULL_PEN。其功能從名字就可以看出,前者就是畫出一個像素寬的白色實線,後者什麼都不畫(那它有什麼作用?下一回你就知道了)。
Windows程式使用HPEN類型的控制代碼來引用畫筆。可以這樣定義這個類型的變數:
HPEN hPen;
調用GetStockObject,可以獲得現有畫筆的控制代碼。例如,假設你想使用名為WHITE_PEN的現有畫筆,可以如下取得畫筆的控制代碼:
hPen = (HPEN)GetStockObject (WHITE_PEN) ; //這裡需要強制類型轉換
接著你就要將畫筆選進裝置描述表:
SelectObject (hdc, hPen) ;
此時當前的畫筆才是白色。在這個調用後,你畫的線將使用WHITE_PEN,直到你將另外一個畫筆選進裝置描述表或者釋放裝置描述表控制代碼為止。
你也可以不定義hPen變數,而將GetStockObject和SelectObject調用合并成一個語句:
SelectObject (hdc, (HPEN)GetStockObject (WHITE_PEN)) ;
如果想恢複到使用BLACK_PEN的狀態,可以用一個語句取得這種畫筆的控制代碼,並將其選進裝置描述表:
SelectObject (hdc, (HPEN)GetStockObject (BLACK_PEN)) ;
SelectObject的傳回值是此調用前裝置描述表中的畫筆控制代碼。如果啟動一個新的裝置描述表並調用
hPen = (HPEN)SelectObject (hdc, (HPEN)GetStockobject (WHITE_PEN)) ;
則裝置描述表中的當前畫筆將為WHITE_PEN,變數hPen將會是BLACK_PEN的控制代碼。以後通過調用
SelectObject (hdc, hPen) ;
就能夠將BLACK_PEN選進裝置描述表。
畫筆的建立、選擇和刪除
儘管使用現有畫筆非常方便,但卻受限於實心的黑畫筆、實心的白畫筆或者沒有畫筆這三種情況。如果想得到更豐富多彩的效果,就必須建立自己的畫筆。
這一過程通常是:
首先使用函數CreatePen或CreatePenIndirect建立一個“邏輯畫筆”,這僅僅是對畫筆的描述。這些函數返回邏輯畫筆的控制代碼。
然後,調用SelectObject將畫筆選進裝置描述表。現在,就可以使用新的畫筆來畫線了。在任何時候,都只能有一種畫筆選進裝置描述表。
最後,在釋放裝置描述表(或者在選擇了另一種畫筆到裝置內容中)之後,就可以調用DeleteObject來刪除所建立的邏輯畫筆了。在刪除後,該畫筆的控制代碼就不再有效了。
這個過程確實有點繁瑣,不過我們沒辦法改變,還給聽微軟的。
CreatePen函數的文法形如:
hPen = CreatePen (iPenStyle, iWidth, crColor) ;
其中,iPenStyle參數確定畫筆是實線、點線還是虛線④,iWidth參數是畫筆寬度,crColor參數是一個COLORREF值,它指定畫筆的顏色。註:如果指定畫筆是點劃線或虛線,則線寬必須不能大於1,否則Windows將使用實線畫筆代替。
你也可以通過建立一個類型為LOGPEN(“邏輯畫筆”)的結構,並調用CreatePenIndirect來建立畫筆。
要使用CreatePenIndirect,首先定義一個LOGPEN類型的結構:
LOGPEN logpen ;
此結構有三個成員:lopnStyle(無加號或減號整數或UINT)是畫筆線型,lopnWidth(POINT結構)是按邏輯單位度量的畫筆寬度,lopnColor (COLORREF)是畫筆顏色。Windows只使用lopnWidth結構的x值作為畫筆寬度,而忽略y值。
將此結構的地址傳遞給CreatePenIndirect結構就可以建立畫筆了:
hPen = CreatePenIndirect (&logpen) ;
注意,CreatePen和CreatePenIndirect函數不需要裝置描述表控制代碼作為參數。這些函數建立與裝置描述表沒有聯絡的邏輯畫筆。直到調用了SelectObject之後,畫筆才與裝置描述表發生聯絡。因此,可以對不同的裝置(如螢幕和印表機)使用相同的邏輯畫筆。
下面介紹一些建立、選擇和刪除畫筆的例子,以供大家參考和模仿。
假設您的程式使用三種畫筆——一種寬度為1的黑畫筆、一種寬度為3的紅畫筆和一種黑色點式畫筆,您可以先定義三個變數來存放這些畫筆的控制代碼:
static HPEN hPen1, hPen2, hPen3 ;
在處理WM_CREATE期間,您可以建立這三種畫筆:
hPen1 = CreatePen (PS_SOLID, 1, 0) ;
hPen2 = CreatePen (PS_SOLID, 3, RGB (255, 0, 0)) ;
hPen3 = CreatePen (PS_DOT, 0, 0) ;
在處理WM_PAINT期間,或者是在擁有一個裝置描述表有效控制代碼的任何時間裡,您都可以將這三個畫筆之一選進裝置描述表並用它來畫線:
SelectObject (hdc, hPen2) ;
[line-drawing functions]
SelectObject (hdc, hPen1) ;
[line-drawing functions]
在處理WM_DESTROY期間,您可以刪除您建立的三種畫筆:
DeleteObject (hPen1) ;
DeleteObject (hPen2) ;
DeleteObject (hPen3) ;
這是建立、選擇和刪除畫筆最直接的方法。 但是邏輯畫筆需要在整個程式運行期間佔用儲存,為此,你可以採用這種方法:在每個WM_PAINT訊息處理期間建立畫筆並選入裝置描述表,並在調用EndPaint之後刪除它們(你可以在調用EndPaint之前刪除它們,比如你把新畫筆選進了裝置描述表,但是要小心,不要刪除裝置描述中當前選擇的畫筆)。
你還可以隨時建立畫筆,並將CreatePen和SelectObject調用組合到同一個語句中:
SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
現在再開始畫線,你將使用一個紅色虛線畫筆。在畫完紅色虛線之後,可以刪除畫筆。糟了!由於沒有儲存畫筆控制代碼,怎麼才能刪除這些畫筆呢?
一種方法是由於SelectObject將返回裝置描述表中上一次選擇的畫筆控制代碼,所以你可以通過調用SelectObject將BLACK_PEN選進裝置描述表,並刪除從SelectObject返回的值:
DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ;
另一種方法在將新建立的畫筆選進裝置描述表時,儲存SelectObject返回的畫筆控制代碼:
hPen = SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
現在hPen是什麼呢?如果這是在取得裝置描述表之後第一次調用SelectObject,則hPen是BLACK_PEN對象的控制代碼。現在,可以將hPen選進裝置描述表,並刪除所建立的畫筆(第二次SelectObject調用返回的控制代碼),只要一條語句即可:
DeleteObject (SelectObject (hdc, hPen)) ;
Ps:如果有一個畫筆的控制代碼,就可以通過調用GetObject取得LOGPEN結構各個成員的值:
GetObject (hPen, sizeof (LOGPEN), (LPVOID) &logpen) ;
如果需要當前選進裝置描述表的畫筆控制代碼,可以調用:
hPen = GetCurrentObject (hdc, OBJ_PEN) ;
實驗
前面講了這麼多,我們練練手吧。
大家還記的我們在第二回見到的代碼嗎?那是我們所有程式的架構,以後我們所有的實驗都要用到它。我們先把它搬到VC6.0中並把“case
WM_CREATE
”語段(行59-61)注釋掉,目前我們不需要它。
一般我們把畫圖的代碼放在WM_PAINT訊息處理語句中接下來我們就要重寫“case
WM_PAINT
”語段了,先畫個矩形吧:
case WM_PAINT:
HDC hDC;
PAINTSTRUCT ps;
hDC=BeginPaint(hwnd,&ps);
Rectangle(hDC,100,,100,400,400);
EndPaint(hwnd,&ps);
break;
運行一下,大家看到矩形框了吧,同理大家再試一下Ellipse、RoundRect等這一系列的其他五個函數吧。(大家試著保持參數不變,看一下這一系列函數是不是共用一個矩形框)
我們接著在使用一下畫筆吧,還記得使用自己建立的畫筆的必要步驟麼:建立、選入裝置描述表、刪除。
case WM_PAINT:
HDC hDC;
PAINTSTRUCT ps;
HPEN hPen;
hDC=BeginPaint(hwnd,&ps);
LOGPEN logpen;
logpen.lopnStyle=PS_SOLID;//設定線型為實線
logpen.lopnWidth.x=3;//設定現款為3像素
logpen.lopnColor=RGB(255,0,0);//設直線條顏色為紅色
hPen=CreatePenIndirect(&logpen);
SelectObject(hDC,hPen);
Rectangle(hDC,100,100,300,300);
EndPaint(hwnd,&ps);
DeleteObject(hPen); //不要忘了刪除畫筆呀
break;
大家可以自行改一下線型、寬度、顏色再畫一下其他圖案吧,最後記得一定要試一下CretePen建立畫筆,它其實更簡單更常用。
最後我們做一點挑戰東西來結束回會吧,我們就來繪製一個周期的正弦曲線,我們應該用什麼函數來著?
請先在開頭即“#include <windows.h>”下面加上以下幾行:
#include <math.h>//我們要用正弦函數,所以要引用數學函數庫
#define TWOPI (2*3.14159)//定義TWOPI(即2π)為(2*3.14159)
重寫“case
WM_CREATE
”語段:
case WM_PAINT:
HDC hDC;
PAINTSTRUCT ps;
hDC=BeginPaint(hwnd,&ps);
int i ;
POINT apt [1000] ;
for (i = 0 ; i <1000 ; i++)
{
apt[i].x = i * 500 / 1000 ;
apt[i].y = (int) (500 / 2 * (1- sin (TWOPI * i / NUM))) ;//客戶區//座標原點在左上方,x軸y軸向右向下遞增,這與平時數學座標//不太一樣
}
Polyline (hDC, apt, NUM) ;
EndPaint(hwnd,&ps);
break;
本程式有一個含有1000個POINT結構的數組。隨著for迴圈從0增加到999,結構的x成員設定為從0遞增到數值500。結構的y成員設定為一個周期的正弦曲線值,並被放大以填滿500×500的地區。整個曲線的繪製僅僅使用了一個Polyline調用。
注邏輯畫筆是一種“GDI對象”,它是您可以建立的六種GDI對象之一,其它五種是畫刷、位元影像、地區、字型和調色盤。除了調色盤之外,這些對象都是通過SelectObject選進裝置描述表的。
Ps:在使用畫筆等GDI對象時,應該遵守以下三條規則:
- 最後要刪除自己建立的所有GDI對象。
- 當GDI對象正在一個有效裝置描述表中使用時,不要刪除它。
- 不要刪除現有對象。(如畫筆中的WHITE_PEN、BLACK_PEN和NULL_PEN)
①COLORREF類型用來描繪一個RGB顏色。其定義如下:
typedef DWORD COLORREF;
typedef DWORD *LPCOLORREF;
COLORREF類型變數值描繪一個顏色時對應於下面16進位的格式:
0x00bbggrr
可以用這樣一個結構體來描述。
RGB_value struct
{
byte unused ;
byte blue ;
byte green ;
byte red;
};
其中第一位元組為 0 而且始終為 0,其它三個位元組分別表示蘭色、綠色和紅色,剛好和 RGB 的次序相反。這個結構體用起來挺彆扭。對於COLORREF,我們通常使用宏RGB對其進行賦值。
宏的定義如下:
COLORREF RGB
(
BYTEbyRed, // red component of color
BYTEbyGreen, // green component of color
BYTEbyBlue // blue component of color
);
COLORREF 是一個 32-bit 整型數值,它代表了一種顏色。你可以使用 RGB 函數來初始化 COLORREF。例如:
COLORREF color=RGB(0,255,0);
RGB函數接收三個 0-255 數值,一個代表紅色,一個代表綠色,一個代表藍色。在上面的例子中,紅色和藍色值都為 0,所以在該顏色中沒有紅色和藍色。綠色為最大值255。所以該顏色為綠色。0,0,0 為黑色,255,255,255 為白色。
②POINT(點)是一個結構,它定義了一個點的座標(x,y)
結構的定義如下:
typedef struct tagPOINT{
LONG x;
LONG y;
}POINT;
參數:
x: 指出一個點的x座標.
y: 指出一個點的y座標.
③如果你需要當前位置,就可以通過以下調用獲得:
GetCurrentPositionEx (hdc, &pt) ;
其中,pt是POINT結構的。
④ iPenStyle 指定畫筆樣式,可以是下述標識符之一