Windows編程 第五回 GDI初窺

終於又見面了

隔了好一陣子,終於又和大家見面了。最近我有點忙,忙得已經好幾周沒看過電影了,不過我喜歡這種感覺,這讓我過的充實,過的問心無愧。我最近喜歡寫 東西,因爲每當我提筆寫字或在鍵盤上碼字時,我就能靜下浮躁的心來學習思考,看不下去的書可以看得下去。我總是靠寫讀書筆記來迫使我自己讀書,感覺這方法 不錯,如果你看不下書或感到浮躁時,你可以試試。書這東西不管你喜不喜歡,還是要多讀的。有句話叫什麼來的?書到用時方恨少!

GDI?何方神聖?

GDI是GraphicsDevice Interface(圖形設備接口)的簡稱,當Windows應用程序需要顯示點、線、圖像、文字等內容,在顯示器或打印機輸出這些內容時,就需要使用到 GDI。圖形設備接口是Windows圖形界面的基礎。正如你所認爲的那樣,GDI是Windows非常重要的部分。當然GDI不是可以實現這種功能的唯 一程序設計接口,GDI只是其中最基本的。除了GDI外,還有GDI+、OpenGL、DirectX、WindowsImage Acquisition等可以實現類似或更高級的功能。

GDI的意義在於將程序對圖形界面的操作和硬件設備隔絕開來,即支持與設備無關的圖形,在程序中可以將所有的圖形設備都看成是虛擬設備,包括視頻顯 示器和打印機等,然後通過GDI函數用同樣的方法去操作它們,由Windows負責將函數調用轉化成針對具體硬件的操作。只要一個設備提供了和 Windows兼容的驅動程序,它就可以被看做是一個標準的設備。GDI的出現使程序員無需要關心硬件設備及設備驅動,就可以將應用程序的輸出轉化爲硬件 設備上的輸出,實現了程序開發者與硬件設備的隔離,大大方便了開發工作。

三探GDI

從程序員的觀點來看,GDI由很多個函數調用和一些相關的數據類型、宏和結構組成。不幸的是,如果要對GDI進行全面的講述,將需要一大本書的內容。爲此,我們對GDI操作可以從3個方面去簡要了解——When,Where和How:

When——指的是進行圖形操作的時機,究竟什麼時刻最適合程序進行圖形操作呢?

Where——指的是圖形該往哪裏畫,既然Windows隔離了硬件圖形設備,那麼該把什麼地方當做“下筆”的地方呢?

How——瞭解了上面兩個問題後,最後還要知道“如何畫”,這就涉及如何使用大部分GDI函數的問題了。

一、When——WM_PAINT消息

1、客戶區的刷新

正如上面所說的,這裏討論的是“When”的問題,讀者可能會問:爲什麼會有這個問題,如果要向窗口輸出圖形,程序想在什麼時候輸出那就是什麼時候,難道這個時刻還有規定不成?

是的,在Windows操作系統中,屏幕是多個程序“公用”的,用戶程序不要指望輸出到窗口中的內容經過一段時間後還會保留在那裏,它們可能被別的 東西覆蓋,如其他窗口、鼠標箭頭或下拉的菜單等。在Windows中,恢復被覆蓋內容的責任大部分屬於用戶程序自己,理由很簡單:Windows是個多任 務的操作系統,假如程序B覆蓋了程序A的窗口內容,覆蓋掉的內容由程序B負責恢復的話,它就必須保存它覆蓋掉的內容,但是在它將保存的內容恢復之前,程序 A也在運行,並可能在程序B恢復以前已經向它自己的窗口輸出新的內容,結果當程序B恢復它保存的窗口內容時,保存的內容可能是過時的。

Windows系統採用的方法是:當Windows檢測到窗口被覆蓋的地方需要恢復的時候,它會向用戶程序發送一個WM_PAINT消息,然後由用戶程序來決定如何恢復被覆蓋的內容。

如果程序因爲忙於處理其他事務以至於無法及時響應WM_PAINT消息,那麼窗口客戶區原先被覆蓋的地方可能暫時會被Windows畫成一塊白色(或者背景色)的矩形,或者根本就是保留被覆蓋時的情形,直到程序有時間去響應WM_PAINT消息爲止。

所以對於“When”這個問題,答案是:程序應該在Windows要求的時候繪製客戶區,也就是在收到WM_PAINT 消息的時候。處理WM_PAINT消息要求程序員改變自己向顯示器輸出的思維方式,僅當“有需求”——即Windows給窗口過程發送WM_PAINT消 息時才進行繪製。如果程序在其它時間需要更新其客戶區,它可以強制Windows產生一個WM_PAINT消息(例如可以通過調用 InvalidateRect等函數引發一條WM_PAINT消息)。這看來似乎是在屏幕上顯示內容的一種捨近求遠的方法,但是請相信,你的程序結構會從 中受益的。

2、系統何時發送WM_PAINT消息?

大多數Windows程序在WinMain中進入消息循環之前的初始化期間都要調用函數UpdateWindow。Windows利用這個機會給窗口過程發送第一個WM_PAINT消息。這個消息通知窗口過程:必須繪製客戶區。

此後,窗口過程應在任何時刻都準備好處理其它WM_PAINT消息,必要的話,甚至重新繪製窗口的整個客戶區。當然Windows並不是在任何情況下都發送WM_PAINT消息的,下面是幾種不同的情況介紹:

i  當鼠標光標移過窗口客戶區以及圖標拖過客戶區這兩種情況,Windows總是自己保存被覆蓋的區域並恢復它,並不需要發送WM_PAINT消息通知用戶程序。

ii  當窗口客戶區被自己的下拉式菜單覆蓋,或者被自己彈出的對話框、消息框覆蓋後,Windows會嘗試保存被覆蓋的區域並在以後恢復它,如果因爲某種原因無法保存並恢復的話,Windows會發送一個WM_PAINT消息通知程序。

iii  當用戶移動窗口或顯示窗口,窗口中先前被隱藏的區域重新可見,比如其他的窗口覆蓋程序客戶區後移開或程序從最小化的狀態恢復;用戶改變了窗口的大小(如果 窗口類風格具有CS_HREDRAW和CS_VREDRAW設置);用戶按動滾動條;程序調用UpdateWindow,InvalidateRect以 及InvalidateRgn等函數。在這些情況下,Windows會向窗口發送WM_PAINT消息。

3、無效矩形與有效矩形

儘管窗口過程一接收到WM_PAINT消息之後,就準備更新整個客戶區,但它經常只需要更新一個較小的區域(最常見的是客戶區中的矩形區域)。顯 然,當對話框覆蓋了部分客戶區時,情況即是如此。在擦除對話框之後,需要重新繪製的只是先前被對話框遮住的矩形區域。這個區域稱爲“無效區域”或“更新區 域”。正是客戶區內無效區域的存在,纔會讓Windows將一個WM_PAINT消息放在應用程序的消息隊列中。只有在客戶區的某一部分無效時,窗口才會接收WM_PAINT消息。(可見“無效”纔是產生WM_PAINT消息的根本誘因)

Windows內部爲每個窗口保存一個“繪圖信息結構”①,這個結構包含了包圍無效區域的最小矩形的座標以及其它信息, 這個矩形就叫做“無效矩形”。如果在窗口過程處理WM_PAINT消息之前客戶區中的另一個區域變爲無效,則Windows計算出一個包圍兩個區域的新的 無效區域(以及一個新的無效矩形),並將這種變化後的信息放在上面提到的繪圖信息結構中。Windows不會將多個WM_PAINT消息都放在消息隊列 中。

窗口過程可以通過調用InvalidateRect使客戶區內的矩形無效。如果消息隊列中已經包含一個WM_PAINT消息,Windows將計算出新的無效矩形。否則,它將一個新的WM_PAINT消息放入消息隊列中。

 

二、where——設備描述表(Device Context,簡稱DC)

解決了“When”的問題後,再考慮一下“Where”的問題。在Windows中,GDI把程序和硬件分隔出來,那麼,究竟該往哪裏輸出圖形呢——這就是“Where”的問題。答案是:通過“設備描述表”來輸出圖形。

什麼是設備描述表?

在Windows中,所有與圖形相關的操作都是用統一的方法來完成的(不然就不能稱爲“圖形設備接口”了)。不管是繪畫 屏幕上的一個窗口,還是把圖形輸出到打印機,或者對一幅位圖進行繪製,使用的繪圖函數都是相同的,爲了實現方法上的統一,必須將所有的圖形對象看成是一個 虛擬的設備,這些設備可能有不同的屬性,如黑白打印機和彩色屏幕的顏色深度是不同的,不同打印機的尺寸和分辨率可能是不同的,繪圖儀只支持矢量而不支持位 圖等。不同設備的不同屬性就構成了一個繪圖的“環境”,這個繪圖的“環境”就是Win32編程中圖形操作的對象,把它叫做設備描述表②。設備描述表又稱爲 設備上下文,或者設備環境。

在Windows應用程序中,設備描述表與圖形對象共同工作,協同完成繪圖顯示工作。就像畫家繪畫一樣,設備描述表好比是畫家的畫布,圖形對象好比是畫家的畫筆。用畫筆在畫布上繪畫,不同的畫筆將畫出不同的畫來。選擇合適的繪圖對象和圖形對象,才能按照要求完成繪圖任務。

在實際使用中,通過設備描述表可以操作的對象很廣泛,除了可以是打印機或繪圖儀等硬件設備外,也可以是窗口的客戶區,包括大大小小的所有可以被稱爲窗口的按鈕與控件等的客戶區,也可以是一個位圖。總之,任何需要用到圖形操作的東西都可以通過設備描述表進行繪圖。

三、How——見例子吧

大家在對以上兩個問題有了初步瞭解後,我們就最後來看看“How”的問題吧。GDI函數還真不少,它們的具體用法估計可以寫一本厚書了,當然我們也沒必要涉及到每個函數。在後面我們再具體展開一些常用的GDI函數,現在嘛我們來解決一下“歷史遺留問題”。

我們就先來看一個簡單顯示文本例子吧:

62case WM_PAINT:
63 HDC hDC;
64 PAINTSTRUCT ps;
65 hDC=BeginPaint(hwnd,&ps);
66 TextOut(hDC,0,0,"Hello World!",strlen("Hello World!"));
67 EndPaint(hwnd,&ps);
68 break;

眼熟吧,這就是第二回代碼的一部分,其功能是在窗口客戶區的左上角顯示"HelloWorld!",這部分我一直沒有講,就是爲了等到這兒再來告訴大家。

行63定義了一個設備描述表句柄變量hDC,行64就是定義一個上面曾提到的“繪圖信息結構”變量,行65來把申請的設備描述表的句柄賦給變量hDC,行66 表示我們就可以在這個申請的這個“環境”上操作了,即在屏幕窗口客戶去輸出文本,行67釋放設備描述表句柄。

通過這個例子我們可以更好的瞭解到設備描述表,只是“環境”而不是真正的“設備”,這個“環境”與特定的顯示設備(本例中的顯示器)相關。我們只是在這個“環境”上表達我們要做什麼(如這個例子,我們要輸出文本"Hello World!"),具體這個“環境”怎麼讓設備(顯示器)去做,這就是系統的事了,我們就不管了,這不是簡化了程序員的工作了嗎。

 

再補充一點,設備描述表中的有些值是圖形化的“屬性”,這些屬性定義了一些GDI繪圖函數工作情況的特殊內容。例如,對於 TextOut(hdc,x,y,psText,iLength),設備描述表的屬性確定了文本的顏色、文本的背景色、TextOut函數的 x 座標和 y 座標映射到窗口的客戶區的方式,以及顯示文本時Windows 使用的字體。其實設備描述表實際上是一個數據結構,結構中保存的就是設備的屬性,當對設備描述表進行圖形操作的時候,Windows可以根據這些屬性找到 對應的設備進行相關的操作。

 

由此推廣開來這個例子還告訴我們:當你想在一個圖像輸出設備(諸如屏幕或者打印機)上繪圖時,你首先必須獲得一個設備描 述表(或者DC)的句柄。在獲取該句柄後,Windows用默認的屬性值填充內部設備描述表結構。在後面文章中你會看到,可以通過調用不同的GDI函數改 變這些默認值。利用其它的GDI函數可以取得這些屬性的當前值。當然,還有其它的GDI函數能夠真正地繪圖。接着,我們就是調用GDI函數在當前設備描述 表上繪圖來完成我們的任務呀。最後繪圖完畢後,必須釋放設備描述表句柄。句柄被釋放後就不再有效,且不能再被使用。程序必須在處理單個消息期間獲取和釋放 句柄。
可能你對這一系列繁瑣的操作感到反感,我勸你還是忍受吧,既然你享用Windows提供的便利,就要無條件地遵守它的“規則”。

 

讀到此我想“How”的問題你大概已經有所瞭解了,從三個方面瞭解GDI的任務我們已經基本完成了,可以放鬆一下了,我們下回再見吧。

 

①Windows爲每個窗口保存一個“繪圖信息結構”,這就是PAINTSTRUCT,定義如下:

typedef struct tagPAINTSTRUCT {

  HDC hdc;

  BOOL fErase;

  RECT rcPaint;

  BOOL fRestore;

  BOOL fIncUpdate;

  BYTE rgbReserved[32];

  } PAINTSTRUCT, *PPAINTSTRUCT;         

在程序調用BeginPaint(下回講)時,Windows會適當填入該結構的各個字段值。用戶程序只使用前三個字段,其它字段由Windows內部使用。

hdc字段是設備描述表句柄。

fErase字段記錄Windows是否已經擦除了無效矩形的背景,在大多數情況下,如果被標誌爲FALSE(0),這意味着Windows已經擦 除了無效矩形的背景。(Windows使用WNDCLASS結構的hbrBackground字段指定的畫刷來擦除背景,這個WNDCLASS結構是程序 在WinMain初始化期間登錄窗口類時使用的。許多Windows程序使用白色畫刷。以下顯示了程序設定窗口類結構字段的語句:

wndcls.hbrBackground=(HBRUSH)GetStockObject(WHILE_BRUSH); 
//我們之前寫過的,還記得嗎       
不過,如果程序通過調用Windows函數InvalidateRect使客戶區中的矩形失效,該函數的最後一個參數會指定是否擦除背景。如果這個參數爲FALSE(即0),則Windows將不會擦除背景,並且在調用完BeginPaint後PAINTSTRUCT結構的fErase字段將爲TRUE(非零)。)

PAINTSTRUCT結構的rcPaint字段是RECT類型的結構。

rect結構定義了一個矩形框左上角以及右下角的座標

  typedef struct _RECT {

  LONG left;

  LONG top;

  LONG right;

  LONG bottom;

  } RECT, *PRECT;

成員  left : 指定矩形框左上角的x座標

      top: 指定矩形框左上角的y座標

      right: 指定矩形框右下角的x座標

bottom:指定矩形框右下角的y座標

PAINTSTRUCT結構的rcPaint字段定義了無效矩形的邊界,如圖所示。這些值均以圖素爲單位,並相對於顯示 區域的左上角。無效矩形是應該重畫的區域。PAINTSTRUCT中的rcPaint矩形不僅是無效矩形,它還是一個剪裁矩形。(繪圖可以限制在客戶區的 某一部分中,這就是所謂的剪裁,剪裁區域可以是矩形或非矩形)

②爲方便大家對設備描述表的理解這裏還有兩種敘述,供大家參考。

a設備描述表是一個定義一組圖形對象及其屬性、影響輸出的圖形方式(數據)結構。windows提供設備描述表,用於應用程序和物理設備之間進行交互,從而提供了應用程序設計的平臺無關性。設備描述表又稱爲設備上下文,或者設備環境。
設備描述表是一種數據結構,它包括了一個設備(如顯示器和打印機)的繪製屬性相關的信息。所有的繪製操作通過設備描述表進行。設備描述表與大多WIN32結構不同,應用程序不能直接訪問設備描述表,只能由各種相關API函數通過設備描述表的句柄間接訪問該結構。
設備描述表總是與某種系統硬件設備相關。比如屏幕設備描述表與顯示設備相關,打印機設備描述表與打印設備相關等等。
屏幕設備描述表,一般我們簡單地稱其爲設備描述表。它與顯示設備具有一定的對應關係,在windows GDI界面下,它總是相關與某個窗口或這窗口上的某個顯示區域。通常意義上窗口的設備描述表,一般指的是窗口的客戶區,不包括標題欄、菜單欄所佔有的區 域,而對於整個窗口來說,其設備描述表嚴格意義上來講應該稱爲窗口設備描述表,它包含窗口的全部顯示區域。二者的操作方法完全一致,所不同的僅僅是可操作 的範圍不同而已。
windows 窗口一旦創建,它就自動地產生了與之相對應的設備描述表數據結構,用戶可運用該結構,實現對窗口顯示區域的GDI操作,如劃線、寫文本、繪製位圖、填充等,並且所有這些操作均要通過設備描述表句柄了進行。

b 孫鑫:我們可以用一個形象的比喻來說明它的作用。現在有一個美術老師,他讓

他的學生畫一幅森林的圖像,有的學生採用素描,有的學生採用水彩畫,有的學生採用油

畫,每個學生所作的圖都是森林,然而表現形式卻各不相同。如果讓我們來畫圖,老師指

定了一種畫法(例如用水彩畫),我們就要去學習它,然後才能按照要求畫出圖形。如果

畫法(工具)經常變換,我們就要花大量的時間和精力去學習和掌握它。在這裏,畫法就

相當於計算機中的圖形設備及其驅動程序。我們要想作一幅圖,就要掌握我們所用平臺的

圖形設備和它的驅動程序,調用驅動程序的接口來完成圖形的顯示。不同圖形設備的設備

驅動程序是不一樣的,對於程序員來說,要掌握各種不同的驅動程序,工作量就太大了。

因此,Windows 就給我們提供了一個設備描述表,讓我們從學生的角色轉變爲老師的角色,只要下命令去畫森林這幅圖,由設備描述表 去和設備驅動程序打交道,完成圖形的繪製。至於圖形的效果,就要由所使用的圖形設備來決定了。對於老師來說,只要畫出的是森林圖像就可以了。對於程序員來 說,充當老師的角色,只需要獲取設備描述表的句柄,利用這個句柄去作圖就可以了。

:部分內容援引自羅雲彬《Windows環境下32位彙編語言程序設計》

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