Windows編程 第七回 繪圖課(上)

之前講了很多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之前沒有設置當前位置,那麼它將從客戶區的左上角開始畫線。③

我們來個例子吧:嘗試畫一個矩形。

我們可以先定義一個數組

POINTapt[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類型的句柄來引用畫筆。可以這樣定義這個類型的變量:

HPENhPen;

調用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 指定畫筆樣式,可以是下述標識符之一

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