上次介紹的WINDOW.ASM只能畫一個視窗,如果要畫多個視窗,只要把繪製程式重複寫幾次即可。不過,這裡有更優雅的做法,那就是利用呼叫「副程式」的技巧。
我們把WINDOW.ASM做了一點更動,首先是在main程式前面加了一小段show副程式。
副程式的開頭用一個簡單的「show標記」來表示「這是副程式的進入點」,以及「這是副程式的名稱的。最後則以一個ret指令來回到呼叫show的主程式。
.code
show: ← show副程式的進入點
mov ah,09H
int 21H
ret ← show副程式的返回點
主程式只要把dx指向win1視窗變數,再以call show這樣的指令呼叫show副程式即可。call會記住返回位置,待副程式執行完畢,ret會回到call的底下繼續執行程式。請看圖一:
圖一:
詳細的WINS1.ASM列表請看程式一,粗體字是表示與WINDOW.ASM不同的地方。
程式一 :
; File Name : wins1.asm
.286
.model small
.stack ;堆疊區
.data ;資料區
win1 db "-----------------",10,13
db "| |",10,13
db "| |",10,13
db "| |",10,13
db "-----------------",10,13,"$"
.code ;程式區
show: ;顯示視窗副程式
mov ah,09H
int 21H
ret ;回到主程式
.code ;程式區
main: ;WINS1主程式
mov dx,@data
mov ds,dx ;把ds指向資料區
lea dx,win1 ;把dx指向win1
call show ;呼叫show畫出視窗
mov ax,4c00H
int 21H ;結束程式回到DOS
end main
程式的編譯方式很簡單,您可以用標準的指令:
C:\>masm wins1; <Enter>
C:\>link wins1; <Enter>
也可以利用上次介紹的ASM.BAT來編譯:
C:\>asm wins1 <Enter>
WINS1的執行結果與WINDOW完全相同,這裡就不重複介紹了。
要特別說明的是,其實ret之後的 .CODE宣告是可有可無的。small模式只有一個程式區,即使多加幾個 .CODE,主、副程式還是會放在一起。這裡只是習慣的把副程式區用 .CODE做個區隔。
因此,如果忘了寫副程式的ret返回指令,會變成int 21H之後,緊接著主程式的啟始指令mov dx,@data,再也沒有機會回到原呼叫的位置。您可以想像會發生什麼事:電腦不斷繪製視窗,最後就當機了!
用proc宣告副程式,也有同樣的問題,那只是「宣告」方式不同罷了,可不會自動加上ret。
用「標記」宣告副程式其實並不正確,比較好的做法是利用proc與endp假指令來宣告副程式。理由之一是proc(procdure)支援高階語言機制,如BASIC、PASCAL、C等。proc的寫法如下:
show proc
:
ret
show endp
我們告訴MASM,show副程式從proc開始到endp為止。很簡單吧!但不要忘記寫ret或endp,否則從proc開始到程式最後,統統是show副程式的勢力範圍。
只畫一個視窗不過癮吧?WINS2.ASM試著繪製兩個視窗,請看程式二。執行結果與執行過程如圖二和圖三所示。
程式二:
; File Name : wins2.asm
.286
.model small
.stack
.data
win1 db "-----------------",10,13
db "| |",10,13
db "| |",10,13
db "| |",10,13
db "-----------------",10,13,"$"
win2 db "-----------------------",10,13
db "| |",10,13
db "| |",10,13
db "| |",10,13
db "| |",10,13
db "| |",10,13
db "-----------------------",10,13,"$"
.code
show proc ;宣告show副程式
mov ah,09H
int 21H
ret ;別忘了ret
show endp ;結束show副程式
.code
main:
mov bx,@data
mov ds,bx
lea dx,win1
call show ;呼叫show副程式
lea dx,win2
call show ;呼叫show副程式
mov ax,4c00H
int 21H
end main
圖二:
圖三:
lea dx,win1
call show
lea dx,win2
call show
Show 只能繪製簡單而固定的視窗,如果希望能畫出隨意大小的窗子,show就做不到了!問題是出在資料結構。之前設計的視窗,把視窗結構直接寫在程式裡頭,因而 缺乏彈性。現在,我們希望藉由更換WINS2.ASM的「資料結構」與「show副程式」兩個組件,做出更具彈性的窗子。
譬如,我們設計了這樣的資料結構,來表示一個視窗能有多大:
windows db Width,Length
Width表示視窗的寬度,Length則表示視窗的長度。很簡單吧!新的ShowWindow副程式並不難,只是略長了些。請看程式三。提醒您,WIN3.ASM的main主程式與WIN2.ASM完全一樣喲!
我們需要一個新的列印字元服務,查「技術手冊」找到了一個相當好用的DOS服務:
DOS 服務代號 02H
功能:列印字元。
說明:DOS 會把存在 DL 的字元以「黑底白字」
的方式印到標準螢幕(STDOUT)。
用法:
.data
char db 'R‘
.code
mov dl,char ; dl = char
mov ah,02H ; ah = 2
int 21H ; 呼叫 DOS 的第 2 號功能
扣掉將字元存入dl暫存器的mov指令,我們把這個列印服務寫在printch副程式。另外,還設計了一個「換列」專用的newline副程式,以簡化ShowWindow程式。
與其研究ShowWindow每個指令的設計流程,不如把程式劃分為幾個段落,您一定會發現ShowWindow原來是如此簡單啊!
以單線視窗為例,我們把整個窗子劃為圖四幾個部份:
圖四:
第一個數字表示繪製順序,第二個數字表示該框線的字元碼(以10進制表示)。我們從視窗左上角開始畫到右下角為止。以開始的1,128來說,其實只要寫:
mov dl,218
call printch
真的很簡單吧!緊接著就是2,196的繪製程式:
mov cx,0 ;把 cx 歸零
mov cl,[bx] ;從 [bx] 所指的變數,取出視窗寬度
sub cx,2 ;將視窗寬度減 2
line1:
mov dl,196
call printch
loop line1 ;重複畫出一條橫線
3,191就如同1,218一般簡單,因為游標正好印到視窗的右上角,我們只要印191字元並換列再印即可:
mov dl,191
call printch
call newline ;換列
第4步的4,179程式比較冗長,因為我們想同時印出視窗左右兩邊的框線:
mov cl,[bx+1] ;取出視窗長度到 cl
sub cx,2 ;把 cx 減 2
mov di,cx ;di=cx,用 di 做為計數器
line2:
mov dl,179
call printch ;印左邊的垂直線
mov cl,[bx] ;cl=視窗寬度
sub cx,2 ;將 cx 減 2
space:
mov dl,32
call printch
loop space ;這個迴圈用來印視窗內的空白字元
mov dl,179
call printch ;印右邊的垂直線
call newline ;換列
dec di ;將視窗長度減一
jnz line2 ;若 di 不為 0 則跳回到 line2
5、6、7步驟的繪製方式與1、2、3幾乎完全相同,這裡就不再重複說明了。
程式三:
; File Name : wins3.asm
.286
.model small
.stack
.data
win1 db 10,3 ;寬度=10,長度=20
win2 db 20,6 ;寬度=20,長度=6
.code
printch proc ;列印字元副程式
push ax
mov ah,02H
int 21H
pop ax
ret
printch endp
newline proc ;換行副程式
push dx
mov dl,13
call printch ;將游標移到左端
mov dl,10
call printch ;游標往下移動一列
pop dx
ret
newline endp
ShowWindow proc
mov bx,dx ;bx = dx
mov dl,218 ;視窗的左上角字元
call printch
mov cx,0
mov cl,[bx] ;cl = width
sub cx,2 ;cx = cx - 2
line1:
mov dl,196 ;印出視窗的水平框線
call printch
loop line1
mov dl,191 ;視窗的右上角字元
call printch
call newline ;換列
mov cl,[bx+1] ;cl = length
sub cx,2 ;cx = cx - 2
mov di,cx
line2:
mov dl,179 ;印出視窗的垂直框線
call printch
mov cl,[bx] ;cl = width
sub cx,2 ;cx = cx - 2
space:
mov dl,32 ;印空白字元
call printch
loop space
mov dl,179 ;印右邊的垂直框線
call printch
call newline
dec di ;長度減一
jnz line2
mov dl,192 ;視窗的左下角字元
call printch
mov cl,[bx] ;cl = width
sub cx,2 ;cx = cx - 2
line3:
mov dl,196 ;印底部的水平框線
call printch
loop line3
mov dl,217 ;視窗的右下角字元
call printch
call newline
ret
ShowWindow endp
.code
main:
mov cx,@data
mov ds,cx
lea dx,win1
call ShowWindow
lea dx,win2
call ShowWindow
mov ax,4c00H
int 21H
end main
相信讀者看到這裡都皺著眉,WINS3.ASM肥胖的身軀著實沉重,沒關係,只要把WINS3.ASM送到「美容瘦身中心」,曲線窈窕非夢事喲!
請跟著我們的美容程序做一次,來!One More、Two More...
一、把WINS3.ASM複製為WINS4.ASM。
二、把WINS4.ASM整段 .CODE副程式,用文字編輯器的區段功能mark起來,另存新檔到WINSLIB.ASM,並刪掉整個副程式區段。
三、以相同的手法,把WINS4.ASM從 .286到 .STACK部份mark起來並另存新檔到WINSLIB.INC。
四、WINS4.ASM現在只剩下 .data與main程式了,請在程式前加上include winslib.inc含括指令,如程式四所示。粗體字表示新增的指令。為什麼要這樣做,稍後會解釋。
五、由於main程式叫用的副程式已不在WINS4.ASM了,所以得在WINSLIB.INC含括檔追加「外部」extrn宣告指令,如程式五。
六、最後請在WINSLIB.ASM加上幾個宣告性的假指令,特別是「公用」宣告指令public(與extrn相對),把ShowWindow等副程式宣告為開放供大家使用。不要忘了程式最後要加上END指令,如程式六所示。
七、不難吧!這套程式的編譯與連結方式如下:
C:\>masm wins4; <Enter>
C:\>masm winslib; <Enter>
c:\>link wins4+winslib; <Enter>
八、如此即可得到一個全新的WINS4.EXE!由於美容期間未加上任何CPU指令(只追加假指令),因此WINS4.EXE的大小應該與WINS4.EXE相等才是。比對WINS3.ASM與WINS4.ASM程式,WINS4簡潔多了!您喜歡那一種設計呢?
我們把這套美容課程稱為「模組化程式設計基礎」。通常能完成的學員比例小於30:5!不會吧?有這麼難嗎?如果做不出來,乾脆參考程式四、五、六的內容,重新輸入一次算了!
探討問題的原因有二:一是學員對文字編輯器的區段操作指令不熟,不會把區段另寫成新檔。另一個原因是,很多學校不教「程式模組化」,以致學生學會一堆組合語言指令,卻不會劃分程式!幾個假指令就弄得一頭霧水!
您注意到了嗎,其實整個模組化程序,只用了三個新的include、extrn、public假指令而已!
程式四(主程式):
; File Name : wins4.asm
include winslib.inc
.data
win1 db 10,3
win2 db 20,6
.code
main:
mov bx,@data
mov ds,bx
lea dx,win1
call ShowWindow
lea dx,win2
call ShowWindow
mov ax,4c00H
int 21H
end main
程式五(含括檔):
; File Name : winslib.inc
.286
.model small
.stack
.code
extrn printch:proc
extrn newline:proc
extrn ShowWindow:proc
程式六(副程式):
; File Name : winslib.asm
.286
.model small
.code
public printch, newline, ShowWindow
.code
printch proc
: (同 WINS3.ASM)
ret
printch endp
newline proc
: (同 WINS3.ASM)
ret
newline endp
ShowWindow proc
: (同 WINS3.ASM)
ret
ShowWindow endp
end
include假指令會告訴MASM在編譯WINS4.ASM程式時,把WINSLIB.INC檔含括進來,就像直接寫在WINS4.ASM一般。
含括檔的副檔名隨便您取,甚至用 .ASM都可以,但不要用 .H或 .HPP,以免與C或C++的含括檔混淆。這樣可以把常用的指令搬到WINSLIB.INC含括檔裡頭,使main程式看起來清爽多了!
由於所有副程式都搬到另一個WINSLIB.ASM檔,不在WINS4.ASM裡頭。因此,您必須在WINS4.ASM把這些副程式宣告為extrn(external)外部的副程式,以免MASM找不到這些程式,而告訴您程式寫錯了!
當然,這些extrn宣告可直接寫在WIN4.ASM,但如果寫在含括檔會更優雅!至於WINSLIB.ASM副程式檔,則必須把需要對外公開的副程式宣告為public,否則將會是WINSLIB.ASM的私有程式。
對WINS4.ASM來說,其實只要把ShowWindow宣告為public即可。請試試,程式仍可以正常編譯執行!
整套WINS4.ASM程式的結構如圖五所示。
圖五:
WIN4.ASM與WINSLIB.ASM要分別編譯:
C:\>masm wins4;
C:\>masm winslib;
含括檔則不必用MASM編譯,其實WINSLIB.INC就是WINS4.ASM的一部份,只是寫成另一個檔案罷了。
接著再用LINK把這兩個程式「加」起來即可。請注意加號的先後順序,以免造出WINSLIB.EXE檔。
c:\>link wins4+winslib;
如果有多個副程式檔,每次連結程式都得一一相加,少了一個就做不出執行程式,這未免太麻煩了。
我們可以把所有的 .OBJ副程式檔,收集到WINSLIB.LIB圖書館裡,叫LINK自己到圖書館(函式庫)找尋所需的 .OBJ檔,不錯吧!
製作函式庫的方法很簡單,請輸入:
C:\>lib winslib.lib +winslib.obj; <Enter>
輸入時可省略 .LIB和 .OBJ副檔名。這樣就會建好一個新的WINSLIB.LIB檔。
如果WINSLIB.ASM曾修正並重新編譯,請用底下的指令更新函式庫裡的WINSLIB.OBJ檔:
C:\>lib winslib -+winslib; <Enter>
函式庫程式編譯方式
做好的WINSLIB.LIB得通知WINS4.ASM才行,請在含括檔WINSLIB.INC追加程式七的includelib指令。然後只要這樣編譯程式即可:
C:\>masm wins4;
C:\>link wins4;
很簡單吧!程式的編譯方式如同未模組化的WINS1.ASM一般。
程式模組化,可說是簡化程式設計的最佳「利器」,而不是成為您的「負擔」,希望本文能提供您一個更快更好的設計方向。對本文有任何的看法或意見,非常歡迎您提出來討論,謝謝!
程式七:
; File Name : winslib.inc
includelib winslib.lib
.286
.model small
.stack
: : :(以下同程式六)