Windows 作業系統能成為大多數人使用的作業系統,不可諱言的,圖形界面的操作方便及親和力是重要的原因。雖然現在大部分的 Win32 程式都已有漂亮的圖形界面,但是我們仍然可以使用或撰寫以文字導向操作方式的程式。例如,在 Windows XP 中提供『命令提示字元』執行以文字畫面輸出或輸入的程式,就像以前的 DOS 程式一樣。
這些程式雖然沒有漂亮的圖形界面,但是小而快速是其優點,所以仍然有許多人還在使用著這些程式,像是 ping、ipconfig、ftp 等程式都是常常被使用的。這些程式仍然是 32 位元的程式,它們也都屬於 PE 格式,也能執行多執行緒,它們除了沒有華麗的視窗外,和一般的 Win32 程式一樣,這些程式稱為『控制台程式』( console program,或稱『主控台程式』),讀者須知,他們和以前的 DOS 程式是不同的。這一章將介紹如何撰寫控制台程式。
在 16 位元的 DOS 系統裏,螢幕與鍵盤被視為標準輸出輸入設備,它們是在 DOS 啟動時,由 DOS 開啟,DOS 把它們視為檔案,其檔案代碼早已分配且固定不變 ( 參閱組合語言第 15 章 )。當我們的程式要對 DOS 控制下的螢幕鍵盤做輸出或輸入時,可對這些『檔案代碼』做輸出或輸入動作。例如,對標準輸出設備 ( 檔案代碼為 01H ) 寫入,意即在螢幕上輸出文字。
Win32 系統不一樣的地方在標準輸出輸入代碼不固定,因此在做輸入或輸出之前要先取得它們的代碼 ( handle ),這項工作可以用 GetStdHandle API 來做,GetStdHandle 的原型是
HANDLE GetStdHandle( DWORD nStdHandle // input, output, or error device );
這個 API 根據 nStdHandle 的不同,在 EAX 裏傳回標準輸出、輸入或錯誤代碼,nStdHandle 可以是下面三個數值的其中之一:
如果要輸入資料可以用 ReadFile 或 ReadConsole。ReadConsole API 的原型是:
BOOL ReadConsole( HANDLE hConsoleInput, // handle to a console input buffer LPVOID lpBuffer, // address of buffer to receive data DWORD nNumberOfCharsToRead, // number of characters to read LPDWORD lpNumberOfCharsRead, // address of number of characters read LPVOID lpReserved // reserved );
hConsoleInput 是之前用 GetStdHandle API 的傳回值,此傳回值當然得用 STD_INPUT_HANDLE 為參數呼叫 GetStdHandle 所得之結果。lpBuffer 是指緩衝區位址,使用者輸入的資料會存在這個緩衝區裏,同時系統還會在資料的最後加上換行及歸位碼 ( 0ah,0dh )。nNumberOfCharsToRead 是指緩衝區容量。lpNumberofCharsRead 是一個指標,這個指標應該指向一個雙字組 ( 32 位元長 ) 變數,當 ReadConsole 結束時會在這個雙字組變數裏存放實際有幾個位元組被放入緩衝區裏,此位元組數包含了換行及歸位兩位元組。。如果使用者輸入的位元組數 ( 包含 0dh、0ah ) 比 nMumberOfCharsToRead 大,那麼超過的部份,雖然會顯示在螢幕上,但是卻不會存放在緩衝區裏。
如果要在控制台上輸出文字,則用 WriteConsole,其原型為:
BOOL WriteConsole( HANDLE hConsoleOutput, // handle to a console screen buffer CONST VOID *lpBuffer, // pointer to buffer to write from DWORD nNumberOfCharsToWrite, // number of characters to write LPDWORD lpNumberOfCharsWritten,// pointer to number of characters written LPVOID lpReserved // reserved );
WriteConsole 的參數和 ReadConsole 類似,小木偶就不多做介紹了。底下是一個簡單的控制台程式:
.586 .model flat,stdcall option casemap:none include windows.inc include kernel32.inc include user32.inc includelib kernel32.lib includelib user32.lib BufferSize equ 10h ;*********************************************************** .data hOutput HANDLE ? hInput HANDLE ? dWrittee DWORD ? dRead DWORD ? sName BYTE '請輸入您的姓名:' sBuffer BYTE BufferSize dup (0) sHowAreYou BYTE ',您好嗎?' ;*********************************************************** .code start: invoke GetStdHandle,STD_OUTPUT_HANDLE mov hOutput,eax invoke GetStdHandle,STD_INPUT_HANDLE mov hInput,eax invoke WriteConsole,hOutput,offset sName,16,offset dWritten,NULL invoke ReadConsole,hInput,offset sBuffer,BufferSize-2,offset dRead,NULL mov edi,offset sBuffer mov esi,offset sHowAreYou mov al,0dh mov ecx,dRead repne scasb jne @f ;沒找到0dh,表示輸入字元比BufferSize大 dec edi ;找到了0dh,表示輸入字元比BufferSize小 jcxz tail ;若CX=0,表示0dh恰好是緩衝區的最後一字元 dec dRead tail: dec dRead @@: mov ecx,10 ;sHowAreYou 字串長度 mov edx,ecx rep movsb add edx,dRead invoke WriteConsole,hOutput,offset sBuffer,edx,offset dWritten,NULL invoke ExitProcess,NULL ;*********************************************************** end start
把它存成 CONSOLE1.ASM 後再以下面步驟組譯:
C:\>cd e:\homepage\source [Enter] C:\>e: [Enter] E:\HomePage\SOURCE>
E:\HomePage\SOURCE>path C:\masm32\bin;%path% [Enter]
E:\HomePage\SOURCE>set ml=/coff /link /SUBSYSTEM:CONSOLE [Enter]
E:\HomePage\SOURCE>ml console1.asm
就大功告成了。或許各位讀者曾經注意到,XP 的『命令提示字元』右側有一捲軸,可以向上捲動觀察不在螢幕上的資料,的確,這點和 Windows 95/98/98SE/Me 的 DOS 模式或 DOS 系統是不同的。在 XP 裏,作業系統會為『命令提示字元』分配一塊資料區域,稱為『控制台螢幕緩衝區』( console screen buffer ),這塊區域存放著曾經或正顯示在命令提示字元視窗的資料,而『命令提示字元』的視窗所能見到的只是其中的一部份。就像我們由房間向窗戶往外望去,只能看到美麗風景的一小部份一樣,假如窗戶可向上移或下移,那麼就可能可以看見其他部份的風景了。只不過在電腦上,並不是視窗移動,而是用捲軸把要顯示的資料移進視窗的範圍內。
在 XP 系統中,也可以不用『命令提示字元』來做為文字導向界面,另有一家 JP Software 所出品的 4NT 也是常見的文字導向界面,假設您以 4NT 為文字導向界面的話,就不稱為『命令提示字元視窗』了,應該稱為『4NT 視窗』。不管是『命令提示字元視窗』或是『4NT 視窗』,都只顯示『控制台螢幕緩衝區』的一部份文字而已。
XP 利用 CONSOLE_SCREEN_BUFFER_INFO 結構體記錄控制台螢幕緩衝區的資料,底下是它的各欄位的說明:
CONSOLE_SCREEN_BUFFER_INFO STRUCT dwSize COORD ? ;控制台螢幕緩衝區大小 dwCursorPosition COORD ? ;游標位置 wAttributes WORD ? ;屬性 srWindow SMALL_RECT ? ;控制台視窗位置在螢幕緩衝區那兒 dwMaximumWindowSize COORD ? ;控制台視窗可能最大範圍 CONSOLE_SCREEN_BUFFER_INFO ENDS
顧名思義,dwSize 是控制台螢幕緩衝區的大小,以行與列來表示,記錄於 COORD 結構體,其定義為:
COORD STRUCT x WORD ? y WORD ? COORD ENDS
x 表示橫座標有幾個字元,y 則表示縱座標有幾個字元。x 亦可看成一列 ( 由左而右 ) 有幾個字元,y 也可以看成共有幾列。一般而言,開啟『命令提示字元』後如果沒有特別更改,每列可顯示 80 個字元,共有 300 列。
dwCursorPosition 表示目前游標在那一個位置,以 COORD 結構體表示,此處的 x、y 由零開始。所以一般 x 介於 0 到 79 之間。
wAttributes 是字元屬性 ( 亦即顏色 ),可以是 FOREGROUND_BLUE、FOREGROUND_GREEN、FOREGROUND_RED、FOREGROUND_INTENSITY、BACKGROUND_BLUE、BACKGROUND_GREEN、BACKGROUND_RED 和 BACKGROUND_INTENSITY 等保留字的組合。其實他們和在 DOS 文字模式中的字元屬性定義相同,文字的顏色,藍、綠、紅及強度以一個字組的最低四個位元 ( 第 0~3 位元 ) 來表示;文字的背景也是以藍、綠、紅及強度,以該字組的第 4~7 位元表示。這些數值所代表的顏色,可以參考組合語言第 19 章,以低亮度白色顯示於黑色背景為例,此字組值為 7。( 白色是藍、綠、紅三色組成,黃色是藍、綠兩色組成……)
前面曾提及,『命令提示字元』視窗所能顯示的僅為控制台螢幕緩衝區的一部份,而 srWindow 就是用來表示所顯示的部份在那一位置。srWindow 是一 SMALL_RECT 結構體,其欄位說明如下:
SMALL_RECT STRUCT Left WORD ? Top WORD ? Right WORD ? Bottom WORD ? SMALL_RECT ENDS
Left、Top 是『命令提示字元』視窗左上角在控制台螢幕緩衝區的位置;Right、Bottom 則是視窗右下角在控制台螢幕緩衝區的位置,四者皆從 0 開始算起。在 MSDN 中,Left、Top、Right、Bottom 這四個欄位都以 SHORT 表示,其實 SHORT 這種資料型態就佔有一個字組 ( WORD,即 16 位元長 ) 的大小。
最後一個參數,dwMaximumWindowSize,表示『命令提示字元』視窗的最大值,這個最大值是依據控制台螢幕緩衝區的大小、字型的大小及螢幕的大小來決定的。您應當可以想像,若是字形越大、螢幕越小,那麼『命令提示字元』視窗所能顯示的範圍就會越小,dwMaximumWindowSize 也會越小。底下是一張螢幕緩衝區和命令提示字元視窗的關係圖,圖中淡藍色的長方形所框起來的是控制台螢幕緩衝區,而以『命令提示字元』為標題的視窗就是命令提示字元視窗,此圖中的 dwCursorPosition,即游標位置,之值為 00460004h;srWindow 結構體的各欄位,亦即控制台視窗位置,Left、Top、Right、Bottom 分別為 0000、002Eh、004Fh、0046h。
欲取得控制台螢幕緩衝區的資料,可以用 GetConsoleScreenBufferInfo API,其原型如下:
BOOL GetConsoleScreenBufferInfo( HANDLE hConsoleOutput, // handle to console screen buffer PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo // address of screen buffer info. );
呼叫 GetConsoleScreenBufferInfo 之前,得傳入一個指向 CONSOLE_SCREEN_BUFFER_INFO 結構體的位址,如果執行成功,GetConsoleScreenBufferInfo 會傳回 TURE,並將此結構體的各欄位填入目前之數值。
如果要設定『命令提示字元』視窗大小及位置,則用 SetConsoleWindowInfo,其參數為:
BOOL SetConsoleWindowInfo( HANDLE hConsoleOutput, // handle to console screen buffer BOOL bAbsolute, // coordinate type flag CONST SMALL_RECT *lpConsoleWindow // address of new window rectangle );
此 API 的第三個參數為一 SMALL_RECT 結構體位址,這個結構體指定了要設定的『命令提示字元』視窗範圍,SMALL_RECT 結構體內包含了視窗的左上角及右下角位置。第二個參數,bAbsolute,可以為 TRUE 或 FALSE,前者表示在 SMALL_RECT 所存的視窗大小是由螢幕緩衝區的絕對位置 ( 即第零列、第零行 ) 開始算起;若為後者,則表示由現在視窗位置開始算起。若 SMALL_RECT 所存位置不合邏輯 ( 例如負數、左邊位置比右邊大等等 ),則會產生錯誤,傳回 FALSE 值。
SetConsoleWindowInfo 僅僅表面上改變了『命令提示字元』視窗大小並把視窗移到控制台螢幕緩衝區的某個位置,實際上使用者仍然可以操作右邊的捲動軸移動視窗與螢幕緩衝區的相對位置,如果想要讓捲動軸也消失,就得設定控制台螢幕緩衝區的大小,可以用 SetConsoleScreenBufferSize 來達到這目的。SetConsoleScreenBufferSize 的原型是:
BOOL SetConsoleScreenBufferSize( HANDLE hConsoleOutput, // handle to console screen buffer COORD dwSize // new size in character rows and cols. );
dwSize 是新設定的控制台螢幕緩衝區大小,要注意控制台螢幕緩衝區大小的行與列由一開始,並不是由零開始。所以,如果程式設計師想要把命令提示字元視窗設為 80 行寬、10 列高,並使捲動軸消失,必須呼叫兩項 API:
srct SMALL_RECT <0,0,79,9> invoke SetConsoleWindowInfo,hOutput,TRUE,offset srct invoke SetConsoleScreenBufferSize,hOutput,0a0050h ;低位元是行,高位元是列,50h=80d,0ah=10d
SetConsoleScreenBufferSize 設定成功會傳回 TRUE,否則傳回 FALSE。設定失敗的原因,大部分是『命令提示字元』視窗大小比控制台螢幕緩衝區還大,這時可呼叫 GetLastError 而得到 ERROR_INVALID_PARAMETER。
在控制台中,也像在 DOS 一樣,可以設定游標位置,一但設定好游標位置之後,所輸出到控制台的文字便會由此位置顯示出來。設定游標位置的 Win32 API 是 SetConsoleCursorPosition,其原型為:
BOOL SetConsoleCursorPosition( HANDLE hConsoleOutput, // handle to console screen buffer COORD dwCursorPosition
dwCuusorPosition 是一個雙字組整數,存有欲設定的游標位置,較低的 16 位元是 x 座標,較高的 16 位元是 y 座標,這兩個座標都由零開始算。假如所設游標位置不在控制台視窗的範圍內,但仍在控制台螢幕緩衝區範圍內,系統會移動控制台視窗使之到游標新設定的位置;如果所設定游標位置超出控制台螢幕緩衝區範圍,則 SetConsoleCursorPosition 會返回 FALSE。例如想把游標設於第 5 行 ( x=5 ),第 2 列 ( y=2 ),可用下面方式:
invoke SetConsoleCursorPosition,hConsoleOutput,20005h
呼叫 GetConsoleCursorInfo 可以獲得游標資料,其原型為:
BOOL GetConsoleCursorInfo( HANDLE hConsoleOutput, // handle to console screen buffer PCONSOLE_CURSOR_INFO lpConsoleCursorInfo // address of cursor information );
呼叫 GetConsoleCursorInfo 時的第二個參數,lpConsoleCursorInfo,是一個位址指標,指向 CONSOLE_CURSOR_INFO 結構體。這個結構體的內容是:
CONSOLE_CURSOR_INFO STRUCT dwSize DWORD ? bVisible DWORD ? CONSOLE_CURSOR_INFO ENDS
dwSize 是指游標大小,以百分比表示,在 1 與 100 之間。bVisible 為 TRUE 時,表示游標可見;反之,表示游標不可見。如欲設定游標資料,可用 SetConsoleCursorInfo,SetConsoleCursorInfo 用法與 GetConsoleCursorInfo 類似,請自行查閱 MSDN。
所謂『文字的屬性』可以看成控制台中文字的顏色,如前所提過的,文字是以 FOREGROUND_BLUE、FOREGROUND_GREEN、FOREGROUND_RED、FOREGROUND_INTENSITY、BACKGROUND_BLUE、BACKGROUND_GREEN、BACKGROUND_RED 和 BACKGROUND_INTENSITY 等保留字的組合。如果要改變控制台文字的顏色,可用 SetConsoleTextAttribute API,其原型如下:
BOOL SetConsoleTextAttribute( HANDLE hConsoleOutput, // handle to console screen buffer WORD wAttributes // text and background colors );
在呼叫過 SetConsoleTextAttribute 之後,控制台所顯示的顏色均會變成 wAttributes 所指定的顏色,但是在這之前的文字顏色並不會改變。如果只要改變某一個字串的顏色,可用 WriteConsoleOutputAttribute,其原型為:
BOOL WriteConsoleOutputAttribute( HANDLE hConsoleOutput, // handle to screen buffer CONST WORD *lpAttribute, // to buffer to write attributes DWORD nLength, // number of character cells COORD dwWriteCoord, // coordinates of first cell LPDWORD lpNumberOfAttrsWritten // number of cells written );
WriteConsoleOutputAttribute 其實是在控制台中的某一個位置寫入一串屬性,lpAttribute 是指向一個由屬性所組成的陣列 ( 可以把陣列看成是在一段連續的記憶體內的資料 ),此陣列的每個屬性佔有一個字組。nLength 是 lpAttribute 所指的屬性陣列的長度,亦即 nLength 是指屬性陣列有幾個屬性。dwWriteCoord 是指由控制台那一個位置開始寫入屬性,lpNumberOfAttrsWritten 是一個位址,指向一個變數,此變數在 WriteConsoleOutputAttribute 返回時,傳回實際上在控制台寫了幾個屬性。WriteConsoleOutputAttribute 通常會和 WriteConsoleOutputCharacter 配合,WriteConsoleOutputAttribute 是在控制台中的某一個位置寫入一串屬性,WriteConsoleOutputCharacter 則是在某一個位置寫入一字串,它的參數和 WriteConsoleOutputAttribute 相似,其原型為:
BOOL WriteConsoleOutputCharacter( HANDLE hConsoleOutput, // handle to screen buffer LPCTSTR lpCharacter, // buffer to write characters DWORD nLength, // number of characters to write COORD dwWriteCoord, // coordinates of first cell LPDWORD lpNumberOfCharsWritten // number of cells written );
一般在控制台程式中可以用 ReadConsole 來讀取使用者所輸入的按鍵,但是如果要獲得特殊按鍵,例如方向鍵、功能鍵、Shift 鍵等等,就得用 ReadConsoleInput 來讀取鍵盤訊息了。ReadConsoleInput 除了可以讀取鍵盤的輸入外,也可以讀取滑鼠、選單等事件 ( event ) 的輸入,其原型為:
BOOL ReadConsoleInput( HANDLE hConsoleInput, // handle to a console input buffer PINPUT_RECORD lpBuffer, // address of the buffer for read data DWORD nLength, // number of records to read LPDWORD lpNumberOfEventsRead // address of number of records read );
第一個參數,hConsoleInput,顯然是具有可輸入性質的控制台代碼。第二個參數較為複雜,稍後再說。第三個參數,nLength,是用來指示 ReadConsoleInput 要讀取幾筆輸入的事件。Windows 作業系統會為控制台建立並維護一個輸入緩衝區,當有事件發生時,這些事件都會被系統保存在緩衝區裏等程式提取,而程式可以利用 ReadConsoleInput、ReadConsole 等 API 提出在輸入緩衝區內的事件。當用 ReadConsoleInput 提出緩衝區內的事件時,可以在 nLength 指定要提出的事件的數目,如果 nLength 所指定的事件數比在緩衝區內的事件數大時,ReadConsoleInput 並不會等到滿足了 nLength 所指定的事件數才返回,而是立即返回,並於 lpNumberOfEventsRead 所指位址存放實際讀取的事件數。但是如果呼叫 ReadConsoleInput 之前,在緩衝區裏並沒有任何事件,那麼 ReadConsoleInput 就會一直等待,直到有事件出現才返回。為了避免等待,可以在呼叫 ReadConsoleInput 之前,先呼叫 GetNumberOfConsoleInputEvents 來檢查控制台輸入緩衝區裏是否有輸入事件未被讀取,GetNumberOfConsoleInputEvents 的原型是:
BOOL GetNumberOfConsoleInputEvents( HANDLE hConsoleInput, // handle to console input buffer LPDWORD lpcNumberOfEvents // address for number of events );
如果成功,會傳回 TRUE,並於 lpcNumberOfEvents 所指位址存放緩衝區內尚有幾筆事件未被提出。
再回過頭來說 ReadConsoleInput API 的第二個參數,lpBuffer,它是一個指標,指向使用者輸入後的資料存放處。如果所讀取的事件不只一筆,那麼第二筆事件會接在第一筆事件之後,換句話說,lpBuffer 常常是指向一個由許多事件所組成的陣列。這個陣列中的每一筆資料不盡相同,會隨著發生的事件不同,而有不同的形式,但是全部統合在 INPUT_RECORD 結構體內,質言之,我們也可以說 lpBuffer 是指向 INPUT_RECORD 結構體所組成陣列的指標。INPUT_RECORD 結構體的定義是:
INPUT_RECORD STRUCT EventType WORD ? Unknown WORD ? ;註一 UNION event KeyEvent KEY_EVENT_RECORD <> MouseEvent MOUSE_EVENT_RECORD <> ;註二 WindowBufferSizeEvent WINDOW_BUFFER_SIZE_RECORD <> MenuEvent MENU_EVENT_RECORD <> FocusEvent FOCUS_EVENT_RECORD <> ENDS INPUT_RECORD ENDS
UNION/ENDS 是一個 MASM 6.x 以後所新增的假指令,請參考註三,當一事件發生時,從 UNION 到 ENDS 之間的 KeyEvent、MouseEvent、WindowBufferSizeEvent……五個結構體中,只有一個是有意義的,而發生了那一事件被記錄在 EventType。例如,當使用者按下鍵盤上的某一按鍵,EventType 會被 ReadConsoleInput 設為 KEY_EVENT,而 event 所表示的內容為 KEY_EVENT_RECORD 結構體,這個 KEY_EVENT_RECORD 也會被 ReadConsoleInput 設為適當的數值。其他的 EventType 與相對應的結構體列在下表中:
事 件 | EventType 種類 | 數值 | 相對應的結構體 |
按下或放開某個按鍵 | KEY_EVENT | 1h | KEY_EVENT_RECORD |
移動滑鼠或按滑鼠按鈕 | MOUSE_EVENT | 2h | MOUSE_EVENT_RECORD |
更改控制台螢幕緩衝區上的資料 | WINDOW_BUFFER_SIZE_EVENT | 4h | WINDOW_BUFFER_SIZE_RECORD |
系統內不使用,應予忽略 | MENU_EVENT | 8h | MENU_EVENT_RECORD |
系統內不使用,應予忽略 | FOCUS_EVENT | 10h | FOCUS_EVENT_RECORD |
此處僅說明 KEY_EVENT_RECORD 結構體,其餘的請查閱 MSDN。KEY_EVENT_RECORD 結構體被定義成:
KEY_EVENT_RECORD STRUCT bKeyDown BOOL ? ;若為 TRUE,表示按鍵被壓下;FALSE 表示按鍵被鬆開 wRepeatCount WORD ? wVirtualKeyCode WORD ? ;虛擬鍵碼 wVirtualScanCode WORD ? ;虛擬掃描碼 UNION uChar ;uChar 表示按鍵的 ASCII 碼或萬國碼 UnicodeChar WCHAR ? ;萬國碼,一個 WCHAR 相當於一個字組 AsciiChar CHAR ? ;ASCII 碼,一個 CHAR 相當於一個位元組 ENDS dwControlKeyState DWORD ? ;控制鍵 ( 如 Alt、Shift、Ctrl、Scroll Lock 等鍵 ) 的狀態 KEY_EVENT_RECORD ENDS
先說最後面的 dwControlKeyState,它表示控制鍵的狀態,如下表:
dwControlKeyState | 意義 | 數值 |
RIGHT_ALT_PRESSED | 右邊的 Alt 鍵被壓下 | 1h |
LEFT_ALT_PRESSED | 左邊的 Alt 鍵被壓下 | 2h |
RIGHT_CTRL_PRESSED | 右邊的 Ctrl 鍵被壓下 | 4h |
LEFT_CTRL_PRESSED | 左邊的 Ctrl 鍵被壓下 | 8h |
SHIFT_PRESSED | Shift 鍵被壓下 | 10h |
NUMLOCK_ON | Num Lock 被鎖住為數字狀態 | 20h |
SCROLLLOCK_ON | Scroll Lock 被鎖住 | 40h |
CAPSLOCK_ON | Caps Lock 被鎖住為大寫狀態 | 80h |
ENHANCED_KEY | 鍵盤右側的數字鍵或方向鍵被按下 | 100h |
bKeyDown 為一雙字組 ( 即 32 位元 ) 長度的欄位,它表示按鍵被按下或鬆開,若為 TRUE,表示按鍵被壓下;FALSE 表示按鍵被鬆開。在一般情形下,例如秘書小姐打字時,當她按下一個鍵後會立即鬆開該鍵,系統會在她按下該鍵時,在控制台輸入緩衝區裏建立一筆 INPUT_RECORD 事件,且記錄該按鍵的資料 bKeyDown 為 TRUE;而當她鬆開此按鍵時,系統也會再一次記錄一筆 INPUT_RECORD 事件,而此時按鍵事件中的 bKeyDown 為 FALSE。至於按下某一鍵不放,會發生什麼事?或又按下某一鍵不放,又再按下另一鍵,會發生什麼事?又或輸入一中文字,會發生什麼事情?小木偶已經寫好一個程式,CONSOLE2.ASM,讀者可以自行組譯後試試看。
.586 .model flat,stdcall option casemap:none include windows.inc include kernel32.inc include user32.inc includelib kernel32.lib includelib user32.lib INPUT_RECORD STRUCT EventType WORD ? Unknown WORD ? UNION event KeyEvent KEY_EVENT_RECORD <> MouseEvent MOUSE_EVENT_RECORD <> WindowBufferSizeEvent WINDOW_BUFFER_SIZE_RECORD <> MenuEvent MENU_EVENT_RECORD <> FocusEvent FOCUS_EVENT_RECORD <> ENDS INPUT_RECORD ENDS nir equ sizeof INPUT_RECORD ;一個INPUT_RECORD結構體所佔位元組數 ;********************************************************************* .data hIn HANDLE ? hOut HANDLE ? dRead DWORD ? dWritten DWORD ? idx DWORD ? nIR WORD nir ;一個INPUT_RECORD結構體所佔位元組數 wTime WORD 0 ;ReadConsoleInput第幾次讀取 sBlank BYTE ' ' sBlankSize equ $-offset sBlank szFmt1 BYTE '第%03d次:',0 szFmt2 BYTE '%04d',0 szFmt3 BYTE ' %02X',0 ir INPUT_RECORD 40h dup () ;由40H個INPUT_RECORD結構體所組成的陣列 ;存放由ReadConsoleInput所讀取的事件 buffer BYTE 400h dup (' ') ;以ASCII字串格式存放事件處 ;********************************************************************* .code ;--------------------------------------------------------------------- ;deal副程式是把40h個INPUT_RECORD結構體所組成的陣列,以ASCII字串格式存放於buffer處 ;每個INPUT_RECORD結構體有nIR個位元組,因此在此副程式以一個迴圈處理這nIR個位元組, ;並以 deal proc n_i_r:DWORD ;n_i_r是ReadConsoleInput讀取的事件數 LOCAL pBuffer:DWORD ;pBuffer是buffer的指標,表示事件填入到那一位址 LOCAL counterIR:DWORD ;ReadConsoleInput所讀取資料筆數的計數器 LOCAL cIRMember:WORD ;INPUT_RECORD裏第幾個位元組 mov pBuffer,offset buffer invoke wsprintf,offset buffer,offset szFmt1,wTime add pBuffer,eax mov counterIR,0 nxtIR: cmp counterIR,0 jz @f push esi push edi mov ecx,sBlankSize mov esi,offset sBlank add pBuffer,ecx mov edi,pBuffer rep movsb pop edi pop esi @@: invoke wsprintf,pBuffer,offset szFmt2,counterIR add pBuffer,eax mov cIRMember,nir mov eax,counterIR ;計算第counterIR個INPUT_RECORD結構 mul nIR ;體在ir陣列的起始位址,並保存於EAX @@: push eax ;迴圈起始 movzx ecx,byte ptr ir[eax] invoke wsprintf,pBuffer,offset szFmt3,ecx add pBuffer,eax pop eax inc eax dec cIRMember jnz @b ;迴圈結束 inc counterIR mov edx,pBuffer mov word ptr [edx],0a0dh add pBuffer,2 dec n_i_r jnz nxtIR mov ecx,pBuffer mov edx,offset buffer sub ecx,edx ;返回時ECX=要印出的位元組數 ret deal endp ;--------------------------------------------------------------------- start: invoke GetStdHandle,STD_OUTPUT_HANDLE mov hOut,eax invoke GetStdHandle,STD_INPUT_HANDLE mov hIn,eax invoke SetConsoleMode,eax,ENABLE_MOUSE_INPUT get: invoke ReadConsoleInput,hIn,offset ir,40h,offset dRead inc wTime cmp dRead,0 jz get invoke deal,dRead ;把ReadConsoleInput讀取到的事件填入buffer字串中 invoke WriteConsole,hOut,edx,ecx,offset dWritten,NULL ;檢查使用者是否按下Esc鍵,因每次ReadConsoleInput所讀取的事件可能不只一個,所以 ;必須檢查每一個事件,先檢查是否為KEY_EVENT,若是,在檢查是否為Esc鍵 mov idx,0 nxt: mov eax,idx cmp eax,dRead je get inc idx mul nIR cmp word ptr ir[eax],KEY_EVENT jne nxt cmp word ptr ir[eax+0ah],VK_ESCAPE jnz nxt invoke ExitProcess,NULL ;********************************************************************* end start
將上述原始檔案存成 CONSOLE2.ASM 後,依照以下方式組譯,即可得 CONSOLE2.EXE:
E:\HomePage\SOURCE\CONSOLE>set ML=/coff /link /SUBSYSTEM:CONSOLE E:\HomePage\SOURCE\>ml console2.asm Microsoft (R) Macro Assembler Version 6.14.8444 Copyright (C) Microsoft Corp 1981-1997. All rights reserved. Assembling: console2.asm Microsoft (R) Incremental Linker Version 5.12.8078 Copyright (C) Microsoft Corp 1992-1998. All rights reserved. /SUBSYSTEM:CONSOLE "console2.obj" "/OUT:console2.exe" E:\HomePage\SOURCE>
有了 CONSOLE2.EXE 之後,我們就可以用它來做許多事,例如底下是用它在控制台中以注音輸入法輸入一個中文字『一』,執行 CONSOLE2.EXE 之後,畫面是一片空白,接著按 Ctrl-Space,你會看到第一次讀取時,左邊的 Ctrl 鍵被按下,然後接著第二次讀取是空白鍵被按下,接著第三次是左邊的 Ctrl 鍵被鬆開,到此完成切換『注音輸入法』的動作,螢幕左下角出現『注音 半:』,如下圖:
小木偶另外寫了一個程式,CONSOLE3.ASM ,讀者可將滑鼠移至其上,按滑鼠右鍵選另存新檔下載。這是因為 neocities.org 禁止免費用戶上傳 *.ASM 檔,故而掛羊頭賣狗肉把 CONSOLE3.ASM 改名為 CONSOLE3.JPG,讀者可將其名改回來,其執行畫面為:
註一:所有的文獻,包含 MSDN 與網際網路上的資料記載 INPUT_RECORD 結構體的定義是:
INPUT_RECORD STRUCT EventType WORD ? UNION event KeyEvent KEY_EVENT_RECORD <> MouseEvent MOUSE_EVENT_RECORD <> WindowBufferSizeEvent WINDOW_BUFFER_SIZE_RECORD <> MenuEvent MENU_EVENT_RECORD <> FocusEvent FOCUS_EVENT_RECORD <> ENDS INPUT_RECORD ENDS
但是小木偶以 OllyDebug 觀察的結果是 EventType 欄位後還有一個字組,但小木偶不知其原因,也不知為什麼所有文獻都沒有此記錄,或許諸位先進賢達知道而能來信告知,小木偶感激不盡。在小木偶電腦中的 WINDOWS.INC 是於民國 90 年十月 22 日發表的的 1.25a 版,這版中並沒有定義 INPUT_RECORD 結構體,必須用 1.30 版以後的 WINDOWS.INC,否則就得於原始檔中自行定義。
註二:一般控制台並不接收滑鼠訊息,必須用 SetConsoleMode 啟動它,SetConsoleMode 的原型是:
BOOL SetConsoleMode( HANDLE hConsoleHandle, // handle to console input or screen buffer DWORD dwMode // input or output mode to set );
如果 hConsoleHandle 是標準輸入代碼 的話,dwMode 所代表的意義如下表:
dwMode | 數值 | 意義 |
ENABLE_PROCESSED_INPUT | 1h | 此旗標作用時,由系統處理 Ctrl-C 事件,而不放入控制台輸入緩衝區裏,因此 ReadConsole、ReadFile 無法讀出 Ctrl-C 事件。 |
ENABLE_LINE_INPUT | 2h | 擁有此旗標時,只有收到歸位字元 ( carriage return ) 時,ReadConsole、ReadFile 才會返回;否則只要控制台輸入緩衝區有字元就會返回。如果控制台同時擁有 ENABLE_LINE_INPUT 和 ENABLE_LINE_INPUT 兩旗標時,不只 Ctrl-C 由系統處理,連退位、歸位、換行字元都由系統處理。 |
ENABLE_ECHO_INPUT | 4h | 擁有此旗標的控制台代碼,會在 ReadConsole、ReadFile 返回時,於控制台螢幕緩衝區顯示字元。此旗標只有在 ENABLE_LINE_INPUT 有作用時才有用。 |
ENABLE_WINDOW_INPUT | 8h | 擁有此旗標時,ReadConsolInput 能讀取控制台螢幕緩衝區的大小因程式改變的事件,而 ReadConsole、ReadFile 仍會忽視此事件而無法取得。 |
ENABLE_MOUSE_INPUT | 10h | 當控制台視窗擁有輸入焦點時,滑鼠游標在控制台視窗內移動或按下滑鼠鍵時,系統會把滑鼠事件放入控制台輸入緩衝區裏。但是即使控制台擁有 ENABLE_MOUSE_INPUT 旗標,ReadConsole、ReadFile 仍會忽視滑鼠事件而無法取得,只可用 ReadConsoleInput 取得。 |
如果 hConsoleHandle 是標準輸出代碼 的話,dwMode 所代表的意義如下表:
dwMode | 數值 | 意義 |
ENABLE_PROCESSED_OUTPUT | 1h | 擁有此旗標的控制台會由系統處理退位、跳格、鈴聲、歸位與換行字元。 |
ENABLE_WRAP_AT_EOL_OUTPUT | 2h | 擁有此旗標時,當 WriteConsole、WriteFile 對控制台輸出字元或擁有 ENABLE_ECHO_INPUT 的控制台以 ReadConsole、ReadFile 回應字元,而且游標到達控制台視窗的最右邊時,游標會自動跳到下一行的開始位置繼續輸出字元。 |
UNION 是定義一種特殊的變數型態或結構體,其語法為:
名稱 UNION 欄位一 資料型態 ? 欄位二 資料型態 ? …… …… …… 名稱 ENDS
以 UNION 定義的變數型態或結構體,雖然可能包含數個欄位,但是和 STRUCT ( 有關 STRUCT 請參考組合語言第十九章 ) 不同。這些包含在 UNION 與 ENDS 之間的欄位都是由同一位址開始,換句話說,這些欄位都是重疊的。這個意思是說在 UNION 裏的欄位雖然可能有好幾個,卻只能選擇其中一欄作為資料,但是因為事前不確定,所以不知應該用那一個欄位。舉個例子來說,例如我們常把一個大程式分工給許多人負責,每人僅負責一小部份。假如這個大程式要使用年份作為變數,某些人可能用中華民國 97 年,某些人可能用西元 2008 年,那麼程式可以寫成這樣:
YEAR UNION ROC DB ? ;以民國紀元,一個位元組長度就夠了 AD DW ? ;以西曆紀元,要一個字組長度才夠 YEAR ENDS ;********************************************************* .data sale YEAR <> ;發售年份 ;********************************************************* .code ………… mov sale.ROC,97 ;某人可能這樣寫 ………… mov sale.AD,2008 ;另一人可能這樣寫
組譯器組譯 UNION 時,會以長度最長的欄位最為最後的資料長度。以上面的例子,組譯器會為 sale 變數保留一個字組的空間。UNION 在宣告時,也可以同時就指定其初始值,如下面所示:
sale YEAR <97>
UNION 也可以包含在一個結構體內,這時候定義 UNION 時必須像下面這樣,把名稱放在 UNION 之後,而不是之前,例如 INPUT_RECORD 中的 event 就是如此:
結構體名稱 STRUCT 欄位一 資料型態 ? …… ……… UNION 名稱 ;這幾行是 UNION 的定義 欄位A 資料型態 ? ; 欄位B 資料型態 ? ; …… ……… … ; ENDS ;此行 UNION 結束 結構體名稱 ENDS
上面這個例子中,在欄位A、欄位B…中,只能選擇其中一個,並與欄位一、欄位二…形成一個結構體。在 MASM32 V7.0 裏的 WINDOWS.INC 定義 LARGE_INTEGER 是這樣子的:
LARGE_INTEGER STRUCT QuadPart QWORD ? LARGE_INTEGER ENDS
但是在 MASM32 V11 裏的 WINDOWS.INC 定義 LARGE_INTEGER 卻是像下面這樣子的:
LARGE_INTEGER UNION STRUCT LowPart DWORD ? HighPart DWORD ? ENDS QuadPart QWORD ? LARGE_INTEGER ENDS