Ch 19 更多設備內容的知識(2)

小木偶在這一章堙A利用 Windows 提供的 GDI 服務副程式,粗淺地介紹動畫的技巧,並且示範一個程式,BALL.ASM,這個程式視窗埵酗@顆球不停地做等速度運動,直到碰觸到視窗邊框時,會以彈性碰撞的方式反彈。此外,這個視窗沒有選單、放大盒、縮小盒,除非重新組譯,使用者也無法改變視窗大小,其執行畫面如下:

ball.exe 執行畫面

小木偶把 ball.asm 以及所需位元圖、資源描述檔都壓縮成一個檔案,ball.rar,講它解開至同一子目錄,然後開啟『命令提示字元』,切換到解壓縮的子目錄,輸入下面指令就可以得到 ball.exe 可執行檔。

E:\HomePage\SOURCE>cd ball [Enter]

E:\HomePage\SOURCE\BALL>rc ball.rc [Enter]

E:\HomePage\SOURCE\BALL>ml ball.asm /link ball.res [Enter]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: ball.asm
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS
"ball.obj"
"/OUT:ball.exe"
"ball.res"

E:\HomePage\SOURCE\BALL>

原理

解決閃爍的問題

在前一章堙A小木偶也展示了動畫,時針、分針、秒針的走動,當然也是動畫。不過在那個例子堙A所移動的物體很小,所以小木偶是讓 CLOCK 視窗每次收到 WM_PAINT 訊息時,把背景圖選入到這個記憶體設備內容,再利用 BitBlt API 把這個記憶體設備內容的背景圖拷貝到工作區上面,最後再畫上時針、分針、秒針。因為在工作區的設備內容上做了好幾個動作:把記憶體設備內容的背景圖拷貝到工作區、在工作區畫出時針、分針、秒針等四個動作,所以在慢速的電腦上,或許會看到閃爍的現象,但因移動物體小或工作區小,這種現象並不明顯。假如要設計電動玩具,就會需要大的工作區,也需要移動大的物體,那麼以這種方式所做出來的程式,閃爍的問題就會很嚴重了。

要解決這個問題,可以用 CreateCompatibleDC 先準備好一個緩衝的設備內容,這個緩衝區設備內容是建立在記憶體堙A並不直接輸出於螢幕上的工作區。我們把所有的背景、動畫操作都先在這個緩衝區的設備內容埵w排好,然後把緩衝區的設備內容堛犒炷蚺@次拷貝到工作區的設備內容上,之後就不再對工作區的設備內容進行繪製圖片。這樣在使用者的眼中看來,工作區只有一次的變化,就不會有閃爍的情況了。

PatBlt API 畫出背景圖

BALL 的背景是用一個較小的位元圖,以類似瓷磚方式拼湊而成一個較大的背景圖案,這種較小的位元圖也可以稱為圖樣 ( pattern )。事實上許多網頁的背景圖,甚至有時候 Windows 的桌面有時也是以這種方式拼湊的,這種方式的好處是所需背景圖不需要很大,就可以讓它填充整個畫面。當然啦!這種背景圖是會一直重複的,比較單調是其缺點。BALL 程式是利用下圖左邊的 pattern.bmp 當做圖樣,拼湊成背景圖。pattern.bmp 是長寬各 300 圖素 ( pixel ) 的位元圖,而 BALL 工作區是長 300 圖素、寬 600 圖素,所以重複一次,請參考下圖及上圖:

所需 BMP 圖

要使圖樣拼揍成整個背景圖,可以使用 PatBlt API,意義為『Pattern Block Transfer』,其原型是:

BOOL PatBlt(
    HDC hdc,    // handle to device context 
    int nXLeft, // x-coord. of upper-left corner of rect. to be filled 
    int nYLeft, // y-coord. of upper-left corner of rect. to be filled 
    int nWidth, // width of rectangle to be filled 
    int nHeight,// height of rectangle to be filled 
    DWORD dwRop // raster operation code 
   );

PatBlt 會把 hdc 目前的筆刷作為圖樣,從座標 ( nXLeft,nYLeft ) 開始填充,填充區域的寬度與高度分別是 nWidth、nHeight。填充方式可以由 dwRop 指定,dwRop 只可以是下面五種情形:

dwRop意  義
PATCOPY填充筆刷
PATINVERT筆刷 XOR 目地
DSTINVERTNOT 目的
BLACKNESS全部填充黑色
WHITENESS全部填充白色

由此可知,假如要用位元圖為圖樣,填充背景,可以用 LoadBitmap 由資源載入位元圖,然後用 CreatePatternBrush 把這個位元圖變成筆刷,最後還要以 SelectObject 把這個筆刷選入到設備內容才可以達到目的。此處還須注意一個觀念,每當我們用 GDI 服務程式繪圖 ( 如畫線、畫矩形等 ) 或其他操作 ( 如選擇 GDI 物件時 ),其對象都是設備內容而非位元圖。所以要把 GDI 服務程式作用於位元圖時,必須先用 CreateCompatibleDC 建立一個相容的設備內容,再把位元圖選入這個相容的設備內容,之後的 GDI 服務程式才會作用在位元圖上。一般而言,要操作一個位元圖,就得建立一個設備內容。位元圖大多存放於資源堙A需要時由資源載入即可,這時的位元圖是已經初始化了的;但是也有未初始化的位元圖,例如要把圖樣填充到緩衝的設備內容當做背景圖時,這個背景圖就是未初始化的,因為背景圖都尚未填充完成,當然還沒初始化,我們也就無法用 SelectObject 選入某一個位元圖。這時候可以用 CreateCompatibleBitmap 建立未初始化的位元圖,然後再用 SelectObject 選入到緩衝的設備內容,最後再以 PatBlt 畫出背景圖,才算是完成初始化,這時就可以顯出背景圖案了。

CreateCompatibleBitmap API

CreateCompatibleBitmap 的原型是

HBITMAP CreateCompatibleBitmap(
    HDC hdc,    // handle to device context 
    int nWidth, // width of bitmap, in pixels  
    int nHeight // height of bitmap, in pixels  
   );

CreateCompatibleBitmap 建立與 hdc 相容的位元圖,所謂相容的位元圖意思是指這個位元圖顏色多寡與 hdc 一樣,並不是把位元圖選入到 hdc 堙CnWidth、nHeight 是位元圖的寬度與高度,以圖素為單位。如果成功建立位元圖,則傳回位元圖代碼;否則傳回 NULL。

CreatePatternBrush API

CreatePatternBrush 是把特定的位元圖製成筆刷,其語法是

HBRUSH CreatePatternBrush(
    HBITMAP hbmp    // handle to bitmap 
   );

hbmp 是想要製作成筆刷的位元圖代碼。如果成功,CreatePatternBrush 會傳回筆刷代碼;否則傳回 NULL。

講了這麼多,現在我們可以自資源堥出位元圖作為圖樣,然後把它填充在緩衝設備內容堙A最後再傳到工作區的設備內容堙A其過程大約是像底下這樣:

        INVOKE  LoadBitmap,hInstance,OFFSET szBkGround      ;載入 pattern.bmp 位元圖
        mov     hBmpBk,eax                                  ;記錄位元圖代碼於 hBmpBk 變數
        INVOKE  CreatePatternBrush,hBmpBk                   ;把 pattern.bmp 製作成筆刷
        mov     hBrushBk,eax                                ;把這個筆刷代碼記錄在 hBrushBk 變數
        ... ... ... ...                                     ;其他程式片段
        INVOKE  GetDC,hWnd                                  ;取得視窗工作區的設備內容
        mov     hdc,eax                                     ;記錄於 hdc 變數
        INVOKE  CreateCompatibleDC,hdc                      ;建立緩衝區的記憶體設備內容
        mov     hdcbuffer,eax                               ;緩衝區的記憶體設備內容代碼存於 hdcbuffer 變數
        INVOKE  CreateCompatibleBitmap,hdc,xClient,yClient  ;以 hdc 為本,建立未初始化的位元圖
        INVOKE  SelectObject,hdcbuffer,eax                  ;把位元圖選入緩衝區的記憶體設備內容
        INVOKE  SelectObject,hdcbuffer,hBrushBk             ;選入筆刷
        INVOKE  PatBlt,hdcbuffer,0,0,xClient,yClient,PATCOPY;填充筆刷

到此,背景圖就已經全部完成了。

動畫原理

當球移動時,程式必須先把舊的球體圖案擦掉變成背景,再把球體圖案畫在新的位置上,每隔一次極短的時間都重複這個過程做,那麼在使用者看來,就好像球體會移動一樣。小木偶在處理 WM_CREATE 訊息時,建立了一個計時器:

                INVOKE  SetTimer,hWnd,TimerID,time,NULL

這個計時器每隔 time 毫秒發出一個 WM_TIMER 訊息,小木偶在此訊息中計算球體圖案新的左上角座標,並記錄於 ( x,y )。

在把舊的球體圖案擦掉的過程中,我們可以把整個工作區都重新繪製,但是這樣做事比較耗費資源。另一種做法是,只在舊的球體圖案的那一部份位置,以背景圖相對應的位置畫上去。在 ball 程式堙A小木偶採用後種做法。所以小木偶準備了兩個設備內容:hdcbk 與 hdcbuffer,這兩個設備內容都是記憶體設備內容,兩者也都存有背景圖,但是前者塗上背景圖後,就不會再變動;而後者則會畫上運動的球體,每一次移動球體要在舊的球體位置擦掉球體圖案時,程式就從 hdcbk 相對應舊的球體位置取出背景圖覆蓋上。程式

                INVOKE  BitBlt,hdcbuffer,x0,y0,BallSize,BallSize,hdcbk,x0,y0,SRCCOPY

就是在執行這樁工作,x0、y0 是球體圖案的舊位置的左上角座標。接下來就是把新的球體位置,( x,y ),畫上球體圖案。球體圖案的樣子如圖一中間的 ball.bmp,它是長寬各 80 圖素的位元圖。假如只用單純的以 SRCCOPY 為操作參數呼叫 BitBlt 把球體拷貝到 hdcbuffer,那麼球體周圍會有黑框;假如想把黑框去掉,得和背景圖進行 OR 運算 ( 因為黑色的顏色為 0,與背景圖作 OR 運算後,仍會得到背景圖 ),查 MSDN,得知應用 SRCPAINT 為操作參數呼叫 BitBlt。但如此一來,球體的部份也會與背景圖作 OR 運算,但是假如能先讓背景圖的球體部份變成黑色又不影響到球體周圍的部份,就可以解決這個問題了。一般的做法是再建立一張圖片,這張圖片一般稱為遮罩圖,也就是圖一最右邊的 mask.bmp,圖案的主要部份,也就是球體,為黑色,而背景為白色,先讓遮罩圖與背景圖作 AND 運算,再使位元圖與背景圖作 OR 運算即可:

                INVOKE  BitBlt,hdcbuffer,x,y,BallSize,BallSize,hdcmask,0,0,SRCAND
                INVOKE  BitBlt,hdcbuffer,x,y,BallSize,BallSize,hdcball,0,0,SRCPAINT

以上的步驟可以用下圖來說明:

上圖左的背景圖上的白框代表此處將畫上球體的圖案,首先是得準備好一張遮罩圖,此遮罩圖是把要畫出的圖案部份以黑色表示,而其他仍為背景的部份以白色表示,接著把背景圖白框部份與遮罩圖作 AND 運算,最後再把剛剛的結果與球體作 OR 運算,就可以只把球體畫上去,而在白框內且在球體以外的部份仍為背景圖。

在 WM_TIMER 最後,呼叫 InvalidateRect 使工作區變為無效。而在 WM_PAINT 訊息中,僅有一條指令,就是把緩衝區的設備內容拷貝到工作區:

                INVOKE  BitBlt,ps.hdc,0,0,xClient,yClient,hdcbuffer,0,0,SRCCOPY

無法調整大小的視窗

BALL 程式無法調整視窗大小,主要是在呼叫 CreateWindowEx 時,視窗風格僅用 WS_CAPTION or WS_SYSMENU,不使用常見的 WS_OVERLAPPEDWINDOW:

                INVOKE  CreateWindowEx,NULL,OFFSET szClassName,OFFSET szAppName,\
                        WS_CAPTION or WS_SYSMENU,0,0,xClient,yClient,0,NULL,hInstance,\
                        NULL

到第十八章回到首頁到第二十章