圖形設備接口GDI(Graphics Device Interface)是Win32的一個重要組成部分,其作用是允許Windows的應用程序將圖形輸出到計算機屏幕、打印機或其他輸出設備上。GDI實際上是一個函數庫,包括直線、畫圖和字體處理等數百個函數。
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程序(Terminate and Stay Resident 的縮寫,中文常翻譯為 “終止并駐留”程序),但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消息。也正是因為Windows僅僅根據是否存在“無效區域”來決定是否發送WM_PAINT消息,而不是根據程序是否執行了刷新過程,所以程序也可以不去刷新客戶區,而是簡單地用一個ValidateRect函數直接讓客戶區變得有效,以此來“欺騙”Windows已經沒有無效區域了,當Windows檢查“繪圖信息結構”的時候發現沒有了無效區域,也就不會繼續發送WM_PAINT消息了。
WM_PAINT消息的處理流程一般是:
.if eax == WM_PAINT ;eax為uMsginvoke BeginPaint,hWnd,addr stPS;刷新客戶區的代碼invoke EndPaint,hWnd,addr stPSxor eax,eaxret
讀者可以發現中間并沒有調用ValidateRect來使無效區域變得有效,這是因為BeginPaint函數和EndPaint函數隱含有這個功能,如果不是以BeginPaint/EndPaint當做消息處理代碼的頭尾的話,那么在WM_PAINT消息返回的時候就必須調用ValidateRect函數。
BeginPaint函數的第二個參數是一個繪圖信息結構的緩沖區地址,Windows會在這里返回繪圖信息結構,結構中包含了無效區域的位置和大小,繪圖信息結構的定義如下:
PAINTSTRUCT STRUCThdc 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中的代碼如下:
;DcCopy.asm
;測試設備環境的代碼,將一個窗口 DC 對應的象素拷貝到另一個窗口中
;使用 nmake 或下列命令進行編譯和鏈接:
;ml /c /coff DcCopy.asm
;Link /subsystem:windows DcCopy.obj
.386
.model flat,stdcall
option casemap:none ;include 文件定義
include C:/masm32/include/windows.inc
include C:/masm32/include/gdi32.inc
includelib C:/masm32/lib/gdi32.lib
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib ID_TIMER equ 1;數據段
.data?
hInstance dword ?
hWin1 dword ?
hWin2 dword ?
.const
szClass1 byte 'SourceWindow', 0
szClass2 byte 'DestWindow', 0
szCaption1 byte '請嘗試用別的窗口覆蓋本窗口!', 0
szCaption2 byte '本窗口圖像拷貝自另一窗口', 0
szText byte '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, @hDc1invoke 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 @stPsmov @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 pop @stWndClass.hInstance mov @stWndClass.cbSize, sizeof WNDCLASSEX mov @stWndClass.style, CS_HREDRAW or CS_VREDRAWmov @stWndClass.lpfnWndProc, offset _ProcWinMain mov @stWndClass.hbrBackground, COLOR_WINDOW + 1 mov @stWndClass.lpszClassName, offset szClass1 invoke RegisterClassEx, addr @stWndClass invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClass1, offset szCaption1, \WS_OVERLAPPEDWINDOW, \450, 100, 300, 300, \NULL, NULL, hInstance, NULL mov hWin1, eax invoke ShowWindow, hWin1, SW_SHOWNORMAL invoke UpdateWindow, hWin1 ;------------------------------------------------------mov @stWndClass.lpszClassName, offset szClass2 invoke RegisterClassEx, addr @stWndClass invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClass2, offset szCaption2, \WS_OVERLAPPEDWINDOW, \100, 100, 300, 300, \NULL, NULL, hInstance, NULL mov hWin2, eax invoke ShowWindow, hWin2, SW_SHOWNORMAL invoke UpdateWindow, hWin2 ;設置定時器invoke SetTimer, NULL, NULL, 100, addr _ProcTimer mov @hTimer, eax ;消息循環.while TRUE invoke GetMessage, addr @stMsg, NULL, 0, 0.break .if eax == 0invoke TranslateMessage, addr @stMsginvoke DispatchMessage, addr @stMsg .endw ;清除定時器invoke KillTimer, NULL, @hTimer ret
_WinMain endp main proc call _WinMain invoke ExitProcess, 0
main endp
end main
這個程序的代碼用到的大部分知識都是前面各章已經講到的,在_WinMain中,用一個同樣的窗口類建立了兩個窗口,由于兩個窗口屬于同一個窗口類,所以它們的窗口過程都是_ProcWinMain,為了關閉任何一個窗口都可以結束程序,WM_CLOSE消息中用DestroyWindow函數摧毀了兩個窗口。程序設置了一個周期為100 ms的定時器,Windows會每隔100 ms調用_ProcTimer子程序。在_ProcTimer中,將其中一個窗口的客戶區拷貝到另一個窗口的客戶區中,方法是通過GetDC獲取窗口的DC句柄,并用BitBlt函數完成拷貝工作(這些函數的具體用法在下面的內容中會講到),所以在右邊的窗口顯示了一句“Win32 Assembly, Simple and powerful!”,左邊的窗口中也會出現這句話。
這個程序能演示出什么效果來呢?圖7.2就是程序運行的結果,屏幕上的兩個并排的正方形窗口就是DcCopy程序建立的窗口,程序每100 ms將右邊窗口的客戶區拷貝到左邊的窗口客戶區中,通過左邊窗口的客戶區就可以了解右邊客戶區DC對應的究竟是什么內容。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖7.2 DcCopy程序的運行結果
現在用其他程序將右邊窗口客戶區的一部分覆蓋掉,通過左邊窗口的變化可以驚奇地發現:右邊窗口客戶區的內容并不是程序自己輸出到客戶區的那句文本,而是以客戶區為矩形區域的屏幕上我們真正看到的內容,它竟然包括其他窗口覆蓋在上面的部分。這就意味著,掃雷游戲和紙牌游戲通過自己客戶區對應的設備環境畫圖形,圖形數據竟然畫到了DcCopy窗口客戶區對應的設備環境中。
這個例子驗證了“設備環境”只是“環境”而不是“設備”,它并不存儲發給它的圖形數據,圖形數據透過它寫到了它所描述的“設備”上,每個窗口客戶區的“設備環境”對應的設備都是屏幕,但由于它們在位置上可能重疊,所以向一個窗口的客戶區寫數據相當于同時寫了下層窗口的客戶區。
為了讓當前激活的窗口在視覺上保持在最上面,下層窗口向自己客戶區寫的內容首先要經過Windows的“過濾”,只有沒有被其他窗口覆蓋掉的部分才真正被寫到了屏幕上。
讀者應該時刻提醒自己——“設備環境”只是一個環境,是設備屬性的一組定義,程序輸出的圖形數據透過“設備環境”被定向到了具體的設備上,“設備環境”本身并不存儲這些數據(在這里也可以看出Device Context中Context一詞的含義:設備環境的上面是應用程序,下面是具體設備,而它是用來“聯系上下關系”用的)。
讀者可能認為:屏幕上的窗口就像放在桌面上的一張張紙,雖然一張紙可能暫時被另一張遮住,但紙上寫的內容還是存在的,移開另一張紙就可以再次露出來。但實際情況是:桌面更像一個用粉筆寫的公告黑板,一個窗口相當于劃了一塊空間寫告示,寫另一個告示的時候要把老告示的內容擦去一部分以便寫新的內容,擦去的內容也就不存在了,如果要恢復老告示,那么必須把擦去的部分重新寫上去。
2.獲取設備環境句柄
要想對任何設備繪圖,首先必須獲取設備的“設備環境句柄”(hDC),幾乎所有的GDI函數的操作目標都是hDC,在程序中得到一個hDC有幾種方法。
最常用的方法是在WM_PAINT消息中用BeginPaint函數得到hDC,WM_PAINT消息的代碼結構一般是:
.if eax == WM_PAINT ;eax為uMsginvoke BeginPaint,hWnd,addr stPS;刷新客戶區的代碼invoke EndPaint,hWnd,addr stPSxor eax,eaxret
BeginPaint函數的返回值就是需要刷新區域的hDC。要注意的是:BeginPaint返回的hDC對應的尺寸僅是無效區域,無法用它繪畫到這個區域以外的地方去。由于窗口過程每次接收WM_PAINT消息時的無效區域可能都是不同的,所以這個hDC的值僅在WM_PAINT消息中有效,程序不應該保存它并把它用在WM_PAINT消息以外的代碼中。基于同樣的道理,BeginPaint和EndPaint函數只能用在WM_PAINT消息中,因為只有這時候才存在無效區域。
程序中常常有這種需求,就是在非WM_PAINT消息中主動繪畫客戶區,由于BeginPaint和EndPaint函數必須在WM_PAINT消息中使用,所以這時必須用另外的方法獲取hDC,可以使用以下的方法:
invoke GetDC,hWnd ;獲取hDC
;返回值是hDC
;繪圖代碼
invoke ReleaseDC,hWnd,hDc ;釋放hDC
GetDC函數返回的hDC對應窗口的整個客戶區,當使用完畢的時候,hDC必須用ReleaseDC函數釋放。對于用GetDC獲取的hDC,Windows建議使用的范圍限于單條消息內,當程序在處理某條消息的時候需要繪畫客戶區時,可以用GetDC獲取hDC,但在消息返回前,必須用ReleaseDC將它釋放掉,如果在下一條消息中需要繼續用到hDC,那么必須重新用GetDC函數獲取。
上面兩種方法獲取的hDC都是窗口的hDC,如果要操作的是其他的對象,如打印機、位圖等,就不能使用BeginPaint或GetDC函數了。當繪圖的對象是一個設備的時候,可以用Create DC函數來建立一個DC:
invoke CreateDC,lpszDriver,lpszDevice,lpszOutput,lpInitData
lpszDriver指向設備名稱,如顯示設備的設備名是DISPLAY,打印機的設備名一般為WINSPOOL,下面這幾句代碼建立的DC對應整個屏幕:
szDriver db "DISPLAY",0...invoke CreateDC,addr szDriver,NULL,NULL,NULLmov hDC,eax
當繪圖對象是位圖的時候,同樣需要一個與位圖句柄相聯系的DC,這時可以用函數CreateCompatibleDC來創建一個顯示表面僅存在于內存中的DC:
invoke CreateCompatibleDC,hDc
參數中的hDC是用來參考的DC句柄,如果指定的參數是NULL,那么建立的DC將和當前屏幕的設置兼容,為了用CreateCompatibleDC建立的DC繪制一個位圖,還需要用SelectObject函數將hDC和位圖句柄聯系起來。在這之后,通過hDC進行的繪圖操作會將像素數據更新到位圖中。
用CreateDC和CreateCompatibleDC函數建立的hDC在使用結束以后,必須用DeleteDC函數刪除,注意這里不能用ReleaseDC,這個函數是和GetDC配合用的。
用BeginPaint/EndPaint,以及GetDC獲取的hDC的使用時間不能超出本條消息,與此相比,用CreateDC,以及CreateCompatibleDC建立的hDC就沒有這個限制,可以在任何時刻建立它并且一直使用到不再需要為止。
7.1.3 色彩和坐標
1.Windows中的色彩
可以表示的顏色總數由顏色深度決定,也就是存儲每個像素所用的位數,各種顯示設備可以顯示的顏色總數可能大不相同,如果設備支持的顏色深度太淺,就會影響到圖像的質量,會讓人看起來覺得很粗糙和不自然。
一種顏色可以分解成紅、綠、藍三原色,所以可以用紅、綠、藍3個分量的組合來表示各種顏色。
當設備支持的顏色深度少于等于8位時(如8位(256色)、4位(16色)、2位(4色)或1位(2色)),總體位數太少,不足以用來表達3個顏色分量,這時系統建立一個色彩表,像素數據用來做索引在色彩表中獲取顏色值,所以低于8位的顏色稱為索引色。
只有當顏色深度大于8位的時候,像素數據中才直接包含紅、綠、藍3個分量。當顏色深度為16位的時候,紅、綠、藍各用5位表示,剩下的1位用做屬性位,實際可以表示的顏色數目為215=32768種,16位深度的彩色又稱為16位色、高彩色或增強色。當顏色深度為24位的時候,3個分量各用8位表示,實際可以表示的顏色數目為224=16777216種,24位深度的彩色又稱為24位色、16M色或真彩色。對于人的雙眼來說,超過16位的顏色就已經很難分辨了。
在Win32的編程中,統一使用32位的整數來表示一個深度為24位的顏色,在這32位中只使用低24位,每一種原色分量占用8位,其中0~7位為紅色,8~15位為綠色,16~23位為藍色。在程序中用到一種顏色常數的時候,可以如下使用:
mov eax,紅色+綠色*100h+藍色*10000h ;將顏色放入eax中
當顯示設備無法表示24位色的時候,Windows會自動用設備可以顯示的最接近的顏色來代替它,當顯示設備的顏色深度比較低的時候,可以通過函數GetNearestColor來得知一種顏色(dwColor)會被系統替換成哪種顏色:
invoke GetNearestColor,hDC,dwColor ;返回真正使用的顏色值
但是當顯示設備顏色深度太低的時候,經過Windows自動轉換的圖像可能會讓人覺得很不自然,所以在有些時候,程序員可能希望預先得知設備的顏色深度,然后根據具體情況顯示不同的圖形。
顯示設備的顏色深度可以用以下函數獲取:
invoke GetDeviceCaps,hDC,PLANES
mov ebx,dwPlanes
invoke GetDeviceCaps,hDC,BITSPIXEL
mul ebx
mov dwColorDepth,eax
第一個函數調用返回DC的色彩平面數,第二個函數調用返回每個像素的色彩位數,顏色深度最后可以通過dwPlanes乘以dwBitsPixel得到。、
2.Windows中的坐標系
要用GDI函數繪圖,就必須首先了解這些函數使用的坐標系,在默認的狀態下,Windows坐標系以左上角作為坐標原點,以右方當做X坐標的正方向,以下方當做Y坐標的正方向。坐標的數值用一個有符號的16位數來表示,范圍從-32768~32767,坐標的單位為像素,如圖7.3所示。這種坐標系定義方法的好處是:窗口中每一點的坐標不會因為窗口的大小改變而改變,試想一下,如果以數學中通常的表示方法,以左下角作為坐標原點,那么當窗口高度被用戶調整的時候,客戶區中每一點的Y坐標都會變化,在具體使用中就會有諸多不便。
? ? ? ? ? ? ? ? ? ? ?圖7.3 Windows中的默認坐標系
但是Windows也提供了其他的一些坐標映射方法供程序員使用,可以用SetMapMode函數來為一個DC設置新的坐標映射方法:
invoke SetMapMode,hDC,iMapMode
可以設置的參數包括坐標原點、坐標的邏輯單位和坐標的正方向等,參數中的iMapMode為新的映射方式,其可以選擇的取值如表7.1所示,Windows默認使用的映射方法為MM_TEXT。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 表7.1 Windows中可用的坐標映射方式
可以看到,除了默認的MM_TEXT方式外,下面5種映射方式:MM_HIENGLISH,MM_LOENGLISH,MM_HIMETRIC,MM_LOMETRIC和MM_TWIPS采用的都是原點位于左上角、Y正方向向上的映射方式,另外,它們的坐標邏輯單位是不同的。
最后的兩種映射方式MM_ISOTROPIC和MM_ANISOTROPIC提供了更靈活的選擇,設置為這兩種映射方式后,程序可以繼續調用SetViewportOrgEx,SetViewportExtEx和SetWindowExtEx函數來自由設置坐標系的原點、邏輯單位和坐標的正方向等所有參數。在其他映射方式下的時候,不能使用這3個設置函數,這時任何對它們的調用都會被忽略。
7.2 繪制圖形
有了前面的這些基礎,這一節將用一個時鐘的例子來演示如何進行簡單的繪圖,例子的源代碼可以在所附光盤的Chapter07\Clock目錄中找到,程序運行的結果如圖7.4所示。
? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖 7.8 FillRect,FrameRect和InvertRect函數的運行結果
? ? ? ? ?圖7.4 時鐘程序的運行結果
資源腳本文件Clock.rc中簡單定義了一個用做圖標的ico文件:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#include <c:/masm32/include/resource.h>//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#define ICO_MAIN 0x1000ICO_MAIN ICON "Main.ico"
源文件Clock.asm如下:
;Clock.asm-------------時鐘例子:使用 GDI 函數繪畫指針
;使用 nmake 或下列命令進行編譯和鏈接:
;ml /c /coff Clock.asm
;rc Clock.rc
;Link /subsystem:windows Clock.obj Clock.res
.386
.model flat,stdcall
option casemap:none ; include 文件定義
include c:/masm32/include/windows.inc
include c:/masm32/include/user32.inc
includelib c:/masm32/lib/user32.lib
include c:/masm32/include/kernel32.inc
includelib c:/masm32/lib/kernel32.lib
include c:/masm32/include/gdi32.inc
includelib c:/masm32/lib/gdi32.lib ;equ 等值定義
ICO_MAIN equ 1000h
ID_TIMER equ 1 ;數據段
.data?
hInstance dword ?
hWinMain dword ?
dwCenterX dword ? ;圓心X
dwCenterY dword ? ;圓心Y
dwRadius dword ? ;半徑
.const
szClassName byte 'Clock', 0;代碼段
.code
;計算時鐘的位置、大小等參數
_CalcClockParam proc local @stRect:RECT invoke GetClientRect, hWinMain, addr @stRect mov eax, @stRect.right sub eax, @stRect.left ;eax = 寬度mov ecx, @stRect.bottom sub ecx, @stRect.top ;ecx = 高度;比較客戶區寬度和高度,以小的值作為時鐘的直徑.if ecx > eax mov edx, eax ;高度 > 寬度sub ecx, eax shr ecx, 1 mov dwCenterX, 0mov dwCenterY, ecx .else mov edx, ecxsub eax,ecx shr eax, 1mov dwCenterX, eax mov dwCenterY, 0.endif shr edx, 1 mov dwRadius, edx add dwCenterX, edx add dwCenterY, edx ret
_CalcClockParam endp ;計算時鐘圓周上某個角度對應的 X 坐標
;X = 圓心X + Sin(角度) * 半徑
_dwPara180 dw 180
_CalcX proc _dwDegree, _dwRadius local @dwReturn fild dwCenterX fild _dwDegree fldpi fmul ;角度*Pifild _dwPara180 fdivp st(1), st ;角度*Pi/180fsin ;Sin(角度*Pi/180)fild _dwRadius fmul ;半徑*Sin(角度*Pi/180)fadd ;X+半徑*Sin(角度*Pi/180)fistp @dwReturn mov eax, @dwReturn ret
_CalcX endp ;計算時鐘圓周上某個角度對應的 Y 坐標
;Y = 圓心Y - Cos(角度) * 半徑
_CalcY proc _dwDegree, _dwRadius local @dwReturn fild dwCenterY fild _dwDegree fldpi fmul fild _dwPara180 fdivp st(1), st fcos fild _dwRadius fmul fsubp st(1), st fistp @dwReturn mov eax, @dwReturn ret
_CalcY endp ;按照 _dwDegreeInc 的步進角度,畫 _dwRadius 為半徑的小圓點
_DrawDot proc _hDc, _dwDegreeInc, _dwRadius local @dwNowDegree, @dwR local @dwX, @dwY mov @dwNowDegree, 0mov eax, dwRadius sub eax, 10mov @dwR, eax .while @dwNowDegree <= 360finit ;計算小圓點的圓心坐標invoke _CalcX, @dwNowDegree, @dwR mov @dwX, eax invoke _CalcY, @dwNowDegree, @dwR mov @dwY, eax ;畫點mov eax, @dwX mov ebx, eax mov ecx, @dwY mov edx, ecx sub eax, _dwRadius add ebx, _dwRadius sub ecx, _dwRadius add edx, _dwRadius invoke Ellipse, _hDc, eax, ecx, ebx, edx mov eax, _dwDegreeInc add @dwNowDegree, eax .endw ret
_DrawDot endp ;畫 _dwDegree 角度的線條,半徑=時鐘半徑-參數_dwRadiusAdjust
_DrawLine proc _hDc, _dwDegree, _dwRadiusAdjust local @dwR local @dwX1, @dwY1, @dwX2, @dwY2 mov eax, dwRadius sub eax, _dwRadiusAdjust mov @dwR, eax ;計算線條兩端的坐標invoke _CalcX, _dwDegree, @dwR mov @dwX1, eax invoke _CalcY, _dwDegree, @dwR mov @dwY1, eax add _dwDegree, 180invoke _CalcX, _dwDegree, 10mov @dwX2, eax invoke _CalcY, _dwDegree, 10mov @dwY2, eax invoke MoveToEx, _hDc, @dwX1, @dwY1, NULL invoke LineTo, _hDc, @dwX2, @dwY2 ret
_DrawLine endp _ShowTime proc _hWnd, _hDClocal @stTime:SYSTEMTIME pushad invoke GetLocalTime, addr @stTime invoke _CalcClockParam ;畫時鐘圓周上的點invoke GetStockObject, BLACK_BRUSH invoke SelectObject, _hDC, eax invoke _DrawDot, _hDC, 360/12, 3 ;畫12個大圓點invoke _DrawDot, _hDC, 360/60, 1 ;畫60個小圓點;畫時鐘指針invoke CreatePen, PS_SOLID, 1, 0invoke SelectObject, _hDC, eax invoke DeleteObject, eax movzx eax, @stTime.wSecond mov ecx, 360/60mul ecx ;秒針度數 = 秒 * 360/60invoke _DrawLine, _hDC, eax, 15 ;----------------------------------------invoke CreatePen, PS_SOLID, 2, 0invoke SelectObject, _hDC, eax invoke DeleteObject, eax movzx eax, @stTime.wMinute mov ecx, 360/60mul ecx ;分針度數 = 分 * 360/60invoke _DrawLine, _hDC, eax, 20;-----------------------------------------invoke CreatePen, PS_SOLID, 3, 0invoke SelectObject, _hDC, eax invoke DeleteObject, eax movzx eax, @stTime.wHour .if eax >= 12 sub eax, 12 .endif mov ecx, 360/12 mul ecx movzx ecx, @stTime.wMinute shr ecx, 1 add eax, ecx invoke _DrawLine, _hDC, eax, 30;------------------------------------------invoke GetStockObject, NULL_PEN invoke SelectObject, _hDC, eax invoke DeleteObject, eax popad ret
_ShowTime endp _ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam local @stPS:PAINTSTRUCT mov eax, uMsg .if eax == WM_TIMER invoke InvalidateRect, hWnd, NULL, TRUE .elseif eax == WM_PAINT invoke BeginPaint, hWnd, addr @stPS invoke _ShowTime, hWnd, eax invoke EndPaint, hWnd, addr @stPS .elseif eax == WM_CREATE invoke SetTimer, hWnd, ID_TIMER, 1000, NULL .elseif eax == WM_CLOSE invoke KillTimer, hWnd, ID_TIMER invoke DestroyWindow, hWinMain invoke PostQuitMessage, NULL .else invoke DefWindowProc, hWnd, uMsg, wParam, lParam ret .endif xor eax, eax ret
_ProcWinMain endp _WinMain proc local @stWndClass:WNDCLASSEX local @stMsg:MSG invoke GetModuleHandle, NULL mov hInstance, eax ;注冊窗口類invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass invoke LoadIcon, hInstance, ICO_MAIN mov @stWndClass.hIcon, eax mov @stWndClass.hIconSm, eax invoke LoadCursor, 0, IDC_ARROW mov @stWndClass.hCursor, eax push hInstance pop @stWndClass.hInstance mov @stWndClass.cbSize, sizeof WNDCLASSEX mov @stWndClass.style, CS_HREDRAW or CS_VREDRAW mov @stWndClass.lpfnWndProc, offset _ProcWinMain mov @stWndClass.hbrBackground, COLOR_WINDOW + 1 mov @stWndClass.lpszClassName, offset szClassName invoke RegisterClassEx, addr @stWndClass ;建立并顯示窗口invoke CreateWindowEx, WS_EX_CLIENTEDGE, \offset szClassName, offset szClassName, \WS_OVERLAPPEDWINDOW, \100, 100, 250, 270, \NULL, NULL, hInstance, NULL mov hWinMain, eax invoke ShowWindow, hWinMain, SW_SHOWNORMAL invoke UpdateWindow, hWinMain ;消息循環.while TRUE invoke GetMessage, addr @stMsg, NULL, 0, 0.break .if eax == 0invoke TranslateMessage, addr @stMsg invoke DispatchMessage, addr @stMsg .endw ret
_WinMain endp main proc call _WinMaininvoke ExitProcess, 0
main endp
end main
下面簡單分析一下程序的結構。
程序首先用標準的方法建立了一個窗口,在窗口的初始化消息WM_CREATE中用SetTimer建立了一個周期為1秒的定時器,用來在窗口的客戶區中繪畫時鐘。這個定時器在WM_CLOSE消息中用KillTimer函數撤銷。在定時器消息中,程序用InvalidateRect函數讓整個客戶區失效,相當于讓Windows在消息循環中放入一條WM_PAINT消息,整個時鐘的繪畫在WM_PAINT消息中完成。
在WM_PAINT消息中程序用標準的方法調用BeginPaint函數獲取窗口客戶區的hDC,以便在上面繪畫時鐘,在消息返回的時候用EndPaint函數釋放hDC,兩個函數的中間,程序把hDC傳給_ShowTime子程序,由這個子程序完成整個繪畫工作。
在第6章中已經講到:因為獲取系統時間不能依賴于WM_TIMER消息的計數,所以在_ShowTime子程序的開始,程序調用GetLocalTime來獲取當前的系統時間,并根據這個時間來繪畫時鐘的時、分、秒指針。由于繪畫的過程很快,所以整個程序的結構使用前面圖7.1中所示的A結構,也就是每次有WM_PAINT消息的時候,程序總是重畫整個客戶區,所以讀者在速度比較慢的計算機上運行這個程序時,可能會看到有個閃爍的過程,因為程序每次總是先將整個客戶區清除成背景色(InvalidateRect函數最后的TRUE參數要求Windows在發送WM_PAINT消息前清除客戶區),然后繪畫四周的刻度,最后畫上指針。繪畫刻度是由_DrawDot子程序完成的,繪畫指針是由_DrawLine子程序完成的。
GetLocalTime后面的_CalcClockParam子程序根據客戶區的尺寸計算時鐘尺寸參數,它比較客戶區高度和寬度,以其中的較小值用做時鐘的直徑,計算得到的圓心最后存放于全局變量dwCenterX和dwCenterY中,計算得到的半徑存放于dwRadius中。
程序中有兩個公用的子程序:_CalcX和_CalcY,它們用來計算角度對應的坐標,如圖7.5所示,時鐘0點時間是從垂直方向開始的,以時間值為角度配合Windows的默認坐標系,對應某個時間點(x,y),x應該是圓心x加上角度的正弦值乘以半徑,y應該是圓心y減去角度的余弦值乘以半徑。_CalcX和_CalcY輸入的參數是角度_dwDegree和半徑_dwRadius。子程序中使用80×86的協處理器指令,首先將角度值換算成弧度值——乘以π并除以180,然后用上面分析的公式進行浮點計算并將結果返回。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖7.5 時鐘程序的坐標計算
在接下來的內容中,先介紹一些繪畫操作的背景知識。
7.2.1 畫筆和畫刷
GDI中的繪畫函數有3大類:畫點、畫線和畫填充區域。使用過Photoshop等圖形軟件的讀者一定知道,在畫線之前需要選擇一種畫筆,這樣畫出來的線條都是基于這種畫筆的;同樣,填充一個區域之前需要選擇一種畫刷,這樣整個填充區域將重復使用這個畫刷的顏色或圖案。
GDI中也有同樣的畫筆和畫刷的概念,畫筆、畫刷,以及其他一些GDI中要使用的東西,包括字體、區域、路徑、圖案和位圖統稱GDI中的“對象”,通過SelectObject函數可以指定一個DC當前使用的對象對應哪個對象句柄,稱為“當前對象”,當設置了一個當前對象的時候,以后和這種對象相關的函數都將使用當前對象,直到再次用SelectObject選擇新的對象為止。比如,當選擇了新的畫筆后,以后所有畫線函數畫出來的線條樣式都是由這個畫筆決定的,而選擇了新的畫刷后,則所有填充函數填充的樣式都將使用這個畫刷。
SelectObject函數的用法是:
invoke SelectObject,hDC,hGDIObject
mov hOldObject,eax
其中參數hGDIObject就是對象的句柄,它可以是位圖句柄、畫筆句柄、畫刷句柄、字體句柄或區域句柄,函數會根據句柄的種類自動替換原有的對象,并將原來使用的對象句柄返回(當對象類型是區域的時候除外),如果DC中原來沒有設置當前對象,那么函數的返回值是GDI_ERROR或NULL。
1.使用預定義的畫筆和畫刷
Windows預定義了一些常用的畫筆和畫刷,在程序中可以用GetStockObject來獲取它們的句柄,Stock的中文含義是“常備的、庫存的”,所以這個函數字面上的意思就是“獲取常用的對象”,注意并沒有類似于GetStockPen或GetStockBrush之類的函數,所有獲取常用對象的操作統一使用GetStockObject函數。
GetStockObject函數的用法是:
invoke GetStockObject,fnObject
mov hObject,eax
fnObject參數是預定義的對象類型,可以是表7.2所示的取值。
NULL_PEN和NULL_BRUSH是空畫筆和空畫刷,之所以有空的對象,是因為繪制填充區域的函數同時用到了畫筆和畫刷——繪制的外框使用當前畫筆,中間用當前畫刷填充。使用空對象可以有機會畫出沒有邊框線只有填充圖案,或者只有邊框線而不填充的區域來。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 表7.2 GDI中的常用對象
用GetStockObject函數得到對象句柄以后,就可以用SelectObject函數將對象句柄設置到DC中了。例子文件Clock.asm中的_ShowTime函數中用GetStockObject函數獲取了一個BLACK_BRUSH畫刷,用來繪畫時鐘的刻度。
2.使用自定義的畫筆和畫刷
使用GetStockObject函數得到的對象是最“簡陋”的,如畫筆只能是白色或黑色的寬度為1像素的實線,畫刷只能是白色、黑色和有限的幾種灰色色塊。要想使用彩色的、多種多樣風格的畫筆和畫刷,就必須用自定義的方法。
創建自定義的畫筆可以使用CreatePen,ExtCreatePen或CreatePenIndirect函數,CreatePen函數的使用方法是:
invoke CreatePen,fnPenStyle,dwWidth,dwColor
mov hPen,eax
fnPenStyle參數是畫筆風格,它可以是兩種實線風格PS_SOLID,PS_INSIDEFRAME或空畫筆PS_NULL,以及幾種虛線風格PS_DASH,PS_DOT,PS_DASHDOT或PS_DASHDOTDOT。它們對應的線條如圖7.6所示,圖中從上到下分別是PS_SOLID,PS_DASH,PS_DOT,PS_DASHDOT,PS_DASHDOTDOT和PS_INSIDEFRAME風格的線條,幾種虛線的風格很好記,只要記得“點”就是DOT,“劃”就 是DASH就 可 以了,如PS_DASHDOTDOT風格就是由“劃、點、點”重復組成的虛線。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖7.6 幾種自定義畫筆風格
PS_SOLID和PS_INSIDEFRAME風格的畫筆使用的都是實線線條,它們之間的區別在于當畫筆的寬度大于1像素并使用區域繪畫函數時,PS_SOLID線條會居中畫于邊線上,而PS_INSIDEFRAME線條會全部畫在邊線里面,它的寬度會向區域的內部擴展,所以它的名稱是INSIDEFRAME。
CreatePen函數的dwWidth參數定義了畫筆的寬度,單位是DC坐標映射方法中定義的邏輯單位,如果這個參數使用NULL,那么函數會使用1像素的寬度。寬度參數會影響到風格參數:當寬度大于1的時候,畫筆風格不能使用虛線,這時候即使指定了虛線風格,函數也會自動使用PS_SOLID風格。dwColor參數指定了畫筆的顏色。
例子源代碼的_ShowTime子程序中用不同寬度的線條來繪畫時、分、秒指針,繪畫前就使用CreatePen函數創建了不同寬度的畫筆。
如果需要創建更復雜的畫筆,可以使用ExtCreatePen函數。這個函數除了有CreatePen的全部功能外,還可以讓用戶自己定義線條的樣子,這樣可以不必限制于上面的點點劃劃了。函數的用法讀者可以參考函數手冊。
創建自定義畫刷可以使用的函數有:CreateSolidBrush,CreateHatchBrush,CreatePatternBrush和CreateBrushIndirect。
CreateSolidBrush創建單色的畫刷:
invoke CreateSolidBrush,dwColor
mov hBrush,eax
要輸入的唯一參數是畫刷的顏色。而CreateHatchBrush可以創建幾種預定義圖案的畫刷:
invoke CreateHatchBrush,iHatchStyle,dwColor
mov hBrush,eax
dwColor指定了圖案線條的顏色,iHatchStyle定義了不同的圖案線條,這些圖案線條實際上是以8×8的位圖重復鋪開組成的,iHatchStyle的定義值可以是HS_BDIAGONAL,HS_CROSS,HS_DIAGCROSS,HS_FDIAGONAL,HS_HORIZONTAL和HS_VERTICAL,這6種圖案的花樣在圖7.7中從左到右排列顯示。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖7.7 CreateHatchBrush中的畫刷圖案
如果這些簡單的圖案不能滿足使用要求,CreatePatternBrush是個很好的選擇:
invoke CreatePatternBrush,hBitmap
mov hBrush,eax
這個函數用一個位圖當做畫刷的圖案,當要繪畫的區域大于位圖尺寸的時候,位圖被重復鋪開,就像HTML文件中的背景圖案一樣。讀者可以嘗試一下用一幅做網頁文件背景的位圖創建一個位圖畫刷,并且在RegisterClassEx時在WNDCLASSEX結構中的hbrBackground字段中使用這個畫刷,這樣創建出來的窗口背景會和網頁背景一樣華麗!演示代碼請參考所附光盤的Chapter07\TestObject目錄中的源代碼。
對于自定義的畫筆和畫刷,還有其他自定義的對象,在不再需要的時候必須使用DeleteObject函數刪除,但是要注意:當對象還是一個DC的當前對象的時候不要將它刪除,在刪除前應該確定DC中已經選入了其他的對象。與之相反,用GetStockObject獲取的預定義對象使用后不需要刪除,但是對它們調用DeleteObject也沒有關系,因為它們不會被真正刪除。由于SelectObject返回值就是DC原來使用的對象句柄,所以刪除對象的一個好時機就是當SelectObject返回的時候,如例子程序的_ShowTime子程序中用的:
invoke CreatePen,PS_SOLID,2,0
invoke SelectObject,_hDC,eax
invoke DeleteObject,eax
SelectObject將CreatePen創建的畫筆句柄選入DC,返回值eax就是以前使用的畫筆句柄,這個句柄不再使用了,所以可以在下面用DeleteObject直接刪除,而這次建立的畫筆可以在下次執行SelectObject后用同樣的方法刪除。
7.2.2 繪制像素點
在DC上繪制像素點是繪圖最基本的操作,使用的方法是:
invoke SetPixel,hDC,dwX,dwY,dwColor
SetPixel函數在hDC的dwX、dwY位置以dwColor為顏色畫上一個像素點,如果需要獲取hDC中某個像素點當前的顏色值,那么可以使用GetPixel函數:
invoke GetPixel,hDC,dwX,dwY
mov dwColor,eax
雖然繪畫像素是最基本的繪圖操作方法,但是在程序中一般很少使用SetPixel函數,因為它的開銷太大了,只適合用在需要少量繪畫像素的地方,如果要繪畫一個線條或者整個區域,那么最好使用畫線函數或者填充函數,因為這些函數是在驅動程序級別上完成的,所有的硬件加速功能都可以用上。
圖形處理前最基本的步驟是獲取像素,但也不應該用GetPixel函數來獲取一大塊的像素數據,理由是同樣的。如果要分析整個區域的像素數據,最好的辦法就是用GetDIBits函數將全部數據拷貝到內存中再進行處理。
7.2.3 繪制圖形
GDI的圖形繪制函數主要有繪制線條和填充區域兩大類。繪制線條的函數以當前畫筆繪制線條;繪制填充區域的函數以當前畫筆繪制邊線,并以當前畫刷填充中間的區域。
1.繪制線條
繪制線條的函數有畫直線的LineTo,畫多條直線的Polyline和PolylineTo,畫貝塞兒曲線的PolyBezier和PolyBezierTo,畫弧線的Arc和ArcTo。
DC的數據結構中有一個“當前點”,LineTo函數就是從當前點畫一條直線到參數中指定的點,并把參數中指定的點設置為新的當前點。畫線函數中所有以To結尾的函數都是從當前點開始繪制的,如LineTo,PolylineTo,PolyBezierTo和ArcTo,由于這些函數在繪畫結束后會把繪制的最后一點設置為新的當前點,所以在使用這些函數的時候要考慮到當前點也是參與繪制的坐標一部分。而其余的Polyline,PolyBezier和Arc函數則和當前點沒有關系,也不會影響當前點的位置。
如果要設置當前點的位置,可以使用MoveToEx函數:
invoke MoveToEx,hDC,dwX,dwY,lpPoint
dwX和dwY指出了新的當前點的坐標,lpPoint指向一個空的POINT結構,用來返回原來的當前點位置,如果不需要的話,這個參數可以使用NULL。
另一個函數也可以得到當前點的坐標:
invoke GetCurrentPositionEx,hDC,lpPoint
同樣,lpPoint指向一個用來返回當前點坐標的POINT結構地址。
如果要繪制一條直線,必須配合使用MoveToEx和LineTo函數,首先由MoveToEx函數設置一個當前點當做起始坐標,然后用LineTo繪畫到結束坐標,如Clock.asm中的_DrawLine子程序中就是這樣繪制時鐘指針的:
invoke MoveToEx,_hDC,@dwX1,@dwY1,NULL
invoke LineTo,_hDC,@dwX2,@dwY2
這兩句代碼繪畫一條從@dwX1,@dwY1到@dwX2,@dwY2的直線。
如果要繪制是相連的多條直線,可以使用Polyline或PolylineTo函數:
invoke PolylineTo,hDC,lpPoint,cPoints
invoke Polyline,hDC,lpPoint,cPoints
lpPoint指向一個包含一系列POINT結構的緩沖區,由于POINT結構只有X和Y兩個字段,所以緩沖區中的數據實際上是x1,y1,x2,y2,x3,y3,…,cPoints參數指出了點的數目,注意:PolylineTo畫出的直線是從當前點坐標(x,y)開始,然后到(x1,y1),再到(x2,y2),…,而Polyline函數畫出的直線是從(x1,y1)開始的,對于這個函數,如果cPoints參數指定了n個點,那么直線的數量實際上是n?1。當繪制的相連直線很多的時候,用Polyline或PolylineTo比多次使用LineTo的速度要快很多,就像用填充函數比多次使用SetPixel要快一樣。
表7.3舉例說明了這些畫線函數的功能,表中的(x1,y1)或(x2,y2)等表示點1或點2的坐標,(xc,yc)表示當前點的坐標,當前點在圖中用c表示。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?表7.3 畫線函數的功能
對于Arc和ArcTo函數,參數(x1,y1)和(x2,y2)定義了一個矩形的對角點,然后在和這個矩形相切的橢圓上面,以橢圓的中心(也就是矩形的中心)畫兩條假想的直線到(x3,y3)和(x4,y4),這兩條直線和橢圓相交的點就是圓弧的起始點和結束點。在默認情況下,圓弧由起始點沿著橢圓從逆時針方向畫到結束點。不過繪畫方向可以由SetArcDirection函數重新規定:
invoke SetArcDirection,hDC,AD_COUNTERCLOCKWISE ;逆時針方向
讀者一定注意到了一個問題:在畫線的時候,如果當前的畫筆是虛線的話,虛線的不連續部分實際上是由白色組成的,當虛線畫在非白色的背景上的時候這一點顯得特別明顯。實際上,可以選擇這些不連續部分的顏色,用以下的語句就可以做到這一點:
invoke SetArcDirection,hDC,AD_CLOCKWISE ;順時針方向
invoke SetBkColor, hDC, dwColor
調用后不連續的部分就將用dwColor指定的顏色繪畫。
但是改變顏色也并不是唯一的選擇,GDI允許這部分并不繪畫任何顏色,也就是可以是“透明”的,用下面的調用可以將模式在透明和非透明之間切換:
invoke SetBkMode,hDC,OPAQUE ;非透明模式
invoke SetBkMode,hDC,TRANSPARENT ;透明模式
兩種模式以及繪畫顏色不單影響虛線的空隙部分,同樣也影響CreateHatchBrush函數創建的畫刷,因為這種畫刷使用幾種由線條構成的圖案,當用這種畫刷填充一個區域的時候,線條圖案的空隙部分同樣受SetBkColor函數和SetBkMode函數的影響。
2.繪制邊界框和填充區域
繪制邊界框和填充區域其實是同一件事情。如果當前畫筆是NULL_PEN的話,畫出來的是沒有邊線的填充區域;如果當前畫刷是NULL_BRUSH的話,那么只有邊線而不會填充;如果當前畫刷既不是NULL_PEN也不是NULL_BRUSH,那么畫出來的圖形既有邊線也是填充的。
繪制區域的函數有畫矩形的Rectangle,畫圓角矩形的RoundRect,畫多邊形的Polygon,畫弦的Chord,畫圓餅的Pie和畫橢圓的Ellipse。這些函數的使用效果如圖7.4所示。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 表7.4 填充函數的功能
在這些函數中,Polygon的調用方式和Polyline很相似,只不過如果最后一點和第一點不同的話,函數自動再畫一條和起始點相連的直線將整個區域閉合起來。用Polygon繪畫的多邊形中各條直線可能相交,Windows允許程序自行選擇填充的模式,可以是表7.4中Polygon一欄中的上面那個圖例(填充全部區域),也可以是下面那個圖例(間隔填充區域)。可以用下面的函數切換填充的模式:
invoke SetPolyFillMode,_hDC,ALTERNATE? ;間隔填充
invoke SetPolyFillMode,_hDC,WINDING? ? ? ?;填充全部區域
Chord函數和Pie函數的參數使用和畫弧線的Arc函數相似,只不過Chord函數將弧線的兩端直接相連,形成一個“弦”,而Pie函數將兩端和圓心相連,形成一個“圓餅”,這兩個函數繪畫的方向同樣受SetArcDirection函數設置的影響。
在例子Clock.asm中,程序在_DrawDot子程序中用Ellipse函數繪畫時鐘的刻度,讀者也可以將程序改動一下,嘗試著用Polygon畫五角星來當做時鐘的刻度。
除了這些函數,還有3個和矩形有關的填充函數:FillRect,FrameRect和InvertRect,這些函數不使用當前畫筆畫邊線,也不用當前畫刷填充,其中FillRect函數用指定的畫刷hBrush填充一個lpRect指定的矩形區域,lpRect指向一個RECT結構;FrameRect函數用指定畫刷hBrush繪畫邊線;InvertRect函數將lpRect指定的矩形區域中的顏色值取反。用法如下:
invoke FillRect,hDC,lpRect,hBrush
invoke FrameRect,hDC,lpRect,hBrush
invoke InvertRect,hDC,lpRect
假設背景為白色,而參數中hBrush指定的畫刷為灰色畫刷,那么上述3個函數的運行結果如圖7.8所示。
圖中左邊是FillRect的運行結果,可以看到圖案沒有邊線;中間是FrameRect的運行結果,它用灰色畫刷繪畫邊線,得到了一個灰色的矩形邊框;右邊是InvertRect的運行結果,由于底色是白色的,白色取反得到的是黑色,所以整個矩形都變成了黑色。
7.2.4 繪圖模式
在前面的內容中我們都是嘗試在DC上用繪圖函數畫出需要的圖形,對于DC上被繪畫上去的像素來說,相當于用畫筆(或畫刷)的像素點代替了原來的像素點,但Windows也可以用畫筆的像素點和原來的像素點進行計算以后的值當做新的像素點,這個計算的過程就叫做光柵運算,光柵運算的方法用“光柵運算符”來定義——英文縮寫是ROP(Raster Operation),ROP碼是一些取反、異或、拷貝、或及與等位運算方法的組合。
對于繪圖函數,Windows定義了16種ROP碼,如表7.5所示。表中的“像素”指DC中要繪畫位置原來的像素值,畫筆指要畫上去的顏色值,當然ROP碼影響的并不單是畫筆畫出的線條,同樣影響用畫刷填充的區域,所以讀者不要被表中的“PEN”搞混淆了,這個“PEN”指的是“Pen and Brush”!
ROP為一些應用提供了方便,比如需要在背景上拖動一個圖形,如果用普通的繪畫方法,那么在繪畫前必須保存原來背景的數據,在圖形拖動后再恢復,然后在新的位置再保存、再繪畫,如此重復。但如果使用R2_XORPEN或R2_NOTXORPEN的繪畫模式,因為xor操作兩遍就是原來的數值,所以無須保存原來的像素,在相同的地方再繪畫一遍就相當于恢復原來的圖形。而用R2_BLACK和R2_WHITE就相當于不管畫筆和畫刷是什么顏色,畫出來的全部是黑色或白色。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?表7.5 繪圖模式中可以使用的ROP碼
對于一個DC來說,默認的繪圖模式是R2_COPYPEN,就是用畫筆或畫刷的顏色替換掉原來像素的顏色。如果要設置新的繪圖模式,可以使用SetROP2函數。如下面的語句將繪圖模式設置為R2_NOTCOPYPEN模式,這樣以后的所有的繪圖函數就將以畫筆或畫刷取反后的顏色繪圖了:
invoke SetROP2,hDC,R2_NOTCOPYPEN
如果要獲取當前的繪圖模式,可以使用GetROP2函數,函數返回當前的模式:
invoke GetROP2,hDC