雖然大部分的人都習慣使用滑鼠操作 Windows 作業系統上的應用程式,但 Windows 也提供了不使用滑鼠的方式,當然這並不好用。Windows 提供了一套標準按鍵介面,可以用來操作功能表;除此之外,應用程式也可以自行設計兩種方法操作功能表,所以,總共有三種方法可以用鍵盤操作功能表:
①、標準按鍵介面(standard keyboard interface)。
②、功能表存取鍵或便捷鍵,英文是 menu access keys,也稱為助憶鍵(mnemonic keys)。
③、鍵盤加速鍵,簡稱加速鍵,英文是 accelerators,也稱為 menu shortcut keys,可翻譯成選單快捷鍵或簡稱快捷鍵。
第一種是 Windows 提供的,後兩種是應用程式提供的。
當然,Windows 也提供了一些系統加速鍵,這種系統加速鍵作用範圍並不是僅限於應用程式,有些與作業系統及其他程式有關,例如 Alt+Esc 切換到下一個視窗、Alt+PrintScreen 把當前活動的視窗內容以圖片格式複製到剪貼簿、Alt+F4 關閉視窗……等,這些加速鍵不在本章討論範圍。
標準按鍵介面是由 Windows 提供的,使用者可以用它來選擇或執行功能表中的選項,就好像使滑鼠游標在功能表上移動或以左鍵點擊選項一樣。差別在於,鍵盤介面不需要特別的程式碼。無論使用者是透過鍵盤還是滑鼠選擇選項,應用程式都會接收命令訊息。
按 鍵 | 說 明 |
Alt 鍵 | 切換進入和退出功能表模式。 |
Alt+空白鍵 | 顯示系統功能表 |
向右鍵 | 使高亮度移到下一層功能表;如果高亮度選項沒有下一層子功能表,那麼就會移到主功能表的下個選項;如果已到主功能表最右邊選項,那麼就輪到系統功能表變為高亮度;如果已到系統功能表,就會移到主功能表最左邊的選項 |
向左鍵 | 與向右鍵類似,只是方向不同或移到上層功能表 |
向上鍵 向下鍵 | 使高亮度背景在子功能表中向上或向下移動;如果高亮度的選項在主功能表,按向上或向下鍵能顯示下層子功能表 |
Esc 鍵 | 退出功能表模式 |
Enter 鍵 | 顯示下一層子功能表或是執行選項(這時視窗函式會收到 WM_COMMAND 或 WM_SYSCOMMAND) |
功能表存取鍵(menu access keys),簡稱存取鍵也稱為助憶鍵(mnemonic keys)。當使用者按下 Alt 鍵進入功能表模式時,功能表的選項文字中會有某個英文字母或阿拉伯數字的下面出現底線,它對應的按鍵,就稱為存取鍵。進入功能表模式後,使用者可以直接按下這個存取鍵來選擇該選項,就好像以滑鼠左鍵點擊這個選項一樣。特別的是,這時不需要特別的程式碼,就能產生 WM_COMMAND 或 WM_SYSCOMMAND 訊息,其中 wParam 的 0~15 位元是選項識別碼,lParam 是 0。
製作存取鍵的方法很簡單。只需在資源描述檔中,把 MENUITEM 之後的選項文字中的某個字元前加上「&」即可。例如:
MENUITEM "離開(&E)",IDM_EXIT
按下 Alt 鍵後,就會看見選項文字是「離開(E)」,「E」鍵稱為存取鍵或助憶鍵。
最後一項是鍵盤加速鍵,通常加速鍵會與功能表中的選項命令相同,這句話的意思是,按下某個加速鍵,相當於用滑鼠左鍵點擊某個選項執行其功能。與助憶鍵不同的是,隨時隨地都可以直接按加速鍵觸發,並不需要進入功能表模式。
鍵盤加速鍵(keyboard accelerators),或稱加速鍵,是指能夠產生 WM_COMMAND 或 WM_SYSCOMMAND 的按鍵或組合按鍵。與標準按鍵或存取鍵不同的地方,是隨時隨地都可以直接按加速鍵觸發。要使用鍵盤加速鍵,必須先在資源描述檔內建立加速鍵表格,原始程式中搭配適當的程式碼處理加速鍵訊息。
快捷鍵通常定義在資源描述檔內的 ACCELERATORS 區塊內。必須先給 ACCELERATORS 區塊指定名稱,稱為加速鍵表格名稱,然後在一對 BEGIN/END 之間才是各種加速鍵的定義。整個 ACCELERATORS 區塊語法如下:
AccTableName ACCELERATORS [optional-statements] BEGIN event, idvalue [,type] [,options] ⁝ END
底下是 ACCELERATORS 區塊的說明:
"b", 2000, ASCII "B", 2001, ASCII "+", 2002
"^A", 2003
"Y", 2004,ASCII "Z", 2005,VIRTKEY
下面例子例舉了六種加速鍵,啟動加速鍵的按鍵在後面的註解中。其中以一對雙引號(「"」)括住的是字元,沒有用雙引號括住的是按鍵。
#define IDM_HELP 5005 ⁝ MY_ACCE ACCELERATORS { VK_PRIOR,5000,VIRTKEY,ALT,CONTROL //Alt+Ctrl+PageUp "B", 5001,VIRTKEY //B "^r", 5002 //Ctrl+"r" 或 Ctrl+"R" "G", 5003 //"G" 0x55, 5004,ASCII,ALT //Alt+"U" "h", IDM_HELP //"h" }
由上面的說明,可以得知,如果 type 設為 ASCII 的話,要考慮 CapsLock 的狀態以及是否按下 Shift 鍵,它們都會影響大小寫,而且又有例外,比較複雜。因此,除非想用小寫的英文字母作為加速鍵才使用 ASCII 類型,否則建議在設置加速鍵時,儘量採用 VIRTKEY 類型,這樣就只管按鍵而不理大小寫,較為單純。此外還有個理由,採用 VIRTKEY 類型已能指定鍵盤上的所有按鍵了。
在資源中定義了加速鍵之後,還必須呼叫 LoadAccelerators 將其載入,並取得加速鍵代碼,LoadAccelerators 的語法如下:
invoke LoadAccelerators,\ hInstance,\ ; handle of application instance lpTableName ; address of table-name string
hInstance 是模組代碼,可以呼叫 GetModuleHandle 得到。lpTableName 是加速鍵表格名稱的位址,加速鍵表格名稱可以是英文字母、阿拉伯數字等組成的字串,也可以是 1~65535 之間的整數。必須與資源描述檔中的 ACCELERATORS 區塊配合。
如果 LoadAccelerators 執行成功,回傳值是加速鍵表格的代碼;如果執行失敗,回傳值為零。
TranslateAccelerator 會在訊息迴圈中檢查 WM_KEYDOWN 或 WM_SYSKEYDOWN 兩訊息,看看是否有符合加速鍵表格中的某項加速鍵,如果有的話就向目標視窗發送 WM_COMMAND 或 WM_SYSCOMMAND 訊息,並把回傳值設為一;如果沒有的話不做處理,回傳值為零。TranslateAccelerator 的語法如下:
invoke TranslateAccelerator,\ hWnd,\ ; handle of destination window hAccTable,\ ; handle of accelerator table lpMsg ; address of structure with message
hWnd 為目標視窗代碼,TranslateAccelerator 會把 WM_COMMAND 或 WM_SYSCOMMAND 訊息傳送給目標視窗。hAccTable 是加速鍵表格代碼,TranslateAccelerator 會把使用者輸入的鍵盤訊息與此加速鍵表格代碼中的加速鍵比對。lpMsg 為 MSG 結構體位址,訊息迴圈中的訊息都存於此。
加速鍵並不是使用者真正想輸入視窗的資料,比如使用者在記事本中輸入文字,然後按 Ctrl+C 是為了「複製」,而並不是想輸入 Ctrl+C 鍵對應的字元,所以 TranslateAccelerator 處理完 Ctrl+C 鍵之後就應該丟棄此訊息。也就是說符合加速鍵的鍵盤訊息不應該再發送給視窗函式,只有不符合加速鍵的訊息(回傳值為零)才要呼叫 TranslateMessage 與 DispatchMessage 繼續處理。因此訊息迴圈的寫法如下:
.while TRUE invoke GetMessage,ADDR msg,0,0,0 .break .if rax==0 invoke TranslateAccelerator,hwnd,hAcce,ADDR msg .if rax==0 invoke TranslateMessage,ADDR msg invoke DispatchMessage,ADDR msg .endif .endw
底下實作一個例子,ACCE,看這名字就知道 ACCE 是指 accelerator 的意思。執行時如下圖,在工作區正中央有幾行說明文字,內容是按下哪些加速鍵能改變文字的顏色。除此之外,另有三個加速鍵沒有寫出來,分別是按下「Q」字元離開 ACCE.EXE、按下「h」字元彈出一視窗顯示 ACCE 的中文名稱、按下「Ctrl+V」彈出一視窗顯示版本。總共七個加速鍵。下圖是以滑鼠左鍵點擊「檢視」的結果。ACCE 需要三個檔案:ACCE.ASM、ACCE.RC、MENU.ico。底下是 ACCE.RC 的內容:
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 |
#include "RESOURCE.H"
#define IDM_QUIT 3000
#define IDM_BLACK 3001
#define IDM_RED 3002
#define IDM_GREEN 3003
#define IDM_BLUE 3004
#define IDM_HELP 3005
#define IDM_VERSION 3006
![]() MY_MENU ICON MENU.ico
MY_MENU MENU
{
MENUITEM "離開(&Q)",IDM_QUIT
POPUP "檢視(&V)"
{
MENUITEM "黑\tCtrl-B",IDM_BLACK
MENUITEM "紅\tAlt-R",IDM_RED
MENUITEM "綠\tAlt-Shift-G",IDM_GREEN
MENUITEM "藍\tCtrl-Shift-U",IDM_BLUE
}
MENUITEM "幫助(&h)",IDM_HELP,HELP
}
HOT_KEY ACCELERATORS
{
"Q", IDM_QUIT
"^B", IDM_BLACK,ASCII
"R", IDM_RED,VIRTKEY,ALT
"G", IDM_GREEN,VIRTKEY,ALT,SHIFT
"U", IDM_BLUE,VIRTKEY,CONTROL,SHIFT
"h", IDM_HELP,ASCII
"V", IDM_VERSION,VIRTKEY,CONTROL
} |
ACCE.RC 的第一行是「#include "RESOURCE.H"」,這行是把 RESOURCE.H 包含檔含括起來,因為它裏面宣告了虛擬鍵碼。
ACCE.RC 的 MENU 區塊中,在頂層功能表中的三個選項:離開、檢視、幫助,其選項文字都有「&英文字母」,這個英文字母的按鍵就是助憶鍵。在檢視子功能表中的每個 MENUITEM 中,所定義的選項文字中含有「\t」(也可以使用「\T」),會讓在這特殊的兩字元之後的文字靠右對齊,代表這後面的是加速鍵。
助憶鍵只需在 MENU 區塊定義好即可,Windows 會依據選項文字後面的選項識別碼執行相對應的功能,因此在資源描述檔與原始程式不需要添加其他程式碼。但是加速鍵就不是這樣了,必須在 ACCELERATORS 區塊中定義相對應的按鍵,而且還要在原始程式中添加處理 WM_COMMAND 或 WM_SYSCOMMAND 訊息的程式碼,並在訊息迴圈中呼叫 TranslateAccelerator 才行。
上面 ACCE.RC 的第 25~34 行定義了 ACCELERATORS 區塊。其中第 27、28、32 行的 type 是 ASCII,而其他加速鍵的 type 是 VIRTKEY。前面提過,type 是 ASCII 的加速鍵必須要在鍵盤上按出該字元,也就是說,要按加速鍵離開 ACCE.EXE,必須按出「Q」字元,如果在 CapsLock 燈熄滅的情形下必須按「Shift+Q」,或是在 CapsLock 燈亮著的情形下按「Q」鍵才能啟動加速鍵離開程式。啟動幫助的快速鍵也是如此。但使文字顏色變黑色的加速鍵是例外,前面已提過。其他四個加速鍵的 type 是 VIRTKEY,情況單純,依據其後 options 的組合鍵按下,就能啟動加速鍵。
底下來看看組合語言原始程式,ACCE.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 118 119 120
121 122 123 124
125 126 127 128
129 130 131 132
133 134 135 136
137 138 139 140
141 142 143 144
145 146 147 148
149 150 151 152
153 154 155 156
157 158 159 160
161 162 163 164
165 166 167 168
169 170 171 172
173 174 175 176
177 178 179 180
181 182 183 184
185 186 187 188
189 |
;示範加速鍵
;組譯與連結:
; uasm64 -win64 acce.asm
; rc acce.rc
; link acce.obj acce.res
OPTION CASEMAP:NONE
OPTION WIN64:7
INCLUDE WINDOWS.INC
INCLUDELIB GDI32.LIB
INCLUDELIB KERNEL32.LIB
INCLUDELIB USER32.LIB
IDM_QUIT EQU 3000
IDM_BLACK EQU 3001
IDM_RED EQU 3002
IDM_GREEN EQU 3003
IDM_BLUE EQU 3004
IDM_HELP EQU 3005
IDM_VERSION EQU 3006
;*****************************************************************************************
.CONST
szMenuName LABEL BYTE
szClassName LABEL BYTE
szIconName DB "MY_MENU",0
szAcce DB "HOT_KEY",0
szAppName DB "加速鍵範例",0
szHelp DB "加速鍵範例程式。",0
szText DB "按底下加速鍵改變顏色:",0dh,0ah,\
" Ctrl-B變黑色",0dh,0ah,\
" Alt-R變紅色",0dh,0ah,\
" Alt-Shift-G變綠色",0dh,0ah,\
" Ctrl-Shift-U變藍色",0
szVersion DB "版本(&V)",0
szVerText0 DB "版本編號:0.01版",0dh,0ah,"你按的是助憶鍵",0
szVerText1 DB "版本編號:0.01版",0dh,0ah,"你按的是加速鍵",0
szVerCaption DB "版本",0
;*****************************************************************************************
.DATA
hInstance QWORD ? ;模組代碼
hwnd HWND ? ;視窗代碼
hMenu HMENU ? ;功能表代碼
hViewSubMenu HMENU ? ;檢視子功能表代碼
hSysMenu HMENU ?
hAcce HANDLE ? ;加速鍵表格代碼
color DD 0 ;顯示於工作區szText的顏色
;*****************************************************************************************
.CODE
;-----------------------------------------------------------------------------------------
DrawTextCentered PROC hdc:HDC,pText:LPCTSTR,pRect:QWORD
;DrawTextCentered能把多行的字串顯示在所指定的矩形(外部矩形)中央(水平置中且鉛錘置中)
;輸入:hdc-裝置內容代碼
; pText-字串位址,此字串為ASCII或Big5編碼且以零為結尾。字串格式如下:
; szText DB "第一行文字",0dh,0ah,\
; "第二行文字",0dh,0ah,\
; ⁝
; "最後一行文字",0
; pRect-外部矩形位址,矩形以RECT結構體表示
;輸出:成功回傳值為非零;失敗回傳值為零(通常是外部矩形不足以容納字串)
LOCAL min_rect:RECT ;能包含在位址pText上字串的最小矩形
mov min_rect.top,0
mov min_rect.left,0
invoke DrawText,hdc,pText,-1,ADDR min_rect,DT_CALCRECT
ASSUME rax:PTR RECT
mov rax,pRect ;EAX=外部矩形位址
mov r10d,[rax].right
mov r11d,[rax].bottom
sub r10d,[rax].left ;R10D=外部矩形寬度
sub r11d,[rax].top ;R11D=外部矩形高度
sub r10d,min_rect.right ;R10D=扣除包含字串寬度之後的寬度
jb error ;如果R10D小於能容納字串最小矩形的寬度,跳躍至error處
sub r11d,min_rect.bottom ;R11D=扣除包含字串高度之後的高度
jb error ;如果R11D小於能容納字串最小矩形的高度,跳躍至error處
shr r10d,1 ;R10D=在字串左或右兩邊的空白寬度
shr r11d,1 ;R11D=在字串上或下兩邊的空白高度
add [rax].left,r10d
add [rax].top,r11d
sub [rax].right,r10d
sub [rax].bottom,r11d
ASSUME rax:NOTHING
invoke DrawText,hdc,pText,-1,rax,DT_LEFT or DT_TOP
jmp exit
error: xor rax,rax
exit: ret
DrawTextCentered ENDP
;-----------------------------------------------------------------------------------------
WndProc PROC hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
LOCAL ps:PAINTSTRUCT,rect:RECT
.switch uMsg
.case WM_COMMAND
.if r8w==IDM_QUIT
jmp quit
.elseif r8w==IDM_BLACK
mov color,0
.elseif r8w==IDM_RED
mov color,0ffh
.elseif r8w==IDM_GREEN
mov color,0ff00h
.elseif r8w==IDM_BLUE
mov color,0ff0000h
.elseif r8w==IDM_HELP
invoke MessageBox,hWnd,OFFSET szHelp,OFFSET szAppName,MB_OK or MB_ICONINFORMATION
jmp @f
.endif
movzx rax,r8w
invoke CheckMenuRadioItem,hViewSubMenu,IDM_BLACK,IDM_BLUE,eax,MF_BYCOMMAND
@@: invoke InvalidateRect,hWnd,0,1
.case WM_PAINT
invoke BeginPaint,hWnd,ADDR ps
invoke GetClientRect,hWnd,ADDR rect
invoke SetTextColor,ps.hdc,color
invoke DrawTextCentered,ps.hdc,OFFSET szText,ADDR rect
invoke EndPaint,hWnd,ADDR ps
.case WM_SYSCOMMAND
.if r8w==IDM_VERSION
mov rdx,OFFSET szVerText0
test r9d,10000h
jz @f ;若ZR,表示按下助憶鍵(在功能表模式下按「V」鍵)
mov rdx,OFFSET szVerText1 ;若NZ,表示按下加速鍵「Ctrl-V」
@@: invoke MessageBox,hWnd,rdx,OFFSET szVerCaption,MB_OK or MB_ICONQUESTION
.else
jmp deflt
.endif
.case WM_CREATE
invoke GetSubMenu,hMenu,1
mov hViewSubMenu,rax
invoke CheckMenuRadioItem,hViewSubMenu,IDM_BLACK,IDM_BLUE,IDM_BLACK,MF_BYCOMMAND
invoke GetSystemMenu,hWnd,0
mov hSysMenu,rax
invoke AppendMenu,hSysMenu,MF_STRING,IDM_VERSION,OFFSET szVersion
.case WM_DESTROY
quit: invoke PostQuitMessage,0
.default
deflt: invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endsw
xor rax,rax
ret
WndProc ENDP
;-----------------------------------------------------------------------------------------
main PROC
LOCAL wc:WNDCLASSEX,msg:MSG
invoke GetModuleHandle,0
mov hInstance,rax
lea rdx,WndProc
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style,CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc,rdx
mov wc.cbClsExtra,0
mov wc.cbWndExtra,0
mov wc.hInstance,rax
invoke LoadIcon,hInstance,OFFSET szIconName
mov wc.hIcon,rax
mov wc.hIconSm,rax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,rax
lea r10,szClassName
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,0
mov wc.lpszClassName,r10
invoke RegisterClassEx,ADDR wc
invoke LoadMenu,hInstance,OFFSET szMenuName
mov hMenu,rax
invoke LoadAccelerators,hInstance,OFFSET szAcce ;載入加速鍵表格
mov hAcce,rax
invoke CreateWindowEx,0,OFFSET szClassName,OFFSET szAppName,\
WS_CAPTION or WS_SYSMENU,100,100,400,300,0,hMenu,hInstance,0
mov hwnd,rax
invoke ShowWindow,hwnd,SW_SHOWNORMAL
invoke UpdateWindow,hwnd
.while TRUE
invoke GetMessage,ADDR msg,0,0,0
.break .if rax==0
invoke TranslateAccelerator,hwnd,hAcce,ADDR msg ;把翻譯加速鍵訊息
.if rax==0
invoke TranslateMessage,ADDR msg
invoke DispatchMessage,ADDR msg
.endif
.endw
invoke ExitProcess,0
main ENDP
;*****************************************************************************************
END main |
ACCE.ASM 中的加速鍵依據與之搭配的選項在哪個功能表,可分成三類:
前兩種情形是一樣的,必須處理 WM_COMMAND 訊息;而第三種必須處理 WM_SYSCOMMAND 訊息。但不論是哪一種,都由資源描述檔中的 ACCELERATORS 定義,並呼叫 LoadAccelerators 載入,然後在訊息迴圈中呼叫 TranslateAccelerator,依據加速鍵所搭配的選項是否在系統功能表內,將其轉換成 WM_COMMAND(選項不在系統功能表內) 或 WM_SYSCOMMAND(選項在系統功能表內)。
當使用者按下加速鍵且與這加速鍵搭配的選項不在系統功能表內,系統會把 WM_COMMAND 訊息傳給 ACCE 的視窗函式,視窗函式檢查選項識別碼做相對應的工作。不知有沒有人會有疑問,怎麼按下加速鍵,視窗函式卻是比對選項識別碼處理訊息?其實答案很簡單,因為 ACCE 把加速鍵識別碼設為相對應的選項識別碼,也就是兩者相同,這樣就不須撰寫額外的程式碼去處理按下加速鍵事件所產生的訊息。是不是很方便呢?
這裏處理加速鍵的方式(或選項)可分為兩種:①離開與幫助選項、②改變顏色的選項。後者比較複雜,並不是只要改變顏色就好了,還必須呼叫 InvalidateRect,才能看見工作區內的文字改變顏色。除此之外,還要呼叫 CheckMenuRadioItem,才能設定好現在選擇是哪個選項。
以上這段程式在第 90~107 行。
當使用者按下加速鍵,且與這加速鍵搭配的選項在系統功能表內,系統會把 WM_SYSCOMMAND 訊息傳送給視窗函式。ACCE 只需處理它自己定義與加速鍵搭配的選項即可,這種選項的識別碼必須小於 0F000H。而在系統功能表內的其他選項是系統內建的,須交由 DefWindowProc 處理。這段程式在第 116~125 行。
在 ACCE 中還展示了使用者按加速鍵或按助憶鍵不同的處理方式,檢測 lParam 的第 16 位元零還是一,就能達到目的。因為第七行,設定了「OPTION WIN64:7」,因此一進入視窗函式後,R9 之值就已經設為 lParam 了;所以第 119 行的 TEST 指令就是檢測第 16 位元是零還是一。如果是零,表示按下的是助憶鍵;如果是一,表示按下的是加速鍵。
DrawTextCentered 副程式是用來把一個多行字串顯示在一個矩形範圍的正中央,不僅水平置中,也是鉛錘置中。它並不是 Win64 API,而是小木偶自行撰寫的副程式,它的語法是:
invoke DrawTextCentered,\ hdc,\ pText,\ pRect
hdc 是裝置內容。pText 是字串位址,這個字串必須含有換行符號(換行符號就是 0DH、0AH 組合)。pRect 是 RECT 結構體位址,這個結構體指定了一個矩形範圍,可稱為外部矩形,pText 位址上的字串將顯示在外部矩形的正中間。如果 DrawTextCentered 執行成功,回傳值是繪製文字的高度,因為是多行文字所以是總高度;如果執行失敗,回傳值是 0。
那麼 DrawTextCentered 是怎麼把多行字串顯示在外部矩形的正中央呢?第八章曾提過 DrawText 可以用「DT_SINGLELINE or DT_CENTER or DT_VCENTER」參數使單行文字水平置中且鉛錘置中,如果多行文字就無能為力了。不過還是可以利用「DT_CALCRECT」參數,去計算恰好能容納多行字串所需最小矩形的寬度與高度。
過程如下:首先定義區域變數 min_rect 為最小矩形,將其 left、top 欄位設為零並以 min_rect 的位址及 DT_CALCRECT 為參數。呼叫 DrawText,成功呼叫後,min_rect 中的 right、bottom 就是最小矩形的寬度及高度,意即 min_rect.right 及 min_rect.bottom 就是最小矩形的寬度及高度。因其名稱太長了,在下圖中分別以 ω、h 表示。
DrawTextCentered 的最後一個參數是外部矩形位址,而這外部矩形的四個欄位 left、top、right、bottom 分別是上圖中的 x1、y1、x2、y2。外部矩形的寬度與高度,分別是 x2-x1、y2-y1,各以 W、H 表示,如上圖。既然要把多行字串顯示於螢幕正中央,所以要調整最小矩形的位置,使最小矩形左邊到外部矩形左邊的距離,與最小矩形右邊到外部矩形右邊的距離相等,都是
;上下兩邊也是如此,其值為
,見上圖。
此處有件事值得一提。如果外部矩形比最小矩形還小,表示無法將完整的字串顯示出來,這時候 DrawTextCentered 有兩種選擇,一是停止繪製字串傳回錯誤的回傳值,二是盡量繪製即使有部分字串無法顯示。小木偶把 DrawTextCentered 設計成第一種,所以必須要判斷 W 是否大於或等於ω,H 是否大於或等於 h,只要前兩項比較的結果有一項是否定的,就得將零傳回給主程式然後退出。
如果直接以兩個 CMP 指令比較,會增加程式碼也降低效率。剛才提過,DrawTextCentered 會計算最小矩形左右兩邊及上下兩邊空白的大小,會有兩個步驟要計算 W-ω、H-h。注意了!還記得第一章介紹 SUB 指令時,提到如果目的運算元較來源運算元小,相減會發生借位而設定進位旗標(CF=1);如果目的運算元較大,就不發生借位,那麼就會清除進位旗標(CF=0)。還有在第三章也提及過,JB 指令是指當 CF=1 時發生跳躍。由以上兩點,這段程式可以寫成下面的樣子:(下面程式未執行前,R10D 為 W,R11D 為 H)
sub r10d,min_rect.right ;R10D=扣除包含字串寬度之後的寬度 jb error ;如果R10D小於能容納字串最小矩形的寬度,跳躍至error處 sub r11d,min_rect.bottom ;R11D=扣除包含字串高度之後的高度 jb error ;如果R11D小於能容納字串最小矩形的高度,跳躍至error處 shr r10d,1 ;R10D=在字串左或右兩邊的空白寬度 shr r11d,1 ;R11D=在字串上或下兩邊的空白高度
上面第一行 SUB 指令執行時,如果 R10D 小於 min_rect.right(此值為最小矩形的寬度),那麼會設定 CF,使 CF=1,接著是 JB 指令,就會發生跳躍至 error: 處;反之則不跳躍。僅需一個 JB 指令就能檢查外部矩形是否比最小矩形還寬。第三、四行也是如此。經過兩個 SHR 指令後,R10D 之值為,R11D 之值為
。
接下來就可以設定最小矩形的左上角與右下角座標。這裏要解釋右下角座標,本來右下角的 X 座標應該是 x1++ω,可以經過下面計算:
因為 W=x2-x1,所以 x1=x2-W
x1++ω=x2-W+
+ω=x2+
( -2W+W-ω+2ω)=x2-
可以得到 x1++ω=x2-
,同理 y1+
+h=y2-
。上面是用數學的方法證明最小矩形右下角的座標,可以轉換成另一種寫法,其值不變。事實上,也可以這樣想,在最小矩形左右兩側外面的空間是一樣大的,都是
;所以最小矩形右下角的 X 座標是外部矩形右下角 X 座標減去
,同理最小矩形右下角的 Y 座標是外部矩形右下角 Y 座標減去
。
因為此刻 R10D 之值為,R11D 之值為
。假設換成新的寫法,就會發現原先輸入的外部矩形座標經過加減 R10D、R11D 就是最小矩形的座標,這樣就減少很多程式碼:
add [rax].left,r10d ;[rax].left原先是外部矩形左上角的X座標 add [rax].top,r11d ;[rax].top原先是外部矩形左上角的Y座標 sub [rax].right,r10d sub [rax].bottom,r11d
用這樣的方法,雖然 DrawTextCentered 一開始定義了 min_rect 為最小矩形,但此時要印出多行字串時卻用原先輸入的外部矩形:
mov rax,pRect ;EAX=外部矩形位址
⁝
invoke DrawText,hdc,pText,-1,rax,DT_LEFT or DT_TOP
DrawTextCnetered 的完整程式在第 50~85 行,小木偶撰寫這段程式,以最少的程式碼作為優先考量,這也是用組合語言最大的好處之一,不過這樣的情形已越來越少了。不管怎樣,這一章就到這兒結束。
使用者按下鍵盤上的按鍵時,Windows 會把 WM_KEYDOWN 訊息發送給視窗函式。其中的 wParam 是該按鍵的虛擬鍵碼,有關虛擬鍵碼與底下的 WM_KEYUP 一起說明。WM_KEYDOWN 訊息的 lParam 是其他有關該按鍵的資料,說明如下:
民國七十年發表的 IBM PC 所配備的鍵盤是 83/84 鍵,後來發售的電腦所配備的鍵盤,通常會在右邊的數字鍵與英文字母鍵之間,加入方向鍵及「Insert」、「Delete」等十個鍵,這些多出來的就是延伸鍵(extended key)。除此之外,還有一些按鍵也是延伸鍵。
最後整理一下,延伸鍵有下面幾種:
使用者鬆開鍵盤上的按鍵時,Windows 會把 WM_KEYUP 訊息發送給視窗函式。其中的 wParam 是該按鍵的虛擬鍵碼,稍後說明。WM_KEYUP 訊息的 lParam 是其他有關該按鍵的資料,說明如下:
Windows 作業系統為了分辨使用者到底在鍵盤上按了什麼鍵,而把每個按鍵設定了虛擬鍵碼。每個虛擬鍵碼都是一個常數,用這些常數用來標識鍵盤上的按鍵。當使用者按下鍵盤上的按鍵時,Windows 會把 WM_KEYDOWN 訊息發送給視窗函式;鬆開按鍵時,則發送 WM_KEYUP。這兩則訊息中的 wParam 參數就是該按鍵的虛擬鍵碼。下圖在鍵盤上各按鍵的虛擬鍵碼。各按鍵的虛擬鍵碼在按鍵側面,以十六進位紅色數值表示。
比較特別的是鍵盤右邊的數字鍵,有些鍵有兩種虛擬鍵碼。當 NumLock 開啟時(NumLock 的 LED 燈亮著),右邊的數字鍵變成方向鍵,其虛擬鍵碼如上圖;當 NumLock 按鍵關閉時(NumLock 的 LED 燈熄滅),右邊的數字鍵變成方向鍵,其虛擬鍵碼也就變成和它左邊的方向鍵一樣了。整理如下表(表中數值均為十六進位):
NumLock | . | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
開啟 | 6E | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
關閉 | 2E | 2D | 23 | 28 | 22 | 25 | 0C | 27 | 24 | 26 | 21 |
Alt 鍵有兩個,分布在空白鍵的左右兩側,其虛擬鍵碼相同,僅從 WM_KEYDOWN、WM_KEYUP 的 wParam 無法分辨使用者是按下左邊的還是右邊的,但可以配合 lParam 第 24 位元是否延伸鍵判斷。Ctrl 鍵也是同樣的狀況,但是 Shift 鍵就無法這樣判斷了,可以呼叫 GetAsyncKeyState API 判斷左邊的 Shift 鍵按下還是右邊的。