Ch 05 取得滑鼠訊息

本章將介紹如何處理滑鼠的輸入訊息。


原理

WM_LBUTTONDOWN / WM_LBUTTONUP、
WM_RBUTTONDOWN / WM_RBUTTONUP、
WM_MBUTTONDOWN / WM_MBUTTONUP 訊息

一般滑鼠上有兩個按鍵,分別稱為滑鼠左鍵以及滑鼠右鍵,也有三鍵的滑鼠,中間的稱為滑鼠中鍵。當滑鼠游標在某個視窗的工作區 ( client area ) 時,這些按鍵按下會分別產生 WM_LBUTTONDOWN、WM_RBUTTONDOWN 以及 WM_MBUTTONDOWN 訊息;反之放開時,則分別產生 WM_LBUTTONUP、WM_RBUTTONUP 以及 WM_MBUTTONUP 訊息。

這些訊息中的 lParam 欄位堻ㄕs有這些事件發生時的滑鼠位置,其中較低的 16 位元為當時的 X 座標,較高的 16 位元為當時的 Y 座標,該座標是把視窗工作區左上角的座標定為 (0,0),而 X 座標向右增加,Y 座標向下增加的方式表示。

而 wParam 欄位堳h存有當時鍵盤的狀態。假如在按下滑鼠上的按鍵時,也同時按下鍵盤上的鍵,那麼 wParam 會存有該按鍵的虛擬鍵碼。假如按下滑鼠左鍵再按滑鼠右鍵,那麼 wParam 會有存有 MK_RBUTTON。詳細的數值如下表:

數值說 明
MK_CONTROL鍵盤上的 Ctrl 鍵被按下
MK_LBUTTON滑鼠左鍵被按下
MK_MBUTTON滑鼠中鍵被按下
MK_RBUTTON滑鼠右鍵被按下
MK_SHIFT鍵盤上的 Shift 鍵被按下
MK_XBUTTON1第一個滑鼠延伸的按鍵被按下
MK_XBUTTON2第二個滑鼠延伸的按鍵被按下

WM_MOUSEMOVE 訊息

另外還有一個與滑鼠有關的訊息,WM_MOUSEMOVE。顧名思義,這個訊息是系統通知程式,滑鼠游標在視窗座標的哪個位置上,此座標是以視窗工作區 ( client area ) 的左上角為 ( 0,0 )。系統會把滑鼠游標的相對於工作區左上角的座標放在 WM_MOUSEMOVE 的 lParam 參數媔З僱礸﹛AlParam 的較高的 16 位元是 y 座標,較低的 16 位元是 x 座標。wParam 欄位則存有當時的按鍵情形,和 WM_LBUTTONDOWN 一樣。

WM_MOUSEMOVE 有一個地方與其他滑鼠訊息不同,那就是即使視窗不是正在作用中,系統也會發出 WM_MOUSEMOVE 訊息給應用程式。換句話說,在 Windows 系統桌面上的視窗,只要滑鼠游標從視窗上滑過,系統就會發出 WM_MOUSEMOVE 訊息給該視窗。

GetCursorPos API

但是如果滑鼠游標在視窗外,要取得滑鼠座標,就無法由 WM_MOUSEMOVE 訊息取得了,這時必須用 GetCursorPos API 來取得滑鼠位置,其原型是

BOOL GetCursorPos(
    LPPOINT lpPoint     // address of structure for cursor position  
   );

這個 API 僅一個參數,它指向一個 POINT 結構體的位址,當呼叫 GetCursorPos API 時,GetCursorPos 會把滑鼠座標填入此結構體。此結構體內容如下,

POINT   STRUCT
x       DWORD   ?
y       DWORD   ?
POINT   ENDS

既然有了 GetCursorPos,即使滑鼠游標不在視窗內,也可以取得游標位置,但要怎樣觸發這個 API 呢?一般的做法是利用 SetTimer 定時對視窗發出 WM_TIMER 訊息,然後處理此訊息時呼叫 GetCursorPos 以取得滑鼠游標位置。


原始程式︰畫出軌跡

底下的 MOUSE4.ASM 程式會在視窗左邊顯示紅、藍、綠三個邊長為 70 點的正方形,其作用相當於調色盤,當滑鼠游標移到這三個正方形,再單擊滑鼠左鍵可以選擇紅、綠、藍三色之任一種顏色。在視窗右邊是一塊空白的區域,相當於畫紙,當滑鼠移到視窗右邊,按下滑鼠左鍵可以顯示出滑鼠移動的軌跡,再按一下左鍵就不顯示軌跡,好像模擬鉛筆向下碰觸紙可以畫出線來,提起鉛筆就無法畫出線來一樣。其原始程式如下︰

;左邊有調色盤,可以以滑鼠左鍵選擇顏色,畫出軌跡
        .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

red             equ     0ffh
green           equ     0ff00h
blue            equ     0ff0000h
WndProc         proto   :HWND,:UINT,:WPARAM,:LPARAM

        .DATA
ClassName       db          'SimpleWinClass',0
AppName         db          '滑鼠軌跡',0
AskExit         db          '是否退出此程式?',0
AskExitTitle    db          '詢問',0
PenDown         db          FALSE       ;23 TRUE︰下筆,FALSE︰提筆
DrawPaletteOK   db          FALSE       ;24 是否已畫出紅綠藍三個正方形調色盤
color           dd          red         ;25 畫筆顏色
color_id        dd          red,green,blue
hInstance       HINSTANCE   ?
hwnd            HWND        ?
CommandLine     LPSTR       ?
wc              WNDCLASSEX  <30h,?,?,0,0,?,?,?,?,0,offset ClassName,?>
msg             MSG         <?>
click_crd       POINT       <?> ;32 單擊滑鼠左鍵時的座標,也是小正方形的左上角座標
lw_rt_x         dd          ?   ;33 lw_rt_x 與 lw_rt_y 是小正方形的右下角座標
lw_rt_y         dd          ?   

        .CODE
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,NULL,IDI_APPLICATION
        mov     wc.hIcon,eax
        mov     wc.hIconSm,eax
        invoke  LoadCursor,NULL,IDC_ARROW
        mov     wc.hCursor,eax
        invoke  RegisterClassEx,offset wc
        invoke  CreateWindowEx,NULL,offset ClassName,offset AppName,\ 
                WS_OVERLAPPEDWINDOW,0,10,400,250,0,0,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  TranslateMessage,offset msg
        invoke  DispatchMessage,offset msg
.endw        
        mov     eax,msg.wParam
        invoke  ExitProcess,eax
;---------------------------------------
;畫出位於視窗左邊的紅、綠、藍三個邊長為 70 的正方形調色盤
DrawPalette     proc    hWin:HWND       ;67 DrawPalette 副程式開始
        local   PS:PAINTSTRUCT 
        local   hdc:HDC
        local   newPen:DWORD
        local   oldPen:DWORD
        local   newBrush:HBRUSH
        local   oldBrush:HBRUSH
        local   counter:DWORD   ;74 因為要畫三個正方形︰0-紅色,1-綠色…

        invoke  BeginPaint,hWin,addr PS ;76 取得 DC 代碼
        mov     hdc,eax                 ;77 存入 DC 代碼
        mov     counter,0               ;78 先畫出紅色正方形
nxt:    mov     edx,counter
        shl     edx,2
        mov     eax,color_id[edx]       ;81 取得顏色
        invoke  CreatePen,PS_SOLID,1,eax;82 建立新的色筆
        mov     newPen,eax              ;83 存入新色筆代碼
        invoke  SelectObject,hdc,eax    ;84 選擇新色筆
        mov     oldPen,eax              ;85 保存舊色筆代碼
        mov     edx,counter
        shl     edx,2
        mov     eax,color_id[edx]       ;88 取得顏色
        invoke  CreateSolidBrush,eax    ;89 建立新筆刷
        mov     newBrush,eax            ;90 保存新筆刷代碼
        invoke  SelectObject,hdc,eax    ;91 選擇新筆刷
        mov     oldBrush,eax            ;92 保存舊筆刷代碼
        mov     eax,counter             ;93 計算正方形區域範圍,
        mov     dl,70   ;94 該正方形左上角座標為 (0,70*counter)
        mul     dl      ;95 ,右下角座標是 (69,70*counter+69)
        mov     edx,eax
        add     dx,69
        invoke  Rectangle,hdc,0,eax,69,edx      ;98 畫出正方形並著色
        invoke  SelectObject,hdc,oldPen         ;99 恢復舊色筆
        invoke  DeleteObject,newPen             ;100 刪除新色筆
        invoke  SelectObject,hdc,oldBrush       ;101 恢復舊筆刷
        invoke  DeleteObject,newBrush           ;102 刪除新筆刷
        inc     counter
        cmp     counter,3
        jne     nxt
        invoke  EndPaint,hWin,addr PS
        ret
DrawPalette     endp
;---------------------------------------
;由 WM_LBUTTONDOWN、WM_MOUSEMOVE 等訊息的 lParam 算出 x、y 座標,
;並存於 click_crd 結構體及使 click_crd 之 x、y 座標再加上 5
get_mouse       proc    crd:LPARAM
        mov     eax,crd
        mov     edx,5
        and     eax,0ffffh
        mov     click_crd.x,eax
        add     eax,edx
        mov     lw_rt_x,eax
        mov     eax,crd
        shr     eax,10h
        mov     click_crd.y,eax
        add     eax,edx
        mov     lw_rt_y,eax
        ret
get_mouse       endp
;---------------------------------------
;當畫筆接觸紙時,依所指定的顏色及座標,畫出邊長 5 點的正方形
draw    proc    hWin:HWND
        local   hDC:HDC
        local   PS:PAINTSTRUCT
        local   newPen:DWORD
        local   oldPen:DWORD
        local   newBrush:HBRUSH
        local   oldBrush:HBRUSH

        invoke  BeginPaint,hWin,addr PS
        mov     hDC,eax
        invoke  CreatePen,PS_SOLID,1,color
        mov     newPen,eax
        invoke  SelectObject,hDC,eax
        mov     oldPen,eax
        invoke  CreateSolidBrush,color
        mov     newBrush,eax
        invoke  SelectObject,hDC,eax
        mov     oldBrush,eax
        invoke  Rectangle,hDC,click_crd.x,click_crd.y,lw_rt_x,lw_rt_y
        invoke  SelectObject,hDC,oldPen
        invoke  DeleteObject,newPen
        invoke  SelectObject,hDC,oldBrush
        invoke  DeleteObject,newBrush
        invoke  EndPaint,hWin,addr PS
        ret
draw    endp
;---------------------------------------
WndProc proc    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.if     uMsg==WM_LBUTTONDOWN            ;156 單擊滑鼠左鍵時,
        invoke  get_mouse,lParam        ;157 先算出單擊時滑鼠游標位置
        .if     click_crd.x>69          ;158 若該座標之 x 座標大於 69 時
                xor     PenDown,1       ;159 切換色筆下碰紙面或提起
        .else                           ;160 否則
                .if     click_crd.y<70  ;161 滑鼠游標在調色盤區,以 y 座標
                        mov     color,red       ;162 為依據選擇顏色,並存於
                .elseif click_crd.y<140         ;163 color 變數
                        mov     color,green
                .else
                        mov     color,blue
                .endif
        .endif

.elseif uMsg==WM_MOUSEMOVE      ;170 移動滑鼠時
        .if     PenDown         ;171 檢查畫筆是否向下碰觸紙,假如是的話就
                invoke  get_mouse,lParam        ;172 取得移動時滑鼠的位置
                .if     click_crd.x<70  ;173 檢查滑鼠位置是否在調色盤區
                        mov     PenDown,FALSE   ;174 是的話,使畫筆提起
                .else           ;175 否的話,設定無效區
                        invoke  InvalidateRect,hWnd,offset click_crd,TRUE
                .endif
        .endif

.elseif uMsg==WM_PAINT          ;180 重繪時
        .if     DrawPaletteOK   ;181 先檢查是否已經畫好紅綠藍調色盤
                invoke  draw,hWnd               ;182 是的話,畫出軌跡
        .else
                invoke  DrawPalette,hWnd        ;184 否的話,畫出調色盤
                mov     DrawPaletteOK,TRUE      ;185 設定已經畫好調色盤了
        .endif

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

.elseif uMsg==WM_DESTROY
        invoke  PostQuitMessage,NULL

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

end     start

在執行這個程式時,您畫出的軌跡可能會斷斷續續的,除非是您慢慢的移動滑鼠,這些點才會是連續的,改善的方法可以用畫線代替畫點。至於畫線可用 MoveToEx API 來達成。


解說

有關這個程式的邏輯,小木偶已寫在原始程式的註解中,故不再贅敘,底下將介紹如何改變顏色及繪圖的觀念。

事實上第三章已經稍微介紹過如何在視窗上顯示資料︰首先要設定無效區以發出 WM_PAINT 訊息,當程式收到 WM_PAINT,必須以 BeginPaint 及 EndPaint 這兩個 API 為起始及結束來初始化裝置內文及結束裝置內容 ( device context ),而我們要如何繪製圖形的程式碼則夾在這兩個 API 之間。底下將更進一步介紹如何改變這些圖形及文字的顏色、樣式等等。

在裝置內容堙A有關圖形或文字的樣式,很像真實世界的實物,例如要畫一個紅色的矩形,那麼您必須先有一個紅色的畫筆,然後再選擇這個紅色筆,當畫完後要把它放回原處。所以,我們執行的方式是以 CreatePen 建立畫筆,以 SelectObject 為裝置內容選擇畫筆,以 DeleteObject 刪除畫筆,再用 SelectObject 還原原色筆。

CreatePen API

原型為︰

HPEN CreatePen(
    int         fnPenStyle, // pen style 
    int         nWidth,     // pen width  
    COLORREF    crColor     // pen color 
   );

crColor 是畫筆的顏色,nWidth 是指畫筆的寬度,假如 nWidth 為 0,那麼 CreatePen 會視為一,而忽略零值,fnPenStyle 是畫筆的樣式,常用的樣式如下表所示︰

數值 樣式 數值 樣式
PS_SOLID實線 PS_DASH虛線,由許多短線組成
PS_DOT點線,由許多點組成的線 PS_DASHDOT短線、點交互組成的線
PS_DASHDOTDOT由短線、兩點交互組成的線 PS_NULL看不見的線

上表的 PS_ 應該是指 pen style 的意思,除了 PS_SOLID、PS_NULL 外,其餘的樣式都只有在線寬為一時可正常顯示出線來。CreatePen 呼叫成功會傳回該色筆的代碼,設計者可以用 SelectObject 把此色筆代碼選入裝置內容堙A假如呼叫失敗會傳回 NULL。

SelectObject API

這個 API 是把新建立的物件選入指定的裝置內容堙A取代舊物件,其原型為︰

HGDIOBJ SelectObject(
    HDC     hdc,    // handle of device context 
    HGDIOBJ hgdiobj // handle of object  
   );

hdc 是指定的裝置內容,hgdiobj 是要被選入裝置內容的圖形物件代碼,可以用的圖形物件有位元圖 ( Bitmap )、筆刷 ( Brush )、字型 ( Font )、畫筆 ( Pen )、範圍 ( Region ) 等,建立這些圖形物件的詳細方法,請自行參考 Win32 Programmer's Reference。

如果選擇的物件不是範圍的話,呼叫 SelectObject 成功,傳回值為舊的物件代碼,程式可以保留舊的圖形物件代碼,以便還原,如果用不到舊的圖形物件代碼也可以把它刪除;若呼叫失敗,傳回值為 NULL。如果選擇的物件是範圍的話,呼叫成功時,傳回值為 SIMPLEREGION ( 一個矩形範圍 )、COMPLEXREGION ( 多個矩形範圍 ) 或是 NULLREGION ( 空範圍 );如果呼叫失敗,則傳回 GDI_ERROR。

DeleteObject API

DeleteObject 是用來刪除畫筆、筆刷、字型、位元圖、範圍或或調色盤 ( Palette ),以節省資源。其原型為

BOOL DeleteObject(
    HGDIOBJ hObject     // handle to graphic object  
   );

hObject 是要刪除的物件代碼。DeleteObject 成功刪除物件時,會傳回非零值,失敗時傳回 NULL。

CreateSoildBrush API

CreateSoildBrush 會建立一個新的筆刷,原型是︰

HBRUSH CreateSolidBrush(
    COLORREF    crColor     // brush color value  
   );

crColor 是筆刷的顏色,假如呼叫成功,會傳回筆刷代碼,失敗則傳回 NULL。筆刷是用來把一塊範圍塗上指定的顏色及各種不同的樣式,CreateSolidBrush 會建立一個塗滿顏色的筆刷,其他的 API 也可以建立不同樣式的筆刷,像 CreatePatternBrush 會建立一個圖樣的筆刷,詳細情形請自行參考 Win32 Programmer's Reference。

Rectangle API

Rectangle 會在工作區上畫出一個矩形,矩形的邊線是指定的畫筆畫出,矩形內部是以指定的筆刷塗滿。如果呼叫成功,傳回非零值,失敗傳回 NULL。Rectangle 的原型是︰

BOOL Rectangle(
    HDC hdc,        // handle of 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. of bounding rectangle's lower-right corner  
   );

hdc 是裝置內容,nLeftRect、nTopRect、nRightRect、nBottomRect 分別是矩形的左上角 X、Y 座標、右下角 X、Y 座標。

解說 DrawPalette 副程式

這段副程式是畫出視窗左邊的三塊紅、綠、藍正方形。當滑鼠移到此處,按下左鍵可以選擇此三色畫出軌跡,故小木偶稱為調色盤,這跟繪圖物件的調色盤不同。這個副程式只需要在程式剛顯示視窗時執行一次就可以了,所以小木偶在程式第 24 行設定一個 DrawPaletteOK 變數為 FALSE,只有當 DrawPaletteOK 為 FALSE 時才執行 DrawPalette 副程式 ( 第 181 行 ),等 DrawPalette 副程式執行完畢,設定 DrawPaletteOK 為 TRUE ( 第 185 行 ),這樣 DrawPalette 副程式就只有在剛顯示視窗時執行一次。

DrawPalette 的主要目的是畫出三個不同顏色的正方形,小木偶在草稿紙上先計算出這三個正方形顏色與座標關係,如下表︰

顏色左上角座標 右下角座標
(0,0)(69,69)
(0,70)(69,139)
(0,140)(69,209)

仔細觀察這些座標後,您應當會發現其正方形的左上角 X 座標均為 0,右下角 X 座標均為 69,左上角的 Y 座標均為 70 的倍數,倍率由零開始,而右下角 Y 座標均為左上角 Y 座標加 69。有了這個關係之後,小木偶設一個區域變數,counter,來表示第幾個正方形,而其左上角座標很容易就可用 ( 0, 70*counter ) 表示,右下角座標可用 ( 69, 70*counter+69 ) 表示。

至於顏色,小木偶是利用程式 counter 來當做指標,指向程式第 26 行的 color_id 所列出的顏色值,不過這個指標之值不是 0、1、2,而是 0、4、8,所以程式第 79 行到第 81 行把 counter 乘以 4 再存入 EDX。換句話說 EDX 才是真正指標,但 EDX 是根據 counter 之值來計算的。

在接下來的程式堙A就是用 CreatePen 建立新色筆,用 CreateSoildBrush 建立新筆刷,再把它們分別選進裝置內容,然後畫出正方形,然後還原這些繪圖物件,然後又進入處理下一個正方形循環。


原始程式︰取得桌面上的滑鼠游標

底下這個程式,MOUSE2.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

TimerID         EQU     74
WndProc         PROTO   :HWND,:UINT,:WPARAM,:LPARAM
crd_2_string    PROTO   :DWORD,:DWORD,:DWORD,:DWORD

;*****************************************************************************************
.CONST
ClassName       DB          'SimpleWinClass',0
szTitleFmt      DB          '(%04d,%04d)',0
szMousePosiFmt  DB          '現在滑鼠在螢幕座標︰',0dh,0ah,'X=%04d,Y=%04d',0
nMouseData      EQU         $-OFFSET szMousePosiFmt
AskExit         DB          '是否退出此程式?',0
AskExitTitle    DB          '詢問',0
;*****************************************************************************************
.DATA
AppName         DB          12 DUP (0)
szString        DB          nMouseData DUP (0)
hInstance       HINSTANCE   ?
hwnd            HWND        ?
CommandLine     LPSTR       ?
wc              WNDCLASSEX  <30h,?,?,0,0,?,?,?,?,0,OFFSET ClassName,?>
msg             MSG         
click_crd       POINT       
;*****************************************************************************************
.CODE
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,NULL,IDI_APPLICATION
        mov     wc.hIcon,eax
        mov     wc.hIconSm,eax
        INVOKE  LoadCursor,NULL,IDC_ARROW
        mov     wc.hCursor,eax
        INVOKE  RegisterClassEx,OFFSET wc
        INVOKE  CreateWindowEx,0,OFFSET ClassName,OFFSET AppName,WS_SYSMENU or WS_MINIMIZEBOX\
                ,0,10,210,70,0,0,hInstance,NULL                 ;051 建立無邊框的視窗
        mov     hwnd,eax
        INVOKE  ShowWindow,hwnd,SW_SHOWDEFAULT
        INVOKE  UpdateWindow,hwnd

.WHILE  TRUE
        INVOKE  GetMessage,OFFSET msg,NULL,0,0
.BREAK .IF !eax
        INVOKE  TranslateMessage,OFFSET msg
        INVOKE  DispatchMessage,OFFSET msg
.ENDW        
        mov     eax,msg.wParam
        INVOKE  ExitProcess,eax
;-----------------------------------------------------------------------------------------
WndProc PROC    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
        LOCAL   ps:PAINTSTRUCT
        LOCAL   rcClient:RECT

.IF     uMsg==WM_CREATE
        INVOKE  SetTimer,hWnd,TimerID,300,NULL

.ELSEIF uMsg==WM_TIMER
        INVOKE  GetCursorPos,OFFSET click_crd                           ;073 取得滑鼠位置
        INVOKE  wsprintf,OFFSET AppName,OFFSET szTitleFmt,click_crd.x,click_crd.y
        INVOKE  SetWindowText,hWnd,OFFSET AppName                       ;075 設定視窗標題欄
        INVOKE  wsprintf,OFFSET szString,OFFSET szMousePosiFmt,click_crd.x,click_crd.y
        INVOKE  InvalidateRect,hWnd,NULL,TRUE

.ELSEIF uMsg==WM_PAINT
        INVOKE  BeginPaint,hWnd,ADDR ps
        INVOKE  GetClientRect,hWnd,ADDR rcClient
        INVOKE  DrawText,ps.hdc,OFFSET szString,-1,ADDR rcClient,DT_LEFT;082 顯示滑鼠位置座標
        INVOKE  EndPaint,hWnd,ADDR ps

.ELSEIF uMsg==WM_CLOSE
        INVOKE  MessageBox,hWnd,ADDR AskExit,ADDR AskExitTitle,MB_YESNO or MB_ICONQUESTION
    .IF eax==IDYES
        INVOKE  KillTimer,hWnd,TimerID
        INVOKE  DestroyWindow,hWnd
    .ENDIF

.ELSEIF uMsg==WM_DESTROY
        INVOKE  PostQuitMessage,NULL

.ELSE
        INVOKE  DefWindowProc,hWnd,uMsg,wParam,lParam
        ret

.ENDIF
        xor     eax,eax
        ret
WndProc ENDP
;*****************************************************************************************
END     start

限制改變視窗大小的方法︰dwStyle 設定不同的視窗

剛才提到,這個程式會限制使用者改變視窗的大小,這是因為其標題欄的放到最大被設為無效,而沒有設定視窗邊框,這是和程式第 50 行 CreateWindowEx API 參數的 dwStyle 有關,您可以參考 Win32 Programmer's Reference 及第二章CreateWindowEx API,底下看看幾種風格。

1.假如 dwStyle 參數用 WS_OVERLAPPED,那麼視窗沒有邊框、圖示、系統選單、縮到最小、放到最大、關閉等,所以不僅無法改變視窗大小,連關閉此程式都得按 Ctrl-Alt-Del。如右圖所示。   WS_OVERLAPPED 風格
2.假如 dwStyle 參數用 WS_SYSMENU,那麼顯示的視窗會有系統選單及關閉按鈕,但無邊框,所以可以關閉但無法改變視窗大小。   WS_SYSMENU 風格
3.假如 dwStyle 是 WS_SYSMENU or WS_MINIMIZEBOX,那麼視窗會有系統選單、關閉扭、縮到最小按鈕,但無邊框及最大化按鈕。   WS_SYSMENU or WS_MINIMIZEBOX 風格
4.假如 dwStyle 是 WS_SIZEBOX 的話,那麼視窗會形成一個邊框,但沒有系統選單、最大化、最小化、關閉按鈕,所以也只能按 Ctrl-Alt-Del 結束程式。此外 WS_SIZEBOX 和 WS_THICKFRAME 的結果是一樣的。   WS_SIZEBOX 風格
5.假如 dwStyle 是 WS_OVERLAPPEDWINDOW 的話,那麼視窗會有邊框、系統選單、最大化、最小化、關閉按鈕,如右圖所示。WS_OVERLAPPEDWINDOW 和 WS_TILEDWINDOW 效果是一樣的。   WS_TILEDWINDOW 風格

SetWindowText API

SetWindowText 用來改變視窗的標題欄。其原型如下︰

BOOL SetWindowText(
    HWND    hWnd,       // handle of window or control
    LPCTSTR lpString    // address of string
   );

hWnd 是要改變標題欄的視窗代碼,lpString 是將改變標題的字串位址,此字串須以 0 結尾。呼叫此 API 後會對改變的視窗發出 WM_SETTEXT 訊息。

wsprintf API

wsprintf 能夠把一些數值、字串以指定格式填入記憶體緩衝區堙A它的原型是:

int wsprintf( LPTSTR lpOut,
              LPCTSTR lpFmt,
              ...
   );

lpOut 是指向緩衝區位址,wsprintf 將把指定格式的數值或字串填入此緩衝區堙A最大只能存入 1024 個位元組。lpFmt 是指定格式化字串的位址,此字串的內容稍後再描述。lpFmt 之後的參數是要顯示的變數,這些變數的個數並不一定,這大概是 wsprintf 的特色,在 Win32 API 堙A除了 wsprintf 與其類似的 API 之外,似乎沒有其他 API 像這樣參數個數是不固定的,或者小木偶孤陋寡聞以致於沒見過。如果 wsprintf 執行成功,則 EAX 埵s放的是有多少位元組被填入緩衝區堙A但不包含最後的 NULL 字元;如果失敗,EAX 之值小於 lpFmt 所指字串的長度。

lpFmt 是格式化字串位址,格式化字串是用來指示數字的格式,例如顯示多少位數,或是以十進位還是十六進位表示等等,此外格式化字串也可以填入其他文字;每個變數的格式是以「%」為起頭的,其語法是「%[-][#][0][width]type」,type 是資料形態,與「%」配合後,所代表意義如下表:

資料形態意  義
%d使 32 位元數值輸出成十進位有號整數
%u使 32 位元數值輸出成十進位無號整數
%s輸出字串
%x使 32 位元數值輸出成十六進位無號數,以 0∼9、a∼f 阿拉伯數字及英文小寫字母輸出
%X使 32 位元數值輸出成十六進位無號數,以 0∼9、A∼F 阿拉伯數字及英文大寫字母輸出
%i與「%d」相同
%I64d使 64 位元數值輸出成十進位有號整數
%I64x使 64 位元數值輸出成十六進位無號數,以 0∼9、a∼f 輸出
%I64X使 64 位元數值輸出成十六進位無號數,以 0∼9、A∼F 輸出

上表中的最後三項只能用在 Windows XP 以後的系統,稍後再說明,其餘的均可用在 Windows 9x/Me 或 XP 系統。在「%」與「資料形態」之間還可以插入「width」表示輸出的寬度,「width」只能是一個阿拉伯數字,如果「width」為 8,表示輸出八個字元的寬度,不足的八個字元的以空白表示。但是如果在「width」之前加上「0」,則輸出結果不足寬度的以「0」補足,當然「0」也可以不加,不加的話就以空白補足。加上「#」,表示在十六進位數字加上字首「0X」或「0x」,不是十六進位的則不影響。加上「-」,表示輸出為向左對齊,不加表示向右對齊,這個只影響設有寬度且輸出的數值小於寬度。底下是一些例子:

        .586
        .MODEL  FLAT,STDCALL
        OPTION  CASEMAP:NONE

INCLUDE         WINDOWS.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB

;*********************************************************************
.CONST
kilo    DWORD   1024
mega    DWORD   1024*1024
giga    DWORD   1024*1024*1024
szFmtK  BYTE    '1%c Bytes=%-#8X Bytes=%8d位元組',0dh,0ah,0
szFmtM  BYTE    '1%c Bytes=%08Xh Bytes=%d位元組',0dh,0ah,0
szFmtG  BYTE    '1%c Bytes=%8Xh Bytes=%d位元組',0dh,0ah,0
szTitle BYTE    '單位',0
;*********************************************************************
.DATA
pBuffer         LPSTR   ?
szBuffer        BYTE    1024 DUP (0)
;*********************************************************************
.CODE
;---------------------------------------------------------------------
start:          xor     eax,eax
                mov     al,'K'
                mov     pBuffer,OFFSET szBuffer
                INVOKE  wsprintf,pBuffer,OFFSET szFmtK,eax,kilo,kilo
                add     pBuffer,eax
                xor     ecx,ecx
                mov     cl,'M'
                INVOKE  wsprintf,pBuffer,OFFSET szFmtM,ecx,mega,mega
                sub     edx,edx
                add     pBuffer,eax
                mov     dl,'G'
                INVOKE  wsprintf,pBuffer,OFFSET szFmtG,edx,giga,giga
                INVOKE  MessageBox,NULL,OFFSET szBuffer,OFFSET szTitle,\
                        MB_OK or MB_ICONINFORMATION
                INVOKE  ExitProcess,NULL
;*********************************************************************
END             start
 此程式執行後的畫面是:

%I64d、%I64x 和 %I64X 是用來處理 64 位元數值輸出到某個記憶體中,因為在 Win32 系統中,每個參數只能是 32 位元,因此得分兩次推入堆疊,先推入較高的 32 位元,後推入較低的 32 位元,例如要輸出 tera ( tera=1099511627776d=10000000000h ),程式如下:

szFmt   DB      '%I64d',0
buffer  DB      1024 DUP (0)
        mov     eax,0
        mov     edx,100h    ;EDX:EAX=10000000000h
        INVOKE  wsprintf,OFFSET buffer,OFFSET szFmt,eax,edx

在 Win32 系統堙A呼叫協定是 stdcall,亦即最右邊的參數最先被推入堆疊,最左邊的參數最後被推入堆疊,所以 EDX 比 EAX 先推入堆疊。因此在原始碼中,如果呼叫 wsprintf,輸出 64 位元數值,較低的 32 位元應寫在左邊,較高的 32 位元寫在右邊。


總結

此章,小木偶說明如何處理滑鼠訊息,假如您從『在 Win32 環境下撰寫組合語言』的第零章開始一直能堅持到這一章都能下工夫研究的話,我想您可能會覺得,在 Win32 環境撰寫程式入門的門檻很高,但是假如您能掌握訊息驅動的大方向後,其實並不會比在 DOS 環境下困難。以這章的第一個程式,MOUSE4.ASM,來說,您假如想在 DOS 環境下撰寫類似的程式,大概是不容易的,但是在 Win32 環境執行的 MOUSE4.ASM 只用了約兩百行就結束了。

當然,MOUSE4.ASM 堜珛e出來的軌跡是斷斷續續的,沒有連在一起不太好看,您可以用 MoveToEx 和 LineTo 這兩個 API 加以改進,請參考 Win32 Programmer's Reference 自行改良。

另外,對於視窗內使用不同的字型時,其想法也像改變畫筆的方式相同,


到第四章回到首頁到第六章