Ch 04 在視窗上繪製圖形


在這一章堙A小木偶將介紹繪製圖形,但是僅僅繪製圖形卻又太過簡單,因此再加上定時送訊息給自己的程式片段,以及結束程式時會詢問是否真的退出這些功能。先說第一點,繪圖,此程式是在一個視窗上描繪出拋物線,此拋物線的數學方程式是 y=兩百分之一(x-200)2,當然小木偶僅畫出 x=0 開始到 399 的這段範圍,所以 y 的範圍是從 0 到 200,共 400 點。畫出圖形如下圖:

拋物線

圖中藍色的座標並不是程式所顯示的,而是小木偶畫上去的,您應當可以注意到,在視窗的工作區堛漁y標和數學上的座標不太相同,工作區中左上角座標為 (0,0),x 座標向右增加,但 y 座標卻是向下增加。第二,小木偶不打算立刻就畫出整個畫面,而是每隔 0.1 秒畫出一點,這樣我們的程式就必須使系統每隔 0.1 秒對自己發出訊息,而我們的程式再加以處理。第三是按下右上角的退出鈕退出程式時會彈出一個視窗,詢問是否真要離開,如下圖所示:

退出拋物線程式
上圖中,即使拋物線尚未畫完,也可以離開。


原理

這個程式的大部分原理都在前面敘述過了,新的部份是如何每隔 0.1 秒畫出一點。假如是在 DOS 環境下,我們大概會用一個迴圈不斷檢查是否已經到達時間。但是在 Win32 環境的思考方式不是這樣,在 Win32 環境媕雩茈H訊息驅動的方式思考,也就是能夠使系統在每隔一段時間時發出一個訊息給我們的程式,要達到這個目的可以使用 SetTimer API。

SetTimer API

這個 API 建立一個計時器,此計時器每隔一段指定的時間,向某個指定的視窗送出 WM_TIMER 訊息。其原型如下:

UINT SetTimer(
    HWND        hWnd,           // handle of window for timer messages
    UINT        nIDEvent,       // timer identifier
    UINT        uElapse,        // time-out value
    TIMERPROC   lpTimerFunc     // address of timer procedure
   );

hWnd 是計時器所屬的視窗代碼。nIDEvent 是計時器的識別碼,任意給一個非零值即可。uElapse 是所設定的時間,單位是毫秒。lpTimerFunc 是處理 WM_TIMER 的函式,假如 lpTimerFunc 為 NULL 的話,就是指由 hWnd 的視窗函式來處理 WM_TIMER,也就是說 WM_TIMER 會被發送到 hWnd 的視窗函式。

KillTimer API

當用不著計時器時,應該釋放計時器,此時就用 KillTimer,其原型如下:

BOOL KillTimer(
    HWND    hWnd,       // handle of window that installed timer
    UINT    uIDEvent    // timer identifier
   );

hWnd 是計時器所屬的視窗代碼。uIDEvent 是計時器的識別碼。這兩個參數應該和 SetTimer 的前兩個參數相同。假如釋放成功的話,此 API 會傳回非零值,失敗的話,會傳回零。


原始碼

此程式小木偶名之為 PARABOLA.ASM,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
        .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      ;計時器ID
color           EQU     0ffh    ;顏色碼
WndProc         PROTO   :HWND,:UINT,:WPARAM,:LPARAM
;*******************************************************************************
.DATA
ClassName       db          'SimpleWinClass',0
AppName         db          '拋物線',0
AskExit         db          '是否退出此程式?',0
AskExitTitle    db          '詢問',0
x               dd          0   ;拋物線的 x 座標
y               dd          ?   ;拋物線的 y 座標
two_hundred     dw          200 ;常數
hInstance       HINSTANCE   ?
hwnd            HWND        ?
CommandLine     LPSTR       ?
wc              WNDCLASSEX  <30h,?,?,0,0,?,?,?,?,0,OFFSET ClassName,?>
msg             MSG         <?>
;*******************************************************************************
        .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,410,250,0,0,hInstance,NULL
        mov     hwnd,eax
        INVOKE  ShowWindow,hwnd,SW_SHOWDEFAULT
        INVOKE  UpdateWindow,hwnd
 
gt_msg: INVOKE  GetMessage,OFFSET msg,NULL,0,0
        or      eax,eax
        jz      wm_qut
        INVOKE  TranslateMessage,OFFSET msg
        INVOKE  DispatchMessage,OFFSET msg
        jmp     gt_msg
wm_qut: mov     eax,msg.wParam
        INVOKE  ExitProcess,eax
;-------------------------------------------------------------------------------
WndProc PROC    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
        LOCAL   PS:PAINTSTRUCT
        LOCAL   invalid:RECT
 
        cmp     uMsg,WM_CREATE                  ;收到 WM_CREATE
        jne     nx_wm0
        INVOKE  SetTimer,hWnd,TimerID,100,NULL  ;建立計時器
        jmp     exit
 
nx_wm0: cmp     uMsg,WM_TIMER   ;收到 WM_TIMER
        jne     nx_wm1
        cmp     x,400           ;若 x 大於或等於 400 則不處理
        je      no_pnt
        mov     invalid.top,0   ;計算拋物線的 x 及 y 座標,以及無效區
        mov     invalid.bottom,250
        mov     eax,x
        finit                   ;---st ---;---st1---;
        fild    x               ;    x    ;         ;
        fisub   two_hundred     ;  x-200  ;         ;
        fmul    st,st           ; (x-200)2;         ;
        fidiv   two_hundred     ;    y    ;         ;
        fistp   y
        mov     invalid.left,eax
        add     eax,2
        mov     invalid.right,eax
        INVOKE  InvalidateRect,hWnd,ADDR invalid,FALSE
        inc     x                       ;使 x 增加一
no_pnt: jmp     exit
 
nx_wm1: cmp     uMsg,WM_PAINT           ;處理 WM_PAINT
        jne     nx_wm2
        INVOKE  BeginPaint,hWnd,ADDR PS
        INVOKE  SetPixel,eax,x,y,color  ;畫出一點
        INVOKE  EndPaint,hWnd,ADDR PS
        jmp     exit
 
nx_wm2: cmp     uMsg,WM_CLOSE           ;處理 WM_CLOSE
        jne     nx_wm3
        INVOKE  MessageBox,hWnd,ADDR AskExit,\  ;彈出詢問視窗
                ADDR AskExitTitle,MB_YESNO or MB_ICONQUESTION
        cmp     eax,IDYES
        jne     exit
        INVOKE  KillTimer,hWnd,TimerID  ;釋放計時器
        INVOKE  DestroyWindow,hWnd      ;摧毀視窗
        jmp     exit
 
nx_wm3: cmp     uMsg,WM_DESTROY
        jne     nx_wm4
        INVOKE  PostQuitMessage,NULL
        jmp     exit
 
nx_wm4: INVOKE  DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
exit:   xor     eax,eax
        ret
WndProc ENDP
;*******************************************************************************
END     start

程式解說

這個程式大部分的和前面相同,小木偶僅解釋不同的地方。程式第 12 行是計時器識別碼,任意的非零值即可。

顏色

程式第 13 行是顏色。在 Windows 系統堛疑C色是以一個 32 位元 ( 也就是 4 個位元組 ) 長度的數值表示,其格式如下:

00BBGGRRh

最高位址的位元組必設為零,剩下的三個位元組依位址由低而高,分別表示紅、綠、藍,也就是光的三原色。每一種色光的強度由 0 到 255,此數值越大表示該色光越強,0 表示不含該種色光。所以 00000000 表示黑色,00FFFFFFh 表示白色,00FF0000h 表示藍色,0000FF00h 表示綠色,000000FFh 表示紅色。

第 21 行到第 23 行是定義變數 x、y 和常數 200 的地方。要注意的是變數 x 無法設在 WndProc 堶捧禨粥炾嚃僂ヾA因為每一次訊息循環區域變數都會被重建及銷毀,所以無法保持其數值。因此小木偶把 x 設為全域變數,在資料段中定義。此外,x、y 最好是設為雙字組的大小,否則在第 92 行呼叫 SetPixel 時的參數大小會不合,但組譯器卻不會通知錯誤,而因用了錯誤的資料,以致結果是錯的。

WM_CREATE 訊息

當應用程式呼叫 CreateWindow 或 CreateWindowEx 建立視窗時,CreateWindow 或 CreateWindowEx 會把 WM_CREATE 訊息傳給新建立視窗的視窗函式。即使這個視窗並沒有立即顯示出來,WM_CREATE 還是會被發送到該視窗的視窗函式中。這個訊息也只會在視窗建立時,由 CreateWindow 或 CreateWindowEx 送出一次給視窗函式,因此許多程式常常在 WM_CREATE 訊息中,處理程式一開始必須要完成的工作,例如初始化某些變數等等。

在 PARABOLA.ASM 程式堙A計時器只需在程式開始時建立一次即可,因此處理 WM_CREATE 訊息的步驟就是建立計時器。在第 66 行就是建立一個計時器,此計時器每隔 0.1 秒( 100 毫秒等於 0.1 秒 )向自己發出一個 WM_TIMER 訊息。

當計時器向拋物線程式的視窗函式發出 WM_TIMER 訊息時,要處理的工作是設定無效區,有了無效區後,系統會再向該視窗函式發出 WM_PAINT 訊息,而後就能進行描點的繪圖任務了。

至於無效區的設定是利用前一章所提到的 InvalidateRect API,但此處不設定整個工作區都是無效區,這樣要重繪的部份太多會降低效率,小木偶僅把工作區的某部份設為無效區。該區域的位置是依據拋物線的 x 座標決定,其範圍是由左上角座標 ( x,0 ) 到右下角座標 ( x+2,250 ) 這樣一塊狹長形的區域,這塊範圍放在 invalid 結構體堙C無效區設定後,程式將收到 WM_PAINT 訊息就可以描點了。

SetPixel API

SetPixel 是用來在視窗上描繪一個點的 API,原型為:

COLORREF SetPixel(
    HDC         hdc,    // handle of device context  
    int         X,      // x-coordinate of pixel 
    int         Y,      // y-coordinate of pixel 
    COLORREF    crColor // pixel color 
   );

hdc 是設備內容,此處直接由上一行的 BeginPaint 返回值填入。X 是要描出點的 X 座標,Y 是要描出點的 Y 座標,crColor 表示要設描繪點的顏色。假如成功,會傳回被畫出點的顏色,即 crColor,於 EAX;若失敗 EAX 為-1。

WM_CLOSE 訊息

第二、三章的程式,當使用者按下關閉鈕或者由選單的『檔案』選擇『離開』時,小木偶都是利用 DefWindowProc 來處理摧毀視窗的工作,WM_CLOSE 傳送到 DefWinProc 會引發一連串的訊息,如 WM_DESTROY、WM_QUIT。參考第二章結束程式

除了用內定的 DefWindowProc 處理 WM_CLOSE 訊息外,當然我們也可以自己處理 WM_CLOSE。PARABOLA.ASM 這個程式就是一個例子。

此處小木偶用很簡單的方式,MessageBox 來詢問是否真的離開程式。假如使用者按下『否』的話,表示不是真的要結束程式,所以,於是什麼事也不做直接跳到 exit 標記處。

假如使用者按下『是』的話,那麼表示要結束程式了。就必須釋放計時器及摧毀視窗。和以前不同的是,第二、三章的程式是由 DefWindowProc 自動處理,但在這個程式奡N必須自己處理摧毀視窗的工作了。有個 API 函式,DestroyWindow,可以達成這個目的。

DestroyWindow API

其語法如下:

BOOL DestroyWindow(
    HWND    hWnd    // handle to window to destroy  
   );

這個 API 是摧毀指定的視窗的,它只有一個參數,就是將被摧毀視窗的視窗代碼,假如成功的話, EAX 會傳回非零值,並且發出 WM_DESTROY 和 WM_NCDESTROY 訊息並且使該視窗變成不是在最上層的關注焦點上。假如失敗的話 EAX 傳回零。

我們可以這樣說,當視窗函式收到 WM_DESTROY 訊息時,視窗已被摧毀,程式無法再反悔。當視窗函式收到 WM_CLOSE 時,視窗還未被摧毀,這個訊息只是告知並未真的行動,程式可以再確定是否真的要執行摧毀動作。


高級假指令︰程式流程控制

用 MASM 6.x 組譯器還可以使用許多種高級的條件判斷式及條件跳躍的假指令,這些假指令使得 MASM 6.x 越來越像 PASCAL 或 C/C++,也使得程式設計師得以更容易維護原始程式。他們會被 MASM 6.x 組譯器自動地組譯成適當的機械碼以符合程式設計師所想要的控制流程,而程式設計師就可以不必再浪費心力於這些煩瑣的細節。假如您想在 DOS 系統下撰寫組合語言,這些高級的控制流程假指令也能夠使用,這樣您的程式就比別人好維護得多。

在組合語言教學的第 26 章巨集與第 27 章條件組譯兩章堙A曾經提到 IF/ELSE/ENDIF 假指令,這些指令前沒有小數點,而且在意義上和底下小木偶將介紹的兩種高級流程控制完全不同。

.WHILE/.ENDW 假指令

先來看看它的語法︰

.WHILE  判斷式
        程式
.ENDW

這個 .WHILE/.ENDW 迴圈的執行過程如下︰當程式執行到 .WHILE 時,會檢查判斷式是否為真,假如為真,則執行 .WHILE 與 .ENDW 之間的程式,直到遇到 .ENDW 時,再回到開頭 .WHILE 處檢查判斷式是否為真,若為真時,再度執行 .WHILE 與 .ENDW 之間的程式,若為假,則跳到 .ENDW 下一行程式執行。所以看起來,.WHILE/.ENDW 就像是 C/C++ 的 WHILE {} 迴圈一樣。

至於判斷式的模樣,大致可分為兩種︰

  暫存器或變數 邏輯運算子 暫存器或數值
  暫存器或變數或數值

上面那一種情形時,運算子左右兩邊可以同時為暫存器,但不能同時為變數,這是因為在 80X86 指令堙A可以有 cmp ax,bx,但沒有 cmp a,b 這樣的指令。假如為下面那一種情形時,暫存器、變數、數值為非零時,會被組譯器認為『真』;為零時,被認為『偽』。

常用的邏輯運算子如下表︰

運算子 描述 運算子 描述
==等於 !=不等於
>大於 >=大於等於
<小於 <=小於等於
&& ¦¦
!

除了這些邏輯運算子之外,兩個判斷式之間也可以做『或』與『且』的邏輯運算,這時『或』要用¦¦,『且』要用&&,來連接兩個判斷式,並且這兩個判斷式應該用兩對小括號括起來。例如,底下的例子是當 AX 大於 SI 且 BX 大於 DI 時,使 CX 增加一,並使 BX 減一,直到上述條件不成立時︰

.WHILE  (ax>si)&&(bx>di)
        inc     cx
        dec     bx
.ENDW

底下的例子是當 AX 不等於零時,把 BX 所指位址之數值移入 AX,直到 AX 等於零時停止︰

.WHILE  AX
        mov     ax,[bx]
        inc     bx
        inc     bx
.ENDW

在 .WHILE/.ENDW 迴圈堙A也可以加上 .BREAK 假指令,強制退出迴圈,假如在 .BREAK 後加上 .IF 表示只有在 .IF 之後的條件為『真』時,才退出迴圈。例如︰

.WHILE  TRUE
        lodsw
.BREAK  .IF     !AX
        add     bx,ax
.ENDW

上面這個例子是把 SI 所指的記憶體數值移入 AX 中,同時 SI 自動指向下一位址,並使 BX 之數值與 AX 相加,再存入 AX 堙A直到 AX 等於零時退出 .WHILE/.ENDW 迴圈。

.IF/.ESLEIF/.ELSE/.ENDIF 假指令

最簡單的情形是︰

.IF     判斷式
        程式一
.ELSE
        程式二
.ENDIF

這應該很容易了解,當判斷式為『真』時執行程式一堛澈令;為『偽』時就執行程式二堛澈令,假如為『偽』時不用執行任何指令,則 .ELSE 以及程式二可以刪除。底下是一個比較複雜的例子,

.IF     判斷式一
        程式一
.ELSEIF 判斷式二
        程式二
.ELSEIF 判斷式三
        程式三
.ELSE
        程式四
.ENDIF

這是分支指令,假如判斷式一為真,則執行程式一的指令;假如判斷式二為真,則執行程式二的指令;假如判斷式三為真,則執行程式三的指令……,假如都不是上面的情形,則執行 .ELSE 之後的程式。


改寫 PARABOLA.ASM 成為 PARABOL1.ASM

此處小木偶將以 .WHILE/.ENDW 和 .IF/.ELSEIF/.ELSE/.ENDIF 修改 PARABOLA.ASM 使成 PARABOL1.ASM,修改後變得更具可讀性︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
        .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
color           EQU     0ffh
WndProc         PROTO   :HWND,:UINT,:WPARAM,:LPARAM
;*******************************************************************************
.DATA
ClassName       DB          'SimpleWinClass',0
AppName         DB          '拋物線',0
AskExit         DB          '是否退出此程式?',0
AskExitTitle    DB          '詢問',0
x               DD          0
y               DD          ?
two_hundred     DW          200
hInstance       HINSTANCE   ?
hwnd            HWND        ?
CommandLine     LPSTR       ?
wc              WNDCLASSEX  <30h,?,?,0,0,?,?,?,?,0,OFFSET ClassName,?>
msg             MSG         <?>
;*******************************************************************************
.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,410,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
;-------------------------------------------------------------------------------
WndProc PROC    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
        LOCAL   PS:PAINTSTRUCT
        LOCAL   invalid:RECT
.IF     uMsg==WM_CREATE
        INVOKE  SetTimer,hWnd,TimerID,100,NULL
 
.ELSEIF uMsg==WM_TIMER
    .IF     x<400
        mov     invalid.top,0
        mov     invalid.bottom,250
        mov     eax,x
        finit
        fild    x
        fisub   two_hundred
        fmul    st,st
        fidiv   two_hundred
        fistp   y
        mov     invalid.left,eax
        add     eax,2
        mov     invalid.right,eax
        INVOKE  InvalidateRect,hWnd,addr invalid,FALSE
        inc     x
    .ENDIF
 
.ELSEIF uMsg==WM_PAINT
        INVOKE  BeginPaint,hWnd,addr PS
        INVOKE  SetPixel,eax,x,y,color
        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

這個 PARABOL1.ASM 程式和上面 PARABOLA.ASM 程式是一樣的,只是以較具可讀性的方式撰寫程式。PARAMBOL1.ASM 程式小木偶就不加以解釋了。


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