?? 007.txt
字號:
7.1 GDI原理
Windows是基于圖形界面的,所以在Win32編程中,圖形操作是最常用的操作。GDI的意義在于將程序對圖形界面的操作和硬件設備隔絕開來,在程序中可以將所有的圖形設備都看成是虛擬設備,包括視頻顯示器和打印機等,然后通過GDI函數用同樣的方法去操作它們,由Windows負責將函數調用轉化成針對具體硬件的操作。只要一個設備提供了和Windows兼容的驅動程序,它就可以被看做是一個標準的設備。以前在DOS系統下寫應用程序的時候,如果要進行圖形操作,那么就要考慮到市場上每種顯示卡的不同,否則在裝配某種顯卡的計算機上就可能無法正常運行,對匯編程序員來說,這真是一個惡夢。在Win32編程中,正是GDI函數讓這個惡夢成為歷史。
GDI函數全部包括在GDI32.DLL中,在編程的時候,注意要在源程序的開頭加上相應的包含語句:
include gdi32.inc
includelib gdi32.lib
和GDI相關的內容真是太龐大了,只要查看一下gdi32.inc文件就可以發現,函數的總數達到了300多個,和GDI相關的數據結構也非常多,要完全深入GDI編程,用上本書的全部篇幅可能也不夠。在本章中,筆者希望通過幾個例子,讓讀者能了解GDI的原理和基本的使用方法。
歸納起來,GDI操作可以從3個方面去了解——When,Where和How:
● When——指的是進行圖形操作的時機,究竟什么時刻最適合程序進行圖形操作呢?在7.1.1節“GDI程序的結構”中,將探討這個問題。
● Where——指的是圖形該往哪里畫,既然Windows隔離了硬件圖形設備,那么該把什么地方當做“下筆”的地方呢?7.1.2節的“設備環境”就是解答。
● How——了解了上面兩個問題后,最后還要知道“如何畫”,這就涉及如何使用大部分GDI函數的問題了,在本章余下來的篇幅中,將集中討論這個問題。
7.1.1 GDI程序的結構
1. 客戶區的刷新
正如上面所說的,本節討論的是“When”的問題,讀者可能會問:為什么會有這個問題,如果要向窗口輸出圖形,程序想在什么時候輸出那就是什么時候,難道這個時刻還有規定不成?
但這個問題似乎不能這樣來問,讓我們來考慮這些情況:在DOS操作系統中編程的時候,程序把文字或圖形輸出到屏幕,在輸出新的內容之前,這些內容總是保留在屏幕原處,這些內容會被意外覆蓋的惟一情況是激活一個TSR程序,但TSR程序在退出之前有義務恢復原來的屏幕,如果它無法恢復屏幕的內容,那么這是它的責任,我們不會在自己的程序中去考慮屏幕內容會無緣無故消失這種情況,所以可以把屏幕看成是應用程序私有的。
如果程序輸出的內容過多,如用dir顯示一個含有很多文件的目錄,用戶根本無法看清快速上翻的屏幕,這時程序可以設計一個參數來暫停一下,如dir /p。這已經是DOS程序最“體貼”的做法了,如果用戶想回過頭去看已經滾出屏幕的內容,那可對不起,只能再執行一遍了!
所以對DOS程序來說,程序想在什么時候輸出信息那就是什么時候,根本不存在When這個問題。
但在Windows操作系統中,屏幕是多個程序“公用”的,用戶程序不要指望輸出到窗口中的內容經過一段時間后還會保留在那里,它們可能被別的東西覆蓋,如其他窗口、鼠標箭頭或下拉的菜單等。在Windows中,恢復被覆蓋內容的責任大部分屬于用戶程序自己,理由很簡單:Windows是個多任務的操作系統,假如程序B覆蓋了程序A的窗口內容,覆蓋掉的內容由程序B負責恢復的話,它就必須保存它覆蓋掉的內容,但是在它將保存的內容恢復之前,程序A也在運行,并可能在程序B恢復以前已經向它自己的窗口輸出新的內容,結果當程序B恢復它保存的窗口內容時,保存的內容可能是過時的(而DOS的情況就不同,TSR程序激活的時候,用戶程序是被掛起的),所以最好的辦法就是讓程序A自己來決定如何恢復。
Windows系統采用的方法是:當Windows檢測到窗口被覆蓋的地方需要恢復的時候,它會向用戶程序發送一個WM_PAINT消息,消息中包括了需要恢復的區域,然后由用戶程序來決定如何恢復被覆蓋的內容。
如果程序因為忙于處理其他事務以至于無法及時響應WM_PAINT消息,那么窗口客戶區原先被覆蓋的地方可能會被Windows暫時畫成一塊白色(或者背景色)的矩形,或者根本就是保留被覆蓋時的情形,直到程序有時間去響應WM_PAINT消息為止。我們常常可以看到這種情況發生在死鎖程序的客戶區內,這就是因為死鎖的程序無法響應WM_PAINT消息來恢復客戶區造成的。
所以對于“When”這個問題,答案是:程序應該在Windows要求的時候繪畫客戶區,也就是在收到WM_PAINT消息的時候。如果程序需要主動刷新客戶區,那么可以通過調用InvalidateRect等函數引發一條WM_PAINT消息,因為在WM_PAINT消息中刷新客戶區的代碼是必須存在的,所以用這種看似“舍近求遠”的辦法實際上可以節省一份重復的代碼。即使是在游戲程序這種“主動刷新”遠遠多于“被動刷新”的程序中,只要窗口有被其他東西覆蓋的可能,那么這個原則就是適用的。
2. GDI程序的結構
對于Win32程序來說,WM_PAINT消息隨時可能發生,這就意味著,程序再也不能像在DOS下一樣輸出結果后就不管了,反過來,程序在任何時刻都應該知道如何恢復整個或局部客戶區中以前輸出的內容,本著這個要求,可以按圖7.1所示來安排程序結構。
圖7.1 GDI程序的結構
如果程序的功能比較簡單,可以采取圖中左邊的A程序結構,即計算及刷新整個客戶區的代碼全部安排在WM_PAINT消息中完成,這樣,每次當客戶區的全部或部分需要被更新的時候,程序重新執行整個生成客戶區屏幕數據的功能模塊并刷新客戶區。這種結構適用于功能模塊很短小且執行速度很快的情況,整個過程的時間最好不超過幾百ms,否則,用戶會在一個明顯的等待時間后才看到程序把客戶區中的“空洞”補上。考慮一個極端的情況:當程序輸出的內容是經過千辛萬苦才算出來的——這不是一件奇怪的事情,計算圓周率的程序就要動輒計算幾個小時——那么即使客戶區被別的窗口覆蓋掉一點點,程序也要經過整個計算過程后才能重畫客戶區,而且在這個過程中,程序還沒有從WM_PAINT消息返回,以至于無法處理其他消息,結果程序就會以客戶區中有個空洞的難看姿勢呆在屏幕上一動不動達幾個小時!
當生成屏幕數據的功能模塊有些復雜的時候,如剛才計算圓周率的例子,就應該考慮采用圖中B程序所示的結構了。在這個程序中,功能模塊和客戶區刷新模塊分別在不同的子程序中實現,功能模塊單獨用一個子程序完成,這個子程序可以由用戶通過選擇菜單項在WM_COMMAND消息中執行,也可以新建另外一個線程來完成,總之,它最后把計算結果放到一個緩沖區中,而每當客戶區需要刷新時,程序在WM_PAINT消息中調用客戶區刷新子程序,這個子程序從計算好的緩沖區中取出數據并輸出到客戶區中,由于單純的屏幕刷新過程是很快的,所以用戶根本來不及看到客戶區中的空洞。
在本章后面的內容中有兩個時鐘的例子:Clock.exe和BmpClock.exe,前面一個例子采用的是A結構,后面一個例子采用的是B結構,讀者在閱讀的時候可以比較一下它們在結構上的不同。
3. 探討WM_PAINT消息
當客戶區被覆蓋并重新顯示的時候,Windows并不是在所有的情況下都發送WM_PAINT消息,下面是幾種不同的情況:
● 當鼠標光標移過窗口客戶區以及圖標拖過客戶區這兩種情況,Windows總是自己保存被覆蓋的區域并恢復它,并不需要發送WM_PAINT消息通知用戶程序。
● 當窗口客戶區被自己的下拉式菜單覆蓋,或者被自己彈出的對話框覆蓋后,Windows會嘗試保存被覆蓋的區域并在以后恢復它,如果因為某種原因無法保存并恢復的話,Windows會發送一個WM_PAINT消息通知程序。
● 別的情況造成窗口的一部分從不可見變到可見,如程序從最小化的狀態恢復,其他的窗口覆蓋客戶區后移開,用戶改變了窗口的大小和用戶按動滾動條等,在這些情況下,Windows會向窗口發送WM_PAINT消息。
● 一些函數會引發WM_PAINT消息,如UpdateWindow,InvalidateRect以及InvalidateRgn函數等。
窗口過程收到WM_PAINT消息后,并不代表整個客戶區都需要被刷新,有可能客戶區被覆蓋的區域只有一小塊,這個區域就叫做“無效區域”,程序只需要更新這個區域。
和WM_TIMER消息類似,WM_PAINT消息也是一個低級別的消息,雖然它不會像WM_TIMER消息一樣被丟棄,但Windows總是在消息循環空的時候才把WM_PAINT放入其中,實際上,Windows為每個窗口維護一個“繪圖信息結構”,無效區域的坐標就在其中,每當消息循環空的時候,如果Windows發現存在一個無效區域,就會放入一個WM_PAINT消息。
無效區域的坐標并不附帶在WM_PAINT消息的參數中,在程序中有其他方法可以獲取,WM_PAINT消息只是通知程序有個區域需要更新而已,所以Windows也不會同時將兩條WM_PAINT消息放入消息循環,當Windows要放入一條WM_PAINT消息的時候,如果發現已經存在一個無效區域了,那么它只需要把新舊兩個無效區域合并計算出一個新的無效區域就可以了,消息循環中還是只需要一條WM_PAINT消息。
由于存在“無效區域”這樣一個東西,所以程序在WM_PAINT消息中對客戶區刷新完畢后工作并沒有結束,如果不使無效區域變得有效,Windows會在下一輪消息循環中繼續放入一個WM_PAINT消息。還記得4.4.2節中的實驗4嗎,如果沒有這個環節,WM_PAINT消息就會源源不斷地發過來!那個實驗中我們并沒有去刷新客戶區,而是簡單地用一個ValidateRect函數直接讓客戶區變得有效,以此來“欺騙”Windows已經沒有無效區域了,當Windows檢查“繪圖信息結構”的時候發現沒有了無效區域,也就不會繼續發送WM_PAINT消息了。
WM_PAINT消息的處理流程一般是:
.if eax == WM_PAINT ;eax為uMsg
invoke BeginPaint,hWnd,addr stPS
;刷新客戶區的代碼
invoke EndPaint,hWnd,addr stPS
xor eax,eax
ret
讀者可以發現中間并沒有調用ValidateRect來使無效區域變得有效,這是因為BeginPaint函數和EndPaint函數隱含有這個功能,如果不是以BeginPaint/EndPaint當做消息處理代碼的頭尾的話,那么在WM_PAINT消息返回的時候就必須調用ValidateRect函數。
BeginPaint函數的第二個參數是一個繪圖信息結構的緩沖區地址,Windows會在這里返回繪圖信息結構,結構中包含了無效區域的位置和大小,繪圖信息結構的定義如下:
PAINTSTRUCT STRUCT
hdc DWORD ?
fErase DWORD ?
rcPaint RECT <>
fRestore DWORD ?
fIncUpdate DWORD ?
rgbReserved BYTE 32 dup(?)
PAINTSTRUCT ENDS
其中hdc字段是窗口的設備環境句柄(在下一節中將要講到),rcPaint字段是一個RECT結構,它指定了無效區域矩形的對角頂點,fErase字段如果為非零值,表示Windows在發送WM_PAINT消息前已經用背景色擦除了無效區域,后面3個字段是Windows內部使用的,應用程序不必去理會它們。
7.1.2 設備環境
好了,解決了“When”的問題,讓我們來考慮一個新的問題,在DOS操作系統中,向屏幕輸出數據實際上是把輸出內容拷貝到視頻緩沖區中,在第1章的圖1.1中就已經說明:如果在文本模式下顯示信息,只需要把內容拷貝到B8000h處的內存中;顯示圖形信息,可以把圖形數據拷貝到A0000h處的內存中。
在Windows中,GDI接口把程序和硬件分隔開來,在Win32編程中,再也不能通過直接向視頻緩沖區拷貝數據的辦法來顯示信息了,那么,究竟該往哪里輸出圖形呢——這就是“Where”的問題。答案是:通過“設備環境”來輸出圖形。
1. 什么是設備環境
在Windows中,所有與圖形相關的操作都是用統一的方法來完成的(不然就不能稱為“圖形設備接口”了)。不管是繪畫屏幕上的一個窗口,還是把圖形輸出到打印機,或者對一幅位圖進行繪畫,使用的繪圖函數都是相同的,為了實現方法上的統一,必須將所有的圖形對象看成是一個虛擬的設備,這些設備可能有不同的屬性,如黑白打印機和彩色屏幕的顏色深度是不同的,不同打印機的尺寸和分辨率可能是不同的,繪圖儀只支持矢量而不支持位圖等。不同設備的不同屬性就構成了一個繪圖的“環境”,就像DOS操作系統中把視頻緩沖區當做圖形操作的對象一樣,這個繪圖的“環境”就是Win32編程中圖形操作的對象,把它叫做“設備環境”。設備環境實際上是一個數據結構,結構中保存的就是設備的屬性,當對設備環境進行圖形操作的時候,Windows可以根據這些屬性找到對應的設備進行相關的操作。
在實際使用中,通過“設備環境”可以操作的對象很廣泛,除了可以是打印機或繪圖儀等硬件設備外,也可以是窗口的客戶區,包括大大小小的所有可以被稱為窗口的按鈕與控件等的客戶區,也可以是一個位圖。總之,任何需要用到圖形操作的東西都可以通過“設備環境”進行繪圖。
為了更好地理解“設備環境”是什么,先來看一個例子,例子的代碼在所附光盤的Chapter07\DcCopy目錄中,DcCopy.asm中的代碼如下:
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定義
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include gdi32.inc
includelib gdi32.lib
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ID_TIMER equ 1
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 數據段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
hInstance dd ?
hWin1 dd ?
hWin2 dd ?
.const
szClass1 db ~SourceWindow~,0
szClass2 db ~DestWindow~,0
szCaption1 db ~請嘗試用別的窗口覆蓋本窗口!~,0
szCaption2 db ~本窗口圖像拷貝自另一窗口~,0
szText db ~Win32 Assembly, Simple and powerful !~,0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 定時器過程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcTimer proc _hWnd,uMsg,_idEvent,_dwTime
local @hDc1,@hDc2
local @stRect:RECT
invoke GetDC,hWin1
mov @hDc1,eax
invoke GetDC,hWin2
mov @hDc2,eax
invoke GetClientRect,hWin1,addr @stRect
invoke BitBlt,@hDc2,0,0,@stRect.right,@stRect.bottom,\
@hDc1,0,0,SRCCOPY
invoke ReleaseDC,hWin1,@hDc1
invoke ReleaseDC,hWin2,@hDc2
ret
_ProcTimer endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 窗口過程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcWinMain proc uses ebx edi esi,hWnd,uMsg,wParam,lParam
local @stPs:PAINTSTRUCT
local @stRect:RECT
local @hDc
mov eax,uMsg
mov ecx,hWnd
;********************************************************************
.if eax == WM_PAINT && ecx == hWin1
invoke BeginPaint,hWnd,addr @stPs
mov @hDc,eax
invoke GetClientRect,hWnd,addr @stRect
invoke DrawText,@hDc,addr szText,-1,\
addr @stRect,\
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd,addr @stPs
;********************************************************************
.elseif eax == WM_CLOSE
invoke PostQuitMessage,NULL
invoke DestroyWindow,hWin1
invoke DestroyWindow,hWin2
;********************************************************************
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
;********************************************************************
xor eax,eax
ret
_ProcWinMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
local @hTimer
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke RtlZeroMemory,addr @stWndClass,sizeof @stWndClass
;********************************************************************
invoke LoadCursor,0,IDC_ARROW
mov @stWndClass.hCursor,eax
push hInstance
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -