Ch 06 加上選單

這一章,小木偶將說明如何在程式中加入選單 ( menu )。但是首先,先來看看什麼是選單?小木偶想,只要和您講了選單在那兒,您就知道選單是什麼了,一般而言,選單是在位於標題欄下方,下圖以小作家為例說明選單在那兒,

什麼是選單

這樣您是否明白什麼是選單了?前面的幾個例子都沒有選單,在這一章堙A小木偶將說明如何為程式加上選單。


Win32 程式的開發

選單是資源的一部份

Win 32 可執行檔事實上可分為兩部份 ( 註一 ),『程式碼』與『 UI 資源』。UI 資源,是指所表現出的使用者界面 ( user interface )。這些資源包含選單 ( menu )、圖示 ( icon )、游標 ( cursor )、位元圖 ( bitmap )、字型 ( font )、對話盒 ( dialog box )、版本資訊 ( version ) 等等。產生這些資源所使用的軟體不盡相同,所得副檔名也不一樣,例如一般以 *.ICO 副檔名表示圖示檔、以 *.CUR 副檔名表示游標檔……。這些資源最後都必須在一個稱為『資源描述檔』( resource-definition script file,副檔名通常是 *.RC ) 的文字檔奡y述。資源描述檔再交由資源編譯程式 ( RC.EXE ) 編譯成資源檔 ( 資源檔的副檔名通常是 *.RES ),最後資源檔、目的檔以及程式庫再由連結器 ( LINK.EXE ) 連結並產生可執行檔 ( *.EXE )。其流程如下圖︰

Win32 開發時相關檔案

製作選單

選單的製作方法比較簡單,它不像圖示或游標需要用到額外的檔案,它可以直接在資源描述檔 ( *.RC ) 中直接以文字描述。其格式如下︰

選單名稱    MENU
{
        ………
}

選單名稱可以是任意字串,這個字串是將來填入到程式堛 WNDCLASSEX 結構體 lpszMenuName 欄位或是填入 CreateWindowEx API 的參數堛滿A它是用來指定選單的識別符號。在大括號之間 ( 也可以用 BEGIN/END 代替大括號 ) 是程式設計師所要定義的選單,一般而言選單堛瑪龠等i分為兩種,一種是使用者以滑鼠選按後,還會再出現更多的選項供使用者挑選,這種選項通常在選項後面會有『…』,像這種選項是以 POPUP 來定義。另一種是使用者選按後就直接執行的選項,沒有更進一步的子選項,像這種選項以 MENUITEM 定義。這兩種選項的格式為︰

POPUP   選項名稱
{
        MENUITEM    選項名稱, ID 數值 [,option]
        ……
}

在 POPUP 之後或在 MENUITEM 之後的『選項名稱』是以兩個『"』為界,夾在中間的字串表示,這個字串將顯示在選單上面。在這字串堨i以加上『&;』,表示其後所接的一個字元會加上底線,而此字元為快捷鍵。所謂快捷鍵的意思是當使用者按下 Alt 鍵不放,再按下『&』後所接的字元 ( 此字元所表示的按鍵 ) 就相當於以滑鼠選按一樣。

『ID 數值』( 選項識別碼 ) 是可以任意給定的整數,但必須是唯一的,否則會造成兩個選項具有一樣的功能。為了增加可讀性,可以在資源描述檔的一開始,以『#define』定義常數來代替此 ID 數值,當然此常數的名稱必須能讓人一目了然。

option 是表示選項的屬性,常用的屬性有︰

選項名稱也可以使用 SEPARATOR,假如使用 SEPARATOR 時,不須加上『"』,SEPARATOR 表示在子選單會出現一個分隔線。

WM_COMMAND 訊息

當使用者按下快捷鍵、選單或控制元件 ( 稍後幾章再談 ) 等物件時,系統會把 WM_COMMAND 訊息傳給程式。

  1. 假如使用者由選單上選擇一個選項,lParam 為零,wParam 較低的 16 位元為選項識別碼,較高的 16 位元為零。
  2. 假如使用者按下控制元件時,lParam 不為零而是子視窗代碼,wParam 較低的 16 位元為控制元件識別號碼,較高的 16 位元為使用者的動作,一般稱為通知碼 ( notification code )。
  3. 假如使用者按下快捷鍵,lParam 為零,wParam 較低的 16 位元為快捷鍵識別號碼,較高的 16 位元為一。

整理如下表︰

 wParam 高字組 wParam 低字組lParam
使用者按下選單 0選項識別碼 0
使用者按下快捷鍵 1快捷鍵識別碼 0
使用者按下控制元件 通知碼
(即使用者動作)
控制元件識別碼 子視窗代碼

當視窗函式處理完 WM_COMMAND 後,應該把傳回值設為 0 之後,再返回系統。還記得嗎?視窗函式是被系統所呼叫的,所以當然也必須返回系統才行。


畫線段與橢圓

底下小木偶將撰寫一個程式,可由選單選擇畫出線段與橢圓,在此先介紹畫出這兩個圖形的方法。我想凡是學過粗淺幾何的人應該都知道兩點決定一直線,Win32 API 也是如此,要畫出線段得先決定起點,再由起點畫到終點。

LineTo API

這個 API 是在視窗中,由『現在的點』( 即起點 ) 以現在的畫筆畫一條直線到指定的點 ( 即終點 )。原型為

BOOL LineTo(
    HDC     hdc,    // device context handle 
    int     nXEnd,  // x-coordinate of line's ending point  
    int     nYEnd   // y-coordinate of line's ending point  
   );

hdc 是裝置內容代碼,nXEnd 和 nYEnd 分別是終點的 X 座標與 Y 座標。那麼前文所提『現在的點』是什麼呢?您可以這樣想像,每當建立一個視窗時,系統會在此視窗中指定一個點作為『現在的點』,而底下 MoveToEx API 可以改變『現在的點』。另外當呼叫完 LineTo API 後,『現在的點』會改變成 nXEnd 與 nYEnd 所指的點。

MoveToEx API

MoveToEx API 是用來改變『現在的點』,其原型為︰

BOOL MoveToEx(
    HDC     hdc,    // handle of device context 
    int     X,      // x-coordinate of new current position  
    int     Y,      // y-coordinate of new current position  
    LPPOINT lpPoint // address of old current position 
   );

X 與 Y 就是改變後的『現在的點』的座標。lpPoint 是指向一個 POINT 結構體位址,此結構體將在 MoveToEx 返回後存放舊的『現在的點』的座標,假如以 NULL 代替 POINT 結構體位址,則表示不須傳回舊的『現在的點』的座標。

Ellipse API

顧名思義,這個 API 是用來畫出橢圓。系統所用的方法是,先決定一個矩形,然後系統會自動地幫我們畫出這個矩形的內切橢圓來。 Ellipse 的原型是︰

BOOL Ellipse(
    HDC hdc,        // handle to device context 
    int nLeftRect,  // x-coord. of bounding rectangle's upper-left corner 
    int nTopRect,   // y-coord. of bounding rectangle's upper-left corner  
    int nRightRect, // x-coord. of bounding rectangle's lower-right corner  
    int nBottomRect // y-coord. bounding rectangle's f lower-right corner  
   );

hdc 是裝置內容代碼。nLeftRect 和 nTopRect 分別是矩形的左上角的 X 座標與 Y 座標;nRightRect 和 nBottomRect 分別是矩形的右下角的 X 座標與 Y 座標。此外這個函式會自動地以現在的畫筆畫出橢圓的邊線,同時用現在的筆刷替橢圓內部著色。

假如您不希望使橢圓內部著色的話,只好使用變通的方法,想像橢圓其實是由兩個半橢圓組合而成的,畫出這兩個半橢圓的弧形可用 Arc API。

Arc API

這個 API 欲用來畫出橢圓弧形的一部份,其原型為

BOOL Arc(
    HDC hdc,        // handle to device context 
    int nLeftRect,  // x-coordinate of bounding rectangle's upper-left corner
    int nTopRect,   // y-coordinate of bounding rectangle's upper-left corner
    int nRightRect, // x-coordinate of bounding rectangle's lower-right corner
    int nBottomRect,// y-coordinate of bounding rectangle's lower-right corner
    int nXStartArc, // first radial ending point 
    int nYStartArc, // first radial ending point 
    int nXEndArc,   // second radial ending point 
    int nYEndArc    // second radial ending point 
   );

hdc 是裝置內容代碼。nLeftRect 和 nTopRect 分別是矩形的左上角的 X 座標與 Y 座標;nRightRect 和 nBottomRect 分別是矩形的右下角的 X 座標與 Y 座標。nXStartArc 和 nYStartArc 是弧線起點的 X 座標和 Y 座標;nXEndArc 和 nYEndArc 是弧線終點的 X 座標和 Y 座標。起點和終點不一定要在弧線上,系統會自動的計算橢圓中心到起點所成的直線開始畫出橢圓,並且以逆時針方向畫到橢圓中心到終點所連成的線為止,請參考下圖︰

Arc API 之說明
圖中只有實線的橢圓會被畫在螢幕上,橢圓中心到起點和橢圓中心到終點這兩條直線,不會畫出來。


範例︰小小繪圖程式,DRAW.EXE

DRAW.EXE 的使用方法與結果

底下小木偶撰寫一個小小繪圖程式,DRAW.EXE,它有兩個很簡單的選單,第一個是『離開』,第二個是『繪圖』。使用者按下離開,就會跳出一個視窗問你是否確定,若按下『是』,就離開小小繪圖程式。

至於『繪圖』媮晹酗賒茪l選單及一個分隔線,這六個子選單能畫出直線、橢圓以及滑鼠移動的軌跡,並能改變紅、綠、藍三顏色。當選擇直線時,必須先以滑鼠左鍵選起點再選終點,程式便會以當下的顏色畫出直線。若選擇橢圓,必須先以滑鼠左鍵選擇一個矩形的左上角頂點,再選擇右下角頂點,程式會以當下顏色畫出這個矩形的內切橢圓形。如果使用者選擇『手繪圖』,則使用者必須以滑鼠左鍵選擇一起點,表示將畫筆放下,程式便會畫出滑鼠移動之軌跡。底下是手繪圖的執行畫面︰

小小繪圖程式

DRAW.RC 與 DRAW.ASM 原始程式

底下是這個程式的資源描述檔,DRAW.RC,的原始程式。資源描述檔不一定要和程式碼的原始程式同名,我的意思是,您的資源描述檔也可以不要取名為 DRAE.RC,但是這樣做便於管理。

#define IDM_Exit        10
#define IDM_Line        21
#define IDM_Ellipse     22
#define IDM_Trace       23
#define IDM_Red         24
#define IDM_Green       25
#define IDM_Blue        26

DRAW    MENU
{

MENUITEM    "離開(&E)",IDM_Exit

POPUP   "繪圖(&D)"
        {
        MENUITEM    "線段(&L)",IDM_Line
        MENUITEM    "橢圓(&E)",IDM_Ellipse
        MENUITEM    "手繪圖(&T)",IDM_Trace
        MENUITEM    SEPARATOR
        MENUITEM    "紅(&R)",IDM_Red
        MENUITEM    "綠(&G)",IDM_Green
        MENUITEM    "藍(&B)",IDM_Blue
        }

}

底下是程式碼的原始程式,DRAW.ASM︰

        .386
        .model  flat,stdcall
        option  casemap:none
include         windows.inc
include         user32.inc
include         gdi32.inc
include         kernel32.inc
includelib      user32.lib
includelib      gdi32.lib
includelib      kernel32.lib

IDM_Exit        equ     10      ;12 選項識別碼
IDM_Line        equ     21
IDM_Ellipse     equ     22
IDM_Trace       equ     23
IDM_Red         equ     24
IDM_Green       equ     25
IDM_Blue        equ     26
red_color       equ     0ffh    ;19 顏色
green_color     equ     0ff00h
blue_color      equ     0ff0000h

WndProc         proto   :HWND,:UINT,:WPARAM,:LPARAM

        .DATA
ClassName       db          'SimpleWinClass',0
AppName         db          '小小繪圖程式',0
AskExit         db          '是否退出此程式?',0
AskExitTitle    db          '詢問',0
MenuName        db          'DRAW',0    ;30 選單名稱為'DRAW'
PenDown         db          FALSE       ;31 FALSE︰畫筆懸空,TRUE︰畫筆觸紙
command         dw          IDM_Line    ;32 使用者由選單選擇的命令
color           dd          red_color   ;33 使用者由選單選擇的顏色
hMenu           HMENU       ?           ;34 選單代碼
hInstance       HINSTANCE   ?
hwnd            HWND        ?
wc              WNDCLASSEX  <30h,?,?,0,0,?,?,?,?,0,offset ClassName,?>
msg             MSG         <?>
start_point     POINT       <?>         ;39 起點座標
end_point       POINT       <?>         ;40 終點座標

        .CODE
start:  invoke  GetModuleHandle,NULL
        mov     hInstance,eax
        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,NULL,IDI_APPLICATION
        mov     wc.hIcon,eax
        mov     wc.hIconSm,eax
        invoke  LoadCursor,NULL,IDC_ARROW
        mov     wc.hCursor,eax
        invoke  LoadMenu,hInstance,offset MenuName      ;55 取得選單代碼
        mov     hMenu,eax
        invoke  RegisterClassEx,offset wc
        invoke  CreateWindowEx,NULL,offset ClassName,\ 
                offset AppName,WS_OVERLAPPEDWINDOW,0,\
                0,400,400,0,hMenu,hInstance,NULL        ;60 加上選單
        mov     hwnd,eax
        invoke  ShowWindow,hwnd,SW_SHOWNORMAL
        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
;---------------------------------------
;計算 EAX 內的座標,存於 EDX 所指的位址,EAX︰高位元為 Y 座標,低位元為 X 座標
get_crd proc
        push    eax
        and     eax,0ffffh
        mov     [edx],eax
        pop     eax
        shr     eax,10h
        mov     [edx+4],eax
        ret
get_crd endp
;---------------------------------------
WndProc proc    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
        local   rectangle:RECT
        local   PS:PAINTSTRUCT
        local   hdc:HDC
        local   newPen:DWORD
        local   oldPen:DWORD
.if     uMsg==WM_COMMAND
        mov     eax,wParam
        mov     PenDown,FALSE   ;92 只要選擇選單後就設為 FALSE
        .if     ax==IDM_Exit
                jmp     exit
        .elseif (ax==IDM_Line)||(ax==IDM_Ellipse)||(ax==IDM_Trace)
                mov     command,ax
        .elseif ax==IDM_Red
                mov     color,red_color
        .elseif ax==IDM_Green
                mov     color,green_color
        .elseif ax==IDM_Blue
                mov     color,blue_color
        .endif

.elseif uMsg==WM_LBUTTONDOWN                    ;105 按下滑鼠左鍵
        .if     (command==IDM_Line)||(command==IDM_Ellipse)
                .if     PenDown
                        mov     eax,lParam      ;108 畫筆觸紙時
                        mov     edx,offset end_point
                        call    get_crd
                        invoke  GetClientRect,hWnd,addr rectangle
                        invoke  InvalidateRect,hWnd,addr rectangle,FALSE
                .else
                        mov     eax,lParam      ;114 畫筆提起時
                        mov     edx,offset start_point
                        call    get_crd
                .endif

        .elseif command==IDM_Trace              ;119 手繪圖
                .if     (!PenDown)              ;120 檢查畫筆是否懸空
                        mov     eax,lParam      ;121 懸空
                        mov     edx,offset start_point
                        call    get_crd         ;123 取得起點
                .endif                   
        .endif
        xor     PenDown,1       ;118 切換畫筆提起或觸紙

.elseif uMsg==WM_MOUSEMOVE
        .if     (command==IDM_Trace)&&(PenDown)
                mov     eax,lParam              ;122 取得終點座標
                mov     edx,offset end_point
                call    get_crd
                invoke  GetClientRect,hWnd,addr rectangle
                invoke  InvalidateRect,hWnd,addr rectangle,FALSE
        .endif

.elseif uMsg==WM_PAINT
        .if     command==IDM_Line
                invoke  BeginPaint,hWnd,addr PS
                mov     hdc,eax
                invoke  CreatePen,PS_SOLID,1,color
                mov     newPen,eax
                invoke  SelectObject,hdc,newPen
                mov     oldPen,eax
                invoke  MoveToEx,hdc,start_point.x,start_point.y,NULL
                invoke  LineTo,hdc,end_point.x,end_point.y
                invoke  SelectObject,hdc,oldPen
                invoke  DeleteObject,newPen
                invoke  EndPaint,hdc,addr PS
        .elseif command==IDM_Ellipse
                invoke  BeginPaint,hWnd,addr PS
                mov     hdc,eax
                invoke  CreatePen,PS_SOLID,1,color
                mov     newPen,eax
                invoke  SelectObject,hdc,newPen
                mov     oldPen,eax
                mov     eax,start_point.x
                add     eax,end_point.x
                shr     eax,1
                push    eax
                invoke  Arc,hdc,start_point.x,start_point.y,\   ;161 畫出半橢圓
                        end_point.x,end_point.y,eax,400,eax,0
                pop     eax
                invoke  Arc,hdc,start_point.x,start_point.y,\   ;164 畫另一半橢圓
                        end_point.x,end_point.y,eax,0,eax,400
                invoke  SelectObject,hdc,oldPen
                invoke  DeleteObject,newPen
                invoke  EndPaint,hdc,addr PS
        .elseif command==IDM_Trace
                invoke  BeginPaint,hWnd,addr PS
                mov     hdc,eax
                invoke  CreatePen,PS_SOLID,1,color
                mov     newPen,eax
                invoke  SelectObject,hdc,newPen
                mov     oldPen,eax
                invoke  MoveToEx,hdc,start_point.x,start_point.y,NULL
                invoke  LineTo,hdc,end_point.x,end_point.y
                invoke  SelectObject,hdc,oldPen
                invoke  DeleteObject,newPen
                invoke  EndPaint,hdc,addr PS
                mov     eax,end_point.x     ;181 使終點變下次的起點
                mov     edx,end_point.y
                mov     start_point.x,eax
                mov     start_point.y,edx
        .else
                jmp     def
        .endif

.elseif uMsg==WM_CLOSE
exit:   invoke  MessageBox,NULL,offset AskExit,\
                offset AskExitTitle,MB_YESNO or MB_ICONQUESTION
        .if     eax==IDYES
                invoke  DestroyWindow,hWnd
        .endif

.elseif uMsg==WM_DESTROY
        invoke  PostQuitMessage,NULL

.else
def:    invoke  DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
.endif
        xor     eax,eax
        ret
WndProc endp

end     start

組譯方法

要製作出 DRAW.EXE 可執行檔,連結器 ( LINK.EXE ) 必須連結目的檔與資源檔,故組譯方式和以前不太相同。

  1. 先編譯資源描述檔,建立 DRAW.RES 檔。
  2. 再組譯原始檔。因為 ML.EXE 會去呼叫 LINK.EXE,所以 ML.EXE 必須把 LINK.EXE 所需之參數傳給 LINK.EXE,加上『/link DRAW.RES』,表示要連結 DRAW.OBJ 以及 DRAW.RES 兩檔。( DRAW.OBJ 由 ML.EXE 自動傳給 LINK.EXE )

實際操作過程如下。假如您無法正確組譯,可能是環境變數錯誤,請自行參考第零章有關在 Win32 撰寫組合語言程式的準備工作。

e:\homepage\source>rc DRAW.RC [Enter]  →假如資源描述檔編譯成功,則無任何訊息

e:\homepage\source>ml DRAW.ASM /link DRAW.RES [Enter]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

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

/SUBSYSTEM:WINDOWS
"DRAW.obj"
"/OUT:DRAW.exe"
"DRAW.RES"

e:\homepage\source>

這樣就可以得到 DRAW.EXE 了。另外請參考註二,有另外一個較方便的方法產生可執行檔。


程式說明

小木偶想資源描述檔中的選單應已不用再說明,前面已有敘述了。底下說明程式碼的原始檔。

重要的變數與常數

第 12 行到第 18 行定義選項識別碼的數值,此數值和資源描述檔中的選項識別碼相同,這些數值都不會改變,故用 equ 假指令設定為常數。

第 19 行到第 21 行是定義顏色,請參考前一章。

第 31 行是記錄畫筆是否懸空或觸紙的 PenDown 變數。第 32 行是存放現在使用者選擇那一個選項的變數,command,最簡單的方式就是此變數之值和選項識別碼一樣。也就是說,使用者選擇畫線,command 將被設為 IDM_Line,使用者選擇畫橢圓,command 將被設為 IDM_Ellipse……,見程式第 96 行。

第 34 行是儲存使用者所選擇的顏色,程式一開始時,內定為紅色,即 0FFH。程式第 35 行是存放選單代碼,這個變數稍後再說明。程式第 39、40 行是存放起點與終點 ( 畫直線或手繪圖時用 ) 或矩形的左上角頂點與右下角頂點 ( 畫橢圓時的外切矩形用 ) 的座標。

加入選單

原始程式接下來的部份和以前大致一樣,只有在第 55、56、58 行不太相同,這是為了要加入選單的關係。此處小木偶以 LoadMenu API 來載入選單,當程式執行時,選單已隨著程式由磁碟移入記憶體中,但如果您不通知系統要載入選單使其與我們的程式連接起來的話,那麼選單不僅無作用,同時畫面上也不會顯示選單。LoadMenu API 的功能就在此。

LoadMenu API

LoadMenu 的原型為︰

HMENU LoadMenu(
    HINSTANCE   hInstance,  // handle of application instance
    LPCTSTR     lpMenuName  // menu name string or menu-resource identifier
   );

hInstance 為程式的模組代碼,lpMenuName 指向選單名稱的位址,選單名稱必須以 NULL 結尾,並且此名稱應該要和資源描述檔中的選單名稱符合。

其實為程式加入選單還有一種方法,把程式第 34、55、56 行刪除,程式第 60 行的 hMenu 改成 NULL,變成

        invoke  CreateWindowEx,NULL,offset ClassName,\ 
                offset AppName,WS_OVERLAPPEDWINDOW,0,\
                0,400,400,0,NULL,hInstance,NULL

另外在註冊視窗類別之前 ( 即呼叫 RegisterClassEx 之前 ),把 wc 結構體內的 lpszMenuName 填上我們所定義的 MenuName,於是多一行

        mov     wc.lpszMenuName,offset MenuName

這兩種方法都能產生選單,所不同的是以前者的方法所註冊的視窗類別不含選單,所以每次建立新視窗時都不會有選單必須自行設立,當然您也可以為每個視窗設立不同選單。至於後者所註冊的視窗類別已包含選單,故以此視窗類別所建立的視窗不用再指定選單,每個視窗就會有相同的選單了。

說明視窗函式結構

這個視窗函式乍看之下似乎很複雜,但其實仔細分析,就發現並不像想像得那麼難。這個視窗函式其實是有許多層 .if/.elseif/.else/.ednif 巢狀假指令組成的,最外一層是在測試各種我們程式有興趣的訊息,包含 WM_COMMAND、WM_LBUTTONDOWN、WM_MOUSEMOVE、WM_PAINT、WM_CLOSE、WM_DESTROY以及呼叫內定的處理訊息的 DefWindowProc API。我們並不能像在 DOS 系統下的思考方式撰寫程式,要時時記得所謂的『以事件驅動,以訊息為基礎』的思考方式,也就是各種周邊設備觸發訊息時,處理相對應的狀況或者也可能是每觸發一個訊息,可能會引起觸發另一個訊息。所以可能一個功能會拆開在好幾個訊息中處理。

舉個例子來說,例如畫出線段這個功能,當使用者由選單中選擇畫出線段時,接下來可能會以滑鼠左鍵選擇線段的起點,再來是把滑鼠移到線段的終點,再按下滑鼠右鍵選定終點。所以即使如畫線段這樣的功能也必須拆開來,在第一次 WM_LBUTTONDOWN 時寫出處理起點的程式,在第二次 WM_LBUTTONDOWN 時寫出處理終點,選好終點後,就可以對視窗發出 WM_PAINT 的訊息,要求畫出線段來。所以又有一段程式是在 WM_PAINT 堙A這段程式是實際畫出線段的程式。

同樣的畫出橢圓也是一樣,必須分成好幾次訊息處理完成。在第一次 WM_LBUTTONDOWN 時處理矩形左上角頂點,第二次 WM_LBUTTONDOWN 時處理右下角頂點,同時發出 WM_PAINT 畫出橢圓。

這樣似乎很完美,但是有兩個問題產生了,第一,畫線段與畫橢圓都在 WM_PAINT 完成,那麼如何分辨是畫線段或者是畫橢圓呢?第二,也有可能使用者由選單中選擇畫出線段後,卻又後悔了,想以手來描繪,又如何解決呢?小木偶的做法是把畫出圖形的選項儲存在 command 變數堙A這個變數不但可以解決使用者中間反悔的問題外,還可以分辨畫出那一種圖形。

解決了畫出那一種圖形後,還有一個問題,例如在畫直線時,都已 WM_LBUTTONDOWN 來取得起點與終點,那麼怎麼知道是起點還是終點呢?答案在程式第 31 行的 PenDown 變數。程式一開始時,這個變數是設為 FALSE,你可以想像 PenDown 之意是畫筆放下碰觸紙。當其為 FALSE 表示畫筆懸空,此時當然無法在畫紙上畫出線條來,當其為 True 時表示畫筆碰觸紙張,所以可畫出線條來。

當使用者選擇畫線或橢圓時,一開始 PenDown 為 FALSE,假如此時使用者按下滑鼠左鍵引發 WM_LBUTTONDOWN,表示此時畫筆正要放下,故為起點;若引發 WM_LBUTTONDOWN 時 PenDown 為 TRUE 表示畫筆已經放下碰觸紙,所以此刻選擇的是終點。不管在引發 WM_LBUTTONDOWN 時,PenDown 為 TRUE 或為 FALSE,PenDown 都要設定為其相反值,以便畫筆再提起或再放下。

除此之外,PenDown 還有第二個作用,用來記錄當使用者選擇手繪圖後,再移動滑鼠時,畫筆是否已經放下觸紙了,若 PenDown 為 False 表示畫筆懸空移動,沒有接觸紙一樣,此時不需畫出軌跡來;若 PenDown 設為 TRUE,想像畫筆已經放下接觸紙張,此時移動滑鼠就像移動畫筆,要畫出軌跡。

手繪圖 ( 畫軌跡 )

DRAW.ASM 所畫出來的軌跡比前一章好看多了,而且線全都連在一起,其實原理很簡單。小木偶把這些軌跡想像成是由許多短線段連接而成的。在實作上,小木偶利用 WM_MOUSEMOVE 取得滑鼠移動時點的座標,再把這些點連成線即可。程式第 122 到程式第 124 行就是取的滑鼠移動時的座標,並存於 end_point,然後引發 WM_PAINT 訊息,以便畫出每一段短線段來。在 WM_PAINT 堙A檢查是否為手繪圖的功能 ( 第 169 行 ),是的話就連接起點到終點,並把起點改成終點,以便下次取得滑鼠移動時的座標時的起點。

手繪圖的起點則是在 WM_LBUTTONDOWN 時取得,在程式的第 119 行到 123 行,小木偶就不再細說了。


註一;在小木偶的網站中,有關 Win32 環境下所撰寫的組合語言程式,大部分都是會顯示視窗,他們或多或少都會使用 GDI 以便在桌面上顯示視窗,這些程式是含有 UI 資源的。但是還有一種 Win32 程式,稱為 console 程式,他們是在 DOS 模式下執行的或是在 Win XP 的 CMD.EXE 內執行的,他們沒有漂亮的圖形界面,也不含 UI 資源,但是他們仍屬於 32 位元的程式。

Console 程式和 DOS 程式是不同的。Console 程式是 32 位元的,雖然沒有漂亮的圖形界面,但是仍可以呼叫非 GDI 的 Win32 API,此外他的格式也和所有有漂亮圖形界面的 Win32 程式一樣,都是屬於微軟新定義的可執行檔格式,即所謂的 PE 格式 ( portable executable )。

註二︰當我們所撰寫的程式變大時,所需相關檔案也將會變多,在發展程式過程常常會修改某些檔案,但是如果每次都要重頭開始組譯、連結,將會耗去許多時間。有一個自動組譯的好用程式,名叫 NMAKE.EXE,他可以自動的依據各檔案所建立的時間判斷那些檔案已被更新,而需要重建,那些檔案則不需要重建,可以節省不少時間,您可以在 http://spiff.tripnet.se/~iczelion/download.html 免費下載 NMAKE 1.5 版,執行 NMAKE15.EXE 之後,會得到 NMAKE.ERR、NMAKE.EXE、README.TXT 三個檔案,可直接把這三個檔案移到您安裝的 MASM32\BIN 子目錄堙A方便使用。

其實 NMAKE.EXE 是不受那一種電腦語言的限制,它不僅可以自動地組譯組合語言,即使 C/C++、PASCAL 都可以使用。NMAKE.EXE 的語法為︰

NMAKE [options] [/f makefile] [/x stderrfile] [macrodefs] [targets]

NMAKE 的用法很複雜,此處小木偶僅介紹幾個常用的。首先說明 /f 參數,這個參數後面接著一個稱為 makefile 檔的文字檔,此文字檔內是記錄著可執行檔是如何產生的,假如 /f 參數省略的話,NMAKE 就會尋找內定的 makefile 檔,此內定的 makefile 檔就是現在所在目錄檔名為 makefile 的檔案。

Makefile 檔的內容一般有一個 ALL 敘述,此敘述之後是接著最後要製造出來的檔案,此檔案為最後目標,而後則是分成幾個段落,第一個段落是最後目標產生的方式,而這個最後目標可能會由另外幾個檔案經一定的步驟產生,而這幾個檔案的產生方式則分別記錄在其後的幾個段落堙A如此類推。而每個段落的格式是

標的檔案名 : 檔案一 檔案二 檔案三 ……
        程式 參數

檔案一、檔案二、檔案三……是用來產生『標的檔案』,也就是說,標的檔案和檔案一、檔案二、檔案三……有關,只要這些檔案其中的一個被修改了,就會重新產生『標的檔案』。而標的檔案產生則是由下一行的程式所產生的,程式後面所接的參數,和在 DOS 模式中所用的完全相同,就是該程式的參數。此外要注意的是,第二行的程式之前一定要空一格以上,否則會出現

 fatal error U1034: syntax error : separator missing stop

的錯誤。此外您也可以加上註解,『#』字元以後的文字全會被當成註解,而被 NMAKE 忽略,註解可以佔據一行,也可以在一行的最後面。

例如本章堙A最後得到的是 DRAW.EXE 檔,它是由 DRAW.OBJ 與 DRAW.RES 經連結器 ( LINK.EXE ) 連結產生的,而 DRAW.OBJ 則是由組譯器 ( ML.EXE ) 組譯產生的,DRAW.RES 則是由資源編譯器 ( RC.EXE ) 編譯產生的。所以 makefile 檔案寫成下面的樣子︰

# 產生 DRAW.EXE
ALL:DRAW.EXE

#連結器
DRAW.EXE:DRAW.OBJ DRAW.RES
    link /SUBSYSTEM:WINDOWS DRAW.OBJ DRAW.RES

#組譯器
DRAW.OBJ:DRAW.ASM
    ml /c /coff DRAW.ASM    # 僅組譯不呼叫連結器,故加上 /c 參數

#資源編譯器
DRAW.RES:DRAW.RC
    rc DRAW.RC

以文書處理軟體,如筆記簿或 UltraEdit32 等編輯存為 DRAW.MAK,然後在 DOS 模式下輸入

E:\HomePage\SOURCE>nmake /f draw.mak [Enter]

Microsoft (R) Program Maintenance Utility   Version 1.50
Copyright (c) Microsoft Corp 1988-94. All rights reserved.

        ml /c /coff DRAW.ASM
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: DRAW.ASM
        rc DRAW.RC
        link /SUBSYSTEM:WINDOWS DRAW.OBJ DRAW.RES
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

您就可以看見 NMAKE 為您自動產生 DRAW.EXE,在您撰寫程式過程中,即使修改某個程式,而其他程式並無修改,NMAKE 也會為您自動維護。底下是藉助 ML.EXE 會呼叫連結器的特性所製做的 makefile 檔︰

# 產生 DRAW.EXE
ALL:DRAW.EXE

# 組譯器及連結器
DRAW.EXE:DRAW.ASM DRAW.RES
# 組譯器會自動傳 DRAW.OBJ 給連結器,故 /link 之後不用再加上 DRAW.OBJ
    ml /coff DRAW.ASM /link /SUBSYSTEM:WINDOWS DRAW.RES 

# 資源編譯器
DRAW.RES:DRAW.RC
    rc DRAW.RC

底下介紹幾個 NMAKE 常用的 options。這些 options 都以『/』或『-』引導,不管大寫或小寫,結果相同。第一個,大概也是最常用的是『/a』,它表示不管檔案是否更新,全部重新建立。參數『/d』表示顯示各檔案以及相關檔案所建立時間。參數『/help』或『/?』顯示簡短的語法。至於其他的用法可參考手冊。


到第五章回到首頁到第七章