第十九章 鍵盤加速鍵


用鍵盤操作 Windows 應用程式

雖然大部分的人都習慣使用滑鼠操作 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 區塊內。必須先給 ACCELERATORS 區塊指定名稱,稱為加速鍵表格名稱,然後在一對 BEGIN/END 之間才是各種加速鍵的定義。整個 ACCELERATORS 區塊語法如下:

AccTableName    ACCELERATORS
[optional-statements]
BEGIN
  event, idvalue [,type] [,options]
    ⁝
END

底下是 ACCELERATORS 區塊的說明:

  1. AccTableName 是加速鍵表格名稱,可以是英文字母、阿拉伯數字等組成的字串,也可以是 1~65535 之間的整數。不論是哪一種,都要與原始程式相互吻合,這點倒與前面介紹過的圖示、功能表一樣。
  2. optional-statements 在一對「[」、「]」內代表可以省略,也可以是下面幾種情形:
    ①、CHARACTERISTICS dword:定義加速鍵區塊的特徵資料,這資料僅一個雙字組大小,而且只保留在 RES 檔中,在連結之後不會保留在 EXE 檔內。因此這資料只能被資源編輯器或資源編譯器使用,對系統而言沒什麼用途。
    ②、LANGUAGE language, sublanguage:指定主要語言與次要語言,直到下一個 LANGUAGE 語句或檔案末端。
    ③、VERSION dword:指定版本號碼。
  3. BEGIN/END:在 BEGIN/END 之間的內容,是每一種加速鍵的定義,BEGIN/END 也可以用一對「{」、「}」代替。
  4. event:用來作為加速鍵的按鍵,可以是下面三種情形:
    ①、virtual-key 或 character:先說 virtual-key。virtual-key 是虛擬鍵碼的意思,在 Windows 作業系統中,鍵盤上的每個按鍵都用常數表示,這樣作業系統就能分辨使用者按了什麼鍵,此常數就是虛擬鍵碼。以虛擬鍵碼表示加速鍵有兩類情形:
    ⑴、英文字母或阿拉伯數字等一般按鍵:以雙引號把大寫字母或數字括起來,例如,「"9"」或「"C"」。這類按鍵的虛擬鍵碼恰好與其字元的 ASCII 碼相同,因此一般按鍵的虛擬鍵碼就用大寫字母或數字表示。例如在鍵盤上,「C」鍵的虛擬鍵碼是 43H(見註一的圖)而英文字元「C」的 ASCII 碼是 43H(見 DOS 組合語言附錄四ASCII 第二部分)。
    ⑵、特殊按鍵:例如 F1、F2……或方向鍵……,在它們前面加上「VK_」,就變成虛擬鍵碼。F1、F2……、向左鍵、向上鍵……就變成 VK_F1、VK_F2……VK_LEFT、VK_UP……。這些常數都定義在 RESOURCE.H 中。
    事實上,⑴表示方式就是 character 表示方式。不管是用 ⑴,還是 ⑵,type 都必須是 VIRTKEY。
    ②、"char":這種表示加速鍵的方式是以使用者在鍵盤上按出的字元來表示,因此在一對「"」之間的 char 可以是英文字母、阿拉伯數字,甚至是「+」、「-」……等等,而 type 可省略或為 ASCII。例如:
      "b",  2000, ASCII
      "B",  2001, ASCII
      "+",  2002
    上面第一個例子,使用者必須按出小寫的「b」才有作用。在 CapsLock 燈熄滅時按「B」鍵能產生小寫的「b」,或在 CapsLock 燈亮著時按「Shift+B」鍵也能產生小寫的「b」,兩者都有加速鍵的效果;假如在 CapsLock 燈亮著時按「B」鍵,產生的是大寫的「B」,這樣就沒有加速鍵的效果。第二個例子則與第一個例子相反。第三的例子,只有當使用者按「Shift++」鍵才有作用(先按「Shift」鍵不放再按「+」鍵)。
    也可以在「"」與字元之間加上「^」,代表同時按下 Ctrl 鍵及該字元鍵才有作用,type 可省略或為 ASCII,如下面例子必須同時按「Ctlt+"A"」鍵才有作用:
      "^A", 2003
    事實上,上面的例子不論 CapsLock 是熄滅還是亮著,按「Ctlt+Shift+A」鍵或「Ctlt+A」都有效果,只能都做例外的特例吧!
    ③、數值:此數值是 32~127 之間的整數,事實上這個整數其實就是 ASCII 碼,所以其後的 type 參數必須是 ASCII,且不能省略。如果採用這種方式,就無法使用「^」代表必須同時按下 Ctrl 鍵及該字元鍵才有作用。
  5. idvalue:加速鍵的識別碼,是 1~65535 之間的整數。一般而言,加速鍵會與功能表中的某個選項搭配,亦即按下加速鍵相當於以滑鼠左鍵點擊該選項。此時加速鍵的識別碼與該選項的識別碼相同,這樣就能達成目的,同時也不必再寫針對按下加速鍵事件處理的程式,只需沿用點擊選項事件處理的程式即可,省卻許多麻煩。
  6. type:當 event 為 virtual-key 或 character 時才需要設置 type,且 type 只能是 ASCII 或 VIRTKEY 中的一種。如果省略 type,預設值為 ASCII。例如:
      "Y",  2004,ASCII
      "Z",  2005,VIRTKEY
    上面的第一個例子因為有 ASCII 類型,所以使用者必須在鍵盤上按出「Y」字元才行,如果 CapsLock 是熄滅的就得按「Shift+Y」才有效果。第二個例子因為是 VIRTKEY 類型,所以只要按下「Z」鍵且沒有按其他組合鍵(組合鍵是指同時再按下 Shift、Ctrl 或 Alt 鍵),那麼不論 CapsLock 是熄滅還是亮著都有加速鍵的效果。
  7. options:可以是 ALT、CONTROL、SHIFT 的組合,或是 NOINVERT。前三者代表要按下所指定的按鍵才會啟動加速鍵;而 NOINVERT 是為了與 Windows 3.x 相容才保留,已經過時沒有意義了。

下面例子例舉了六種加速鍵,啟動加速鍵的按鍵在後面的註解中。其中以一對雙引號(「"」)括住的是字元,沒有用雙引號括住的是按鍵。

#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 API

在資源中定義了加速鍵之後,還必須呼叫 LoadAccelerators 將其載入,並取得加速鍵代碼,LoadAccelerators 的語法如下:

invoke  LoadAccelerators,\
        hInstance,\     ; handle of application instance
        lpTableName     ; address of table-name string

hInstance 是模組代碼,可以呼叫 GetModuleHandle 得到。lpTableName 是加速鍵表格名稱的位址,加速鍵表格名稱可以是英文字母、阿拉伯數字等組成的字串,也可以是 1~65535 之間的整數。必須與資源描述檔中的 ACCELERATORS 區塊配合。

如果 LoadAccelerators 執行成功,回傳值是加速鍵表格的代碼;如果執行失敗,回傳值為零。

TranslateAccelerator API

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,看這名字就知道 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 中的加速鍵依據與之搭配的選項在哪個功能表,可分成三類:

  ①、在頂層功能表中,例如「Q」字元、「h」字元。
  ②、在子功能表內,例如「Ctrl+B」、「Alt+R」、「Alt+Shift+G」、「Ctrl+Shift+U」改變文字顏色。
  ③、在系統功能表內,例如按「Ctrl+V」。

前兩種情形是一樣的,必須處理 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 行,小木偶撰寫這段程式,以最少的程式碼作為優先考量,這也是用組合語言最大的好處之一,不過這樣的情形已越來越少了。不管怎樣,這一章就到這兒結束。


註一:虛擬鍵

WM_KEYDOWN 訊息

使用者按下鍵盤上的按鍵時,Windows 會把 WM_KEYDOWN 訊息發送給視窗函式。其中的 wParam 是該按鍵的虛擬鍵碼,有關虛擬鍵碼與底下的 WM_KEYUP 一起說明。WM_KEYDOWN 訊息的 lParam 是其他有關該按鍵的資料,說明如下:

  1. 第 0~15 位元:稱為重複次數。使用者按住按鍵不放不太久,會發出一則 WM_KEYDOWN,重複次數會累積超過一;如果使用者按住按鍵足夠久,那麼會發出多則相互獨立的 WM_KEYDOWN,重複次數仍為一。
  2. 第 16~23 位元:鍵盤掃描碼,見 DOS 組合語言第十三章註一
  3. 第 24 位元:延伸鍵(extended key)是否按下,如果按下此位元為一,否則為零。延伸鍵有好幾個,見稍後說明。
  4. 第 25~28 位元:保留,未使用。
  5. 第 29 位元:總是為零。
  6. 第 30 位元:前次按鍵狀態。如果此位元是一,表示按鍵在上一次訊息中是按下的,這意味著按鍵在此次按下之前已經處於按下狀態;如果此位元是零,表示按鍵在上次訊息中是鬆開的,這意味著這是按鍵從鬆開然後按下。因為使用者按下某個鍵不放時,系統會重複傳送 WM_KEYDOWN,第 30 位元可以幫助確定當前的 WM_KEYDOWN 是由於按住不放而自動重複生成的訊息,還是按鍵從鬆開到按下的情形。
  7. 第 31 位元:此位元稱過度狀態(transition state),對 WM_KEYDOWN 而言,此位元總是為零。

民國七十年發表的 IBM PC 所配備的鍵盤是 83/84 鍵,後來發售的電腦所配備的鍵盤,通常會在右邊的數字鍵與英文字母鍵之間,加入方向鍵及「Insert」、「Delete」等十個鍵,這些多出來的就是延伸鍵(extended key)。除此之外,還有一些按鍵也是延伸鍵。

最後整理一下,延伸鍵有下面幾種:

  1. 右邊的 Alt 鍵、右邊的 Ctrl 鍵。
  2. 上、下、左、右、Home、End、PageUp、PageDown八個方向鍵及 Insert、Delete 兩個鍵。
  3. 右邊數字鍵上的除號(/)、Enter、CapsLock 鍵。
  4. 兩邊的 Windows 鍵(見下圖,在左邊 Alt 鍵左邊及右邊 Alt 鍵右邊)、Applications 鍵(在右邊 Windows 鍵右邊)。

WM_KEYUP 訊息

使用者鬆開鍵盤上的按鍵時,Windows 會把 WM_KEYUP 訊息發送給視窗函式。其中的 wParam 是該按鍵的虛擬鍵碼,稍後說明。WM_KEYUP 訊息的 lParam 是其他有關該按鍵的資料,說明如下:

  1. 第 0~15 位元:稱為重複次數,只對 WM_KEYDOWN 有用,對 WM_KEYUP 而言重複次數總是為一。
  2. 第 16~23 位元:鍵盤掃描碼,見 DOS 組合語言第十三章註一
  3. 第 24 位元:延伸鍵(extended key)是否按下,如果按下此位元為一,否則為零。
  4. 第 25~28 位元:保留,未使用。
  5. 第 29 位元:總是為零。
  6. 第 30 位元:前次按鍵狀態。對 WM_KEYUP 而言,只可能是按鍵處於按下狀態,所以第 30 位元總是一。
  7. 第 31 位元:此位元稱過度狀態(transition state),對 WM_KEYUP 而言,此位元總是為一。

虛擬鍵碼

Windows 作業系統為了分辨使用者到底在鍵盤上按了什麼鍵,而把每個按鍵設定了虛擬鍵碼。每個虛擬鍵碼都是一個常數,用這些常數用來標識鍵盤上的按鍵。當使用者按下鍵盤上的按鍵時,Windows 會把 WM_KEYDOWN 訊息發送給視窗函式;鬆開按鍵時,則發送 WM_KEYUP。這兩則訊息中的 wParam 參數就是該按鍵的虛擬鍵碼。下圖在鍵盤上各按鍵的虛擬鍵碼。各按鍵的虛擬鍵碼在按鍵側面,以十六進位紅色數值表示。

比較特別的是鍵盤右邊的數字鍵,有些鍵有兩種虛擬鍵碼。當 NumLock 開啟時(NumLock 的 LED 燈亮著),右邊的數字鍵變成方向鍵,其虛擬鍵碼如上圖;當 NumLock 按鍵關閉時(NumLock 的 LED 燈熄滅),右邊的數字鍵變成方向鍵,其虛擬鍵碼也就變成和它左邊的方向鍵一樣了。整理如下表(表中數值均為十六進位):

NumLock.0123456789
開啟6E60616263646566676869
關閉2E2D232822250C27242621

Alt 鍵有兩個,分布在空白鍵的左右兩側,其虛擬鍵碼相同,僅從 WM_KEYDOWN、WM_KEYUP 的 wParam 無法分辨使用者是按下左邊的還是右邊的,但可以配合 lParam 第 24 位元是否延伸鍵判斷。Ctrl 鍵也是同樣的狀況,但是 Shift 鍵就無法這樣判斷了,可以呼叫 GetAsyncKeyState API 判斷左邊的 Shift 鍵按下還是右邊的。