Ch 14 顯示 BMP 檔 (2)

上一章,小木偶介紹了把 BMP 檔案製作成 BITMAP 資源再將他顯示於視窗的方法,但是難道每次顯示檔案都要把 BMP 製作成資源才能顯示嗎?當然不是,您只要用過 ACDSee 等秀圖軟體就知道了。所以,這一章將更進一步,直接讀取 BMP 檔案並顯示於工作區 ( client area ) 內。

要達到上述目的,所使用的方法與前一章雷同。都在處理 WM_PAINT 訊息時,先以 CreateCompatibleDC API 建立一個與視窗相同的設備內容,稱為記憶體設備內容,再把這個記憶體設備內容內之資料,以 BitBlt API 傳給視窗設備內容即可。但問題來了,在記憶體設備內容的資料是不是就是 BMP 檔內的資料,它們是不是一模一樣呢?

很抱歉,答案是它們並不一樣。事實上,存在磁碟上面的 BMP 檔案是一種與裝置無關的圖片檔 ( device-independent bitmap,簡稱 DIB ),所謂與裝置無關的意思是說,這種檔案不管在那一種機器上所顯示出的顏色都是一樣的,這是因為 DIB 內含有調色盤。而存在資源堛甄I陣圖 ( 如前一章的 VBMP BITMAP game13.bmp ) 是一種與裝置相關的點陣圖 ( device-dependent bitmap,簡稱 DDB ),這種圖片沒有記錄顏色的調色盤,所以在不同機器、印表機上甚至不同設備內容上所顯示的圖案顏色會不同。那麼您可能會問,這樣的話,DDB 還有什麼用途呢?當然有囉,因為它們沒有調色盤,所以佔用較小的空間處理起來也較快,例如螢幕上的滑鼠指標,就是存放在記憶體中的 DDB。

那麼我們怎麼把與裝置無關的 BMP 檔案變成可以被系統所快速處理的 DDB 呢?Win32 系統提供一個 API,CreateDIBitmap,它可以把 DIB 轉換成 DDB,稍後小木偶再細說這個 API,現在,小木偶整理上面內容變成下面的步驟:

  1. 利用 GetOpenFileName 通用對話盒,選擇一個 BMP 檔案,然後用 CreateFile API 開啟這個檔案,接下來用 GetFileSize API 取得檔案大小。
  2. 依據檔案大小,配置適當記憶體區塊。
  3. 用 ReadFile API 把整個 BMP 檔讀入所配置的記憶體區塊。
  4. 依據 BMP 檔的資料,設定視窗大小,以顯示整張圖片。
  5. 取得視窗的設備內容 ( hdc ),並再建立一個相同的記憶體設備內容 ( hdcMem )。然後用 CreateDIBitmap API 把讀入記憶體內的 BMP 資料轉換成與裝置相依的 DDB 點陣圖。
  6. 利用 SelectObject API 把這個 DDB 圖載入 hdcMem 中。再用 BitBlt API 把 hdcMem 的位元圖傳送到 hdc 上。
  7. 最後釋放 hdc、hdcMem、DDB。

最後這三個步驟是顯示 BMP 檔的關鍵,您會發現幾乎與前一章的步驟類似,所差別的只是在於必須多一道手續來建立與點陣圖資源一樣的 DDB 圖,其餘皆不變。底下小木偶就把前面未曾提過的主題:配置記憶體BMP 圖檔結構設定視窗大小建立 DDB 圖片稍作解釋,那麼就從配置記憶體區塊開始吧!


配置記憶體

人盡皆知,BMP 檔案很大,那麼撰寫顯示 BMP 圖片的程式時,是否要在程式碼中預留一塊記憶體來容納將要讀入的圖片資料呢?這樣做也是一種方法,但不是最好的方法。一來,此時尚不知使用者會選擇的檔案到底有多大,若空間不足就很麻煩。二來預留的空間必然要很大,會使程式變得很大。

所以較好的方法是等到使用者選定好檔案後,再依需要決定要用多少記憶體來容納位元圖。這樣就是所謂的動態配置記憶體。

GlobalAlloc API:配置記憶體區塊

在 Win32 系統堙A可用 GlobalAlloc 配置記憶體,其原型如下:

HGLOBAL GlobalAlloc(
    UINT    uFlags  // object allocation attributes 
    DWORD   dwBytes // number of bytes to allocate  
   );

dwBytes 是要配置的記憶體大小,以位元組為單位。uFlags 是指定配置記憶體的方式,可以有下面幾種選擇:

  1. GMEM_FIXED:配置固定的記憶體。不可和 GMEM_MOVEABLE 或 GMEM_DISCARDABLE 混合使用。以 GMEM_FIXED 方式,呼叫 GlobalAlloc 後,GlobalAlloc 會在 EAX 傳回所配置的記憶體位址。

  2. GMEM_MOVEABLE:配置可移動的記憶體。在以前 Win3.1 的時代,電腦所裝配的記憶體 ( DRAM ) 很少,大約只有 8、16MB 左右,所以記憶體的使用需要斤斤計較。而電腦開機後經過多次的執行程式、關閉程式,會留下許多零碎的記憶體空間,這樣會造成浪費。若用 GMEM_MOVEABLE 方式所配置的記憶體,系統會自動搬移記憶體區塊,使許多零碎未用的記憶體集中起來,形成大塊未使用的記憶體供程式使用。
    透過這種方式所配置的記憶體便會隨時間而移動記憶體位址,那我們怎麼使用呢?微軟所提供的方法是,透過 GMEM_MOVEABLE 呼叫 GlobalAlloc 後,所傳回的值是一個記憶體物件 ( memory object ) 的代碼 ( 或記憶體區塊代碼 ),並非位址,當要存取這塊記憶體物件的資料時,必須先呼叫 GlobalLock API 鎖定此記憶體物件,若成功此 API 會傳回該記憶體空間的位址,若失敗則傳回 NULL。而程式就依據傳回的位址存取記憶體。

  3. GMEM_DISCARDABLE:配置可廢棄的記憶體空間。系統會依據需要,例如記憶體不夠用時,系統會把這種記憶體寫入硬碟並暫時刪除,等到要用到時再由硬碟讀回來。配置這種記憶體須和 GMEM_MOVEABLE 配合使用。

  4. GMEM_ZEROINIT:所配置的記憶體均先歸零。

  5. GPTR:相當於 GMEM_FIXED 和 GMEM_ZEROINIT 合用。GPTR 的 PTR 是指『指標』而言,亦即返回值是所配置記憶體的位址。

  6. GHND:相當於 GMEM_MOVEABLE 和 GMEM_ZEROINIT 合用。GHND 的 HND 是指『HANDLE』而言,亦即返回值為記憶體物件代碼。
  7. GMEM_DDESHARE:與 GMEM_SHARE 相同,

GlobalFree API

當配置的記憶體不再用或程式結束時,可以呼叫 GlobalFree 釋放配置過的記憶體。GlobalFree 原型為:

HGLOBAL GlobalFree(
    HGLOBAL hMem    // handle to the global memory object 
   );

hMem 是記憶體物件代碼 ( 以 GMEM_MOVEABLE 配置記憶體 ) 或者是記憶體位址 ( 以 GMEM_FIXED 配置記憶體 )。如果此 API 執行成功,傳回 NULL;如果執行失敗,傳回原來的記憶體物件代碼或位址。


BMP 圖檔的結構

BMP 圖片檔是一種與裝置無關的圖檔 (DIB,device-independent bitmap ),意思是在檔案內包含了顏色的資訊,所以在不同顯示器之下,所顯示的結果仍然相同。BMP 位元圖有兩種格式,一種是 OS/2 格式,另一種是 WINDOWS 格式 ( 註一 )。OS/2 格式現在已經比較少用了,所以這一章堨談的是 Windows 格式,OS/2 格式的 BMP 圖檔在註二。一個 BMP 檔案包含了四部份:BITMAPFILEHEADER、BITMAPINFOHEADER、RGBQUAD 以及位元資料。也有人把 BITMAPINFOHEADER、RGBQUAD 這兩部份合稱為 BITMAPINFO。我們就從 BITMAPFILEHEADER 開始吧。

第一部份是 BITMAPFILEHEADER,它是一個長 14 位元組的結構體,其定義是:

BITMAPFILEHEADER    STRUC
bfType      DW      4D42H
bfSize      DD      ?
bfReserved1 DW      0
bfReserved2 DW      0
bfOffBits   DD      ?
BITMAPFILEHEADER    ENDS

BITMAPFILEHEADER 的第一個欄位是長兩個位元組,其意義為 ASCII 碼的『BM』,它代表 BMP 檔的識別字,假如某個檔案的前兩個位元組是『BM』的話,那就有可能是 BMP 位元圖:如果不是『BM』,那就不是 BMP 位元圖。bfSize 是這個位元圖的檔案長度,以位元組為單位。接下來的兩個欄位是保留欄位,必定為 0。最後一個欄位 bfOffBits 是位元資料相對於檔案頭的偏移量,以位元組為單位。

接下來第二部份是 BITMAPINFOHEADER 結構體,其組成欄位是:

BITMAPINFOHEADER    STRUC  
biSize              DD      ?
biWidth             DD      ?
biHeight            DD      ?
biPlanes            DW      1
biBitCount          DW      ?
biCompression       DD      ?
biSizeImage         DD      ?
biXPelsPerMeter     DD      ?
biYPelsPerMeter     DD      ?
biClrUsed           DD      ?
biClrImportant      DD      ?
BITMAPINFOHEADER    ENDS

biSize 是這個結構體的長度,以位元組為單位,對 Windows 格式而言,其大小為 28h;對 OS/2 格式而言,其大小是 0Ch ( 請參閱註二 )。biWidth、biHeight 分別表示位元圖的寬度和長度,以點為單位。biHeight 有必要加以說明,biHeight 如果為正值,表示這張位元圖的資料由左下方開始向右,一點一點排列,到最右邊時,再往上一列排;biHeight 如果負值,表示這張位元圖的資料由左上方開始向右,一點一點排列,到最右邊時,再往下一列排。biPlanes 是顏色平面數,這是早期顯示卡硬體尚未發達時遺留下來的,如今已廢棄不用,必須設為 1。

biBitCount 表示每一點需要用多少位元,只可以是 1、4、8、16、24 或 32 這些數值。它比原來的 OS/2 格式多了 16 和 32 兩種,詳細情形請參閱註二

biCompression 是壓縮方式,這種壓縮方式是不失真的,和 JPEG 失真壓縮不同。biCompression 可以是 BI_RGB、BI_RLE8、BI_RLE4、BI_BITFIELDS 值,其中 BI_RGB 是完全無壓縮的,也是最常用的。biSizeImage 是 BMP 檔的長度,以位元組為單位。

biXPelsPerMeter、biYPelsPerMeter 是表示這張圖的解析度,以每公尺有多少點,前者是寬的解析度,後者是指高的解析度 ( 這堛爾悛R度和一般螢幕的解析度意義稍微不同 )。

最後兩個欄位 biClrUsed、biClrImportant 是當 biBitCount 不是 1、4、8、16、24 或 32 這些標準的數時使用的。

第三部份是色彩對照表,它是由許多數目不定的 RGBQUAD 結構體組成的,RGBQUAD 結構體各欄位如下:

RGBQUAD         STRUC
rgbBlue         db      ?
rgbGreen        db      ?
rgbRed          db      ?
rgbReserved     db      0
RGBQUAD         ENDS

RGBQUAD 的功用與 OS/2 格式的 RGBTRIPLE 相同,請參考註二,但是多了一個保留位元組,rgbReserved,這個位元組均設為零,有了這個位元組,使得色彩對照表上的每個 RGBQUAD 結構體長度均為 32 位元,可以使 32 位元的 CPU 存取時得到較好的效率。

第四部份是位元資料,也是存有整張圖片的圖案資料存放處。


設定視窗大小

設定視窗大小,可以用 MoveWindow API,用法請參考第 11 章有關 MoveWindow API 的用法。但要注意的是 MoveWindow 參數視窗的高度,nHeight,是包含上下兩邊的邊框厚度、標題欄高度、選單高度、工作區高度。視窗的寬度,nWidth,則包含左右兩邊的邊框厚度及工作區寬度。而 BMP 圖片的寬度與高度應恰好填滿整個工作區,所以呼叫 MoveWindow 時,必須先取得標題欄高度、選單高度、邊框厚度這些數值。這些資料可用 GetSystemMetrics API 獲得。

GetSystemMetrics API

GetSystemMetrics API 的原型是:

int GetSystemMetrics(
    int   nIndex    // system metric or configuration setting to retrieve
   );

nIndex 是想要取得的資料,當呼叫 GetSystemMetrics 後,系統會把這些資料存於 EAX 傳回來。nIndex 可以是下面這些數值:( 小木偶僅列出部份,其餘請參考 MSDN )

位元值 說   明
SM_CXSCREEN
SM_CYSCREEN
螢幕的寬度與高度,以點為單位。
SM_CXFULLSCREEN
SM_CYFULLSCREEN
視窗放大為全螢幕時,工作區的最大寬度與高度,以點為單位。這時高度不會是螢幕解析度的高度,必須再扣除工作列、標題欄的高度。
SM_CXSIZEFRAME
SM_CYSIZEFRAME
圍繞在視窗周圍的邊框厚度。SM_CXSIZEFRAME 是指左右兩邊框的厚度,SM_CYSIZEFRAME 是指上下兩邊框的高度。
SM_CYCAPTION 標題欄高度,以點為單位
SM_CYMENU 選單高度,以點為單位
SM_CMOUSEBUTTONS 滑鼠按鍵數目,若為 0,表示沒裝滑鼠
SM_MOUSEPRESENT 是否安裝滑鼠,TRUE 表示已安裝,FALSE 表示未安裝

藉由 GetSystemMetrics API 所獲得標題欄高度加上上下兩邊框厚度之後,再加上圖片高度就是最後的視窗高度;而左右兩邊框的厚度再加上圖片寬度就是工作區的寬度了。


建立 DDB 圖片

CreateDIBimap API 是依據 DIB 圖內的資料建立 DDB,其原型如下:

HBITMAP CreateDIBitmap(
    HDC     hdc,                        // handle to device context 
    CONST   BITMAPINFOHEADER *lpbmih,   // pointer to bitmap size and format data 
    DWORD   fdwInit,                    // initialization flag 
    CONST   VOID *lpbInit,              // pointer to initialization data 
    CONST   BITMAPINFO *lpbmi,          // pointer to bitmap color-format data 
    UINT    fuUsage                     // color-data usage 
);

hdc 是裝置內容。lpbmih 是一個位址指標,指向 BMP 圖檔的 BITMAPINFOHEADER。fdwInit 是一組旗標,指示作業系統如何把位元資料變成 DDB 圖片,它可以是下面數值:

數值說  明
CBM_INIT 依據 lpbInit 和 lpbmi 所指之位址內的資料,設定位元圖之資料。
0系統不設定位元圖之資料。

lpbInit 和 lpbmi 兩參數都是指標,前者指向 BMP 圖檔的位元資料,這個位元資料所在可以由 bfOffBits 求得;後者指向 BMP 圖檔的 BITMAPINFO 結構體 ( BITMAPINFOHEADER 和 RGBQUAD 兩部份合起來稱為 BITMAPINFO )。


原始碼

底下就利用上面原理,撰寫一個能夠讓人從磁碟機中挑選一個 BMP 檔案,然後把它顯示在螢幕上的程式,ViewBMP.EXE,它執行時畫面為:

按下『開啟』按鈕後,畫面變成:
如果使用者按下『取消』按鈕或開啟對話盒右上角的關閉按鈕,那麼就會出現底下的畫面:

好了,底下來是 ViewBMP 的原始碼,首先是 ViewBMP.RC:

#include "c:\masm32\include\resource.h"

#define IDM_EXIT        4001
#define IDM_OPEN        4000

VBMPMenu        MENU
BEGIN
        MENUITEM        "開啟",IDM_OPEN
        MENUITEM        "離開",IDM_EXIT
END

VBMPIcon ICON   EYE.ICO

而 ViewBMP.ASM 原始碼如下:

        .586
        .model  flat,stdcall
        option  casemap:none

include         windows.inc
include         user32.inc
include         kernel32.inc
include         gdi32.inc
include         comdlg32.inc
includelib      user32.lib
includelib      kernel32.lib
includelib      gdi32.lib
includelib      comdlg32.lib

IDM_OPEN        equ     4000
IDM_EXIT        equ     4001
MAX_FullFN      equ     260     ;017 含磁碟機、路徑、主、副檔名長度
MAX_OnlyFN      equ     100     ;018 僅含主檔名、副檔名之長度
;*************************************************
        .data
wc              WNDCLASSEX      <30h,?,?,0,0,?,?,?,?,0,offset szClassName,?>
msg             MSG             <?>
ofn             OPENFILENAME    <?>
hInstance       HINSTANCE       ?
hwnd            HWND            ?
hMenu           HMENU           ?
hFile           HFILE           ?
hGlobalMem      HGLOBAL         ?
hBitmap         HBITMAP         ?
cyNonClient     dd              0       ;030 非工作區高度總和
cxNonClient     dd              0       ;031 非工作區寬度總和
nBMPWidth       dd              ?       ;032 BMP 圖的寬度
nBMPHeight      dd              ?       ;033 BMP 圖的高度
nFileSize       dd              ?       ;034 BMP 檔的大小
nReadBytes      dd              ?       ;035 實際讀取的位元組
lpCaption       dd              -1      ;036 開啟舊檔時,錯誤字串的位址
bIfGlobal       db              0       ;037 是否曾配置記憶體
szFullFN        db              MAX_FullFN dup (0)        ;038 完整檔名存放處
szDefExt        db              'BMP',0
szFNFilter      db              '點陣圖 (*.BMP, *.DIB)',0,'*.BMP;*.DIB',0,0
szNoFile        db              '您沒有選定檔名。',0
szOpenErr       db              '讀取檔案錯誤。',0
szNotBMP        db              '此檔案非 BMP 格式。',0
szClassName     db              'ViewBMP',0
szAppName       db              '觀看點陣圖',0,'- ',MAX_OnlyFN dup (0)
szMenuName      db              'VBMPMenu',0
szIconName      db              'VBMPIcon',0
;***********************************************************
        .code
;-----------------------------------------------------------
;選擇 BMP 檔,並讀取 BMP 檔
;傳回值:lpCaption=0,表正確的 BMP 檔
;        lpCaption 不為零,則 lpCaption 為字串位址,此字串表示錯誤原因
OpenBMPFile     proc    hwin:HWND
        mov     eax,hwin
        mov     ofn.lStructSize,sizeof ofn      ;056 OPENFILENAME 長度
        mov     ofn.hwndOwner,eax
        mov     ofn.lpstrFilter,offset szFNFilter
        mov     ofn.lpstrCustomFilter,NULL
        mov     ofn.nFilterIndex,1              ;060 預設副檔名 *.BMP 或 DIB
        mov     ofn.lpstrFile,offset szFullFN   ;061 完整檔名存放處
        mov     ofn.nMaxFile,MAX_FullFN
        mov     ofn.lpstrFileTitle,offset szAppName+13
        mov     ofn.nMaxFileTitle,MAX_OnlyFN
        mov     ofn.lpstrInitialDir,NULL        ;065 用程式所在子目錄
        mov     ofn.lpstrTitle,NULL             ;066 用預設標題
        mov     ofn.Flags,OFN_READONLY          ;067 唯讀
        mov     ofn.lpstrDefExt,offset szDefExt
        mov     ofn.lCustData,NULL
        mov     ofn.lpTemplateName,NULL
        invoke  GetOpenFileName,addr ofn
.if eax
        invoke  CreateFile,offset szFullFN,\    ;073 按下開啟舊檔的確定按鈕
                GENERIC_READ,FILE_SHARE_READ,\
                NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
  .if eax!=INVALID_HANDLE_VALUE
        mov     hFile,eax                       ;077 正確地開啟檔案
        invoke  GetFileSize,eax,NULL            ;078 取得 BMP 檔大小
        mov     nFileSize,eax
    .if bIfGlobal                               ;080 檢查是否曾配置記憶體
        invoke  GlobalFree,hGlobalMem           ;081 如果曾,則先釋放記憶體
    .endif
        invoke  GlobalAlloc,GMEM_FIXED,nFileSize;083 依據 BMP 檔大小配置記憶體
        mov     bIfGlobal,1
        mov     hGlobalMem,eax
        invoke  ReadFile,hFile,eax,nFileSize,offset nReadBytes,NULL
        mov     eax,hGlobalMem  ;087 檢查所讀取的前兩字元是否為 'BM'
    .if word ptr [eax]==4d42h   ;088 若是,則為正確的 BMP 檔
        mov     ecx,[eax+12h]   ;089 取得 BMP 圖的寬度
        mov     edx,[eax+16h]   ;090 取得 BMP 圖的長度
        mov     nBMPWidth,ecx
        mov     nBMPHeight,edx
        add     ecx,cxNonClient
        add     edx,cyNonClient
        invoke  MoveWindow,hwin,0,0,ecx,edx,TRUE        ;095 重設視窗大小
        mov     lpCaption,0
    .elseif
        mov     lpCaption,offset szNotBMP       ;098 若否,則非 BMP 格式
    .endif
        invoke  CloseHandle,hFile
  .else
        mov     lpCaption,offset szOpenErr      ;102 開啟檔案錯誤
  .endif
.else
        mov     lpCaption,offset szNoFile       ;105 按下開啟舊檔的取消按鈕
.endif

        mov     ecx,offset szAppName+10         ;108 決定視窗標題的文字
.if lpCaption==0
        mov     byte ptr [ecx],' '              ;110 正確的讀出 BMP 檔
.else
        mov     byte ptr [ecx],0                ;112 開啟不正確或按下取消按鈕
.endif
        invoke  InvalidateRect,hwin,NULL,TRUE   ;113 強迫重新繪製工作區
        ret
OpenBMPFile     endp
;-----------------------------------------------------------
WndProc proc    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
        local   ps:PAINTSTRUCT,hDC:HDC,hdcMem:HDC,rect:RECT
.if uMsg==WM_CREATE
        invoke  GetSystemMetrics,SM_CYCAPTION   ;121 取得標題欄高度
        mov     cyNonClient,eax
        invoke  GetSystemMetrics,SM_CYMENU      ;123 取得選單高度
        add     cyNonClient,eax
        invoke  GetSystemMetrics,SM_CYSIZEFRAME ;125 取得邊框高度
        shl     eax,1
        add     cyNonClient,eax
        invoke  GetSystemMetrics,SM_CXSIZEFRAME ;128 取得邊框寬度
        shl     eax,1
        mov     cxNonClient,eax

.elseif uMsg==WM_COMMAND
        mov     eax,wParam
  .if ax==IDM_EXIT
        jmp     exit
  .elseif ax==IDM_OPEN
        invoke  OpenBMPFile,hWnd
  .endif
        invoke  SetWindowText,hWnd,offset szAppName

.elseif uMsg==WM_PAINT
        invoke  BeginPaint,hWnd,addr ps ;142 取得設備內容
        mov     hDC,eax
  .if lpCaption==0
        mov     edx,hGlobalMem          ;145 開啟正確的 BMP 檔
        mov     ecx,edx
        add     ecx,[edx+10]            ;147 ecx 指向位元圖資料
        add     edx,14                  ;148 edx 指向 BITMAPINFO 結構體
        invoke  CreateDIBitmap,eax,edx,CBM_INIT,ecx,edx,DIB_RGB_COLORS
        mov     hBitmap,eax
        invoke  CreateCompatibleDC,hDC
        mov     hdcMem,eax
        invoke  SelectObject,hdcMem,hBitmap 
        invoke  BitBlt,hDC,0,0,nBMPWidth,nBMPHeight,hdcMem,0,0,SRCCOPY
        invoke  DeleteObject,hBitmap
  .elseif lpCaption==0ffffffffh                 ;156 程式剛執行,尚未選擇檔案
  .else
        invoke  GetClientRect,hWnd,addr rect    ;158 沒有開啟 BMP 檔
        invoke  DrawText,hDC,lpCaption,-1,addr rect,DT_CENTER or \
                DT_VCENTER or DT_SINGLELINE
  .endif
        invoke  EndPaint,hDC,addr ps

.elseif uMsg==WM_DESTROY
exit:   invoke  PostQuitMessage,NULL

.else
def:    invoke  DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
.endif
        xor     eax,eax
        ret
WndProc endp
;-----------------------------------------------------------
start:  invoke  GetModuleHandle,NULL
        mov     hInstance,eax
        invoke  GetCommandLine
        mov     wc.style,CS_HREDRAW or CS_VREDRAW
        mov     wc.lpfnWndProc,offset WndProc
        mov     eax,hInstance
        mov     wc.hInstance,eax
        mov     wc.hbrBackground,COLOR_WINDOW+1
        invoke  LoadIcon,eax,offset szIconName
        mov     wc.hIcon,eax
        mov     wc.hIconSm,eax
        invoke  LoadCursor,NULL,IDC_ARROW
        mov     wc.hCursor,eax
        invoke  LoadMenu,hInstance,offset szMenuName
        mov     hMenu,eax
        invoke  RegisterClassEx,offset wc
        invoke  CreateWindowEx,NULL,offset szClassName,offset szAppName,\ 
                WS_SYSMENU or WS_CAPTION,0,10,400,100,0,hMenu,hInstance,NULL 
        mov     hwnd,eax
        invoke  ShowWindow,hwnd,SW_SHOWDEFAULT
        invoke  UpdateWindow,hwnd

.while  TRUE
        invoke  GetMessage,offset msg,NULL,0,0
.break  .if     !eax
        invoke  DispatchMessage,offset msg
.endw
        mov     eax,msg.wParam
        invoke  ExitProcess,eax
;***********************************************************
        end     start

最後是 ViewBMP.MAK 檔的內容:

ALL:ViewBMP.EXE

ViewBMP.EXE : ViewBMP.ASM ViewBMP.RES
        ML /coff ViewBMP.ASM /link ViewBMP.RES

ViewBMP.RES : ViewBMP.RC EYE.ICO
        RC ViewBMP.RC

解說

這個程式主要部份都已經說明過了,底下只說明一些尚未說明的部份。

lpCaption 變數

這個變數是用來記錄字串位址,這個字串是當錯誤發生時 ( 例如按下取消按鈕、所選定的檔案不是 BMP 檔等等…),在工作區所顯示的錯誤訊息。如果一切正常,lpCaption 就被設為零 ( 第 96 行 )。

除此之外,lpCaption 還有另一個用途。由於程式必須處理 WM_PAINT 訊息以顯示 BMP 圖案檔,但是當程式一開始執行時,使用者還沒有選定檔案,這時要處理的 WM_PAINT 訊息和要顯示圖案時的程式碼並不一樣。其實這時候根本就不須處理 WM_PAINT 訊息,應該直接交給 DefWindowProc API 處理 WM_PAINT。為了區別是不是 ViewBMP 是不是一開始執行,所以小木偶把 lpCaption 當作一個旗標,其初始值為 -1,當程式一開始執行時,lpCaption 等於 -1,這時就不處理 WM_PAINT 訊息,當選定 BMP 檔且正確開啟時, lpCaption 就設定為零。如果使用者按取消按鈕,則 lpCaption 指向錯誤字串所在。


註一:OS/2 是 IBM 出品的一種作業系統,早期與微軟共同開發,以期能取代 DOS,成為第二代作業系統,這可以由其名稱,Operating System/2 看出。後來由於 Windows 3.0 的成功,微軟決定開發下一代 Windows 95 而退出 OS/2 的開發,最後 OS/2 由 IBM 獨自開發並銷售。有關於更多 OS/2 的介紹,請參考 AKIMASA 的網站

小木偶也曾經對 OS/2 很著迷,但因資源、軟體太少,終究抵不住現實的壓力而逐漸轉向用 Windows 9x/Me。不過小木偶仍然很還念 1994∼1999 年那段用 OS/2 的時光,我常在想,假如不是 OS/2 優異性能威脅到微軟的 Windows 3.1,說不定微軟仍然停留在 16 位元的作業系統。因為 OS/2 的競爭,才會促使微軟進步。最近 ( 2005 年 8 月左右 ) 一個例子也是,微軟的 IE6 已經好幾年都不更新,直到功能強勁的火狐 ( Firefox ) 發表後,才見微軟加緊開發 IE7。

註二:OS/2 的 BMP 檔案結構與 Windows 的 BMP 檔案結構類似,喔!應該說 Windows 的 BMP 檔結構是由 OS/2 擴充而來的。OS/2 的 BMP 檔案結構的第一部份是由 14 個位元組組成的 BITMAPFILEHEADER 結構體,其意義與 Windows 格式的相同。在 BITMAPFILEHEADER 結構體之後的第二部份是由 12 個位元組組成的 BITMAPCOREHEADER 結構體:

BITMAPCOREHEADER    STRUC
bcSize      dd      ?
bcWidth     dw      ?
bcHeight    dw      ?
bcPlanes    dw      ?
bcBitCount  dw      ?
BITMAPCOREHEADER    ENDS

第一個欄位,bcSize,指的是這個結構體的大小,以位元組為單位,因此其數值是 0ch,它可以用來分辨是 OS/2 格式或是 Windwos 格式的 BMP 檔。第二、三個欄位,bcWidth、bcHeight 是這個位元圖的寬度和長度,以點為單位。bcPlanes 是顏色平面數,也和 Windows 格式一樣,應該設為一。

最後一個欄位,bcBitCount,是每個點所佔用的位元數,可以是 1、4、8、24。如果是 1,表示每點僅佔用一個位元,一個位元可以是 0 或 1,因此可以表示兩種顏色;如果是 4,那麼會有下面 16 種情形:

0000    0001    0010    0011    0100    0101    0110    0111
1000    1001    1010    1011    1100    1101    1110    1111

可以用 24=16 計算,如果每一種數值代表一種顏色,那就能表示 16 種顏色。其餘顏色如下表:

bcBitCount 計算 顏色數 RGBTRIPLE
結構體數目
1 2122
4 241616
8 28256256
24 ( True-Color ) 224167772160

第三部份是一個由許多 RGBTRIPLE 結構體所組成的色彩對照表,顧名思義,它是記錄色彩用的,它的長度不固定,與顏色數有關。先看它的組成單位,RGBTRIPLE 結構體,由其名字就可猜想得到,RGBTRIPLE 是一個由紅綠藍三原色光所組成代表各種顏色強弱的結構體:

RGBTRIPLE   STRUC
rgbBlue     db      ?       ;藍色光強度
rgbGreen    db      ?       ;綠色光強度
rgbRed      db      ?       ;紅色光強度
RGBTRIPLE   ENDS

可以參考上表,如果 bcBitCount 為 1 時,色彩對照表有兩個 RGBTRIPLE 結構體;如果 bcBitCount 為 4 時,色彩對照表有 16 個 RGBTRIPLE 結構體……,這應不難理解。舉例來說, bcBitCount 為 4 時,可以表示 16 種顏色,但是是那 16 種顏色呢?就由色彩對照表記錄,每個 RGBTRIPLE 結構體表示一種顏色,這種顏色是由三原色光強弱不同而產生,因此 16 種顏色就得有 16 個 RGBTRIPLE 結構體記錄。那麼為什麼 True-Color 不用色彩對照表呢?這個答案很明顯,請讀者自行思考囉。

第四部份也是最後一部份就是位元資料,存有整張圖片的圖案。


到第十三章回到首頁到第十五章