Ch 12 對話盒(5)子控件顏色

前幾章提到如何實作對話盒,但小木偶想,您也會想試著改變這些對話盒或各種子控制元件的顏色吧?在這一章堙A小木偶就對這個主題作一些簡單的介紹。


系統顏色

使用過 Windows 作業系統的人,必定會發現,每個視窗幾乎長得一個樣兒,包含對話盒的顏色,事實上不僅對話盒的顏色,所有視窗的顏色,包含標題欄、選單、狀態欄的顏色也都是一個樣兒。為什麼會這樣呢?原來在 Windows 作業系統,對桌面、標題欄、捲軸……等都用固定的顏色,稱為系統顏色。如果您對這些顏色不滿意,也可以自己修改系統顏色。Win XP 的使用者,可以到『控制台』→『顯示』的『外觀』標籤中按『進階』按鈕更改它,在下圖中的『項目(I)』式一個複合控件,可以在其清單中選擇您想改變的項目:

更改系統顏色,不是改變對話盒顏色最好的方法,因為假如您用這種方式改變對話盒的顏色,那麼系統中所有對話盒的顏色都會隨之改變。而底下的方法是藉由處理訊息的方式改變對話盒的顏色,這樣的話只會改變對話盒程式本身的顏色,而不會改變其他視窗的顏色。


子控件的顏色

WM_CTLCOLOR* 訊息

小木偶在介紹這些控制元件時,曾提及控制元件是 Win32 系統內已經預先定義好類別的子視窗,而這些控制元件的繪製、處理等行為都由系統內部的對話盒管理器統籌管理控制。一般而言,對話盒管理器是以註冊時,WNDCLASSEX 結構體內的 hbrBackground 畫刷為對話盒著色,其他如編輯框、靜態控件等也都是以系統顏色著色。

但是我們有機會可以修改這些內定的顏色。對話盒管理器會呼叫 DialogBoxParam API 參數所指定的對話盒函式來處理我們感興趣的訊息 ( 當對話盒函式結束欲返回對話盒管理器時,這些處理過的訊息傳回 TRUE,而其餘傳回 FALSE 讓給對話盒管理器處理 )。假如想要更改對話盒顏色,必須使對話盒函式處理一些有關顏色的訊息。

這些訊息是

其他依此類推。對話盒管理器在繪製控制元件時,和我們在工作區繪圖類似,都得先取得這些控制元件的設備內容 ( device context ) 代碼,然後再針對這個設備內容代碼繪圖。如果我們不處理 WM_CTLCOLOR* 訊息,那麼對話盒處理程式就以內定的畫刷繪製文字顏色或背景顏色。如果處理 WM_CTLCOLOR* 訊息時,系統傳給對話盒函式的 WM_CTLCOLOR* 訊息堜狴]含的 wParam 參數,就是設備內容代碼,而 lParam 參數是控制元件代碼。設定文字及背景顏色的方法,一般是呼叫 SetTextColor API 設定文字顏色,呼叫 SetBkColor API 設定背景顏色,並且傳回一個畫刷供對話盒管理器著色。注意,WM_CTLCOLOR* 訊息不傳回 TRUE,而是傳回畫刷。對話盒管理器將會以此畫刷為對話盒的子控件著色。

但是,SetTextColor 和 SetBkColor 並不會建立畫刷,所以一般都得先在結束 WM_CTLCOLOR* 訊息之前,建立一個畫刷,而這個畫刷的顏色和 SetBkColor 相同。建立畫刷的時機可以在處理 WM_CTLCOLOR* 訊息時,也可以在處理 WM_INITDIALOG 時,不過為了確保只建立一個畫刷,最好是在 WM_INITDIALOG 時建立。當程式結束時,也就是處理 WM_CLOSE 訊息時,這些畫刷也用不著了,應該刪除這些畫刷。

SetTextColor 和 SetBkColor API

底下介紹 SetTextColor、SetBkColor 這兩個 API 的用法。SetTextColor 是用來對某個設備內容設定特定的顏色,其原型是:

COLORREF SetTextColor(
    HDC         hdc,    // handle of device context  
    COLORREF    crColor // text color 
   );

hdc 是設備內容,一般可以在 wParam 中取得,crColor 是您要設定的顏色,請參考第四章的說明。SetTextColor 的傳回值是原來文字顏色。底下是 SetBkColor 的原型:

COLORREF SetBkColor(
    HDC         hdc,    // handle of device context  
    COLORREF    crColor // background color value
   );

其參數與傳回值和 SetTextColor 相同,就不贅述了。

按鈕風格:BS_OWNERDRAW

除了按鈕控件以外,要改變子控件的顏色按照上述方法做就夠了,但是按鈕控件還得多一些手續。原來按扭控件不會主動發出 WM_CTLCOLORBTN 訊息,除非是指定了 BS_OWNERDRAW。所以必須在資源描述檔堙A定義按鈕風格為 BS_OWNERDRAW。

但是加上了 BS_OWNERDRAW 風格後,對話盒管理器不會多管閒事的處理立體效果了。我的意思是,一般的按鈕按下去時會有沈下去的效果,放鬆時會有浮上來的效果,但是如果把按鈕控件加上了 BS_OWNERDRAW 風格後,就沒有這些立體效果了。雖然這些效果沒了,但是仍能正常運作。假如仍想擁有這效果,那麼就必須自行處理 WM_DRAWITEM 訊息。具有 BS_OWNERDRAW 風格的按鈕會發出 WM_DRAWITEM 訊息,字面上的意思,WM_DRAWITEM 意即自行畫出子控件。

WM_DRAWITEM 訊息與 DRAWITEMSTRUCT 結構體

WM_DRAWITEM 訊息是處理當具有 BS_OWNERDRAW 風格的按鈕自行繪製按鈕圖形的訊息。如果按鈕的風格含有「BS_OWNERDRAW」,那麼作業系統於螢幕上繪出按鈕時,就會對該按鈕的父視窗之視窗函式發出「WM_DRAWITEM」訊息。事實上,不僅按鈕,具有 SS_OWNERDRAW 風格的靜態控件、具有 CBS_OWNERDRAWFIXED 或 CBS_OWNERDRAWVARIABLE 風格的複合框、具有 LBS_OWNERDRAWFIXED 或 LBS_OWNERDRAWVARIABLE 風格的清單以及選單也都會發出 WM_DRAWITEM 訊息。WM_DRAWITEM 的 wParam 參數是子控件的識別碼,但是如果由選單發出 WM_DRAWITEM 訊息的話,wParam 為零。lParam 參數是一個位址,此位址指向一個稱為 DRAWITEMSTRUCT 的結構體,它存有許多系統已給定的資料。

DRAWITEMSTRUCT 內的成員會由作業系統填入適當的數值,而程式設計師只要根據這些數值繪製自己想要的圖片。DRAWITEMSTRUCT 的成員如下:

DRAWITEMSTRUCT  STRUCT
  CtlType       DWORD     ?
  CtlID         DWORD     ?
  itemID        DWORD     ?
  itemAction    DWORD     ?
  itemState     DWORD     ?
  hwndItem      DWORD     ?
  hdc           DWORD     ?
  rcItem        RECT      <>
  itemData      DWORD     ?
DRAWITEMSTRUCT  ENDS

CtlType 是子控件類別,可以是 ODT_BUTTON、ODT_COMBOBOX、ODT_LISTBOX、ODT_STATIC、ODT_MENU 等等,除了 ODT_MENU 是選單外,其於是子控件。CtlID 式子控件的識別碼,如果是選單發出的 WM_DRAWITEM,則此欄廢棄不用。

itemID 是選單中的選項識別碼或是複合框或清單中的選擇項識別碼。如果複合框或清單沒有選擇項的話,此欄位被設為負值。

itemAction 是決定如何繪製,可以有下面三種數值:ODA_DRAWENTIRE、ODA_FOCUS、ODA_SELECT。當整個子控件需要被重新繪製時,此欄位被設為 ODA_DRAWENTIRE。當子控件失去或得到輸入焦點時,此欄位被設為 ODA_FOCUS,至於得到還是失去焦點,得察看 itemState 欄位。當複合框或清單的選擇項被選到後時,此欄位被設為 ODA_SELECT。

itemState 是一個 32 位元的整數,當選單或子控件被改變時,其中某些位元會被設定,或清除。常用的位元如下:

  1. ODS_CHECKED:當選單的選項被選中時,此欄位被設定 ODS_CHECKED ,只有選單才可能設定此旗標。
  2. ODS_DISABLED:當子控件不能接受滑鼠或鍵盤輸入時,此欄位被設為 ODS_DISABLED。
  3. ODS_FOCUS:當子控件獲得輸入焦點時,設定此位元。
  4. ODS_GRAYED:當選單的選項被設為無作用時。只有選單才可能設定此位元。
  5. ODS_SELECTED:當子控件被選擇時,設定此位元。
  6. ODS_COMBOBOXEDIT:當複合框控件的編輯文字被選擇 ( 反白 ) 時,此欄位設為 ODS_COMBOBOXEDIT。
  7. ODS_DEFAULT:內定的。

hwndItem 是子控件的代碼,如果 WM_DRAWITEM 是選單所發出的,此欄位是選單代碼。hdc 是要繪製的子控件設備內容代碼。rcItem 是 hdc 的範圍,有關 RECT 的資料型態,請參考第三章的說明。itemData 是和選單、複合框控件、清單控件有關,當 ctlType 為 ODT_BUTTON 或 ODT_STATIC 時,itemData 為零。


一個例子:DEC2HEX.ASM

提了這麼多有關對話盒的顏色,不實作一個實在手癢,所以簡單寫了一個程式說明。

DEC2HEX 功用

這個程式是用來轉換十進位與十六進位,最多可以轉換 64 位元的正數,執行時畫面如下:

DEC2HEX.EXE 執行畫面
您可以按下『DEC->HEX』按鈕或『HEX->DEC』按鈕選擇作十進位轉換成十六進位或十六進位轉換成十進位,當您選定後,上面的靜態控件會顯示出作那一種轉換,同時底下咖啡色的數字按鈕也會作更動,亦即如果您選定十進位轉換成十六進位,那麼『A』到『F』按鈕是無作用的,所以程式將使得『A』到『F』按鈕上的數字消失,變成和背景一樣的咖啡色。

原始檔內容

觀看這個對話盒,很明顯的,對話盒背景、靜態元件、按鈕元件的文字顏色或背景顏色都已更改。底下先看看原始檔,DEC2HEX.ASM:

        .386
        .model  flat,stdcall
        option  casemap:none

DlgProc         proto   :DWORD,:DWORD,:DWORD,:DWORD
Convert         proto
Dec2Hex         proto   :DWORD
Hex2Dec         proto   :DWORD
DrawBTN         proto   :DWORD,:DWORD
DrawEdge        proto   :DWORD,:DWORD,:DWORD,:DWORD,\
                        :DWORD,:DWORD,:DWORD

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

IDS_NS          equ     2000
IDE_NUM         equ     2500
IDB_N0          equ     3000
IDB_N1          equ     3001
IDB_N2          equ     3002
IDB_N3          equ     3003
IDB_N4          equ     3004
IDB_N5          equ     3005
IDB_N6          equ     3006
IDB_N7          equ     3007
IDB_N8          equ     3008
IDB_N9          equ     3009
IDB_NA          equ     3010
IDB_NB          equ     3011
IDB_NC          equ     3012
IDB_ND          equ     3013
IDB_NE          equ     3014
IDB_NF          equ     3015
IDB_H2D         equ     3100
IDB_D2H         equ     3101
IDB_AC          equ     3102
IDB_ANS         equ     3103
IDB_CVT         equ     3104

IDM_EXIT        equ     4000
IDM_CLEAR       equ     4001

DLG_BK_COLOR    equ     0ffccffh        ;048 淡粉紅色
STATIC_COLOR    equ     09900ffh        ;049 桃紅色
EDIT_BK_COLOR   equ     099cccch        ;050 棕色
EDIT_TEXT_COLOR equ     0               ;051 黑色
BTN_BK_COLOR    equ     0113366h        ;052 咖啡色
BTN_TEXT_COLOR  equ     0ffffffh        ;053 白色

;************************************************
                .data
hInstance       HINSTANCE       ?
CommandLine     LPSTR           ?
DlgName         db              'D2HDlg',0
fNumSys         db              0               ;060 0:十進位變十六進位
                                                ;061 1:十六進位變十進位
hwndEdit        HWND            ?               ;062 編輯框子視窗代碼
hwndBTN         HWND            10h dup (?)     ;063 按鈕子視窗代碼
hwndStatic      HWND            ?               ;064 靜態子視窗代碼
hbrBTN          HBRUSH          ?               ;065 按鈕的畫刷代碼
hbrDlg          HBRUSH          ?               ;066 對話盒的畫刷代碼
hbrStatic       HBRUSH          ?               ;067 靜態子視窗的畫刷代碼
hbrEdit         HBRUSH          ?               ;068 編輯框的畫刷代碼
szAnswer        db              32 dup (?)      ;069 答案存放處
nTenPwr         dd              1000000000,100000000,10000000,1000000
                dd              100000,10000,1000,100,10,1
szNumber        db              ?,0
szD2H           db              '十進位轉換成十六進位',0
szH2D           db              '十六進位轉換成十進位',0
fConvert        db              0               ;075 0:未按下『Convert』
                                                ;076 1:已按下『Convert』
sNumber         db              '0123456789ABCDEF'
szIcon          db              'Dec2Hex',0
;************************************************
                .code
start:  invoke  GetModuleHandle,NULL
        mov     hInstance,eax
        invoke  DialogBoxParam,hInstance,offset DlgName,0,offset DlgProc,0
        invoke  ExitProcess,eax                 ;84 結束程式
;------------------------------------------------
;使 AL 內的數字變成 ASCII 碼。AL 原內容為 0∼0fh
ConvertBIN2ASCII        proc
        add     al,30h
        cmp     al,3ah  ;89
        jb      @f
        add     al,7
@@:     ret             ;92
ConvertBIN2ASCII        endp
;------------------------------------------------
;使 CL 內的 ASCII 碼轉換成數字。CL 原內容為 30h∼39h、41h∼46h
ConvertASCII2BIN        proc
        sub     cl,'0'
        cmp     cl,9
        jbe     @f
        sub     cl,7
@@:     ret
ConvertASCII2BIN        endp
;------------------------------------------------
;畫出按鈕邊框。下圖是按鈕外觀,但不照比例繪製。ncx 是按鈕的寬度,ncy
;是按鈕的高度,邊框有兩層,再以斜邊分左上、右下兩邊,故有四種顏色
;┌—————————┐
;∣nColor1         /∣
;∣ ┌——————┐ ∣
;∣ ∣nColor3   /∣ ∣
;∣ ∣ ┌———┐ ∣ ∣
;∣ ∣ ∣      ∣ ∣ ∣
;∣ ∣ ∣  AC  ∣ ∣ ∣
;∣ ∣ ∣      ∣ ∣ ∣
;∣ ∣ └———┘ ∣ ∣
;∣ ∣/   nColor4∣ ∣
;∣ └——————┘ ∣
;∣/         nColor2∣
;└—————————┘
DrawEdge        proc    hdc:HDC,ncx:DWORD,ncy:DWORD,nColor1:DWORD,\
                        nColor2:DWORD,nColor3:DWORD,nColor4:DWORD
        LOCAL   hOldPen:DWORD
        LOCAL   hNewPen:DWORD
        dec     ncx     ;123 因為傳入的 ncx、ncy 是按鈕寬度,但畫邊框
        dec     ncy     ;124 時由,座標零開始,所以 ncx、ncy 都減去一
        invoke  CreatePen,PS_SOLID,0,nColor1    ;125 外層左上角邊框的畫筆
        mov     hNewPen,eax
        invoke  SelectObject,hdc,eax            ;127 選擇此畫筆
        mov     hOldPen,eax                     ;128 儲存舊畫筆,以利恢復
        invoke  MoveToEx,hdc,0,ncy,NULL
        invoke  LineTo,hdc,0,0
        invoke  LineTo,hdc,ncx,0
        invoke  DeleteObject,hNewPen
        invoke  CreatePen,PS_SOLID,0,nColor2    ;133 由 133∼138 行,畫外層
        mov     hNewPen,eax                     ;134 右下角邊框
        invoke  SelectObject,hdc,eax
        invoke  LineTo,hdc,ncx,ncy
        invoke  LineTo,hdc,0,ncy
        invoke  DeleteObject,hNewPen            ;138

        dec     ncx
        dec     ncy
        invoke  CreatePen,PS_SOLID,0,nColor3    ;142 由 142∼148 行,畫內層
        mov     hNewPen,eax                     ;143 左上角邊框
        invoke  SelectObject,hdc,eax
        invoke  MoveToEx,hdc,1,ncy,NULL
        invoke  LineTo,hdc,1,1
        invoke  LineTo,hdc,ncx,1
        invoke  DeleteObject,hNewPen            ;148
        invoke  CreatePen,PS_SOLID,0,nColor4    ;149 由 149∼154 行,畫內層
        mov     hNewPen,eax                     ;150 右下角邊框
        invoke  SelectObject,hdc,eax
        invoke  LineTo,hdc,ncx,ncy
        invoke  LineTo,hdc,1,ncy
        invoke  DeleteObject,hNewPen
        invoke  SelectObject,hdc,hOldPen        ;155 恢復原畫筆
        ret
DrawEdge        endp
;-------------------------------------------------
DrawBTN proc    nID_BTN:WPARAM,lp:LPARAM
        LOCAL   dis:DRAWITEMSTRUCT
.if (nID_BTN>=IDB_N0)||(nID_BTN<=IDB_NF)        ;161 僅處理數字按鈕
        push    esi
        lea     edx,dis
        mov     esi,lp
        mov     ecx,12
next:   lodsd                   ;166 初始化 dis,意即把從系統傳來,位於
        mov     [edx],eax       ;167 lParam 所指的 DRAWITEMSTRUCT 位
        add     edx,4           ;168 址內的數值,存入 dis 局部變數
        loop    next
        pop     esi

;以下七行畫出按鈕邊框
 .if (dis.itemState&ODS_SELECTED)       
        invoke  DrawEdge,dis.hdc,dis.rcItem.right,\
                dis.rcItem.bottom,0,336699h,0,99ccffh
 .else
        invoke  DrawEdge,dis.hdc,dis.rcItem.right,\
                dis.rcItem.bottom,0ffffffh,0,99ccffh,336699h
 .endif

        invoke  IsWindowEnabled,dis.hwndItem
        or      eax,eax         ;182 檢查按鈕是否可接受輸入,若不可
        jz      Disable_BTN     ;183 ,則跳到 Disable_BTN 處
        mov     ecx,nID_BTN     ;184 此三行,算出要在按鈕上印出什麼數字
        sub     ecx,IDB_N0
        add     ecx,offset sNumber
;以下五行在按鈕中顯示出文字 ( 0∼F )
 .if (dis.itemState&ODS_SELECTED)
        invoke  TextOut,dis.hdc,21,5,ecx,1	;189 下沈
 .else
        invoke  TextOut,dis.hdc,20,4,ecx,1	;191 上浮
 .endif

Disable_BTN:
.endif
        ret
DrawBTN endp
;------------------------------------------------
DlgProc proc    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM

.if uMsg==WM_INITDIALOG
        invoke  LoadIcon,hInstance,offset szIcon
        invoke  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
        invoke  GetDlgItem,hDlg,IDE_NUM
        mov     hwndEdit,eax

;207 以下 12 行,取得各數字的按鈕代碼,並存於 hwndBTN 陣列
        mov     edx,IDB_N0
        sub     ecx,ecx
nxtBTN: push    edx
        push    ecx
        invoke  GetDlgItem,hDlg,edx
        pop     ecx
        mov     hwndBTN[ecx*4],eax
        pop     edx
        inc     ecx
        inc     edx
        cmp     cx,16
        jb      nxtBTN

        invoke  Dec2Hex,hDlg    ;221 設定為十進位轉換成十六進位
        mov     fNumSys,0       ;222 故 fNumSys 設為零
        mov     fConvert,0      ;223 設定未按下 Convert 按鍵
        invoke  CreateSolidBrush,DLG_BK_COLOR
        mov     hbrDlg,eax      ;225 設定對話盒背景畫刷
        invoke  CreateSolidBrush,DLG_BK_COLOR
        mov     hbrStatic,eax   ;227 設定靜態元件的畫刷
        invoke  CreateSolidBrush,EDIT_BK_COLOR
        mov     hbrEdit,eax     ;229 設定編輯框畫刷
        invoke  CreateSolidBrush,BTN_BK_COLOR
        mov     hbrBTN,eax      ;231 設定數字按鍵畫刷

.elseif uMsg==WM_CTLCOLORDLG    ;233 處理對話盒背景顏色
        mov     eax,hbrDlg
        ret

.elseif uMsg==WM_CTLCOLORSTATIC
        mov     ecx,lParam      ;238 檢查是否是編輯框傳來的訊息
        cmp     ecx,hwndEdit    ;239 若是,則跳到 edit_color 標記
        je      edit_color
;處理靜態元件文字顏色及背景顏色
        invoke  SetTextColor,wParam,STATIC_COLOR
        invoke  SetBkColor,wParam,DLG_BK_COLOR
        mov     eax,hbrStatic
        ret

.elseif uMsg==WM_CTLCOLOREDIT   ;247 處理編輯框文字顏色及背景顏色
edit_color:
        invoke  SetTextColor,wParam,EDIT_TEXT_COLOR
        invoke  SetBkColor,wParam,EDIT_BK_COLOR
        mov     eax,hbrEdit
        ret

.elseif uMsg==WM_CTLCOLORBTN    ;254 處理數字按鍵文字顏色及背景顏色
        invoke  SetTextColor,wParam,BTN_TEXT_COLOR
        invoke  SetBkColor,wParam,BTN_BK_COLOR
        mov     eax,hbrBTN
        ret

.elseif uMsg==WM_DRAWITEM       ;260 處理 WM_DRAWITEM 訊息
        invoke  DrawBTN,wParam,lParam

.elseif uMsg==WM_CLOSE
exit:   invoke  DeleteObject,hbrDlg     ;264 刪除畫刷
        invoke  DeleteObject,hbrStatic
        invoke  DeleteObject,hbrEdit
        invoke  DeleteObject,hbrBTN
        invoke  EndDialog,hDlg,NULL

.elseif uMsg==WM_COMMAND
        mov eax,wParam
;以下是使用者點選選單
 .if lParam==0
   .if ax==IDM_CLEAR
        invoke  SetDlgItemText,hDlg,IDE_NUM,NULL
   .elseif ax==IDM_EXIT
        jmp     exit
   .endif
 .else
;以下是使用者選按按鈕
        mov     edx,wParam
        shr     edx,16          ;282 DX=通知碼
   .if dx==BN_CLICKED           ;283 AX=子控件之識別碼
     .if ax==IDB_H2D
        invoke  Hex2Dec,hDlg    ;285 設定『A』∼『F』按鈕可接受輸入
        or      fNumSys,1
        invoke  InvalidateRect,hDlg,NULL,FALSE
     .elseif ax==IDB_D2H
        invoke  Dec2Hex,hDlg    ;289 設定『A』∼『F』按鈕不接受輸入
        and     fNumSys,0
        invoke  InvalidateRect,hDlg,NULL,FALSE
     .elseif ax==IDB_AC
        invoke  SetDlgItemText,hDlg,IDE_NUM,NULL
     .elseif ax==IDB_ANS
        mov     fConvert,1
        invoke  SetWindowText,hwndEdit,offset szAnswer
     .elseif ax==IDB_CVT
        call    Convert
        mov     fConvert,1      ;299 設定 fConvert
     .elseif (ax>=IDB_N0)&&(ax<=IDB_NF)
;如果 fConvert 旗標為一,則設定編輯框之內容為 NULL
      .if fConvert==1
        push    eax
        invoke  SetDlgItemText,hDlg,IDE_NUM,NULL
        mov     fConvert,0      ;305 清除 fConvert
        pop     eax
      .endif
        sub     ax,IDB_N0
        call    ConvertBIN2ASCII
        mov     ecx,offset szNumber
        mov     szNumber,al
        invoke  SendMessage,hwndEdit,EM_REPLACESEL,TRUE,ecx
     .endif
   .endif
 .endif     
.else
        mov     eax,FALSE
        ret
.endif
        mov     eax,TRUE
        ret
DlgProc endp
;------------------------------------------------
;十進位制變成十六進位制,按鈕0∼9啟動,A∼F不啟動
Dec2Hex proc    hwnd:HWND
        sub     ecx,ecx ;326 ECX=0∼F,表示第幾個按鈕
nxt_d:  push    ecx
        mov     edx,hwndBTN[ecx*4]
.if cl<0ah
        invoke  EnableWindow,edx,TRUE
.else
        invoke  EnableWindow,edx,FALSE
.endif
        pop     ecx
        inc     ecx
        cmp     cl,16
        jb      nxt_d
;338 最大為4294967295,共十位數
        invoke  SendMessage,hwndEdit,EM_SETLIMITTEXT,10,0
        invoke  SetDlgItemText,hwnd,IDS_NS,offset szD2H
        ret
Dec2Hex endp
;------------------------------------------------
;十六進位制變成十進位制,按鈕0∼F均啟用
Hex2Dec proc    hwnd:HWND
        sub     ecx,ecx
nxt_h:  push    ecx
        mov     edx,hwndBTN[ecx*4]
        invoke  EnableWindow,edx,TRUE
        pop     ecx
        inc     ecx
        cmp     cl,16
        jb      nxt_h
;354 最大為FFFFFFFF,共 8 位數
        invoke  SendMessage,hwndEdit,EM_SETLIMITTEXT,8,0
        invoke  SetDlgItemText,hwnd,IDS_NS,offset szH2D
        ret
Hex2Dec endp
;------------------------------------------------
;把編輯框之內容轉換成十進位或十六進位
;先取得編輯框內容,再依據 fNumSys 旗標之值判斷是十進位換成十六進位
;還是十六進位換成十進位。
Convert proc
        local   buffer[16]:BYTE ;364 buffer 存放使用者所輸入之字串
        local   nLenBuffer:DWORD;365 使用者輸入之字串長度

        invoke  GetWindowText,hwndEdit,addr buffer,16
.if eax!=0      ;368 若編輯框內容之長度為零,不做任何事
        mov     nLenBuffer,eax
        push    esi
        push    edi
        lea     esi,buffer
        mov     edi,offset szAnswer
        mov     ecx,eax
        rep     movsb           ;375 先把輸入字串拷貝到 szAnser 字串

        sub     eax,eax         ;377 EAX 存放使用者所輸入之數值,先歸零
        lea     esi,buffer
  .if fNumSys==0
@@:     mov     cl,10           ;380 十進位變十六進位
        mul     ecx             ;381 先計算輸入字串所表示的數值,並
        mov     cl,[esi]        ;382 存於 EAX 
        inc     esi
        sub     cl,'0'
        add     eax,ecx
        dec     nLenBuffer
        jnz     @b
        mov     edx,eax
        mov     ax,3d64h        ;389 存入『d=』
        stosw
        mov     ecx,8           ;391 換成十六進位時,最多 8 位數
@@:     rol     edx,4           ;392 每 4 位元表示一位十六進位數
        mov     al,dl           ;393 使 EAX 內的十六進位數存於
        and     al,0fh          ;394 EDI 所指的字串
        call    ConvertBIN2ASCII
        stosb
        loop    @b
        mov     al,'h'          ;398 存入『h』
        stosb
  .else
@@:     shl     eax,4           ;401 十六進位變十進位
        mov     cl,[esi]
        call    ConvertASCII2BIN
        add     eax,ecx
        inc     esi
        dec     nLenBuffer
        jnz     @b
        mov     word ptr[edi],3d68h
        inc     edi
        inc     edi
        mov     ecx,10
        mov     esi,offset nTenPwr
@@:     mov     ecx,[esi]
        sub     edx,edx
        div     ecx
        add     esi,4
        call    ConvertBIN2ASCII
        stosb
        mov     eax,edx
        loop    @b
        mov     al,'d'
        stosb
  .endif
        mov     al,0
        stosb
        pop     edi
        pop     esi
        invoke  SetWindowText,hwndEdit,offset szAnswer
.endif
        ret
Convert endp
;************************************************
end     start

資源描述檔,DEC2HEX.RC 的內容是:

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

#define D2HMenu 1000

#define IDS_NS  2000
#define IDE_NUM 2500
#define IDB_N0  3000
#define IDB_N1  3001
#define IDB_N2  3002
#define IDB_N3  3003
#define IDB_N4  3004
#define IDB_N5  3005
#define IDB_N6  3006
#define IDB_N7  3007
#define IDB_N8  3008
#define IDB_N9  3009
#define IDB_NA  3010
#define IDB_NB  3011
#define IDB_NC  3012
#define IDB_ND  3013
#define IDB_NE  3014
#define IDB_NF  3015
#define IDB_H2D 3100
#define IDB_D2H 3101
#define IDB_AC  3102
#define IDB_ANS 3103
#define IDB_CVT 3104

#define IDM_EXIT        4000
#define IDM_CLEAR       4001

D2HMenu         MENU
BEGIN
  MENUITEM      "離開",IDM_EXIT
  MENUITEM      "清除",IDM_CLEAR
END

D2HDlg  DIALOG  0,0,123,132
STYLE   DS_SETFONT |WS_POPUP |WS_CAPTION |WS_VISIBLE |WS_SYSMENU
FONT    9,"Arial"
MENU    D2HMenu
CAPTION "十進位與十六進位換算"
BEGIN
  CONTROL    "十進位轉換成十六進位",IDS_NS,"STATIC",SS_LEFT,8,4,108,12
  CONTROL    ""   ,IDE_NUM,"EDIT",WS_BORDER|ES_READONLY,8,16,108,12
  PUSHBUTTON "DEC -> HEX",IDB_D2H, 8,34,52,12,WS_CHILD
  PUSHBUTTON "HEX -> DEC",IDB_H2D,64,34,52,12,WS_CHILD
  PUSHBUTTON "C"  ,IDB_NC ,  8, 50, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "D"  ,IDB_ND , 36, 50, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "E"  ,IDB_NE , 64, 50, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "F"  ,IDB_NF , 92, 50, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "8"  ,IDB_N8 ,  8, 66, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "9"  ,IDB_N9 , 36, 66, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "A"  ,IDB_NA , 64, 66, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "B"  ,IDB_NB , 92, 66, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "4"  ,IDB_N4 ,  8, 82, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "5"  ,IDB_N5 , 36, 82, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "6"  ,IDB_N6 , 64, 82, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "7"  ,IDB_N7 , 92, 82, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "0"  ,IDB_N0 ,  8, 98, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "1"  ,IDB_N1 , 36, 98, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "2"  ,IDB_N2 , 64, 98, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "3"  ,IDB_N3 , 92, 98, 24,12,WS_CHILD|BS_OWNERDRAW
  PUSHBUTTON "AC" ,IDB_AC ,  8,114, 24,12,WS_CHILD
  PUSHBUTTON "ANS",IDB_ANS, 36,114, 24,12,WS_CHILD
  PUSHBUTTON "Convert",IDB_CVT, 64,114,52,12,WS_CHILD
END

Dec2Hex ICON    DEC2HEX.ICO

而 makefile 檔案,DEC2HEX.MAK 檔的內容如下:

ALL:DEC2HEX.EXE

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

DEC2HEX.RES : DEC2HEX.RC DEC2HEX.ICO
        RC DEC2HEX.RC

下面是圖示檔,DEC2HEX.ICO:


DEC2HEX 解說

DEC2HEX 使用模式對話盒,在主程式中呼叫 DialogBoxParam API 建立對話盒 ( 見程式第 83 行 ),這樣節省了不少程式碼,你可以看到主程式僅僅 4 行 ( 第 81∼84 行 )。接下來是兩個副程式,ConvertBIN2ASCII、ConvertASCII2BIN 分別處理一些煩瑣的數值轉換,在這兩個副程式中有一個特別的標記名稱『@@:』,值得一提。

@@ 標記

組合語言原始碼中用到標記的頻率比 C/C++、PASCAL 等高階語言多得多,這是因為組合語言中常常需要比較,比較指令之後就是依條件跳躍,所以為這些標記取個不重複的名稱變得比較困難,於是常常有人 ( 當然也包含小木偶 ) 就為這些標記取個 a01、a02、a03……這樣的名稱。萬一以後修改程式,要在中間安插一個標記時,就會出現 a01_1 這樣突兀的名字。我們發現這些標記常常只用一次,所以取個無關緊要的名稱也無妨,於是微軟在推出 MASM 6.x 時,使用 @@: 標記以解決這種問題。

@@: 標記是和

        jmp     @f
        或
        jmp     @b

配合使用的,當然 @@: 也可以配合條件跳躍 ja/jb/je/jz……等指令使用。當使用 jmp @f 時,程式將會向前 ( 亦即往記憶體位址大的方向 ) 尋找到第一個 @@: 標記處,繼續執行;同理,如果使用 jmp @b 就是向後尋找到第一個 @@: 處執行。所以一個程式中 @@: 標記、jmp @f 和 jmp @b 均可以出現很多次,組譯器能自動地尋找要跳躍到那一個位址,只不過使用時要注意,以免因為標記名都是 @@:,而使 @b 和 @f 搞混。

在 DEC2HEX.ASM 程式第 89∼92 行為

        cmp     al,3ah
        jb      @f      ;90
        add     al,7
@@:     ret             ;92

意即若 AL 小於 3AH 時,就向前跳到第 87 行。ConvertASCII2BIN 副程式也使用 @@: 標記,可參考看看。

DrawBTN 副程式

接下來的這兩個副程式,DrawBTN 與 DrawEdge,的功用是畫出按鈕。如前所述,當按鈕控件具有 BS_OWNERDRAW 風格時,我們可以在對話盒函式處理 WM_DRAWITEM 訊息,繪製自己所想要表現的按鈕。程式第 261 行呼叫 DrawBTN 繪製自定的按鈕外觀,DrawBTN 有兩個參數,即 WM_DRAWITEM 訊息的參數 wParam 與 lParam,我們將要靠這兩個參數的資料畫出按鈕外觀。

一進入 DrawBTN 副程式時,先檢查是否由數字按鈕發出的 WM_DRAWITEM,如果是的話就依據 lParam 的位址,把系統提供的 DRAWITEMSTRUCT 結構體內的資料,拷貝到區域變數,dis 結構體內 ( 程式第 162∼170 行 )。接下來呼叫 DrawEdge 副程式畫出邊框 ( 程式第 173∼179 行 ),邊框的顏色是使按鈕可以看起來有壓下或上浮立體效果的關鍵,我們可以檢查 dis 內的 itemState 欄位 ( 程式第 173 行 ),如果 ODS_SELECTED 位元被設定,表示按鈕被壓下,否則按鈕被鬆開。如何使按鈕具立體效果的詳細過程,容稍後再敘述。

接下來的第 181 到第 194 行是在按鈕上印出『0』∼『F』的數字來,但是如果是不能接收滑鼠訊息的按鈕 ( 例如十進位換成十六進位時,就不能輸入『A』∼『F』數字,這些按鈕就不能接收滑鼠訊息 ) 就不顯示出數字來,檢查按鈕是否可接收滑鼠或鍵盤輸入,可以用 IsWindowEnabled。

IsWindowEnabled API

這個 API 是用來檢查指定的視窗是否能接受滑鼠或鍵盤輸入。它的原型如下:

BOOL IsWindowEnabled(
  HWND  hWnd     // handle to window to test
);

hWnd 是要檢測的視窗代碼,如果能接受輸入則 EAX 傳回一個非零數值,否則傳回零。

第 182 行檢查 EAX 是否為零,若為零,不須顯示數字,直接跳到 BTN_Disable: 標記處結束 DrawBTN 副程式即可;若不為零表示可以接收滑鼠輸入 ( 此時表示十六進位轉換成十進位 ),『A』∼『F』數字按鈕上的數字就要顯示出來,方法是呼叫 TextOut 顯示出來。

TextOut API

這個 API 會以選定的背景顏色、文字顏色、字形在指定的位置顯示一個字串。其原型是:

BOOL TextOut(
  HDC     hdc,          // handle to device context
  int     nXStart,      // x-coordinate of starting position
  int     nYStart,      // y-coordinate of starting position
  LPCTSTR lpString,     // pointer to string
  int     cbString      // number of characters in string
);

hdc 是指設備內容 ( 見第三章 ),nXStart、nYStart 是指字串要顯示在那一個位置,lpString 是要顯示的字串位址,cbString 是要顯示的字串長度。至於按鈕文字顏色、背景顏色在處理 WM_CTLCOLORBTN 訊息時設定的,也就是第 255、256 行以 SetTextColor、SetBkColor 設定。此外為了在按下按鈕時,使按鈕具有立體效果,所以在第 189 行使文字稍稍向右下方移一個點。

DrawEdge 副程式

前面曾提到,為了讓按鈕具有壓下時稍稍下沈,放開時稍稍上浮的立體效果,必須對按鈕邊框稍微修飾一番,DrawEdge 副程式就是執行這段任務。

假想按鈕是一個扁扁的長方體,光線從左上方照射它,左上方的區域應該較亮,右下方光線照不到,較暗。但是當按鈕被壓下沈入計算機面板下面時,左上方反而照不到光線,變暗;右下方卻能照到光線,變亮。所以當按鈕沒被按下時,左上角與右下角有兩種不同的顏色,但為了不這麼突兀,小木偶採漸次變亮及變暗,所以左上角用兩種顏色,即逐漸變亮;右下角也是如此,可以參考下圖;

上圖是按鈕未按下時後的樣子,左上角用 nColor1、nColor3 兩種顏色表示;右下角用 nColor2、nColor4 表示。當按鈕被按下後,也用上面四種顏色表示,只是左上角變暗而右下角變亮。

實作上畫出明暗不同的顏色,就用不同顏色的線條畫出來即可。畫出不同顏色的線條得先建立畫筆、選擇畫筆等步驟,請參考第五章的說明。至於線段的長度則是用參數 ncx、ncy 決定,ncx、ncy 表示按鈕的長寬,存於 DRAWITEMSTRUCT 的 rcItem 欄位堙C

對話盒函式

接下來就是對話盒函式,DEC2HEX 處理 WM_INITDIALOG、WM_CTLCOLORDLG、WM_CTLCOLORSTATIC、WM_CTLCOLOREDIT、WM_CTLCOLORBTN、WM_DRAWITEM、WM_COMMAND、WM_CLOSE 這八個訊息。

在處理 WM_INITDIALOG 訊息時,首先發送 WM_SETICON 訊息設定圖示 ( 第 202、203 行 ),然後取得編輯框以及各按鈕的代碼,小木偶把編輯框代碼存於 hwndEdit 變數堙A把十六個按鍵代碼存在 hwndBTN 所組成的陣列中。在此稍稍說明 hwndBTN 的結構,hwndBTN[0] 存入按鍵『0』的視窗代碼 ( 子控制元件也是一種視窗 ),hwndBTN[4] 存入按鍵『1』的視窗代碼……依此類推。此處 ECX 可以用作定址,此處 ECX 稱為基底暫存器,請參考組合語言第三十章

接下來 WM_INITDIALOG 訊息還設定 fNumSys 以及 fConvert 這兩個變數。fNumSys 是用來設定使用者選擇十進位轉換成十六進位,還是十六進位轉換成十進位,如果是前者則設為零,後者則設為一。一開始執行程式時,內定值為零,表示要十進位轉換成十六進位,如果使用者按下『HEX->DEC』按鈕,那麼就設定為十六進位轉換成十進位,fNumSys 也要設為一,同時也設定按鈕『A』∼『F』可以接收滑鼠或鍵盤輸入,可以參考程式第 285 到第 287 行。若使用者按下『DEC->HEX』按鈕,那麼就設定為十進位轉換成十六進位,fNumSys 也要設為零,同時也設定按鈕『A』∼『F』不接收滑鼠或鍵盤輸入,可以參考程式第 289 到第 291 行。

變數 fConvert 是按下『Convert』按鈕後,防止使用者接著按下數字按鈕,以致於使得編輯框畫面混亂而設的一個旗標。一般計算機輸入算式之後再按等於鍵便會於螢幕上出現答案,這時如果還要再計算另一個算式,不需要再按下清除鍵,可以直接按下數字鍵也能清除螢幕上的答案同時輸入最高位的數字,DEC2HEX 也模仿這個功能。但是此處出現一個問題,如果按下數字鍵之前都清除編輯框的內容,那麼如果要輸入『12』的『2』時,就會把『1』給清除了。所以小木偶設了 fConvert 這個變數,當按下『Convert』按鈕後,會把 fConvert 設定一 ( 程式第 299 行 ),此後如果按下的任何數字鍵便會清除編輯框,然後再把 fConvert 清除 ( 程式第 305 行 ),程式只要檢查 fConvert,若為一,就清除編輯框;若為零,則不清除編輯框。

接著 WM_INITDIALOG 訊息還在程式第 224 到 231 行設定了對話盒、靜態元件、編輯框、數字按鈕畫刷,再存入相對應的變數堙AhbrDlg、hbrStatic、hbrEdit、hbrBTN。

接著第 233 行到第 258 行是處理 WM_CTLCOLOR* 訊息的,就如前述原理,就不詳述了,僅挑特殊的說明。其中改變對話盒背景較為簡單,只要把這個畫刷代碼傳回即可。

唯讀編輯框所發出的 WM_CTLCOLOR*

在這個程式堿陘F只讓使用者輸入『0』∼『F』數字,所以編輯框具有唯讀 ( ES_READONLY )風格。根據 MSDN,具有 ES_READONLY 風格的編輯框以及不能接受滑鼠或鍵盤輸入的編輯框,發出 WM_CTLCOLORSTATIC 而不是 WM_CTLCOLOREDIT,所以第 238∼240 行檢查 WM_CTLCOLORSTATIC 的 lParam 是否為編輯框發出的,如果是的話則跳至第 248 行處理。

最後還沒說明的是 WM_COMMAND 訊息,這個訊息處理了所有按鈕被壓下後,所發生的事,請參考第六章的說明。DEC2HEX 比較特別的是『DEC->HEX』和『HEX->DEC』兩個按鈕,按下之後,會使得某些按鈕無法接收滑鼠與鍵盤的輸入。副程式 Dec2Hex 和 Hex2Dec 就處理這個事情,所要用的 API 是 EnableWindow。

EnableWindow API

EnableWindow API 是用來設定子控件是否能接收滑鼠與鍵盤的輸入,其原型是:

BOOL EnableWindow(
    HWND    hWnd,   // handle to window
    BOOL    bEnable // flag for enabling or disabling input
   );

hWnd 是子控件代碼,bEnable 指定子控件是否能接受鍵盤與滑鼠輸入,能的話用 TRUE,不能的話用 FALSE。


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