Ch 02 標準視窗

在上一章堙A小木偶建立了一個只用來顯示文字簡單的視窗,它只有一個按鈕可以關閉視窗。在這一章堙A小木偶將要建立一個標準的 Windows 9x 視窗,STDWND.EXE,它能夠收到從 Windows 作業系統傳來的訊息,當使用者把滑鼠移到邊框壓下滑鼠左鍵再移動滑鼠時,能夠調整視窗大小;當使用者把滑鼠移到視窗右上角的放大、縮至最小、關閉按鈕,再按下滑鼠左鍵時,也能對視窗做出相應動作。雖然外觀上這個程式和前一章的 message.exe 一樣,但小木偶將藉著這個程式說明『訊息』的觀念,它也是 Win32 程式設計的基礎。


原理

建立標準視窗

要建立一個標準的視窗,使視窗能夠接收系統的訊息,也能傳遞訊息給系統,所需步驟如下:

  1. 得先取得程式的模組代碼 ( module handle,稍後說明 )
  2. 建立一個視窗類別 ( window class,註一 ),視窗類別是用來定義視窗外觀、操作方法等,視窗類別的資料是由一個結構體定義,此結構體稱為 WNDCLASSEX。
  3. 以此結構體向作業系統『註冊』
  4. 以這兩個最重要的資料,模組代碼及視窗類別,向系統要求建立視窗,假如成功的話,系統都會發一個『號碼牌』當作這個視窗的『身分證』,這個號碼牌稱為視窗代碼 ( window handle ),以後系統就以此視窗代碼與我們所建立的視窗互通訊息。
  5. 到此僅僅告訴系統一個我們的程式要建立一個視窗已經準備完成,但是並沒有在螢幕上顯示出視窗來,所以還得再呼叫 Win32 API 顯示視窗。(註二)
  6. 接下來,進入訊息迴圈以獲得使用者的訊息。
  7. 再來是進入視窗函式處理訊息。

這些步驟有些程式碼很長,有些背後有很複雜的系統內部運作,但是如果您能夠仔細研究就會慢慢了解,這樣您就踏入了 Windows 程式設計的一大步了。現在先來了解訊息,它是 Windows 程式設計的重要一環,也是上述建立標準視窗的第 6 步的原理。

訊息!訊息!

每當我們執行程式後,大部分的情況下都會顯示一個視窗,當然此時 Windows 桌面上可能不只一個視窗,當我們按下鍵、移動滑鼠時,只有特定的視窗能收到來自鍵盤或滑鼠的資料,系統是怎麼辦到的呢?原來每個程式顯示視窗後都會進入一個無窮迴圈以獲得來自系統的訊息,而這些訊息是使用者按下鍵盤或滑鼠時,系統會把這些事件轉換成一個資料結構,其內包含了事件發生的時間、座標、按鍵等等資料,稱這個資料結構為訊息。在某些特殊的情形下,應用程式也能傳遞訊息給另一個程式。

更詳細地說,當使用者按下鍵盤或移動滑鼠時,這些輸入裝置會經由驅動程式把訊息傳給系統 ( 下圖訊息傳遞圖中的箭頭 a ),系統再把這些訊息存在一個稱為系統訊息佇列 ( system message queue ) 中 ( 圖中箭頭 b ),每一項訊息包含發生時間、發生時游標位置、訊息種類等,然後系統依據訊息內發生時的游標座標把訊息傳到各自的程式訊息佇列 ( application message queue ) 堙A等待程式提取訊息 ( 圖中箭頭 c )。換句話說,Windows 系統中有一個系統訊息佇列透過驅動程式與輸入裝置連接,另外 Windows 還為每個程式建立個別的程式訊息佇列,暫時存放由系統傳來的訊息。不管系統訊息佇列或各自的程式訊息佇列都被 Windows 系統管理。請參考下圖訊息傳遞過程。

當程式執行到呼叫 GetMessage ( 這是在 USER32.DLL 堶悸漱@個 API,是用來從程式訊息佇列取得訊息的,請按看詳細用法 ) 時,程式透過 USER32.DLL ( 箭頭 d ) 到各自程式訊息佇列提取訊息 ( 箭頭 e ),佇列堛滌T息便傳到我們的程式 ( 箭頭 f、g ),程式再呼叫 TranslateMessage API 把訊息中的鍵盤掃描碼轉換成 ASCII 碼,然後再呼叫 DispatchMessage API 加以分派處理訊息 ( 箭頭 h )。接下來是後半部的訊息傳遞過程,也是訊息驅動作業系統的重要觀念之一,但是比較複雜,所以小木偶稍後再說明,現在有了訊息大概概念之後,我們先來看看 STDWND 的原始程式。


原始程式

底下是 STDWND.ASM 的原始碼,看起來似乎長且複雜,但是不需太過擔心,因為這段程式大部分都是『模板』,意思是這段程式的絕大部份都是其他複雜程式的基礎,所以當您學會了這段程式之後,複雜的程式只需把它拷過去即可再稍加修改,就能使用不必另起爐灶。

        .386                                        ;01
        .model  flat,stdcall                        ;02
        option  casemap:none
include         windows.inc
include         user32.inc
include         kernel32.inc 
includelib      user32.lib
includelib      kernel32.lib                        ;08

WndProc proto   :HWND,:UINT,:WPARAM,:LPARAM         ;10 宣告視窗函式原型

        .DATA
ClassName       db          "SimpleWinClass",0      ;13 視窗類別名稱
AppName         db          "Our First Window",0    ;14 
hInstance       HINSTANCE   ?                       ;15 模組代碼
hwnd            HWND        ?                       ;16 視窗代碼
CommandLine     LPSTR       ?                       ;17 命令列位址
wc      WNDCLASSEX      <30h,?,?,0,0,?,?,?,?,0,offset ClassName,?>
msg     MSG             <?>                         ;19

        .CODE
start:  invoke  GetModuleHandle,NULL                ;22 取得模組代碼
        mov     hInstance,eax                       ;23
        invoke  GetCommandLine                      ;24 取得命令列字串參數位址
        mov     CommandLine,eax                     ;25
        mov     wc.style,CS_HREDRAW or CS_VREDRAW   ;26
        mov     wc.lpfnWndProc,offset WndProc       ;27
        mov     eax,hInstance                       ;28
        mov     wc.hInstance,eax                    ;29
        mov     wc.hbrBackground,COLOR_WINDOW+1     ;30
        invoke  LoadIcon,NULL,IDI_APPLICATION       ;31 取得圖示代碼
        mov     wc.hIcon,eax                        ;32 存入圖示代碼
        mov     wc.hIconSm,eax                      ;33 存入小圖示代碼
        invoke  LoadCursor,NULL,IDC_ARROW           ;34 取得游標代碼
        mov     wc.hCursor,eax                      ;35 存入游標代碼
        invoke  RegisterClassEx,offset wc           ;36 註冊視窗類別

        invoke  CreateWindowEx,NULL,offset ClassName,offset AppName,\ 
                WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,\ 
                CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,hInstance,NULL 
        mov     hwnd,eax                        ;41
        invoke  ShowWindow,hwnd,SW_SHOWDEFAULT  ;42
        invoke  UpdateWindow,hwnd               ;43

gt_msg: invoke  GetMessage,offset msg,NULL,0,0  ;45
        or      eax,eax                         ;46
        jz      wm_qut                          ;47
        invoke  TranslateMessage,offset msg     ;48
        invoke  DispatchMessage,offset msg      ;49
        jmp     gt_msg                          ;50
wm_qut: mov     eax,msg.wParam                  ;51 程式結束
        invoke  ExitProcess,eax                 ;52

WndProc proc    hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM 
        cmp     uMsg,WM_DESTROY                 ;55
        jne     msg_process                     ;56
        invoke  PostQuitMessage,NULL            ;57
        jmp     exit                            ;58
msg_process:                                    ;59
        invoke  DefWindowProc,hWnd,uMsg,wParam,lParam
        ret                 ;61
exit:   xor     eax,eax     ;62
        ret                 ;63
WndProc endp                ;64

end     start

底下是它們的分析說明。在分析之前,有句話想跟初學者說明,假如您是第一次學 Win32 組合語言程式設計,訊息傳遞過程是重要的部份,應該先了解,至於程式所使用的結構體中有許多欄位,這些都是末節,應該等到訊息傳遞過程了解之後再來細看。

第一行到第十行是載入包含檔及記憶體模式等宣告,在第一章已有說明。


資料段

第 13 行到第 19 行是資料段,第 15 行到第 19 行的資料型態似乎很特別,其實 HINSTANCE、HWND、LPSTR 這些都是雙字組的長度,它們都被定義在 WINDOWS.INC 堙A您如果都寫成『DD』其實也可以,但一般寫成 HINSTANCE、HWND、LPSTR,是為了可讀性。當您看到

hInstance       HINSTANCE       ?

就知道 hInstance 這個變數是用來表示某一個模組代碼 ( instance handle ),至於什麼是 instance 呢?又,什麼是 handle 呢?原來當程式保存在在磁碟片上是『死』的,因為在磁片上的程式沒有載入記憶體之前不能被執行,只有載入記憶體後才可被執行的程式,稱為執行實例 ( instance )。這是保留自 Win16 ( 可視為 Win 3.1 ) 的術語,在 Win32 系統中,instance 就是 module ( 模組 ) 的意思,所以在程式第 22 行,呼叫 GetModuleHandle 是取得模組的代碼,卻放在 hInstance 變數堙C

hwnd            HWND            ?
CommandLine     LPSTR           ?

這兩行是定義視窗代碼 ( window handle ) 以及字串長程指標 ( long pointer of string,可參考第零章 )。在平滑模式下的記憶體指標都以雙字組表示,稱為長程指標。這是有別於 16 位元系統,在 64KB 以內的稱為近程 ( near ),64KB 以外的稱為遠程 ( far ),近程與遠程都是 16 位元的遺物了。CommandLine 是用來表示使用者從命令列 ( 例如 DOS 模式中 ) 所輸入參數字串的位址,例如如果您在 DOS 模式下輸入

H:\HomePage\SOURCE>stdwnd ABCD [Enter]

那麼『ABCD』會被視為字串,其位址將會存放於 CommandLine 變數中。

變數 hwnd 將會存放我們程式產生的視窗代碼 ( window handle ),每一個執行實例不一定只產生一個視窗,請看註二,所以視窗代碼與模組代碼並不相同,當我們要更新某個視窗畫面時,就要指定那一個視窗要被更新,因此除了定義模組代碼外,也要定義視窗代碼。

WNDCLASSEX 結構體

第 18 行的 wc 是一個結構體 ( 參考組合語言第十九章結構體的說明 ),它定義了視窗的外觀及操作方式等等,這個結構體在 Windows 環境是很重要的變數。您可以從 Win32 API HELP 檔查到它的內容及其說明,它長得像底下左邊的樣子:

typedef struct _WNDCLASSEX {    // wc       WNDCLASSEX STRUC
    UINT    cbSize;                           cbSize            DWORD      ?
    UINT    style;                            style             DWORD      ?
    WNDPROC lpfnWndProc;                      lpfnWndProc       DWORD      ?
    int     cbClsExtra;                       cbClsExtra        DWORD      ?
    int     cbWndExtra;                       cbWndExtra        DWORD      ?
    HANDLE  hInstance;                        hInstance         DWORD      ?
    HICON   hIcon;                            hIcon             DWORD      ?
    HCURSOR hCursor;                          hCursor           DWORD      ?
    HBRUSH  hbrBackground;                    hbrBackground     DWORD      ?
    LPCTSTR lpszMenuName;                     lpszMenuName      DWORD      ?
    LPCTSTR lpszClassName;                    lpszClassName     DWORD      ?
    HICON   hIconSm;                          hIconSm           DWORD      ?
} WNDCLASSEX;                               WNDCLASSEX ENDS

這是為了 C/C++ 的習慣才寫成這樣。在 WINDOWS.INC 堣]有視窗類別的定義,是像上面右邊白色的樣子,這才是組合語言結構體的樣子,它和在 Win32 API HELP 的定義都是一樣的,只是缺乏說明。在 Win32 組合語言埵陶\多文件都要參考用 C/C++ 所習慣的文件,所以您如果有 C/C++ 的基礎也是不錯。在這個結構體堙A所有的變數都是雙字組,在向 Windows 作業系統註冊前應該要把所有資料填好,底下是 WNDCLASSEX 結構體的說明:

  1. cbSize:這是結構體長度,以位元組為單位。共有 12 個欄位 ( field ),且每個欄位是 4 個位元組,故填上 30H。之所以有這個欄位是為了 API 版本的相容性。

  2. style:用來表示屬於此視窗類別的視窗的風格,在第 26 行有
            mov     wc.style,CS_HREDRAW or CS_VREDRAW
    ,CS_HREDRAW 是表示當使用者把滑鼠移到視窗兩邊的邊框並壓下滑鼠右鍵,水平地改變視窗大小,Windows 會負責重新繪製視窗。CS_VREDRAW 只是改成垂直方向。您可以打開 WINDOWS.INC 察看 CS_HREDRAW 和 CS_VREDRAW 分別是兩個數,2 和 1。除了這兩個外,還有其他許多種可用選項,請參考 Win32 API HELP。

  3. lpfnWndProc:這個欄位是指向一個視窗函式 ( window procedure ) 的位址,視窗函式是依據使用者給的訊息執行不同的處理動作,也可以說視窗函式表現出該視窗的操作方式或視窗的行為,當使用者在視窗點按滑鼠或按下鍵盤時,視窗如何回應都由這個視窗函式處理。至於使用者輸入的訊息如何傳入與 DispatchMessage 有關,這部分的說明稍後再說。

  4. cbClsExtra:此欄位告訴系統註冊此視窗類別,還要額外保留多少位元組,這些額外的位元組是給您自己定義屬於自己的屬性,系統不用到這些屬性,必須您自己設計程式來用它。

  5. cbWndExtra:此欄位是用來告訴系統,以此視窗類別產生一個視窗,還要額外保留多少位元組。

  6. hInstance:模組代碼,表示此視窗類別屬於那一個程式模組。

  7. hIcon:視窗圖示代碼。

  8. hCursor:游標代碼。當游標移到該視窗工作區 ( client area ) 時的形狀。

  9. hbrBackground:工作區背景顏色。hbr 表示此欄位是一個刷子代碼,刷子是用來把顏料塗在紙上的,這堛磳雃b工作區著色的意思。系統已經定義了一些顏色值,例如 COLOR_WINDOW,但是系統中規定必須再加上一。

  10. lpszMenuName:表示此視窗類別的選單名稱,這是一個指標,指向一個以零為結尾的字串。假如此欄位為零,可以在呼叫 CreateWindowEx 堛滌捊ぅw義,如果兩者都為零的話,表示沒有選單。

  11. lpszClassName:此視窗類別名稱,也是一個指標,指向視窗類別名稱,也是以零為結尾。

  12. hIconSm:小圖示代碼,這是用來顯示在工作列上的圖示,假如此欄位為零,表示和圖示相同,只是縮小而已。

MSG 結構體

第 19 行是定義一個結構體變數,msg,這個結構體是用來和系統傳遞訊息用的,至於它的內容稍後說明。


程式碼段

從第 21 行開始到結束是程式碼段,大致可分為四部份,建立並在螢幕上顯示視窗 ( 第 22 行到第 43 行 )、進入無窮訊息迴圈得到訊息 ( 第 45 行到第 50 行 )、處理訊息的視窗函式 ( 第 54 行到第 64 行 ) 及結束程式 ( 第 51、52 行 )

建立並在螢幕上顯示視窗

GetModuleHandle API

程式一開始便是得到程式本身的模組代碼,GetModuleHandle 這個 API 就是用來取得模組代碼的,它的樣子如下:

HMODULE GetModuleHandle(
    LPCTSTR lpModuleName    // address of module name to return handle for
);

它只有一個參數,這個參數是指向程式名稱字串位址,這個字串必需以零為結尾。例如您想取得 USER32.DLL 的模組代碼,可以用

szUSER32    db  'USER32.DLL',0
invoke      GetModuleHandle,addr szUSER32

假如參數為 NULL 的話,表示取得本身的模組代碼。GetModuleHandle 假如成功的話,傳回的模組代碼將存於暫存器 EAX,這個數值稍後會用到,所以儲存在 hInstance 變數堙C

GetCommandLine API

接下來的是 GetCommandLine API,這個 API 是用來取得命令列的參數字串位址,它本身不用參數,返回時會將命令列參數字串之位址存於 EAX 暫存器堙C假如不須處理命令列參數,這一步可以省略。GetCommandLine 的用法如下;

LPTSTR GetCommandLine(VOID)

第 26 行到第 36 行是把 wc 結構體變數填好並向系統註冊視窗類別。在 wc 結構體堛漲釣Ь璁鴠i以直接在資料段就賦予數值,像 cbSize、style、lpfnWndProc 等等,但是這樣做會使得第 18 行變得太長,所以小木偶還是在程式堻]定其數值。其中 LoadIcon、LoadCursor 分別是圖示、游標的代碼,由 LoadIcon 及 LoadCursor 這兩個 API 負責取得其代碼。

LoadIcon API

LoadIcon 是用來載入程式的圖示,查 Win32 API 可得:

HICON LoadIcon(
    HINSTANCE   hInstance,      // handle of application instance
    LPCTSTR     lpIconName      // icon-name string or icon resource identifier
   );

由上述資料知,LoadIcon 有兩個參數,第一個參數是模組代碼,第二個是指向圖示資源名稱的位址,此位址必須以 0 為結尾。系統已經預先定義了幾個圖示可供使用,假如要用系統預定的圖示,那麼 hInstance 要設為 NULL,而 lpIconName 可以是下面幾種情形:

符號 數值描述
IDI_APPLICATION32512內定值
IDI_ASTERISK32516 英文字母『i』,通常用於通知
IDI_EXCLAMATION32515 驚歎號,通常用於警告
IDI_HAND32513 交叉,通常用於嚴重警告
IDI_QUESTION32514 問號,通常用於提示訊息

當呼叫 LoadIcon 成功時,會在 EAX 傳回圖示代碼,若失敗,則 EAX 為零。

LoadCursor API

這個 API 是用來傳回游標代碼。查 Win32 API 可得

HCURSOR LoadCursor(
    HINSTANCE   hInstance,      // handle of application instance
    LPCTSTR     lpCursorName    // name string or cursor resource identifier  
   );

得知此 API 返回時會傳回游標代碼於 EAX,它需要兩個參數,模組代碼與指向滑鼠游標資源名稱的位址,此滑鼠游標資源名稱必須以 0 為結尾。同樣的系統也預先定義了幾個可用的游標,假如要使用系統所定義的游標,則 hInstance 必須設為 NULL,而 lpCursorName 可以是下面幾種情形:

符號 數值描述
IDC_ARROW32512 系統使用的箭頭,最常使用的滑鼠游標
IDC_APPSTARTING32650 箭頭與沙漏,當系統忙碌中使用的游標
IDC_SIZENS32645 上下方向的雙箭頭,常用於滑鼠移到視窗上下邊緣
IDC_SIZEWE32644 左右方向的雙箭頭,常用於滑鼠移到視窗左右邊緣
IDC_HAND32649手形

還有幾種情形,請自行參考 Win32 API。

RegisterClassEx API

RegisterClassEx 是向系統註冊一種視窗類別,它只有一個參數,就是結構體的位址,如果註冊失敗,EAX 為零,如果註冊成功,EAX 為非零。至於結構體所需的資料請看上面已經敘述過了,RegisterClassEx 的用法如下;。

ATOM RegisterClassEx(
    CONST WNDCLASSEX *lpwcx     // address of structure with class data
   );

CreateWindowEx API

這個 API 是建立視窗,您可以想像它是在記憶體中保留一塊記憶體描述這個視窗。它的 C/C++ 語法是:

HWND CreateWindowEx(
    DWORD     dwExStyle,    // extended window style
    LPCTSTR   lpClassName,  // pointer to registered class name
    LPCTSTR   lpWindowName, // pointer to window name
    DWORD     dwStyle,      // window style
    int       x,            // horizontal position of window
    int       y,            // vertical position of window
    int       nWidth,       // window width
    int       nHeight,      // window height
    HWND      hWndParent,   // handle to parent or owner window
    HMENU     hMenu,        // handle to menu, or child-window identifier
    HINSTANCE hInstance,    // handle to application instance
    LPVOID    lpParam       // pointer to window-creation data
   );

它有 12 個參數,這些參數描述如下:

  1. dwExStyle:視窗的額外風格。在 Win 3.1 時,有另外一個 CreateWindow API,而 Win 9x/2K/NT/XP 為了使用更多的額外風格,例如使視窗總是在最上面,而多了一個新的 API,就是 CreateWindowEx,若不用這些額外的風格,CreateWindow 和 CreateWindowsEx 兩者大致相同,這時可以使用 NULL。若要使用額外風格,則此欄數值必須指定,至於那些額外的視窗風格可用,請看 Win32 API,這些額外的視窗風格大多以 WS_EX_ 開頭,例如 WS_EX_TOPMOST 是使視窗總是在最上面。( WS 是 Window Style,EX 是 Extra )

  2. lpClassName:指向以零為結尾的視窗類別名字串位址。這個視窗類別必須是已註冊完成的視窗類別名,這樣此視窗便會擁有該視窗類別的所有風格。程式執行到此處,已經向系統註冊過一個視窗類別,其名稱為 ClassName,它是在第 18 行定義的,所以此處就指向 ClassName 的位址。

  3. lpWindowName:指向以零為結尾的視窗名稱位址。此字串會顯示在標題欄上面,假如此欄位為 NULL,視窗的標題欄就沒有字串。

  4. dwStyle:指定視窗風格,常以 WS_ 開頭 ( WS_ 是指 window styles 的意思 ),底下列出幾個常用的:
    符號 數值描述
    WS_OVERLAPPED0000000h 顯示一個沒有標題欄、邊框、系統選單、最大化、最小化、關閉按鈕的視窗
    WS_HSCROLL0100000h 顯示水平捲軸
    WS_VSCROLL0200000h 顯示垂直捲軸
    WS_OVERLAPPEDWINDOW0CF0000h 這是最常用的風格,有標題欄、最大化按鈕、最小化按鈕

  5. x、y:視窗左上角的位置。原點是螢幕最左上角的那一點,該點座標為 (0,0),每向左一點,x 座標加一,每向下一點,y 座標加一,假如您想在座標 (100,200) 顯示的話,就把 x、y 分別設為 100、200。如果這個視窗是子視窗的話,原點的位置是父視窗的左上角。假如 x、y 設為 CW_USEDEFAULT 則由系統設定。

  6. nWidth、nHeight:這兩個欄位分別是用來表視窗的寬度與長度。假如用 CW_USEDEFAULT 則表示由系統設定。

  7. hWndParent:所屬的父視窗代碼,這是為了關閉父視窗時同時也能關閉子視窗。我們所建立的視窗不屬於其他視窗故設為 NULL。

  8. hMenu:選單代碼。假如此欄為零,表示使用註冊視窗類別的選單,假如兩者皆為 NULL,表示沒有選單。

  9. hInstance:此視窗所屬的程式模組。

  10. lpParam:指向一個參數位址,此參數能夠傳訊息給我們所建立的視窗。若不需要,則設為 NULL。

假如成功地建立新視窗,則 EAX 會傳回視窗代碼,程式第 41 行把它存入 hwnd 變數堙C如果建立新視窗失敗,EAX 會被設為 NULL (零)。CreateWindowEx 會傳送 WM_NCCREATE、WM_NCCALCSIZE 和 WM_CREATE 給所屬的視窗函式。

ShowWindow API

在記憶體中建立好一個視窗後,要能在螢幕上顯示,使用者才看得見 ( 有些程式是不顯示視窗的,例如病毒 )。要顯示視窗可以呼叫 ShowWindow API,其函數原型如下:

BOOL ShowWindow(
    HWND    hWnd,       // handle of window
    int     nCmdShow    // show state of window
   );

第一個參數是想要顯示視窗的視窗代碼,第二個參數是顯示方式,常用的顯示方式如下表:

符號 數值描述
SW_HIDE0隱藏
SW_MINIMIZE6最小化
SW_MAXIMIZE3最大化
SW_SHOWDEFAULT10 由系統內定

ShowWindow 有傳回值,假如原先的視窗是可見的,EAX 不為零,反之為零。不過在這個程式堛熄レ^值不重要。

UpdateWindow API

顯示視窗後還得用 UpdateWindow 來更新工作區。假如工作區不是空無一物,UpdateWindow 會送出一個 WM_PAINT 訊息給指定視窗的視窗函式,然後進行更新。假如成功則傳回值為非零,反之為零。它只有一個參數,就是欲更新的視窗代碼。UpdateWindow 的用法是;

BOOL UpdateWindow(
    HWND hWnd   // handle of window  
   );

進入無窮訊息迴圈得到訊息

GetMessage API

接下來的第 45 到第 50 行是進入一個迴圈以得到使用者的訊息。首先是呼叫 GetMessage API,這個 API 是用來從程式訊息佇列取出一項訊息,其原型如下:

BOOL GetMessage(
    LPMSG   lpMsg,          // address of structure with message
    HWND    hWnd,           // handle of window
    UINT    wMsgFilterMin,  // first message
    UINT    wMsgFilterMax   // last message
   );

GetMessage 的第一個參數 lpMsg 是指向一個位址,這個位址是一個稱為 MSG 的結構體變數所在的位址,該結構體是用來接收從訊息佇列所傳來的訊息的結構體。當呼叫 GetMessage 時,系統會從程式訊息佇列中取出一項資料來,並把 lpMsg 所指位址的結構體內的欄位填好。MSG 的定義在 WINDOWS.INC 堙A它長得像下面的樣子:

MSG STRUC
  hwnd      DWORD      ?
  message   DWORD      ?
  wParam    DWORD      ?
  lParam    DWORD      ?
  time      DWORD      ?
  pt        POINT      <>
MSG ENDS

hwnd 是指系統將把訊息傳到那一個視窗代碼的視窗函式。message 是收到的訊息種類,Windows 定義了很多的訊息,都以 WM_ 開頭,意思是 windows message 的意思,但是在我們這個程式堙A只處理退出程式的訊息,就是 WM_QUIT ( 數值 12h )。wParam 和 lParam 是兩項額外的訊息資料,和 message 的種類有關。time 是發生訊息的時間,pt 是一個結構體,它表示發生此訊息時的滑鼠位置。

GetMessage 的第二個參數是 hWnd,表示要取得那一個視窗的訊息,假如 hWnd 為零的話,表示取得程式自己視窗的訊息。GetMessage 的第三個和第四個參數分別表示取得訊息的編號範圍,假如要取得所有訊息的話,兩者均設為零。

GetMessage 的傳回值有三種情形,假如傳回 WM_QUIT 訊息的話,EAX 為零,假如取得訊息失敗的話,EAX 為 0FFFFFFFFH,假如是其他訊息的話,EAX 為非零值也非 0FFFFFFFFh。

接下來的第 46、47 行是從 GetMessage 所得到的訊息判斷,假如是傳回 WM_QUIT 則到第 51 行結束程式,否則呼叫 TranslateMessage 和 DispatchMessage 這兩個 API 處理這些訊息。結束程式還得做一些解釋,待會兒再說,先來看看不是結束程式時的情形。

TranslateMessage API

這個 API 是把指定位址的 MSG 結構體的按鍵翻譯成字元,再填回程式訊息佇列等待下次的 GetMessage 取回,詳細情形請參考第三章有關 WM_CHAR 訊息的說明。它只有一個輸入參數,就是 MSG 結構體的位址。假如轉換成功,傳回非零值,否則傳回零。其實在這個程式堙A小木偶並沒有處理按鍵,所以也可以省略這個 API。TranslateMessage 的原型是:

BOOL TranslateMessage(
    CONST MSG *lpMsg 	// address of structure with message
   );

DispatchMessage API

DispatchMessage 是把 MSG 結構體的訊息傳給視窗函式加以處理。它只有一個參數,MSG 之位址,它的樣子像下面這樣:

LONG DispatchMessage(
    CONST MSG *lpmsg 	// pointer to structure with message
   );

它有傳回值,這個傳回值隨著程式設計師所撰寫的視窗函式的設計而不同,但是常被忽略。有關 DispatchMessage 式如何傳遞訊息給視窗函式,請看下一節,處理訊息的視窗函式。

處理訊息的視窗函式 ( window procedure )

當我們的程式自系統得到訊息之後,必須依照程式有興趣的訊息加以處理,處理這些訊息的一段程式就稱為『視窗函式』。在 Win32 系統堙A我們通常把視窗函式設計成一個副程式的形式,而程式設計師主要的工作就是修改這一段程式,使那些訊息需要加以處理以及如何處理,那些則不用。

所以小木偶一開始就說,STDWND.ASM 這個原始程式的大部分都可重複使用,您如果撰寫其他程式,也只有視窗函式這段需要修改,其餘部份幾乎不須修改。視窗函式和系統之間的訊息傳遞就是小木偶說的訊息傳遞的後半部,所以視窗函式的重要性可想而知。底下就開始說明視窗函式了。

如果您詳細檢查整段 STDWND.ASM 程式,僅僅只有在設定 wc 的第 27 行有

        mov     wc.lpfnWndProc,offset WndProc

的描述而已,既沒有呼叫 WndProc,也沒有傳遞訊息的類似描述。那麼我們的程式如何呼叫視窗函式,訊息又如何傳達到視窗函式?原來在 Windows 系統中都是採用一種稱為『call back』函式,call back 函式的意思是此函式是被系統所呼叫的函式,但是它本身卻是在程式設計師的程式堙C換句話說,當程式執行到 GetMessage,收到來自系統的訊息後,再由 DispatchMessage API 把訊息當成參數呼叫系統,然後系統再呼叫視窗函式,視窗函式處理此訊息。您一定會問為什麼要這麼麻煩,不直接由自己的程式呼叫視窗函式,這樣不是更直接簡單嗎?原因有幾個,一個重要的原因是一個程式可能會產生好幾個視窗,如果程式要自己呼叫視窗函式,那就得自己維護有關視窗代碼等性質,這樣是越俎代庖去處理作業系統該做的事,而要處理的工作反而變得更多更麻煩了。

請參考上面訊息傳遞圖,當程式執行到第 49 行

        invoke  DispatchMessage,offset msg

程式呼叫 DispatchMessage API,所謂『Dispatch』中文是『分派』的意思,其意是指把訊息分送給視窗函式處理。DispatchMessage 是在 USER32.DLL 堙AUSER32.DLL 是系統的一部份。當呼叫這個 API 時 ( 箭頭 h ),會把 msg 結構體傳給系統,由該結構體欄位 msg.hwnd,系統可以知道這是那一個視窗的訊息,然後系統再依據建立這個視窗的視窗類別堛 wc.lpfnWndProc 就可以知道那一個視窗函式應該處理這個訊息,再把訊息傳給這個視窗函式 ( 箭頭 i ),等視窗函式處理完後,再返回系統的 DispatchMessage ( 箭頭 j ),DispatchMessage 再通知原來的程式 ( 箭頭 k ),這樣一個訊息循環就處理完成,假如使用者沒有按下視窗左上角的關閉鈕,程式會再進入下一個訊息循環,直到使用者結束。

視窗函式的原型在程式第 10 行已經宣告:

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

在 Win32 系統堙A視窗函式的參數固定就是這四個,沒有什麼彈性。第一個參數是視窗代碼,因為一個程式可能有好幾個視窗,所以必須指定視窗代碼,否則系統不知道是要處理那一個視窗。第二個參數是訊息編號,第三和第四個參數是額外的訊息資料,您可以發現,事實上視窗函式所需的參數,其實就是 MSG 結構體的前四欄。視窗函式名稱可任意取,此處小木偶命名 WndProc。

在撰寫視窗函式時,還有一件事情要注意,那就是在視窗函式結束後返回系統前,必需保存 EBX、ESI、EDI、EBP,否則必定造成當機。這是因為 Windows 系統內必使用這些暫存器當作指標,所以得注意保存這四個暫存器。其實不只是視窗函式如此,所有被系統呼叫的副程式,像 Windows API,都必須在返回系統前恢復這四個暫存器的數值。

第 54 行是 proc 假指令,它和以前在 DOS 時代的用法差不多,請看第一章註三的說明。

第 55 行是把藉由系統傳過來的訊息和 WM_DESTROY 比較,假如不是 WM_DESTROY 訊息的話則到第 60 行呼叫 DefWindowProc API。至於為什麼是 WM_DESTROY 訊息而不是 WM_QUIT 呢?WM_DESTROY 又如何產生的?稍後的結束程式再說明。

DefWindowProc API

DefWindowProc API 是系統內定的處理訊息函式,它能處理許多系統內定的訊息,例如使用者把滑鼠移到視窗邊框改變視窗大小、按下視窗右上角的最大化或最小化按鈕等,這些訊息均可由 DefWindowProc 處理。DefWindowProc 的語法是;

LRESULT DefWindowProc(
    HWND    hWnd,   // handle to window
    UINT    Msg,    // message identifier
    WPARAM  wParam, // first message parameter
    LPARAM  lParam  // second message parameter
   );

通常在視窗函式堙A把我們有興趣的訊息優先寫在前面,而最後都應該呼叫 DefWindowProc 來處理系統內定的訊息處理方式。當 DefWindowProc 處理完訊息後,會把傳回值存入 EAX,EAX 的數值會和處理的結果有關,它也會可能影響下一次的訊息,所以不可再改變 EAX 之值,應該直接返回系統。至於我們自己處理的訊息,假如正確無誤的話,也應該遵守 Window 系統的規定,把 EAX 設為零傳回給系統。

所以假如我們對兩個訊息,WM_AAA、WM_BBB,需要處理,那麼視窗函式應該要這樣寫︰

WndProc proc    hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM 
        cmp     uMsg,WM_AAA
        jne     nxt1
        處理 WM_AAA 訊息的程式
        jmp     exit
nxt1:   cmp     uMsg,WM_BBB
        jne     nxt2
        處理 WM_BBB 訊息的程式
        jmp     exit
nxt2:   cmp     WM_DESTROY
        jne     nxt3
        invoke  PostQuitMessage,NULL
        jmp     exit
nxt3:   invoke  DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
exit:   xor eax,eax
        ret
WndProc endp

最後還得再加上處理視窗已被摧毀的 WM_DESTROY 訊息,也就是處理結束的程式。以上大致是視窗函式的雛形,可謂數學『公式』,所以小木偶特別以紅色表示。

此程式的第 55 行是一條比較指令,比較 WndProc 參數和 WM_DESTROY 這個訊息是否相同,假如相同的話會跳到第 57 行執行 PostQuitMessage API。PostQuitMessage 僅僅使系統把 WM_QUIT 訊息放到程式訊息佇列堙A等待程式以 GetMessage 提取 WM_QUIT 訊息。

PostQuitMessage API

PostQuitMessage API 的語法如下;

VOID PostQuitMessage(
    int nExitCode   // exit code
   );

nExitCode 是程式結束時的傳回值。至於結束程式的過程以及訊息發生傳遞的經過,還有應該注意的事項,在結束程式婸〝。

結束程式

WM_CLOSE、WM_DESTROY、WM_QUIT 訊息

當使用者按下視窗右上角的關閉鈕或者由選單的『檔案』選擇『離開』時,系統會把 WM_CLOSE 訊息存入該程式的程式訊息佇列堙A當程式呼叫 GetMessage API 時會傳回 WM_CLOSE 訊息,此訊息藉由 backcall 方式把此訊息傳給視窗函式。通常我們不處理這個訊息,而交由 DefWindowProc 去處理。DefWindowProc 收到 WM_CLOSE 後自己不處理而是呼叫 DestroyWindow API 處理,DestoryWindow 會摧毀視窗,並發出 WM_DESTROY 訊息填入該視窗的程式訊息佇列堙A當視窗函式收到此訊息和 WM_DESTROY 比對後便跳到第 57 行呼叫 PostQuitMessage API,PostQuitMessage 這個 API 僅僅把 WM_QUIT 傳給系統,這樣當 GetMessage 再次取得的訊息就是 WM_QUIT,於是程式第 45 行到第 50 行的訊息迴圈得以結束。

您一定覺為何還要呼叫 PostQuitMessage ?視窗既已摧毀不就完成了嗎?事實上不是這麼簡單。假如不這樣做的話,表面上視窗已經消失不見,但是實際上程式的訊息迴圈仍殘留在記憶體堙A還繼續消耗系統資源。不信的話,您可以把此原始碼的 PostQuitMessage 那一行加上『;』再組譯看看,然後執行它再結束這個程式,您會發現能夠順利視窗會在螢幕上消失,但是如果您按下 Ctrl-Alt-Del,會出現一個視窗,其內容是正在執行的程式列表。您可以找到 Stdwnd 這個程式。所以程式設計師要注意隨時保持視窗函式與訊息迴圈之間的聯繫。

最後還有一個問題,為何 DefWindowProc 不在收到 WM_DESTORY 就自動呼叫 PostQuitMessage 而要把這個工作留給程式設計師呢?原來是一個程式可能建立好幾個視窗,假如 DefWindowProc 自動交由 PostQuitMessage 來退出程式,那麼便會把所有程式所建立的視窗關閉,這樣有時會有不合理的現象。例如,假如您在 Word 開啟舊檔時出現一個對話盒視窗,當選定檔案後這個視窗就會關閉,卻被 DefWindowProc 因呼叫 PostQuitMessage 而退出 Word,這樣不是很奇怪嗎?

這一章就到此處結束,下一章將介紹如何鍵盤輸入並把使用者所按下的鍵顯示在視窗上。


註一:類別是物件導向常用的術語,在物件導向的程式設計觀念堙A把螢幕上看到的東西,如視窗、按鈕等都看成真實生活上的物體,每物體都有自己的特性及使用方法,例如書本就是用來記載文字圖片的東西,它們都有相似的外觀,都是一頁、一頁印滿文字圖片的紙張裝訂成冊,書的使用方法也都類似。在 Win32 系統上面有許多的視窗,有些外觀相似,操作方法也相似,於是程式設計時也採用相同的觀念,把這些視窗歸納成同一類別。當我們設計程式時,免不了要用到許多視窗,於是先構想這些視窗的外觀、操作方法,然後再使這些視窗具有相同的類別,於是它們的外觀、操作方法都一樣。

註二:一定有許多人問兩個問題:『視窗與程式的關係』與『執行實例、程式、模組的關係』。

一個程式不一定就是一個視窗,例如您在拷貝檔案時,如果目的資料夾有相同檔名時,就會彈出一個小視窗問您是否覆蓋,這樣這個拷貝的程式就擁有兩個視窗了,以術語來說就是『此執行實例有兩個視窗』。也有程式是不產生視窗的,例如還未發作的病毒和木馬程式都不產生視窗。我想大概可以這麼說,一個程式可以不產生視窗、也可以產生一個視窗,也可以產生一個以上的視窗,看需要而定吧。


到第一章到首頁到第三章