鍵盤知識

在Microsoft Windows 98中,鍵盤和鼠標是兩個標準的使用者輸入來源,在一些連貫操作中常產生互補作用。當然,鼠標在今天的應用程序中比十年前使用得更爲廣泛。甚至在一些應用程序中,我們更習慣於使用鼠標,例如在遊戲、畫圖程序、音樂程序以及Web瀏覽器等程序中就是這樣。然而,我們可以不使用鼠標,但絕對不能從一般的PC中把鍵盤拆掉。

相對於個人計算機的其它組件,鍵盤有非常久遠的歷史,它起源於1874年的第一臺Remington打字機。早期的計算機程序員用鍵盤在Hollerith卡片上打孔,後來在終端機上用鍵盤直接與大型主機溝通。PC上的鍵盤在某些方面進行了擴充,加上了功能鍵、光標移動鍵和單獨的數字鍵盤,但它們的輸入原理基本相同。

鍵盤基礎

您大概已經猜到Windows程序是如何獲得鍵盤輸入的:鍵盤輸入以消息的形式傳遞給程序的窗口消息處理程序。實際上,第一次學習消息時,鍵盤事件就是一個消息如何將不同型態信息傳遞給應用程序的顯例。

Windows用八種不同的消息來傳遞不同的鍵盤事件。這好像太多了,但是(就像我們所看到的一樣)程序可以忽略其中至少一半的消息而不會有任何問題。並且,在大多數情況下,這些消息中包含的鍵盤信息會多於程序所需要的。處理鍵盤的部分工作就是識別出哪些消息是重要的,哪些是不重要的。

忽略鍵盤

雖然鍵盤是Windows程序中使用者輸入的主要來源,但是程序不必對它接收的所有消息都作出響應。Windows本身也能處理許多鍵盤功能。

例如,您可以忽略那些屬於系統功能的按鍵,它們通常用到Alt鍵。程序不必監視這些按鍵,因爲Windows會將按鍵的作用通知程序(當然,如果程序想這麼做,它也能監視這些按鍵)。雖然呼叫程序菜單的按鍵將通過窗口的窗口消息處理程序,但通常內定的處理方式是將按鍵傳遞給DefWindowProc。最終,窗口消息處理程序將獲得一個消息,表示一個菜單項被選擇了。通常,這是所有窗口消息處理程序需要知道的(在第十章將介紹菜單)。

有些Windows程序使用「鍵盤快捷鍵」來啓動通用菜單項。快捷鍵通常是功能鍵或字母同Ctrl鍵的組合(例如,Ctrl-S用於保存文件)。這些鍵盤快捷鍵與程序菜單一起在程序的資源描述文件中定義(我們可以在第十章看到)。Windows將這些鍵盤快捷鍵轉換爲菜單命令消息,您不必自己去進行轉換。

對話框也有鍵盤接口,但是當對話框處於活動狀態時,應用程序通常不必監視鍵盤。鍵盤接口由Windows處理,Windows把關於按鍵作用的消息發送給程序。對話框可以包含用於輸入文字的編輯控件。它們一般是小方框,使用者可以在框中鍵入字符串。Windows處理所有編輯控件邏輯,並在輸入完畢後,將編輯控件的最終內容傳送給程序。關於對話框的詳細信息,請參見第十一章。

編輯控件不必侷限於單獨一行,而且也不限於只在對話框中。一個在程序主窗口內的多行編輯控件就能夠作爲一個簡單的文字編輯器了(參見第九、十、十一和十三章的POPPAD程序)。Windows甚至有一個Rich Text文字編輯控件,允許您編輯和顯示格式化的文字(請參見/Platform SDK/User Interface Services/Controls/Rich Edit Controls)。

您將會發現,在開發Windows程序時,可以使用處理鍵盤和鼠標輸入的子窗口控件來將較高層的信息傳遞迴父窗口。只要這樣的控件用得夠多,您就不會因處理鍵盤消息而煩惱了。

誰獲得了焦點

與所有的個人計算機硬件一樣,鍵盤必須由在Windows下執行的所有應用程序共享。有些應用程序可能有多個窗口,鍵盤必須由該應用程序內的所有窗口共享。

回想一下,程序用來從消息隊列中檢索消息的MSG結構包括hwnd字段。此字段指出接收消息的窗口控件碼。消息循環中的DispatchMessage函數向窗口消息處理程序發送該消息,此窗口消息處理程序與需要消息的窗口相聯繫。在按下鍵盤上的鍵時,只有一個窗口消息處理程序接收鍵盤消息,並且此消息包括接收消息的窗口控件碼。

接收特定鍵盤事件的窗口具有輸入焦點。輸入焦點的概念與活動窗口的概念很相近。有輸入焦點的窗口是活動窗口或活動窗口的衍生窗口(活動窗口的子窗口,或者活動窗口子窗口的子窗口等等)。

通常很容易辨別活動窗口。它通常是頂層窗口-也就是說,它的父窗口句柄是NULL。如果活動窗口有標題列,Windows將突出顯示標題列。如果活動窗口具有對話框架(對話框中很常見的格式)而不是標題列,Windows將突出顯示框架。如果活動窗口目前是最小化的,Windows將在工作列中突出顯示該項,其顯示就像一個按下的按鈕。

如果活動窗口有子窗口,那麼有輸入焦點的窗口既可以是活動窗口也可以是其子窗口。最常見的子窗口有類似以下控件:出現在對話框中的下壓按鈕、單選鈕、複選框、滾動條、編輯方塊和清單方塊。子窗口不能自己成爲活動窗口。只有當它是活動窗口的衍生窗口時,子窗口纔能有輸入焦點。子窗口控件一般通過顯示一個閃爍的插入符號或虛線來表示它具有輸入焦點。

有時輸入焦點不在任何窗口中。這種情況發生在所有程序都是最小化的時候。這時,Windows將繼續向活動窗口發送鍵盤消息,但是這些消息與發送給非最小化的活動窗口的鍵盤消息有不同的形式。

窗口消息處理程序通過攔截WM_SETFOCUS和WM_KILLFOCUS消息來判定它的窗口何時擁有輸入焦點。WM_SETFOCUS指示窗口正在得到輸入焦點,WM_KILLFOCUS表示窗口正在失去輸入焦點。我將在本章的後面詳細說明這些消息。

隊列和同步

當使用者按下並釋放鍵盤上的鍵時,Windows和鍵盤驅動程序將硬件掃描碼轉換爲格式消息。然而,這些消息並不保存在消息隊列中。實際上,Windows在所謂的「系統消息隊列」中保存這些消息。系統消息隊列是獨立的消息隊列,它由Windows維護,用於初步保存使用者從鍵盤和鼠標輸入的信息。只有當Windows應用程序處理完前一個使用者輸入消息時,Windows纔會從系統消息隊列中取出下一個消息,並將其放入應用程序的消息隊列中。

此過程分爲兩步:首先在系統消息隊列中保存消息,然後將它們放入應用程序的消息隊列,其原因是需要同步。就像我們剛纔所學的,假定接收鍵盤輸入的窗口就是有輸入焦點的窗口。使用者的輸入速度可能比應用程序處理按鍵的速度快,並且特定的按鍵可能會使焦點從一個窗口切換到另一個窗口,後來的按鍵就輸入到了另一個窗口。但如果後來的按鍵已經記下了目標窗口的地址,並放入了應用程序消息隊列,那麼後來的按鍵就不能輸入到另一個窗口。

按鍵和字符

應用程序從Windows接收的關於鍵盤事件的消息可以分爲按鍵和字符兩類,這與您看待鍵盤的兩種方式一致。

首先,您可以將鍵盤看作是鍵的集合。鍵盤只有唯一的A鍵,按下該鍵是一次按鍵,釋放該鍵也是一次按鍵。但是鍵盤也是能產生可顯示字符或控制字符的輸入設備。根據Ctrl、 Shift和Caps Lock鍵的狀態,A鍵能產生幾個字符。通常情況下,此字符爲小寫a。如果按下Shift鍵或者打開了Caps Lock,則該字符就變成大寫A。如果按下了Ctrl,則該字符爲Ctrl-A(它在ASCII中有意義,但在Windows中可能是某事件的鍵盤快捷鍵)。在一些鍵盤上,A按鍵之前可能有「死字符鍵(dead-character key)」或者Shift、Ctrl或者Alt的不同組合,這些組合可以產生帶有音調標記的小寫或者大寫,例如,à、á、狻⒛、或擰?/p>

對產生可顯示字符的按鍵組合,Windows不僅給程序發送按鍵消息,而且還發送字符消息。有些鍵不產生字符,這些鍵包括shift鍵、功能鍵、光標移動鍵和特殊字符鍵如Insert和Delete。對於這些鍵,Windows只產生按鍵消息。

按鍵消息

當您按下一個鍵時,Windows把WM_KEYDOWN或者WM_SYSKEYDOWN消息放入有輸入焦點的窗口的消息隊列;當您釋放一個鍵時,Windows把WM_KEYUP或者WM_SYSKEYUP消息放入消息隊列中。

表6-1

 

鍵按下

鍵釋放

非系統鍵

WM_KEYDOWN

WM_KEYUP

系統鍵

WM_SYSKEYDOWN

WM_SYSKEYUP

通常「down(按下)」和「up(放開)」消息是成對出現的。不過,如果您按住一個鍵使得自動重複功能生效,那麼當該鍵最後被釋放時,Windows會給窗口消息處理程序發送一系列WM_KEYDOWN(或者WM_SYSKEYDOWN)消息和一個WM_KEYUP(或者WM_SYSKEYUP)消息。像所有放入隊列的消息一樣,按鍵消息也有時間信息。通過呼叫GetMessageTime,您可以獲得按下或者釋放鍵的相對時間。

系統按鍵與非系統按鍵

WM_SYSKEYDOWN和WM_SYSKEYUP中的「SYS」代表「系統」,它表示該按鍵對Windows比對Windows應用程序更加重要。WM_SYSKEYDOWN和WM_SYSKEYUP消息經常由與Alt相組合的按鍵產生,這些按鍵啓動程序菜單或者系統菜單上的選項,或者用於切換活動窗口等系統功能(Alt-Tab或者Alt-Esc),也可以用作系統菜單快捷鍵(Alt鍵與一個功能鍵相結合,例如Alt-F4用於關閉應用程序)。程序通常忽略WM_SYSKEYUP和WM_SYSKEYDOWN消息,並將它們傳送到DefWindowProc。由於Windows要處理所有Alt鍵的功能,所以您無需攔截這些消息。您的窗口消息處理程序將最後收到關於這些按鍵結果(如菜單選擇)的其它消息。如果您想在自己的窗口消息處理程序中加上攔截系統按鍵的程序代碼(如本章後面的KEYVIEW1和KEYVIEW2程序所作的那樣),那麼在處理這些消息之後再傳送到DefWindowProc,Windows就仍然可以將它們用於通常的目的。

但是,請再考慮一下,幾乎所有會影響使用者程序窗口的消息都會先通過使用者窗口消息處理程序。只有使用者把消息傳送到DefWindowProc,Windows纔會對消息進行處理。例如,如果您將下面幾行敘述:

case        WM_SYSKEYDOWN:
case        WM_SYSKEYUP:
caseWM_SYSCHAR:
return 0 ;

加入到一個窗口消息處理程序中,那麼當您的程序主窗口擁有輸入焦點時,就可以有效地阻止所有Alt鍵操作(我將在本章的後面討論WM_SYSCHAR),其中包括Alt-Tab、Alt-Esc以及菜單操作。雖然我懷疑您會這麼做,但是,我相信您會感到窗口消息處理程序的強大功能。

WM_KEYDOWN和WM_KEYUP消息通常是在按下或者釋放不帶Alt鍵的鍵時產生的,您的程序可以使用或者忽略這些消息,Windows本身並不處理這些消息。

對所有四類按鍵消息,wParam是虛擬鍵代碼,表示按下或釋放的鍵,而lParam則包含屬於按鍵的其它數據。

虛擬鍵碼

虛擬鍵碼保存在WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP消息的wParam參數中。此代碼標識按下或釋放的鍵。

哈,又是「虛擬」,您喜歡這個詞嗎?虛擬指的是假定存在於思想中而不是現實世界中的一些事物,也只有熟練使用DOS彙編語言編寫應用程序的程序寫作者纔有可能指出,爲什麼對Windows鍵盤處理如此基本的鍵碼是虛擬的而不是真實的。

對於早期的程序寫作者來說,真實的鍵碼由實際鍵盤硬件產生。在Windows文件中將這些鍵碼稱爲「掃描碼(scan codes)」。在IBM兼容機種上,掃描碼16是Q鍵,17是W鍵,18是E、19是R,20是T,21是Y等等。這時您會發現,掃描碼是依據鍵盤的實際佈局的。Windows開發者認爲這些代碼過於與設備相關了,於是他們試圖通過定義所謂的虛擬鍵碼,以便經由與設備無關的方式處理鍵盤。其中一些虛擬鍵碼不能在IBM兼容機種上產生,但可能會在其它製造商生產的鍵盤中找到,或者在未來的鍵盤上找到。

您使用的大多數虛擬鍵碼的名稱在WINUSER.H表頭文件中都定義爲以VK_開頭。表6-2列出了這些名稱和數值(十進制和十六進制),以及與虛擬鍵相對應的IBM兼容機種鍵盤上的鍵。下表也標出了Windows執行時是否需要這些鍵。下表還按數字順序列出了虛擬鍵碼。

前四個虛擬鍵碼中有三個指的是鼠標鍵:

表6-2

十進制

十六進制

WINUSER.H標識符

必需?

IBM兼容鍵盤

1

01

VK_LBUTTON

 

鼠標左鍵

2

02

VK_RBUTTON

 

鼠標右鍵

3

03

VK_CANCEL

ˇ

Ctrl-Break

4

04

VK_MBUTTON

 

鼠標中鍵

您永遠都不會從鍵盤消息中獲得這些鼠標鍵代碼。在下一章可以看到,我們能夠從鼠標消息中獲得它們。VK_CANCEL代碼是一個虛擬鍵碼,它包括同時按下兩個鍵(Ctrl-Break)。Windows應用程序通常不使用此鍵。

表6-3中的鍵--Backspace、Tab、Enter、Escape和Spacebar-通常用於Windows程序。不過,Windows一般用字符消息(而不是鍵盤消息)來處理這些鍵。

表6-3

十進制

十六進制

WINUSER.H標識符

必需?

IBM兼容鍵盤

8

08

VK_BACK

ˇ

Backspace

9

09

VK_TAB

ˇ

Tab

12

0C

VK_CLEAR

 

Num Lock關閉時的數字鍵盤5

13

0D

VK_RETURN

ˇ

Enter (或者另一個)

16

10

VK_SHIFT

ˇ

Shift (或者另一個)

17

11

VK_CONTROL

ˇ

Ctrl (或者另一個)

18

12

VK_MENU

ˇ

Alt (或者另一個)

19

13

VK_PAUSE

 

Pause

20

14

VK_CAPITAL

ˇ

Caps Lock

27

1B

VK_ESCAPE

ˇ

Esc

32

20

VK_SPACE

ˇ

Spacebar

另外,Windows程序通常不需要監視Shift、Ctrl或Alt鍵的狀態。

表6-4列出的前八個碼可能是與VK_INSERT和VK_DELETE一起最常用的虛擬鍵碼:

表6-4

十進制

十六進制

WINUSER.H標識符

必需?

IBM兼容鍵盤

33

21

VK_PRIOR

ˇ

Page Up

34

22

VK_NEXT

ˇ

Page Down

35

23

VK_END

ˇ

End

36

24

VK_HOME

ˇ

Home

37

25

VK_LEFT

ˇ

左箭頭

38

26

VK_UP

ˇ

上箭頭

39

27

VK_RIGHT

ˇ

右箭頭

40

28

VK_DOWN

ˇ

下箭頭

41

29

VK_SELECT

   

42

2A

VK_PRINT

   

43

2B

VK_EXECUTE

   

44

2C

VK_SNAPSHOT

 

Print Screen

45

2D

VK_INSERT

ˇ

Insert

46

2E

VK_DELETE

ˇ

Delete

47

2F

VK_HELP

   

注意,許多名稱(例如VK_PRIOR和VK_NEXT)都與鍵上的標誌不同,而且也與滾動條中的標識符不統一。Print Screen鍵在平時都被Windows應用程序所忽略。Windows本身響應此鍵時會將視訊顯示的位圖影本存放到剪貼板中。假使有鍵盤提供了VK_SELECT、VK_PRINT、VK_EXECUTE和VK_HELP,大概也沒幾個人看過那樣的鍵盤。

Windows也包括在主鍵盤上的字母和數字鍵的虛擬鍵碼(數字鍵盤將單獨處理)。

表6-5

十進制

十六進制

WINUSER.H標識符

必需?

IBM兼容鍵盤

48-57

30-39

ˇ

主鍵盤上的0到9

65-90

41-5A

ˇ

A到Z

注意,數字和字母的虛擬鍵碼是ASCII碼。Windows程序幾乎從不使用這些虛擬鍵碼;實際上,程序使用的是ASCII碼字符的字符消息。

表6-6所示的代碼是由Microsoft Natural Keyboard及其兼容鍵盤產生的:

表6-6

十進制

十六進制

WINUSER.H標識符

必需?

IBM兼容鍵盤

91

5B

VK_LWIN

 

左Windows鍵

92

5C

VK_RWIN

 

右Windows鍵

93

5D

VK_APPS

 

Applications鍵

Windows用VK_LWIN和VK_RWIN鍵打開「開始」菜單或者(在以前的版本中)啓動「工作管理員程序」。這兩個都可以用於登錄或註銷Windows(只在Microsoft Windows NT中有效),或者登錄或註銷網絡(在Windows for Applications中)。應用程序能夠通過顯示輔助信息或者當成快捷方式鍵看待來處理application鍵。

表6-7所示的代碼用於數字鍵盤上的鍵(如果有的話):

表6-7

十進制

十六進制

WINUSER.H標識符

必需?

IBM兼容鍵盤

96-105

60-69

VK_NUMPAD0到VK_ NUMPAD9

 

NumLock打開時數字鍵盤上的0到9

106

6A

VK_MULTIPLY

 

數字鍵盤上的*

107

6B

VK_ADD

 

數字鍵盤上的+

108

6C

VK_SEPARATOR

   

109

6D

VK_SUBTRACT

 

數字鍵盤上的-

110

6E

VK_DECIMAL

 

數字鍵盤上的.

111

6F

VK_DIVIDE

 

數字鍵盤上的/

最後,雖然多數的鍵盤都有12個功能鍵,但Windows只需要10個,而位旗標卻有24個。另外,程序通常用功能鍵作爲鍵盤快捷鍵,這樣,它們通常不處理表6-8所示的按鍵:

表6-8

十進制

十六進制

WINUSER.H標識符

必需?

IBM兼容鍵盤

112-121

70-79

VK_F1到VK_F10

ˇ

功能鍵F1到F10

122-135

7A-87

VK_F11到VK_F24

 

功能鍵F11到F24

144

90

VK_NUMLOCK

 

Num Lock

145

91

VK_SCROLL

 

Scroll Lock

另外,還定義了一些其它虛擬鍵碼,但它們只用於非標準鍵盤上的鍵,或者通常在大型主機終端機上使用的鍵。查看/ Platform SDK / User Interface Services / User Input / Virtual-Key Codes,可得到完整的列表。

lParam信息

在四個按鍵消息(WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP)中,wParam消息參數含有上面所討論的虛擬鍵碼,而lParam消息參數則含有對了解按鍵非常有用的其它信息。lParam的32位分爲6個字段,如圖6-1所示。


 

圖6-1 lParam變量的6個按鍵消息字段

重複計數

重複計數是該消息所表示的按鍵次數,大多數情況下,重複計數設定爲1。不過,如果按下一個鍵之後,您的窗口消息處理程序不夠快,以致不能處理自動重複速率(您可以在「控制檯」的「鍵盤」中進行設定)下的按鍵消息,Windows就把幾個WM_KEYDOWN或者WM_SYSKEYDOWN消息組合到單個消息中,並相應地增加重複計數。WM_KEYUP或WM_SYSKEYUP消息的重複計數總是爲1。

因爲重複計數大於1指示按鍵速率大於您程序的處理能力,所以您也可能想在處理鍵盤消息時忽略重複計數。幾乎每個人都有文書處理或執行電子表格時畫面捲過頭的經驗,因爲多餘的按鍵堆滿了鍵盤緩衝區,所以當程序用一些時間來處理每一次按鍵時,如果忽略您程序中的重複計數,就能夠解決此問題。不過,有時可能也會用到重複計數,您應該嘗試使用兩種方法執行程序,並從中找出一種較好的方法。

OEM掃描碼

OEM掃描碼是由硬件(鍵盤)產生的代碼。這對中古時代的彙編程序寫作者來說應該很熟悉,它是從PC相容機種的ROM BIOS服務中所獲得的值(OEM指的是PC的原始設備製造商(Original Equipment Manufacturer)及其與「IBM標準」同步的內容)。在此我們不需要更多的信息。除非需要依賴實際鍵盤佈局的樣貌,不然Windows程序可以忽略掉幾乎所有的OEM掃描碼信息,參見第二十二章的程序KBMIDI。

擴充鍵旗標

如果按鍵結果來自IBM增強鍵盤的附加鍵之一,那麼擴充鍵旗標爲1(IBM增強型鍵盤有101或102個鍵。功能鍵在鍵盤頂端,光標移動鍵從數字鍵盤中分離出來,但在數字鍵盤上還保留有光標移動鍵的功能)。對鍵盤右端的Alt和Ctrl鍵,以及不是數字鍵盤那部分的光標移動鍵(包括Insert和Delete鍵)、數字鍵盤上的斜線(/)和Enter鍵以及Num Lock鍵等,此旗標均被設定爲1。Windows程序通常忽略擴充鍵旗標。

內容代碼

右按鍵時,假如同時壓下ALT鍵,那麼內容代碼爲1。對WM_SYSKEYUP與WM_SYSKEYDOWN而言,此位總視爲1;而對WM_SYSKEYUP與WM_KEYDOW消息而言,此位爲0。除了兩個之外:

  • 如果活動窗口最小化了,則它沒有輸入焦點。這時候所有的按鍵都會產生WM_SYSKEYUP和WM_SYSKEYDOWN消息。如果Alt鍵未被按下,則內容代碼字段被設定爲0。Windows使用WM_SYSKEYUP和WM_SYSKEYDOWN消息,從而使最小化了的活動窗口不處理這些按鍵。
     
  • 對於一些外國語文(非英文)鍵盤,有些字符是通過Shift、Ctrl或者Alt鍵與其它鍵相組合而產生的。這時內容代碼爲1,但是此消息並非系統按鍵消息。
     

鍵的先前狀態

如果在此之前鍵是釋放的,則鍵的先前狀態爲0,否則爲1。對WM_KEYUP或者WM_SYSKEYUP消息,它總是設定爲1;但是對WM_KEYDOWN或者WM_SYSKEYDOWN消息,此位可以爲0,也可以爲1。如果爲1,則表示該鍵是自動重複功能所產生的第二個或者後續消息。

轉換狀態

如果鍵正被按下,則轉換狀態爲0;如果鍵正被釋放,則轉換狀態爲1。對WM_KEYDOWN或者WM_SYSKEYDOWN消息,此字段爲0;對WM_KEYUP或者WM_SYSKEYUP消息,此字段爲1。

位移狀態

在處理按鍵消息時,您可能需要知道是否按下了位移鍵(Shift、Ctrl和Alt)或開關鍵(Caps Lock、Num Lock和Scroll Lock)。通過呼叫GetKeyState函數,您就能獲得此信息。例如:

iState = GetKeyState (VK_SHIFT) ;

如果按下了Shift,則iState值爲負(即設定了最高位置位)。如果Caps Lock鍵打開,則從

iState = GetKeyState (VK_CAPITAL) ;

傳回的值低位被設爲1。此位與鍵盤上的小燈保持一致。

通常,您在使用GetKeyState時,會帶有虛擬鍵碼VK_SHIFT、VK_CONTROL和VK_MENU(在說明Alt鍵時呼叫)。使用GetKeyState時,您也可以用下面的標識符來確定按下的Shift、Ctrl或Alt鍵是左邊的還是右邊的:VK_LSHIFT、VK_RSHIFT、VK_LCONTROL、VK_RCONTROL、VK_LMENU、VK_RMENU。這些標識符只用於GetKeyState和GetAsyncKeyState(下面將詳細說明)。

使用虛擬鍵碼VK_LBUTTON、VK_RBUTTON和VK_MBUTTON,您也可以獲得鼠標鍵的狀態。不過,大多數需要監視鼠標鍵與按鍵相組合的Windows應用程序都使用其它方法來做到這一點-即在接收到鼠標消息時檢查按鍵。實際上,位移狀態信息包含在鼠標信息中,正如您在下一章中將看到的一樣。

請注意GetKeyState的使用,它並非實時檢查鍵盤狀態,而只是檢查直到目前爲止正在處理的消息的鍵盤狀態。多數情況下,這正符合您的要求。如果您需要確定使用者是否按下了Shift-Tab,請在處理Tab鍵的WM_KEYDOWN消息時呼叫GetKeyState,帶有參數VK_SHIFT。如果GetKeyState傳回的值爲負,那麼您就知道在按下Tab鍵之前按下了Shift鍵。並且,如果在您開始處理Tab鍵之前,已經釋放了Shift鍵也沒有關係。您知道,在按下Tab鍵的時候Shift鍵是按下的。

GetKeyState不會讓您獲得獨立於普通鍵盤消息的鍵盤信息。例如,您或許想暫停窗口消息處理程序的處理,直到您按下F1功能鍵爲止:

while (GetKeyState (VK_F1) >= 0) ;    // WRONG !!!

不要這麼做!這將讓程序當死(除非在執行此敘述之前早就從消息隊列中接收到了F1的WM_KEYDOWN)。如果您確實需要知道目前某鍵的狀態,那麼您可以使用GetAsyncKeyState。

使用按鍵消息

如果程序能夠獲得每個按鍵的信息,這當然很理想,但是大多數Windows程序忽略了幾乎所有的按鍵,而只處理部分的按鍵消息。WM_SYSKEYDOWN和WM_SYSKEYUP消息是由Windows系統函數使用的,您不必爲此費心,就算您要處理WM_KEYDOWN消息,通常也可以忽略WM_KEYUP消息。

Windows程序通常爲不產生字符的按鍵使用WM_KEYDOWN消息。雖然您可能認爲藉助按鍵消息和位移鍵狀態信息能將按鍵消息轉換爲字符消息,但是不要這麼做,因爲您將遇到國際鍵盤間的差異所帶來的問題。例如,如果您得到wParam等於0x33的WM_KEYDOWN消息,您就可以知道使用者按下了鍵3,到此爲止一切正常。這時,如果用GetKeyState發現Shift鍵被按下,您就可能會認爲使用者輸入了#號,這可不一定。比如英國使用者就是在輸入£。

對於光標移動鍵、功能鍵、Insert和Delete鍵,WM_KEYDOWN消息是最有用的。不過, Insert、Delete和功能鍵經常作爲菜單快捷鍵。因爲Windows能把菜單快捷鍵翻譯爲菜單命令消息,所以您就不必自己來處理按鍵。

在Windows之前的MS-DOS應用程序中大量使用功能鍵與Shift、Ctrl和Alt鍵的組合,同樣地,您也可以在Windows程序中使用(實際上,Microsoft Word將大量的功能鍵用作命令快捷方式),但並不推薦這樣做。如果您確實希望使用功能鍵,那麼這些鍵應該是重複菜單命令。Windows的目標之一就是提供不需要記憶或者使用複雜命令流程的使用者接口。

因此,可以歸納如下:多數情況下,您將只爲光標移動鍵(有時也爲Insert和Delete鍵)處理WM_KEYDOWN消息。在使用這些鍵的時候,您可以通過GetKeyState來檢查Shift鍵和Ctrl鍵的狀態。例如,Windows程序經常使用Shift與光標鍵的組合鍵來擴大文書處理裏選中的範圍。Ctrl鍵常用於修改光標鍵的意義。例如,Ctrl與右箭頭鍵相組合可以表示光標右移一個字。

決定您的程序中使用鍵盤方式的最佳方法之一是瞭解現有的Windows程序使用鍵盤的方式。如果您不喜歡那些定義,當然可以對其加以修改,但是這樣做不利於其它人很快地學會使用您的程序。

爲SYSMETS加上鍵盤處理功能

在編寫第四章中三個版本的SYSMETS程序時,我們還不瞭解鍵盤,只能使用滾動條和鼠標來捲動文字。現在我們知道了處理鍵盤消息的方法,那麼不妨在程序中加入鍵盤接口。顯然,這是處理光標移動鍵的工作。我們將大多數光標鍵(Home、End、Page Up、Page Down、Up Arrow和Down Arrow)用於垂直捲動,左箭頭鍵和右箭頭鍵用於不太重要的水平捲動。

建立鍵盤接口的一種簡單方法是在窗口消息處理程序中加入與WM_VSCROLL和WM_HSCROLL處理方式相仿,而且本質上相同的WM_KEYDOWN處理方法。不過這樣子做是不聰明的,因爲如果要修改滾動條的做法,就必須相對應地修改WM_KEYDOWN。

爲什麼不簡單地將每一種WM_KEYDOWN消息都翻譯成同等效用的WM_VSCROLL或者WM_HSCROLL消息呢?通過向窗口消息處理程序發送假冒消息,我們可能會讓WndProc認爲它獲得了捲動信息。

在Windows中,這種方法是可行的。發送消息的函數叫做SendMessage,它所用的參數與傳遞到窗口消息處理程序的參數是相同的:

SendMessage (hwnd, message, wParam, lParam) ;

在呼叫SendMessage時,Windows呼叫窗口句柄爲hwnd的窗口消息處理程序,並把這四個參數傳給它。當窗口消息處理程序完成消息處理之後,Windows把控制傳回到SendMessage呼叫之後的下一道敘述。您發送消息過去的窗口消息處理程序,可以是同一個窗口消息處理程序、同一程序中的其它窗口消息處理程序或者其它應用程序,中的窗口消息處理程序。

下面說明在SYSMETS程序中使用SendMessage處理WM_KEYDOWN代碼的方法:

caseWM_KEYDOWN:
switch (wParam)
{
case   VK_HOME:
SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0) ;
break ;
case   VK_END:
SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0) ;
break ;
case   VK_PRIOR:
SendMessage (hwnd, WM_VSCROLL, SB_PAGEUP, 0) ;
break ;

至此,您已經有了大概觀念了吧。我們的目標是爲滾動條添加鍵盤接口,並且也正在這麼做。通過把卷動消息發送到窗口消息處理程序,我們實作了用光標移動鍵進行卷動列的功能。現在您知道在SYSMETS3中爲WM_VSCROLL消息加上SB_TOP和SB_BOTTOM處理碼的原因了吧。在那裏並沒有用到它,但是現在處理Home和End鍵時就有用了。如程序6-1所示的SYSENTS4就加上了這些變化。編譯這個程序時還需要用到第四章的SYSMETS.H文件。

程序6-1 SYSMETS4
SYSMETS4.C
/*----------------------------------------------------------------------
SYSMETS4.C -- System Metrics Display Program No. 4
(c) Charles Petzold, 1998
------------------------------------------------------------------------*/
#include <windows.h>
#include "sysmets.h"
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[]      = TEXT ("SysMets4") ;
HWND                  hwnd ;
MSG                   msg ;
WNDCLASS              wndclass ;
wndclass.style                = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc  = WndProc ;
wndclass.cbClsExtra           = 0 ;
wndclass.cbWndExtra           = 0 ;
wndclass.hInstance            = hInstance ;
wndclass.hIcon                = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor              = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName= NULL ;
wndclass.lpszClassName= szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("Program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 4"),
WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int  cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ;
HDC                   hdc ;
int                   i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ;
PAINTSTRUCT   ps ;
SCROLLINFO    si ;
TCHAR                 szBuffer[10] ;
TEXTMETRIC    tm ;
switch (message)
{
case   WM_CREATE:
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;
cxChar= tm.tmAveCharWidth ;
cxCaps= (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
cyChar= tm.tmHeight + tm.tmExternalLeading ;
ReleaseDC (hwnd, hdc) ;
// Save the width of the three columns
iMaxWidth = 40 * cxChar + 22 * cxCaps ;
return 0 ;
case   WM_SIZE:
cxClient              = LOWORD (lParam) ;
cyClient              = HIWORD (lParam) ;
// Set vertical scroll bar range and page size
si.cbSize     = sizeof (si) ;
si.fMask      = SIF_RANGE | SIF_PAGE ;
si.nMin       = 0 ;
si.nMax       = NUMLINES - 1 ;
si.nPage      = cyClient / cyChar ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
// Set horizontal scroll bar range and page size
si.cbSize     = sizeof (si) ;
si.fMask      = SIF_RANGE | SIF_PAGE ;
si.nMin       = 0 ;
si.nMax       = 2 + iMaxWidth / cxChar ;
si.nPage      = cxClient / cxChar ;
SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ;
return 0 ;
case WM_VSCROLL:
// Get all the vertical scroll bar information
si.cbSize     = sizeof (si) ;
si.fMask      = SIF_ALL ;
GetScrollInfo (hwnd, SB_VERT, &si) ;
// Save the position for comparison later on
iVertPos = si.nPos ;
switch (LOWORD (wParam))
{
case SB_TOP:
si.nPos = si.nMin ;
break ;
case SB_BOTTOM:
si.nPos = si.nMax ;
break ;
case SB_LINEUP:
si.nPos -= 1 ;
break ;
case SB_LINEDOWN:
si.nPos += 1 ;
break ;
case SB_PAGEUP:
si.nPos -= si.nPage ;
break ;
case SB_PAGEDOWN:
si.nPos += si.nPage ;
break ;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos ;
break ;
default:
break ;
}
// Set the position and then retrieve it.  Due to adjustments
//   by Windows it might not be the same as the value set.
si.fMask = SIF_POS ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
GetScrollInfo (hwnd, SB_VERT, &si) ;
// If the position has changed, scroll the window and update it
if (si.nPos != iVertPos)
{
ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos),
NULL, NULL) ;
UpdateWindow (hwnd) ;
}
return 0 ;
case WM_HSCROLL:
// Get all the vertical scroll bar information
si.cbSize             = sizeof (si) ;
si.fMask              = SIF_ALL ;
// Save the position for comparison later on
GetScrollInfo (hwnd, SB_HORZ, &si) ;
iHorzPos      = si.nPos ;
switch (LOWORD (wParam))
{
case SB_LINELEFT:
si.nPos -= 1 ;
break ;
case SB_LINERIGHT:
si.nPos += 1 ;
break ;
case SB_PAGELEFT:
si.nPos -= si.nPage ;
break ;
case SB_PAGERIGHT:
si.nPos += si.nPage ;
break ;
case SB_THUMBPOSITION:
si.nPos = si.nTrackPos ;
break ;
default:
break ;
}
// Set the position and then retrieve it.  Due to adjustments
//   by Windows it might not be the same as the value set.
si.fMask = SIF_POS ;
SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ;
GetScrollInfo (hwnd, SB_HORZ, &si) ;
// If the position has changed, scroll the window
if (si.nPos != iHorzPos)
{
ScrollWindow (hwnd, cxChar * (iHorzPos - si.nPos), 0,
NULL, NULL) ;
}
return 0 ;
case WM_KEYDOWN:
switch (wParam)
{
case VK_HOME:
SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0) ;
break ;
case VK_END:
SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0) ;
break ;
case VK_PRIOR:
SendMessage (hwnd, WM_VSCROLL, SB_PAGEUP, 0) ;
break ;
case VK_NEXT:
SendMessage (hwnd, WM_VSCROLL, SB_PAGEDOWN, 0) ;
break ;
case VK_UP:
SendMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0) ;
break ;
case VK_DOWN:
SendMessage (hwnd, WM_VSCROLL, SB_LINEDOWN, 0) ;
break ;
case VK_LEFT:
SendMessage (hwnd, WM_HSCROLL, SB_PAGEUP, 0) ;
break ;
case VK_RIGHT:
SendMessage (hwnd, WM_HSCROLL, SB_PAGEDOWN, 0) ;
break ;
}
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
// Get vertical scroll bar position
si.cbSize             = sizeof (si) ;
si.fMask              = SIF_POS ;
GetScrollInfo (hwnd, SB_VERT, &si) ;
iVertPos              = si.nPos ;
// Get horizontal scroll bar position
GetScrollInfo (hwnd, SB_HORZ, &si) ;
iHorzPos              = si.nPos ;
// Find painting limits
iPaintBeg             = max (0, iVertPos + ps.rcPaint.top / cyChar) ;
iPaintEnd             = min (NUMLINES - 1,
iVertPos + ps.rcPaint.bottom / cyChar) ;
for (i = iPaintBeg ; i <= iPaintEnd ; i++)
{
x = cxChar * (1 - iHorzPos) ;
y = cyChar * (i - iVertPos) ;
TextOut (hdc, x, y,
sysmetrics[i].szLabel,
lstrlen (sysmetrics[i].szLabel)) ;
TextOut (hdc, x + 22 * cxCaps, y,
sysmetrics[i].szDesc,
lstrlen (sysmetrics[i].szDesc)) ;
SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer,
wsprintf (szBuffer, TEXT ("%5d"),
GetSystemMetrics (sysmetrics[i].iIndex))) ;
SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
}
EndPaint (hwnd, &ps) ;
return 0 ;
case   WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

字符消息

前面討論了利用位移狀態信息把按鍵消息翻譯爲字符消息的方法,並且提到,僅利用轉換狀態信息還不夠,因爲還需要知道與國家/地區有關的鍵盤配置。由於這個原因,您不應該試圖把按鍵消息翻譯爲字符代碼。Windows會爲您完成這一工作,在前面我們曾看到過以下的程序代碼:

while       (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}

這是WinMain中典型的消息循環。GetMessage函數用隊列中的下一個消息填入msg結構的字段。DispatchMessage以此消息爲參數呼叫適當的窗口消息處理程序。

在這兩個函數之間是TranslateMessage函數,它將按鍵消息轉換爲字符消息。如果消息爲WM_KEYDOWN或者WM_SYSKEYDOWN,並且按鍵與位移狀態相組合產生一個字符,則TranslateMessage把字符消息放入消息隊列中。此字符消息將是GetMessage從消息隊列中得到的按鍵消息之後的下一個消息。

四類字符消息

字符消息可以分爲四類,如表6-9所示。

表6-9

 

字符

死字符

非系統字符

WM_CHAR

WM_DEADCHAR

系統字符

WM_SYSCHAR

WM_SYSDEADCHAR

WM_CHAR和WM_DEADCHAR消息是從WM_KEYDOWN得到的;而WM_SYSCHAR和WM_SYSDEADCHAR消息是從WM_SYSKEYDOWN消息得到的(我將簡要地討論一下什麼是死字符)。

有一個好消息:在大多數情況下,Windows程序會忽略除WM_CHAR之外的任何消息。伴隨四個字符消息的lParam參數與產生字符代碼消息的按鍵消息之lParam參數相同。不過,參數wParam不是虛擬鍵碼。實際上,它是ANSI或Unicode字符代碼。

這些字符消息是我們將文字傳遞給窗口消息處理程序時遇到的第一個消息。它們不是唯一的消息,其它消息伴隨以0結尾的整個字符串。窗口消息處理程序是如何知道該字符是8位的ANSI字符還是16位的Unicode寬字符呢?很簡單:任何與您用RegisterClassA(RegisterClass的ANSI版)註冊的窗口類別相聯繫的窗口消息處理程序,都會獲得含有ANSI字符代碼的消息。如果窗口消息處理程序用RegisterClassW(RegisterClass的寬字符版)註冊,那麼傳遞給窗口消息處理程序的消息就帶有Unicode字符代碼。如果程序用RegisterClass註冊窗口類別,那麼在UNICODE標識符被定義時就呼叫RegisterClassW,否則呼叫RegisterClassA。

除非在程序寫作的時候混合了ANSI和Unicode的函數與窗口消息處理程序,用WM_CHAR消息(及其它三種字符消息)說明的字符代碼將是:

(TCHAR) wParam

同一個窗口消息處理程序可能會用到兩個窗口類別,一個用RegisterClassA註冊,而另一個用RegisterClassW註冊。也就是說,窗口消息處理程序可能會獲得一些ANSI字符代碼消息和一些Unicode字符代碼消息。如果您的窗口消息處理程序需要曉得目前窗口是否處理Unicode消息,則它可以呼叫:

fUnicode = IsWindowUnicode (hwnd) ;

如果hwnd的窗口消息處理程序獲得Unicode消息,那麼變量fUnicode將爲TRUE,這表示窗口是用RegisterClassW註冊的窗口類別。

消息順序

因爲TranslateMessage函數從WM_KEYDOWN和WM_SYSKEYDOWN消息產生了字符消息,所以字符消息是夾在按鍵消息之間傳遞給窗口消息處理程序的。例如,如果Caps Lock未打開,而使用者按下再釋放A鍵,則窗口消息處理程序將接收到如表6-10所示的三個消息:

表6-10

消息

按鍵或者代碼

WM_KEYDOWN

「A」的虛擬鍵碼(0x41)

WM_CHAR

「a」的字符代碼(0x61)

WM_KEYUP

「A」的虛擬鍵碼(0x41)

如果您按下Shift鍵,再按下A鍵,然後釋放A鍵,再釋放Shift鍵,就會輸入大寫的A,而窗口消息處理程序會接收到五個消息,如表6-11所示:

表6-11

消息

按鍵或者代碼

WM_KEYDOWN

虛擬鍵碼VK_SHIFT (0x10)

WM_KEYDOWN

「A」的虛擬鍵碼(0x41)

WM_CHAR

「A」的字符代碼(0x41)

WM_KEYUP

「A」的虛擬鍵碼(0x41)

WM_KEYUP

虛擬鍵碼VK_SHIFT(0x10)

Shift鍵本身不產生字符消息。

如果使用者按住A鍵,以使自動重複產生一系列的按鍵,那麼對每條WM_KEYDOWN消息,都會得到一條字符消息,如表6-12所示:

表6-12

消息

按鍵或者代碼

WM_KEYDOWN

「A」的虛擬鍵碼(0x41)

WM_CHAR

「a」的字符代碼(0x61)

WM_KEYDOWN

「A」的虛擬鍵碼(0x41)

WM_CHAR

「a」的字符代碼(0x61)

WM_KEYDOWN

「A」的虛擬鍵碼(0x41)

WM_CHAR

「a」的字符代碼(0x61)

WM_KEYDOWN

「A」的虛擬鍵碼(0x41)

WM_CHAR

「a」的字符代碼(0x61)

WM_KEYUP

「A」的虛擬鍵碼(0x41)

如果某些WM_KEYDOWN消息的重複計數大於1,那麼相應的WM_CHAR消息將具有同樣的重複計數。

組合使用Ctrl鍵與字母鍵會產生從0x01(Ctrl-A)到0x1A(Ctrl-Z)的ASCII控制代碼,其中的某些控制代碼也可以由表6-13列出的鍵產生:

表6-13

按鍵

字符代碼

產生方法

ANSI C控制字符

Backspace

0x08

Ctrl-H

/b

Tab

0x09

Ctrl-I

/t

Ctrl-Enter

0x0A

Ctrl-J

/n

Enter

0x0D

Ctrl-M

/r

Esc

0x1B

Ctrl-[

最右列給出了在ANSI C中定義的控制字符,它們用於描述這些鍵的字符代碼。

有時Windows程序將Ctrl與字母鍵的組合用作菜單快捷鍵(我將在第十章討論),此時,不會將字母鍵轉換成字符消息。

處理控制字符

處理按鍵和字符消息的基本規則是:如果需要讀取輸入到窗口的鍵盤字符,那麼您可以處理WM_CHAR消息。如果需要讀取光標鍵、功能鍵、Delete、Insert、Shift、Ctrl以及Alt鍵,那麼您可以處理WM_KEYDOWN消息。

但是Tab鍵怎麼辦?Enter、Backspace和Escape鍵又怎麼辦?傳統上,這些鍵都產生表6-13列出的ASCII控制字符。但是在Windows中,它們也產生虛擬鍵碼。這些鍵應該在處理WM_CHAR或者在處理WM_KEYDOWN期間處理嗎?

經過10年的考慮(回顧這些年來我寫過的Windows程序代碼),我更喜歡將Tab、Enter、Backspace和Escape鍵處理成控制字符,而不是虛擬鍵。我通常這樣處理WM_CHAR:

case WM_CHAR:
//其它行程序
switch (wParam)
{
case '/b':            // backspace
//其它行程序
break ;
case '/t':            // tab
//其它行程序
break ;
case '/n':            // linefeed
//其它行程序
break ;
case '/r':            // carriage return
//其它行程序
break ;
default:                      // character codes
//其它行程序
break ;
}
return 0 ;

死字符消息

Windows程序經常忽略WM_DEADCHAR和WM_SYSDEADCHAR消息,但您應該明確地知道死字符是什麼,以及它們工作的方式。

在某些非U.S.英語鍵盤上,有些鍵用於給字母加上音調。因爲它們本身不產生字符,所以稱之爲「死鍵」。例如,使用德語鍵盤時,對於U.S.鍵盤上的+/=鍵,德語鍵盤的對應位置就是一個死鍵,未按下Shift鍵時它用於標識銳音,按下Shift鍵時則用於標識抑音。

當使用者按下這個死鍵時,窗口消息處理程序接收到一個wParam等於音調本身的ASCII或者Unicode代碼的WM_DEADCHAR消息。當使用者再按下可以帶有此音調的字母鍵(例如A鍵)時,窗口消息處理程序會接收到WM_CHAR消息,其中wParam等於帶有音調的字母「a」的ANSI代碼。

因此,使用者程序不需要處理WM_DEADCHAR消息,原因是WM_CHAR消息已含有程序所需要的所有信息。Windows的做法甚至還設計了內部錯誤處理。如果在死鍵之後跟有不能帶此音調符號的字母(例如「s」),那麼窗口消息處理程序將在一行接收到兩條WM_CHAR消息-前一個消息的wParam等於音調符號本身的ASCII代碼(與傳遞到WM_DEADCHAR消息的wParam值相同),第二個消息的wParam等於字母s的ASCII代碼。

當然,要感受這種做法的運作方式,最好的方法就是實際操作。您必須加載使用死鍵的外語鍵盤,例如前面講過的德語鍵盤。您可以這樣設定:在「控制檯」中選擇「鍵盤」,然後選擇「語系」頁面標籤。然後您需要一個應用程序,該程序可以顯示它接收的每一個鍵盤消息的詳細信息。下面的KEYVIEW1就是這樣的程序。

鍵盤消息和字符集

本章剩下的範例程序有缺陷。它們不能在所有版本的Windows下都正常執行。這些缺陷不是特意引過程序代碼中的;事實上,您也許永遠不會遇到這些缺陷。只有在不同的鍵盤語言和鍵盤佈局間切換,以及在多字節字符集的遠東版Windows下執行程序時,這些問題纔會出現-所以我不願將它們稱爲「錯誤」。

不過,如果程序使用Unicode編譯並在Windows NT下執行,那麼程序會執行得更好。我在第二章提到過這個問題,並且展示了Unicode對簡化棘手的國際化問題的重要性。

KEYVIEW1程序

瞭解鍵盤國際化問題的第一步,就是檢查Windows傳遞給窗口消息處理程序的鍵盤內容和字符消息。程序6-2所示的KEYVIEW1會對此有所幫助。該程序在顯示區域顯示Windows向窗口消息處理程序發送的8種不同鍵盤消息的全部信息。

程序6-2 KEYVIEW1
KEYVIEW1.C
/*---------------------------------------------------------------------
KEYVIEW1.C --Displays Keyboard and Character Messages
(c) Charles Petzold, 1998
---------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[]      = TEXT ("KeyView1") ;
HWND                  hwnd ;
MSG                   msg ;
WNDCLASS              wndclass ;
wndclass.style                       = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc          = WndProc ;
wndclass.cbClsExtra           = 0 ;
wndclass.cbWndExtra           = 0 ;
wndclass.hInstance                   = hInstance ;
wndclass.hIcon                      = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor                     = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground               = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName         = NULL ;
wndclass.lpszClassName        = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Keyboard Message Viewer #1"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int   cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar ;
static int   cLinesMax, cLines ;
static PMSG  pmsg ;
static RECT  rectScroll ;
static TCHAR szTop[] = TEXT ("Message Key  Char ")
TEXT ("Repeat Scan Ext ALT Prev Tran") ;
static TCHAR szUnd[] = TEXT ("_______        ___       ____     ")
TEXT ("______ ____ ___ ___ ____ ____") ;
static TCHAR * szFormat[2] = {
TEXT ("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"),
TEXT ("%-13s  0x%04X%1s%c %6u %4d %3s %3s %4s %4s") } ;
static TCHAR * szYes  = TEXT ("Yes") ;
static TCHAR * szNo   = TEXT ("No") ;
static TCHAR * szDown = TEXT ("Down") ;
static TCHAR * szUp   = TEXT ("Up") ;
static TCHAR * szMessage [] = {
TEXT ("WM_KEYDOWN"),  TEXT ("WM_KEYUP"),
TEXT ("WM_CHAR"),     TEXT ("WM_DEADCHAR"),
TEXT ("WM_SYSKEYDOWN"),TEXT ("WM_SYSKEYUP"),
TEXT ("WM_SYSCHAR"),  TEXT ("WM_SYSDEADCHAR") } ;
HDC                   hdc ;
int                   i, iType ;
PAINTSTRUCT           ps ;
TCHAR                 szBuffer[128], szKeyName [32] ;
TEXTMETRIC            tm ;
switch (message)
{
case WM_CREATE:
case WM_DISPLAYCHANGE:
// Get maximum size of client area
cxClientMax = GetSystemMetrics (SM_CXMAXIMIZED) ;
cyClientMax = GetSystemMetrics (SM_CYMAXIMIZED) ;
// Get character size for fixed-pitch font
hdc = GetDC (hwnd) ;
SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
GetTextMetrics (hdc, &tm) ;
cxChar = tm.tmAveCharWidth ;
cyChar = tm.tmHeight ;
ReleaseDC (hwnd, hdc) ;
// Allocate memory for display lines
if (pmsg)
free (pmsg) ;
cLinesMax = cyClientMax / cyChar ;
pmsg = malloc (cLinesMax * sizeof (MSG)) ;
cLines = 0 ;
// fall through
case WM_SIZE:
if (message == WM_SIZE)
{
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
}
// Calculate scrolling rectangle
rectScroll.left       = 0 ;
rectScroll.right      = cxClient ;
rectScroll.top        = cyChar ;
rectScroll.bottom     = cyChar * (cyClient / cyChar) ;
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case WM_KEYDOWN:
case WM_KEYUP:
case WM_CHAR:
case WM_DEADCHAR:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
case WM_SYSCHAR:
case WM_SYSDEADCHAR:
// Rearrange storage array
for (i = cLinesMax - 1 ; i > 0 ; i--)
{
pmsg[i] = pmsg[i - 1] ;
}
// Store new message
pmsg[0].hwnd = hwnd ;
pmsg[0].message = message ;
pmsg[0].wParam = wParam ;
pmsg[0].lParam = lParam ;
cLines = min (cLines + 1, cLinesMax) ;
// Scroll up the display
ScrollWindow (hwnd, 0, -cyChar, &rectScroll, &rectScroll) ;
break ;               // i.e., call DefWindowProc so Sys messages work
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
SetBkMode (hdc, TRANSPARENT) ;
TextOut (hdc, 0, 0, szTop, lstrlen (szTop)) ;
TextOut (hdc, 0, 0, szUnd, lstrlen (szUnd)) ;
for (i = 0 ; i < min (cLines, cyClient / cyChar - 1) ; i++)
{
iType =       pmsg[i].message == WM_CHAR ||
pmsg[i].message == WM_SYSCHAR ||
pmsg[i].message == WM_DEADCHAR ||
pmsg[i].message == WM_SYSDEADCHAR ;
GetKeyNameText (pmsg[i].lParam, szKeyName,
sizeof (szKeyName) / sizeof (TCHAR)) ;
TextOut (hdc, 0, (cyClient / cyChar - 1 - i) * cyChar, szBuffer,
wsprintf (szBuffer, szFormat [iType],
szMessage [pmsg[i].message - WM_KEYFIRST],
pmsg[i].wParam,
(PTSTR) (iType ? TEXT (" ") : szKeyName),
(TCHAR) (iType ? pmsg[i].wParam : ' '),
LOWORD (pmsg[i].lParam),
HIWORD (pmsg[i].lParam) & 0xFF,
0x01000000 & pmsg[i].lParam ? szYes  : szNo,
0x20000000 & pmsg[i].lParam ? szYes  : szNo,
0x40000000 & pmsg[i].lParam ? szDown : szUp,
0x80000000 & pmsg[i].lParam ? szUp   : szDown)) ;
}
EndPaint (hwnd, &ps) ;
return 0 ;
case   WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

KEYVIEW1顯示窗口消息處理程序接收到的每次按鍵和字符消息的內容,並將這些消息儲存在一個MSG結構的數組中。該數組的大小依據最大化窗口的大小和等寬的系統字體。如果使用者在程序執行時調整了視訊顯示的大小(在這種情況下KEYVIEW1接收WM_DISPLAYCHANGE消息),將重新分配此數組。KEYVIEW1使用標準C的malloc函數爲數組配置內存。

圖6-2給出了在鍵入「Windows」之後KEYVIEW1的屏幕顯示。第一列顯示了鍵盤消息;第二列在鍵名稱的前面顯示了按鍵消息的虛擬鍵代碼,此代碼是經由GetKeyNameText函數取得的;第三列(標註爲「Char」)在字符本身的後面顯示字符消息的十六進制字符代碼。其餘六列顯示了lParam消息參數中六個字段的狀態。


 

圖6-2 KEYVIEW1的屏幕顯示

爲便於以分行的方式顯示此信息,KEYVIEW1使用了等寬字體。與前一章所討論的一樣,這需要呼叫GetStockObject和SelectObject:

SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

KEYVIEW1在顯示區域上部畫了一個標題以確定分成九行。此列文字帶有底線。雖然可以建立一種帶底線的字體,但這裏使用了另一種方法。我定義了兩個字符串變量szTop(有文字)和szUnd(有底線),並在WM_PAINT消息處理期間將它們同時顯示在窗口頂部的同一位置。通常,Windows以一種「不透明」的方式顯示文字,也就是說顯示字符時Windows將擦除字符背景區。這將導致第二個字符串(szUnd)擦除掉前一個(szTop)。要防止這一現象的發生,可將設備內容切換到「透明」模式:

SetBkMode (hdc, TRANSPARENT) ;

這種加底線的方法只有在使用等寬字體時纔可行。否則,底線字符將無法與顯現在底線上面的字符等寬。

外語鍵盤問題

如果您執行美國英語版本的Windows,那麼您可安裝不同的鍵盤佈局,並輸入外語。可以在 控制檯鍵盤中安裝外語鍵盤佈局。選擇 語系頁面標籤,按下新增 鍵。要查看死鍵的工作方式,您可能想安裝「德語」鍵盤。此外,我還要討論「俄語」和「希臘語」的鍵盤佈局,因此您也可安裝這些鍵盤佈局。如果在「鍵盤」顯示的列表中找不到「俄語」和「希臘語」的鍵盤佈局,則需要安裝多語系支持:從「控制檯」中選擇 新增/刪除程序,然後選擇 Windows安裝程序頁面卷標,確認選中 多語系支持複選框。在任何情況下,這些變更都需要原始的Windows光盤。

安裝完其它鍵盤佈局後,您將在工作列右側的通知區看到一個帶有兩個字母代碼的藍色框。如果內定的是英語,那麼這兩個字母是「EN」。單擊此圖標,將得到所有已安裝鍵盤佈局的列表。從中單擊需要的鍵盤佈局即可更改目前活動程序的鍵盤。此改變隻影響目前活動的程序。

現在開始進行實驗。不使用UNICODE標識符定義來編譯KEYVIEW1程序(在本書附帶的光盤中,非Unicode版本的KEYVIEW1程序位於RELEASE子目錄)。在美國英語版本的Windows下執行該程序,並輸入字符『abcde』。 WM_CHAR消息與您所期望的一樣:ASCII字符代碼0x61、0x62、0x63、0x64和0x65以及字母a、b、c、d和e。

現在,KEYVIEW1還在執行,選擇德語鍵盤佈局。按下=鍵然後輸入一個元音(a、e、i、o或者u)。=鍵將產生一個WM_DEADCHAR消息,元音產生一個WM_CHAR消息和(單獨的)字符代碼0xE1、0xE9、0xED、0xF3、0xFA和字符á、é、í、ó或ú。這就是死鍵的工作方式。

現在選擇希臘鍵盤佈局。輸入『abcde』,您會得到什麼?您將得到WM_CHAR消息和字符代碼0xE1、0xE2、0xF8、0xE4、0xE5和字符á、狻ⅱ、?#160;和濉T謖飫鎘行┳址荒苷廢允盡D訓濫揮Ω玫玫較@白幟副碇械淖幟嘎穡?/p>

現在切換到俄語鍵盤並重新輸入『abcde』。現在您得到WM_CHAR消息和字符代碼0xF4、0xE8、0xF1、0xE2和0xF3,以及字符簟ⅷāⅠ、?#160;和ó。而且,還是有些字母不能正常顯示。您應從斯拉夫字母表中得到這些字母。

問題在於:您已經切換鍵盤以產生不同的字符代碼,但您還沒有將此切換通知GDI,好讓GDI能選擇適當的符號來顯示解釋這些字符代碼。

如果您非常勇敢,還有可用的備用PC,並且是專業或全球版Microsoft Developer Network(MSDN)的訂閱戶,那麼您也許想安裝(例如)希臘版的Windows,您還可以把那四種鍵盤佈局(英語、希臘語、德語和俄語)安裝上去。現在執行KEYLOOK1,切換到英語鍵盤佈局,然後輸入『abcde』。您應得到ASCII字符代碼0x61、0x62、0x63、0x64和0x65以及字符a、b、c、d和e(並且您可以放心:即使在希臘版,ASCII還是正常通行的)。

在希臘版的Windows中,切換到希臘鍵盤佈局並輸入『abcde』。您將得到WM_CHAR消息和字符代碼0xE1、0xE2、0xF8、0xE4和0xE5。這與您在安裝希臘鍵盤佈局的英語版Windows中得到的字符代碼相同。但現在顯示的字符是?、?、?、?和?。這些確實是小寫的希臘字母alpha、beta、psi、delta和epsilon(gamma怎麼了?是這樣,如果使用希臘版的Windows,那麼您將使用鍵帽上帶有希臘字母的鍵盤。與英語c相對應的鍵正好是psi。gamma由與英語g相對應的鍵產生。您可在Nadine Kano編寫的《Developing International Software for Windows 95 and Windows NT》的第587頁看到完整的希臘字母表)。

繼續在希臘版的Windows下運行KEYVIEW1,切換到德語鍵盤佈局。輸入『=』鍵,然後依次輸入a、e、i、o和u。您將得到WM_CHAR消息和字符代碼0xE1、0xE9、0xED、0xF3和0xFA。這些字符代碼與安裝德語鍵盤佈局的英語版Windows中的一樣。不過,顯示的字符卻是?、?、?、?和?,而不是正確的á、é、í、ó和ú。

現在切換到俄語鍵盤並輸入『abcde』。您會得到字符代碼0xF4、0xE8、0xF1、0xE2和0xF3,這與安裝俄語鍵盤的英語版Windows中得到的一樣。不過,顯示的字符是?、?、?、?和?,而不是斯拉夫字母表中的字母。

您還可安裝俄語版的Windows。現在您可以猜到,英語和俄語鍵盤都可以工作,而德語和希臘語則不行。

現在,如果您真的很勇敢,您還可安裝日語版的Windows並執行KEYVIEW1。如果再依美國鍵盤輸入,那麼您將輸入英語文字,一切似乎都正常。不過,如果切換到德語、希臘語或者俄語鍵盤佈局,並且試著作上述介紹的任何練習,您將看到以點顯示的字符。如果輸入大寫的字母-無論是帶重音符號的德語字母、希臘語字母還是俄語字母-您將看到這些字母顯示爲日語中用於拼寫外來語的片假名。您也許對輸入片假名感興趣,但那不是德語、希臘語或者俄語。

遠東版本的Windows包括一個稱作「輸入法編輯器」(IME)的實用程序,該程序顯示爲浮動的工具列,它允許您用標準鍵盤輸入象形文字,即漢語、日語和朝鮮語中使用的複雜字符。一般來說,輸入一組字母后,組成的字符將顯示在另一個浮動窗口內。然後按 Enter鍵,合成的字符代碼就發送到了活動窗口(即KEYVIEW1)。KEYVIEW1幾乎沒什麼響應-WM_CHAR消息帶來的字符代碼大於128,但這些代碼沒有意義(Nadine Kano的書中有許多關於使用IME的內容)。

這時,我們已經看到了許多KEYLOOK1顯示錯誤字符的例子-當執行安裝了俄語或希臘語鍵盤佈局的英語版Windows時,當執行安裝了俄語或德語鍵盤佈局的希臘版Windows時,以及執行安裝了德語、俄語或者希臘語鍵盤佈局的俄語版Windows時,都是這樣。我們也看到了從日語版Windows的輸入法編輯器輸入字符時的錯誤顯示。

字符集和字體

KEYLOOK1的問題是字體問題。用於在屏幕上顯示字符的字體和鍵盤接收的字符代碼不一致。因此,讓我們看一下字體。

我將在第十七章進行詳細討論,Windows支持三類字體-點陣字體、向量字體和(從Windows 3.1開始的)TrueType字體。

事實上向量字體已經過時了。這些字體中的字符由簡單的線段組成,但這些線段沒有定義填入區域。向量字體可以較好地縮放到任意大小,但字符通常看上去有些單薄。

TrueType字體是定義了填入區域的文字輪廓字體。TrueType字體可縮放;而且該字符的定義包括「提示」,以消除可能帶來的文字不可見或者不可讀的圓整問題。使用TrueType字體,Windows就真正實現了WYSIWYG(「所見即所得」),即文字在視訊顯示器顯示與打印機輸出完全一致。

在點陣字體中,每個字符都定義爲與視訊顯示器上的圖素對應的位點陣。點陣字體可拉伸到較大的尺寸,但看上去帶有鋸齒。點陣字體通常被設計成方便在視訊顯示器上閱讀的字體。因此,Windows中的標題列、菜單、按鈕和對話框的顯示文字都使用點陣字體。

在內定的設備內容下獲得的點陣字體稱爲系統字體。您可通過呼叫帶有SYSTEM_FONT標識符的GetStockObject函數來獲得字體句柄。KEYVIEW1程序選擇使用SYSTEM_FIXED_FONT表示的等寬系統字體。GetStockObject函數的另一個選項是OEM_FIXED_FONT。

這三種字體有(各自的)字體名稱-System、FixedSys和Terminal。程序可以在CreateFont或者CreateFontIndirect函數呼叫中使用字體名稱來指定字體。這三種字體儲存在兩組放在Windows目錄內的FONTS子目錄下的三個文件中。Windows使用哪一組文件取決於「控制檯」裏的「顯示器」是選擇顯示「小字體」還是「大字體」(亦即,您希望Windows假定視訊顯示器是96 dpi的分辨率還是120 dpi的分辨率)。表6-14總結了所有的情況:

表6-14

GetStockObject標識符

字體名稱

小字體文件

大字體文件

SYSTEM_FONT

System

VGASYS.FON

8514SYS.FON

SYSTEM_FIXED_FONT

FixedSys

VGAFIX.FON

8514FIX.FON

OEM_FIXED_FONT

Terminal

VGAOEM.FON

8514OEM.FON

在文件名稱中,「VGA」指的是視頻圖形數組(Video Graphics Array),IBM在1987年推出的顯示卡。這是IBM第一塊可顯示640×480圖素大小的PC顯示卡。如果在「控制檯」的「顯示器」中選擇了「小字體」(表示您希望Windows假定視訊顯示的分辨率爲96 dpi),則Windows使用的這三種字體文件名將以「VGA」開頭。如果選擇了「大字體」(表示您希望分辨率爲120 dpi),Windows使用的文件名將以「8514」開頭。8514是IBM在1987年推出的另一種顯示卡,它的最大顯示尺寸爲1024×768。

Windows不希望您看到這些文件。這些文件的屬性設定爲系統和隱藏,如果用Windows Explorer來查看FONTS子目錄的內容,您是不會看到它們的,即使選擇了查看系統和隱藏文件也不行。從開始菜單選擇「尋找」選項來尋找文件名滿足 *.FON限定條件的文件。這時,您可以雙擊文件名來查看字體字符是些什麼。

對於許多標準控件和使用者接口組件,Windows不使用系統字體。相反地,使用名稱爲MS Sans Serif的字體(「MS」代表Microsoft)。這也是一種點陣字體。文件(名爲SSERIFE.FON)包含依據96 dpi視訊顯示器的字體,點值爲8、10、12、14、18和24。您可在GetStockObject函數中使用DEFAULT_GUI_FONT標識符來得到該字體。Windows使用的點值取決於「控制檯」的「顯示」中選擇的顯示分辨率。

到目前爲止,我已提到四種標識符,利用這四種標識符,您可以用GetStockObject來獲得用於設備內容的字體。還有三種其它字體標識符:ANSI_FIXED_FONT、ANSI_VAR_FONT和DEVICE_DEFAULT_FONT。爲了開始處理鍵盤和字符顯示問題,讓我們先看一下Windows中的所有備用字體。顯示這些字體的程序是STOKFONT,如程序6-3所示。

程序6-3 STOKFONT
STOKFONT.C
/*----------------------------------------------------------------------
STOKFONT.C -- Stock Font Objects
(c) Charles Petzold, 1998
-----------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("StokFont") ;
HWND                  hwnd ;
MSG                 msg ;
WNDCLASS              wndclass ;
wndclass.style                = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc  = WndProc ;
wndclass.cbClsExtra           = 0 ;
wndclass.cbWndExtra           = 0 ;
wndclass.hInstance            = hInstance ;
wndclass.hIcon                = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor              = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName= szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (  NULL, TEXT ("Program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow ( szAppName, TEXT ("Stock Fonts"),
WS_OVERLAPPEDWINDOW | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static struct
{
int     idStockFont ;
TCHAR * szStockFont ;
}
stockfont [] = { OEM_FIXED_FONT,             "OEM_FIXED_FONT",
ANSI_FIXED_FONT,      "ANSI_FIXED_FONT",
ANSI_VAR_FONT,        "ANSI_VAR_FONT",
SYSTEM_FONT,          "SYSTEM_FONT",
DEVICE_DEFAULT_FONT,"DEVICE_DEFAULT_FONT",
SYSTEM_FIXED_FONT,    "SYSTEM_FIXED_FONT",
DEFAULT_GUI_FONT,     "DEFAULT_GUI_FONT" } ;
static int  iFont, cFonts = sizeof stockfont / sizeof stockfont[0] ;
HDC                   hdc ;
int                   i, x, y, cxGrid, cyGrid ;
PAINTSTRUCT   ps ;
TCHAR                 szFaceName [LF_FACESIZE], szBuffer [LF_FACESIZE + 64] ;
TEXTMETRIC  tm ;
switch (message)
{
case   WM_CREATE:
SetScrollRange (hwnd, SB_VERT, 0, cFonts - 1, TRUE) ;
return 0 ;
case   WM_DISPLAYCHANGE:
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case   WM_VSCROLL:
switch (LOWORD (wParam))
{
case SB_TOP:       iFont = 0 ;                   break ;
case SB_BOTTOM:          iFont = cFonts - 1 ;      break ;
case SB_LINEUP:
case SB_PAGEUP:   iFont -= 1 ;                     break ;
case SB_LINEDOWN:
case SB_PAGEDOWN:             iFont += 1 ;                         break ;
case SB_THUMBPOSITION:iFont = HIWORD (wParam) ;     break ;
}
iFont = max (0, min (cFonts - 1, iFont)) ;
SetScrollPos (hwnd, SB_VERT, iFont, TRUE) ;
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case   WM_KEYDOWN:
switch (wParam)
{
case VK_HOME: SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0) ;    break ;
case VK_END:  SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0) ;   break ;
case VK_PRIOR:
case VK_LEFT:
case VK_UP:   SendMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0) ;   break ;
case VK_NEXT:
case VK_RIGHT:
case VK_DOWN: SendMessage (hwnd, WM_VSCROLL, SB_PAGEDOWN, 0) ; break ;
}
return 0 ;
case   WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
SelectObject (hdc, GetStockObject (stockfont[iFont].idStockFont)) ;
GetTextFace (hdc, LF_FACESIZE, szFaceName) ;
GetTextMetrics (hdc, &tm) ;
cxGrid = max (3 * tm.tmAveCharWidth, 2 * tm.tmMaxCharWidth) ;
cyGrid = tm.tmHeight + 3 ;
TextOut (hdc, 0, 0, szBuffer,
wsprintf (    szBuffer, TEXT (" %s: Face Name = %s, CharSet = %i"),
stockfont[iFont].szStockFont,
szFaceName, tm.tmCharSet)) ;
SetTextAlign (hdc, TA_TOP | TA_CENTER) ;
// vertical and horizontal lines
for (i = 0 ; i < 17 ; i++)
{
MoveToEx (hdc, (i + 2) * cxGrid,  2 * cyGrid, NULL) ;
LineTo   (hdc, (i + 2) * cxGrid, 19 * cyGrid) ;
MoveToEx (hdc,      cxGrid, (i + 3) * cyGrid, NULL) ;
LineTo   (hdc, 18 * cxGrid, (i + 3) * cyGrid) ;
}
// vertical and horizontal headings
for (i = 0 ; i < 16 ; i++)
{
TextOut (hdc, (2 * i + 5) * cxGrid / 2, 2 * cyGrid + 2, szBuffer,
wsprintf (szBuffer, TEXT ("%X-"), i)) ;
TextOut (hdc, 3 * cxGrid / 2, (i + 3) * cyGrid + 2, szBuffer,
wsprintf (szBuffer, TEXT ("-%X"), i)) ;
}
// characters
for (y = 0 ; y < 16 ; y++)
for (x = 0 ; x < 16 ; x++)
{
TextOut (hdc, (2 * x + 5) * cxGrid / 2,
(y + 3) * cyGrid + 2, szBuffer,
wsprintf (szBuffer, TEXT ("%c"), 16 * x + y)) ;
}
EndPaint (hwnd, &ps) ;
return 0 ;
case   WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

這個程序相當簡單。它使用滾動條和光標移動鍵讓您選擇顯示七種備用字體之一。該程序在一個網格中顯示一種字體的256個字符。頂部的標題和網格的左側顯示字符代碼的十六進制值。

在顯示區域的頂部,STOKFONT用GetStockObject函數顯示用於選擇字體的標識符。它還顯示由GetTextFace函數得到的字體樣式名稱和TEXTMETRIC結構的tmCharSet字段。這個「字符集標識符」對理解Windows如何處理外語版本的Windows是非常重要的。

如果在美國英語版本的Windows中執行STOKFONT,那麼您看到的第一個畫面將顯示使用OEM_FIXED_FONT標識符呼叫GetStockObject函數得到的字體。如圖6-3所示。


 

圖6-3 美國版Windows中的OEM_FIXED_FONT

在本字符集中(與本章其它部分一樣),您將看到一些ASCII。但請記住ASCII是7位代碼,它定義了從代碼0x20到0x7E的可顯示字符。到IBM開發出IBM PC原型機時,8位字節代碼已被穩固地建立起來,因此可使用全8位代碼作爲字符代碼。IBM決定使用一系列由線和方塊組成的字符、帶重音字母、希臘字母、數學符號和一些其它字符來擴展ASCII字符集。許多文字模式的MS-DOS程序在其屏幕顯示中都使用繪圖字符,並且許多MS-DOS程序都在文件中使用了一些擴展字符。

這個特殊的字符集給Windows最初的開發者帶來了一個問題。一方面,因爲Windows有完整的圖形程序設計語言,所以線和方塊字元在Windows中不需要。因此,這些字符使用的48個代碼最好用於許多西歐語言所需要的附帶重音字母。另一方面,IBM字符集定義了一個無法完全忽略的標準。

因此,Windows最初的開發者決定支持IBM字符集,但將其重要性降低到第二位-它們大多用於在窗口中執行的舊MS-DOS應用程序,和需要使用由MS-DOS應用程序建立文件的Windows程序。Windows應用程序不使用IBM字符集,並且隨着時間的推移,其重要性日漸衰退。然而,如果需要,您還是可以使用。在此環境下,「OEM」指的就是「IBM」。

(您應知道外語版本的Windows不必支持與美國英語版相同的OEM字符集。其它國家有其自己的MS-DOS字符集。這是個獨立的問題,就不在本書中討論了。)

因爲IBM字符集被認爲不適合Windows,於是選擇了另一種擴展字符集。此字符集稱作「ANSI字符集」,由美國國家標準協會(American National Standards Institute)制定,但它實際上是ISO(International Standards Organization,國際標準化組織)標準,也就是ISO標準8859。它還稱爲Latin 1、Western European、或者代碼頁1252。圖6-4顯示了ANSI字符集的一個版本-美國英語版Windows的系統字體。


 

圖6-4 美國版Windows中的SYSTEM_FONT

粗的垂直條表示這些字符代碼沒有定義。注意,代碼0x20到0x7E還是ASCII。此外,ASCII控制字符(0x00到0x1F以及0x7F)並不是可顯示字符。它們本應如此。

代碼0xC0到0xFF使得ANSI字符集對外語版Windows來說非常重要。這些代碼提供64個在西歐語言中普遍使用的字符。字符0xA0,看起來像空格,但實際上定義爲非斷開空格,例如「WW II」中的空格。

之所以說這是ANSI字符集的「一個版本」,是因爲存在代碼0x80到0x9F的字符。等寬的系統字體只包括其中的兩個字符,如圖6-5所示。


 

圖6-5 美國版Windows中的SYSTEM_FIXED_FONT

在Unicode中,代碼0x0000到0x007F與ASCII相同,代碼0x0080到0x009F複製了0x0000到0x001F的控制字符,代碼0x00A0到0x00FF與Windows中使用的ANSI字符集相同。

如果執行德語版的Windows,那麼當您用SYSTEM_FONT或者SYSTEM_FIXED_FONT標識符來呼叫GetStockObject函數時會得到同樣的ANSI字符集。其它西歐版Windows也是如此。ANSI字符集中含有這些語言所需要的所有字符。

不過,當您執行希臘版的Windows時,內定的字符集就改變了。相反地,SYSTEM_FONT如圖6-6所示。


 

圖6-6 希臘版Windows中的SYSTEM_FONT

SYSTEM_FIXED_FONT有同樣的字符。注意從0xC0到0xFF的代碼。這些代碼包含希臘字母表中的大寫字母和小寫字母。當您執行俄語版Windows時,內定的字符集如圖6-7所示。


 

圖6-7 俄語版Windows中的SYSTEM_FONT

此外, 注意斯拉夫字母表中的大寫和小寫字母佔用了代碼0xC0和0xFF。

圖6-8顯示了日語版Windows的SYSTEM_FONT。從0xA5到0xDF的字符都是片假名字母表的一部分。


 

圖6-8 日語版Windows中的SYSTEM_FONT

圖6-8所示的日文系統字體不同於前面顯示的那些,因爲它實際上是雙字節字符集(DBCS),稱爲「Shift-JIS」(「JIS」代表日本工業標準,Japanese Industrial Standard)。從0x81到0x9F以及從0xE0到0xFF的大多數字符代碼實際上只是雙字節代碼的第一個字節,其第二個字節通常在0x40到0xFC的範圍內(關於這些代碼的完整表格,請參見Nadine Kano書中的附錄G)。

現在,我們就可以看看KEYVIEW1中的問題在哪裏:如果您安裝了希臘鍵盤佈局並鍵入『abcde』,不考慮執行的Windows版本,Windows將產生WM_CHAR消息和字符代碼0xE1、0xE2、0xF8、0xE4和0xE5。但只有執行帶有希臘系統字體的希臘版Windows時,這些字符代碼才能與?、?、?、?和?相對應。

如果您安裝了俄語鍵盤佈局並敲入『abcde』,不考慮所使用的Windows版本,Windows將產生WM_CHAR消息和字符代碼0xF4、0xE8、0xF1、0xE2和0xF3。但只有在使用俄語版Windows或者使用斯拉夫字母表的其它語言版,並且使用斯拉夫系統字體時,這些字符代碼纔會與字符φ、и、с、в和у相對應。

如果您安裝了德語鍵盤佈局並按下=鍵(或者位於同一位置的鍵),然後按下a、e、i、o或者u鍵,不考慮使用的Windows版本,Windows將產生WM_CHAR消息和字符代碼0xE1、0xE9、0xED、0xF3和0xFA。只有執行西歐版或者美國版的Windows時,也就是說有西歐系統字體,這些字符代碼纔會和字符amp;nbsp;á、é、í、ó和ú相對應。

如果安裝了美國英語鍵盤佈局,則您可在鍵盤上鍵入任何字符,Windows將產生WM_CHAR消息以及與字符正確匹配的字符代碼。

Unicode怎麼樣?

我在第二章談到過Windows NT支持的Unicode有助於爲國際市場程序寫作。讓我們編譯一下定義了UNICODE標識符的KEYVIEW1,並在不同版本的Windows NT下執行(在本書附帶的光盤中,Unicode版的KEYVIEW1位於DEBUG目錄中)。

如果程序編譯時定義了UNICODE標識符,則「KeyView1」窗口類別就用RegisterClassW函數註冊,而不是RegisterClassA函數。這意味着任何帶有字符或文字數據的消息傳遞給WndProc時都將使用16位字符而不是8位字符。特別是WM_CHAR消息,將傳遞16位字符代碼而不是8位字符代碼。

請在美國英語版的Windows NT下執行Unicode版的KEYVIEW1。這裏假定您已經安裝了至少三種我們試驗過的鍵盤佈局-即德語、希臘語和俄語。

使用美國英語版的Windows NT,並安裝了英語或者德語的鍵盤佈局,Unicode版的KEYVIEW1在工作時將與非Unicode版相同。它將接收相同的字符代碼(所有0xFF或者更低的值),並顯示同樣正確的字符。這是因爲最初的256個Unicode字符與Windows中使用的ANSI字符集相同。

現在切換到希臘鍵盤佈局,並鍵入『abcde』。WM_CHAR消息將含有Unicode字符代碼0x03B1、 0x03B2、0x03C8、 0x03B4和0x03B5。注意,我們先看到的字符代碼值比0xFF高。這些Unicode字符代碼與希臘字母?、?、?、d和?相對應。不過,所有這五個字符都顯示爲方塊!這是因爲SYSTEM_FIXED_FONT只含有256個字符。

現在切換到俄語鍵盤佈局,並鍵入『abcde』。KEYVIEW1顯示WM_CHAR消息和Unicode字符代碼0x0444、0x0438、0x0441、0x0432和0x0443,這些字符對應於斯拉夫字母φ、и、с、в和у。不過,所有這五個字母也顯示爲實心方塊。

簡言之,非Unicode版的KEYVIEW1顯示錯誤字符的地方,Unicode版的KEYVIEW1就顯示實心方塊,以表示目前的字體沒有那種特殊字符。雖然我不願說Unicode版的KEYVIEW1是非Unicode版的改進,但事實確實如此。非Unicode版顯示錯誤字符,而Unicode版不會這樣。

Unicode和非Unicode版KEYVIEW1的不同之處主要在兩個方面。

首先,WM_CHAR消息伴隨一個16位字符代碼,而不是8位字符代碼。在非Unicode版本的KEYVIEW1中,8位字符代碼的含義取決於目前活動的鍵盤佈局。如果來自德語鍵盤,則0xE1代碼表示á,如果來自希臘語鍵盤則代表?,如果來自俄語鍵盤則代表?。在Unicode版本程序中,16位字符代碼的含義很明確:a字符是0x00E1,?字符是0x03B1,而?字符是0x0431。

第二,Unicode的TextOutW函數顯示的字符依據16位字符代碼,而不是非Unicode的TextOutA函數的8位字符代碼。因爲這些16位字符代碼含義明確,GDI可以確定目前在設備內容中選擇的字體是否可顯示每個字符。

在美國英語版Windows NT下執行Unicode版的KEYVIEW1多少讓人感到有些迷惑,因爲它所顯示的就好像GDI只顯示了0x0000到0x00FF之間的字符代碼,而沒有顯示高於0x00FF的代碼。也就是說,只是在字符代碼和系統字體中256個字符之間簡單的一對一映射。

然而,如果安裝了希臘或者俄語版的Windows NT,您將發現情況就大不一樣了。例如,如果安裝了希臘版的Windows NT,則美國英語、德語、希臘語和俄語鍵盤將會產生與美國英語版Windows NT同樣的Unicode字符代碼。不過,希臘版的Windows NT將不顯示德語重音字符或者俄語字符,因爲這些字符並不在希臘系統字體中。同樣,俄語版的Windows NT也不顯示德語重音字符或者希臘字符,因爲這些字符也不在俄語系統字體中。

其中,Unicode版的KEYVIEW1的區別在日語版Windows NT下更具戲劇性。您從IME輸入日文字符,這些字符可以正確顯示。唯一的問題是格式:因爲日文字符通常看起來非常複雜,它們的顯示寬度是其它字符的兩倍。

TrueType 和大字體

我們使用的點陣字體(在日文版Windows中帶有附加字體)最多包括256個字符。這是我們所希望的,因爲當假定字符代碼是8位時,點陣字體文件的格式就跟早期Windows時代的樣子一樣了。這就是爲什麼當我們使用SYSTEM_FONT或者SYSTEM_FIXED_FONT時,某些語言中一些字符總不能正確顯示(日本系統字體有點不同,因爲它是雙字節字符集;大多數字符實際上保存在TrueType集合文件中,文件擴展名是.TTC)。

TrueType字體包含的字符可以多於256個。並不是所有TrueType字體中的字符都多於256個,但Windows 98和Windows NT中的字體包含多於256個字符。或者,安裝了多語系支持後,TrueType字體中也包含多於256個字符。在「 控制檯」的「新增 /刪除程序」中,單擊「Windows 安裝程序」頁面卷標,並確保選中了「 多語系支持」。這個多語系支持包括五個字符集:波羅的海語系、中歐語系、斯拉夫語系、希臘語系和土耳其語系。波羅的海語系字符集用於愛沙尼亞語、拉脫維亞語和立陶宛語。中歐字符集用於阿爾巴尼亞語、捷克語、克羅地亞語、匈牙利語、波蘭語、羅馬尼亞語、斯洛伐克語和斯洛文尼亞語。斯拉夫字符集用於保加利亞語、白俄羅斯語、俄語、塞爾維亞語和烏克蘭語。

Windows 98中的TrueType字體支持這五種字符集,再加上西歐(ANSI)字符集,西歐字符集實際上用於其它所有語言,但遠東語言(漢語、日語和朝鮮語)除外。支持多種字符集的TrueType字體有時也稱爲「大字體」。在這種情況下的「大」並不是指字符的大小,而是指數量。

即使在非Unicode程序中也可利用大字體,這意味着可以用大字體顯示幾種不同字母表中的字符。然而,爲了要將得到的字體選進設備內容,還需要GetStockObject以外的函數。

函數CreateFont和CreateFontIndirect建立了一種邏輯字體,這與CreatePen建立邏輯畫筆以及CreateBrush建立邏輯畫刷的方式類似。CreateFont用14個參數描述要建立的字體。CreateFontIndirect只有一個參數,但該參數是指向LOGFONT結構的指針。LOGFONT結構有14個字段,分別對應於CreateFont函數的參數。我將在第十七章詳細討論這些函數。現在,讓我們看一下CreateFont函數,但我們只注意其中兩個參數,其它參數都設定爲0。

如果需要等寬字體(就像KEYVIEW1程序中使用的),將CreateFont的第13個參數設定爲FIXED_PITCH。如果需要非內定字符集的字體(這也是我們所需要的),將CreateFont的第9個參數設定爲某個「字符集ID」。此字符集ID將是WINGDI.H中定義的下列值之一。我已給出註釋,指出和這些字符集相關的代碼頁:

#define ANSI_CHARSET

0

// 1252 Latin 1 (ANSI)

#define DEFAULT_CHARSET

1

 

#define SYMBOL_CHARSET

2

 

#define MAC_CHARSET

77

 

#define SHIFTJIS_CHARSET

128

// 932 (DBCS, 日本)

#define HANGEUL_CHARSET

129

// 949 (DBCS, 韓文)

#define HANGUL_CHARSET

129

// " "

#define JOHAB_CHARSET

130

// 1361 (DBCS, 韓文)

#define GB2312_CHARSET

134

// 936 (DBCS, 簡體中文)

#define CHINESEBIG5_CHARSET

136

// 950 (DBCS, 繁體中文)

#define GREEK_CHARSET

161

// 1253希臘文

#define TURKISH_CHARSET

162

// 1254 Latin 5 (土耳其文)

#define VIETNAMESE_CHARSET

163

// 1258越南文

#define HEBREW_CHARSET

177

// 1255希伯來文

#define ARABIC_CHARSET

178

// 1256阿拉伯文

#define BALTIC_CHARSET

186

// 1257波羅的海字集

#define RUSSIAN_CHARSET

204

// 1251俄文 (斯拉夫語系)

#define THAI_CHARSET

222

// 874泰文

#define EASTEUROPE_CHARSET

238

// 1250 Latin 2 (中歐語系)

#define OEM_CHARSET

255

// 地區自訂

爲什麼Windows對同一個字符集有兩個不同的ID:字符集ID和代碼頁ID?這只是Windows中的一種怪癖。注意,字符集ID只需要1字節的儲存空間,這是LOGFONT結構中字符集字段的大小(試回憶Windows 1.0時期,內存和儲存空間有限,每個字節都必須斤斤計較)。注意,有許多不同的MS-DOS代碼頁用於其它國家,但只有一種字符集ID-OEM_CHARSET-用於MS-DOS字符集。

您還會注意到,這些字符集的值與STOKFONT程序最上頭的「CharSet」值一致。在美國英語版Windows中,我們看到常備字體的字符集ID是0 (ANSI_CHARSET)和255(OEM_CHARSET)。希臘版Windows中的是161(GREEK_CHARSET),在俄語版中的是204(RUSSIAN_CHARSET),在日語版中是128(SHIFTJIS_CHARSET)。

在上面的代碼中,DBCS代表雙字節字符集,用於遠東版的Windows。其它版的Windows不支持DBCS字體,因此不能使用那些字符集ID。

CreateFont傳回HFONT值-邏輯字體的句柄。您可以使用SelectObject將此字體選進設備內容。實際上,您必須呼叫DeleteObject來刪除您建立的所有邏輯字體。

大字體解決方案的其它部分是WM_INPUTLANGCHANGE消息。一旦您使用桌面下端的彈出式菜單來改變鍵盤佈局,Windows都會向您的窗口消息處理程序發送WM_INPUTLANGCHANGE消息。wParam消息參數是新鍵盤佈局的字符集ID。

程序6-4所示的KEYVIEW2程序實作了鍵盤佈局改變時改變字體的邏輯。

程序6-4 KEYVIEW2
KEYVIEW2.C
/*----------------------------------------------------------------------------
KEYVIEW2.C -- Displays Keyboard and Character Messages
(c) Charles Petzold, 1998
-----------------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("KeyView2") ;
HWND                  hwnd ;
MSG                   msg ;
WNDCLASS              wndclass ;
wndclass.style                = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc  = WndProc ;
wndclass.cbClsExtra           = 0 ;
wndclass.cbWndExtra           = 0 ;
wndclass.hInstance            = hInstance ;
wndclass.hIcon                = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor              = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName= NULL ;
wndclass.lpszClassName= szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Keyboard Message Viewer #2"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static DWORD dwCharSet = DEFAULT_CHARSET ;
static int   cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar ;
static int   cLinesMax, cLines ;
static PMSG  pmsg ;
static RECT  rectScroll ;
static TCHAR szTop[] =        TEXT ("Message   Key   Char   ")
TEXT ("Repeat Scan Ext ALT Prev Tran") ;
static TCHAR szUnd[] =        TEXT ("_______  ___  ____   ")
TEXT ("______ ____ ___ ___ ____ ____") ;
static TCHAR * szFormat[2] = {
TEXT ("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"),
TEXT ("%-13s   0x%04X%1s%c %6u %4d %3s %3s %4s %4s") } ;
static TCHAR * szYes  = TEXT ("Yes") ;
static TCHAR * szNo           = TEXT ("No") ;
static TCHAR * szDown= TEXT ("Down") ;
static TCHAR * szUp           = TEXT ("Up") ;
static TCHAR * szMessage [] = {
TEXT ("WM_KEYDOWN"),    TEXT ("WM_KEYUP"),
TEXT ("WM_CHAR"),       TEXT ("WM_DEADCHAR"),
TEXT ("WM_SYSKEYDOWN"), TEXT ("WM_SYSKEYUP"),
TEXT ("WM_SYSCHAR"),    TEXT ("WM_SYSDEADCHAR") } ;
HDC          hdc ;
int          i, iType ;
PAINTSTRUCT  ps ;
TCHAR        szBuffer[128], szKeyName [32] ;
TEXTMETRIC   tm ;
switch (message)
{
case   WM_INPUTLANGCHANGE:
dwCharSet = wParam ;
// fall through
case WM_CREATE:
case WM_DISPLAYCHANGE:
// Get maximum size of client area
cxClientMax = GetSystemMetrics (SM_CXMAXIMIZED) ;
cyClientMax = GetSystemMetrics (SM_CYMAXIMIZED) ;
// Get character size for fixed-pitch font
hdc = GetDC (hwnd) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
GetTextMetrics (hdc, &tm) ;
cxChar = tm.tmAveCharWidth ;
cyChar = tm.tmHeight ;
DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
ReleaseDC (hwnd, hdc) ;
// Allocate memory for display lines
if (pmsg)
free (pmsg) ;
cLinesMax = cyClientMax / cyChar ;
pmsg = malloc (cLinesMax * sizeof (MSG)) ;
cLines = 0 ;
// fall through
case   WM_SIZE:
if (message == WM_SIZE)
{
cxClient              = LOWORD (lParam) ;
cyClient              = HIWORD (lParam) ;
}
// Calculate scrolling rectangle
rectScroll.left       = 0 ;
rectScroll.right      = cxClient ;
rectScroll.top        = cyChar ;
rectScroll.bottom     = cyChar * (cyClient / cyChar) ;
InvalidateRect (hwnd, NULL, TRUE) ;
if (message == WM_INPUTLANGCHANGE)
return TRUE ;
return 0 ;
case WM_KEYDOWN:
case WM_KEYUP:
case WM_CHAR:
case WM_DEADCHAR:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
case WM_SYSCHAR:
case WM_SYSDEADCHAR:
// Rearrange storage array
for (i = cLinesMax - 1 ; i > 0 ; i--)
{
pmsg[i] = pmsg[i - 1] ;
}
// Store new message
pmsg[0].hwnd = hwnd ;
pmsg[0].message = message ;
pmsg[0].wParam = wParam ;
pmsg[0].lParam = lParam ;
cLines = min (cLines + 1, cLinesMax) ;
// Scroll up the display
ScrollWindow (hwnd, 0, -cyChar, &rectScroll, &rectScroll) ;
break ;       // ie, call DefWindowProc so Sys messages work
case   WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
SetBkMode (hdc, TRANSPARENT) ;
TextOut (hdc, 0, 0, szTop, lstrlen (szTop)) ;
TextOut (hdc, 0, 0, szUnd, lstrlen (szUnd)) ;
for (i = 0 ; i < min (cLines, cyClient / cyChar - 1) ; i++)
{
iType =pmsg[i].message == WM_CHAR ||
pmsg[i].message == WM_SYSCHAR ||
pmsg[i].message == WM_DEADCHAR ||
pmsg[i].message == WM_SYSDEADCHAR ;
GetKeyNameText (pmsg[i].lParam, szKeyName,
sizeof (szKeyName) / sizeof (TCHAR)) ;
TextOut (hdc, 0, (cyClient / cyChar - 1 - i) * cyChar, szBuffer,
wsprintf (    szBuffer, szFormat [iType],
szMessage [pmsg[i].message -
WM_KEYFIRST],
pmsg[i].wParam,
(PTSTR) (iType ? TEXT (" ") : szKeyName),
(TCHAR) (iType ? pmsg[i].wParam : ' '),
LOWORD (pmsg[i].lParam),
HIWORD (pmsg[i].lParam) & 0xFF,
0x01000000 & pmsg[i].lParam ? szYes  : szNo,
0x20000000 & pmsg[i].lParam ? szYes  : szNo,
0x40000000 & pmsg[i].lParam ? szDown : szUp,
0x80000000 & pmsg[i].lParam ? szUp : szDown));
}
DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case   WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

注意,鍵盤輸入語言改變後,KEYVIEW2就清除畫面並重新分配儲存空間。這樣做有兩個原因:第一,因爲KEYVIEW2並不是某種字體專用的,當輸入語言改變時字體文字的大小也會改變。程序需要根據新字符大小重新計算某些變量。第二,在接收每個字符消息時,KEYVIEW2並不有效地保留字符集ID。因此,如果鍵盤輸入語言改變了,而且KEYVIEW2需要重畫顯示區域時,所有的字符將用新字體顯示。

第十七章將詳細討論字體和字符集。如果您想深入研究國際化問題,可以在/Platform SDK/Windows Base Services/International Features找到需要的文件,還有許多基礎信息則位於/Platform SDK/Windows Base Services/General Library/String Manipulation。

插入符號(不是光標)

當您往程序中輸入文字時,通常有一個底線、豎條或者方框來指示輸入的下一個字符將出現在屏幕上的位置。這個標誌通常稱爲「光標」,但是在Windows下寫程序,您必須改變這個習慣。在Windows中,它稱爲「插入符號」。「光標」是指表示鼠標位置的那個位圖圖像。

插入符號函數

主要有五個插入符號函數:

  • CreateCaret 建立與窗口有關的插入符號
     
  • SetCaretPos 在窗口中設定插入符號的位置
     
  • ShowCaret 顯示插入符號
     
  • HideCaret 隱藏插入符號
     
  • DestroyCaret 撤消插入符號
     

另外還有取得插入符號目前位置(GetCaretPos)和取得以及設定插入符號閃爍時間(GetCaretBlinkTime和SetCaretBlinkTime)的函數。

在Windows中,插入符號定義爲水平線、與字符大小相同的方框,或者與字符同高的豎線。如果使用調和字體,例如Windows內定的系統字體,則推薦使用豎線插入符號。因爲調和字體中的字符沒有固定大小,水平線或方框不能設定爲字符的大小。

如果程序中需要插入符號,那麼您不應該簡單地在窗口消息處理程序的WM_CREATE消息處理期間建立它,然後在WM_DESTROY消息處理期間撤消。其原因顯而易見:一個消息隊列只能支持一個插入符號。因此,如果您的程序有多個窗口,那麼各個窗口必須有效地共享相同的插入符號。

其實,它並不像聽起來那麼多限制。您再想想就會發現,只有在窗口有輸入焦點時,窗口內顯示插入符號纔有意義。事實上,閃爍的插入符號只是一種視覺提示:您可以在程序中輸入文字。因爲任何時候都只有一個窗口擁有輸入焦點,所以多個窗口同時都有閃爍的插入符號是沒有意義的。

通過處理WM_SETFOCUS和WM_KILLFOCUS消息,程序就可以確定它是否有輸入焦點。正如名稱所暗示的,窗口消息處理程序在有輸入焦點的時候接收到WM_SETFOCUS消息,失去輸入焦點的時候接收到WM_KILLFOCUS消息。這些消息成對出現:窗口消息處理程序在接收到WM_KILLFOCUS消息之前將一直接收到WM_SETFOCUS消息,並且在窗口打開期間,此窗口總是接收到相同數量的WM_SETFOCUS和WM_KILLFOCUS消息。

使用插入符號的主要規則很簡單:窗口消息處理程序在WM_SETFOCUS消息處理期間呼叫CreateCaret,在WM_KILLFOCUS消息處理期間呼叫DestroyCaret。

這裏還有幾條其它規則:插入符號剛建立時是隱蔽的。如果想使插入符號可見,那麼您在呼叫CreateCaret之後,窗口消息處理程序還必須呼叫ShowCaret。另外,當窗口消息處理程序處理一條非WM_PAINT消息而且希望在窗口內繪製某些東西時,它必須呼叫HideCaret隱藏插入符號。在繪製完畢後,再呼叫ShowCaret顯示插入符號。HideCaret的影響具有累積效果,如果多次呼叫HideCaret而不呼叫ShowCaret,那麼只有呼叫ShowCaret相同次數時,才能看到插入符號。

TYPER程序

程序6-5所示的TYPER程序使用了本章討論的所有內容,您可以認爲TYPER是一個相當簡單的文字編輯器。在窗口中,您可以輸入字符,用光標移動鍵(也可以稱爲插入符號移動鍵)來移動光標(I型標),按下Escape鍵清除窗口的內容等。縮放窗口、改變鍵盤輸入語言時都會清除窗口的內容。本程序沒有捲動,沒有文字尋找和定位功能,不能儲存文件,沒有拼寫檢查,但它確實是寫作一個文字編輯器的開始。

程序6-5 TYPER
TYPER.C
/*------------------------------------------------------------------------
TYPER.C --      Typing Program
(c) Charles Petzold, 1998
--------------------------------------------------------------------------*/
#include <windows.h>
#define BUFFER(x,y) *(pBuffer + y * cxBuffer + x)
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR  szAppName[] = TEXT ("Typer") ;
HWND                  hwnd ;
MSG                   msg ;
WNDCLASS              wndclass ;
wndclass.style                               = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc                 = WndProc ;
wndclass.cbClsExtra                  = 0 ;
wndclass.cbWndExtra                          = 0 ;
wndclass.hInstance                          = hInstance ;
wndclass.hIcon                               = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor                             = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground               = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName                        = NULL ;
wndclass.lpszClassName                       = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (  NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Typing Program"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static DWORD          dwCharSet = DEFAULT_CHARSET ;
static int            cxChar, cyChar, cxClient, cyClient, cxBuffer, cyBuffer,
xCaret, yCaret ;
static TCHAR *pBuffer = NULL ;
HDC                           hdc ;
int                           x, y, i ;
PAINTSTRUCT           ps ;
TEXTMETRIC            tm ;
switch (message)
{
case   WM_INPUTLANGCHANGE:
dwCharSet = wParam ;
// fall through
case   WM_CREATE:
hdc = GetDC (hwnd) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
GetTextMetrics (hdc, &tm) ;
cxChar = tm.tmAveCharWidth ;
cyChar = tm.tmHeight ;
DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
ReleaseDC (hwnd, hdc) ;
// fall through
case   WM_SIZE:
// obtain window size in pixels
if (message == WM_SIZE)
{
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
}
// calculate window size in characters
cxBuffer = max (1, cxClient / cxChar) ;
cyBuffer = max (1, cyClient / cyChar) ;
// allocate memory for buffer and clear it
if (pBuffer != NULL)
free (pBuffer) ;
pBuffer = (TCHAR *) malloc (cxBuffer * cyBuffer * sizeof (TCHAR)) ;
for (y = 0 ; y < cyBuffer ; y++)
for (x = 0 ; x < cxBuffer ; x++)
BUFFER(x,y) = ' ' ;
// set caret to upper left corner
xCaret = 0 ;
yCaret = 0 ;
if (hwnd == GetFocus ())
SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case   WM_SETFOCUS:
// create and show the caret
CreateCaret (hwnd, NULL, cxChar, cyChar) ;
SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
ShowCaret (hwnd) ;
return 0 ;
case WM_KILLFOCUS:
// hide and destroy the caret
HideCaret (hwnd) ;
DestroyCaret () ;
return 0 ;
case   WM_KEYDOWN:
switch (wParam)
{
case VK_HOME:
xCaret = 0 ;
break ;
case   VK_END:
xCaret = cxBuffer - 1 ;
break ;
case   VK_PRIOR:
yCaret = 0 ;
break ;
case   VK_NEXT:
yCaret = cyBuffer - 1 ;
break ;
case   VK_LEFT:
xCaret = max (xCaret - 1, 0) ;
break ;
case   VK_RIGHT:
xCaret = min (xCaret + 1, cxBuffer - 1) ;
break ;
case   VK_UP:
yCaret = max (yCaret - 1, 0) ;
break ;
case   VK_DOWN:
yCaret = min (yCaret + 1, cyBuffer - 1) ;
break ;
case   VK_DELETE:
for (x = xCaret ; x < cxBuffer - 1 ; x++)
BUFFER (x, yCaret) = BUFFER (x + 1, yCaret) ;
BUFFER (cxBuffer - 1, yCaret) = ' ' ;
HideCaret (hwnd) ;
hdc = GetDC (hwnd) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0,FIXED_PITCH, NULL)) ;
TextOut (hdc, xCaret * cxChar, yCaret * cyChar,
& BUFFER (xCaret, yCaret),
cxBuffer - xCaret) ;
DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
ReleaseDC (hwnd, hdc) ;
ShowCaret (hwnd) ;
break ;
}
SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
return 0 ;
case   WM_CHAR:
for (i = 0 ; i < (int) LOWORD (lParam) ; i++)
{
switch (wParam)
{
case '/b':                                           // backspace
if (xCaret > 0)
{
xCaret-- ;
SendMessage (hwnd, WM_KEYDOWN, VK_DELETE, 1) ;
}
break ;
case '/t':                                                  // tab
do
{
SendMessage (hwnd, WM_CHAR, ' ', 1) ;
}
while (xCaret % 8 != 0) ;
break ;
case '/n':                                                  // line feed
if (++yCaret == cyBuffer)
yCaret = 0 ;
break ;
case '/r':                                                  // carriage return
xCaret = 0 ;
if (++yCaret == cyBuffer)
yCaret = 0 ;
break ;
case '/x1B':                                       // escape
for (y = 0 ; y < cyBuffer ; y++)
for (x = 0 ; x < cxBuffer ; x++)
BUFFER (x, y) = ' ' ;
xCaret = 0 ;
yCaret = 0 ;
InvalidateRect (hwnd, NULL, FALSE) ;
break ;
default:                                                    // character codes
BUFFER (xCaret, yCaret) = (TCHAR) wParam ;
HideCaret (hwnd) ;
hdc = GetDC (hwnd) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
TextOut (hdc, xCaret * cxChar, yCaret * cyChar,
& BUFFER (xCaret, yCaret), 1) ;
DeleteObject (
SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
ReleaseDC (hwnd, hdc) ;
ShowCaret (hwnd) ;
if (++xCaret == cxBuffer)
{
xCaret = 0 ;
if (++yCaret == cyBuffer)
yCaret = 0 ;
}
break ;
}
}
SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
return 0 ;
case   WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
for (y = 0 ; y < cyBuffer ; y++)
TextOut (hdc, 0, y * cyChar, & BUFFER(0,y), cxBuffer) ;
DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case   WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

爲了簡單起見,TYPER程序使用一種等寬字體,因爲編寫處理調和字體的文字編輯器要困難得多。程序在好幾個地方取得設備內容:在WM_CREATE消息處理期間,在WM_KEYDOWN消息處理期間,在WM_CHAR消息處理期間以及在WM_PAINT消息處理期間,每次都通過GetStockObject和SelectObject呼叫來選擇等寬字體。

在WM_SIZE消息處理期間,TYPER計算窗口的字符寬度和高度並把值保存在cxBuffer和cyBuffer變量中,然後使用malloc分配緩衝區以保存在窗口內輸入的所有字符。注意,緩衝區的字節大小取決於cxBuffer、cyBuffer和sizeof(TCHAR),它可以是1或2,這依賴於程序是以8位的字符處理還是以Unicode方式編譯的。

xCaret和yCaret變量保存插入符號位置。在WM_SETFOCUS消息處理期間,TYPER呼叫CreateCaret來建立與字符有相同寬度和高度的插入符號,呼叫SetCaretPos來設定插入符號的位置,呼叫ShowCaret使插入符號可見。在WM_KILLFOCUS消息處理期間,TYPER呼叫HideCaret和DestroyCaret。

對WM_KEYDOWN的處理大多要涉及光標移動鍵。Home和End把插入符號送至一行的開始和末尾處,Page Up和Page Down把插入符號送至窗口的頂端和底部,箭頭的用法不變。對Delete鍵,TYPER將緩衝區中從插入符號之後的那個位置開始到行尾的所有內容向前移動,並在行尾顯示空格。

WM_CHAR處理Backspace、Tab、Linefeed(Ctrl-Enter)、Enter、Escape和字符鍵。注意,在處理WM_CHAR消息時(假設使用者輸入的每個字符都非常重要),我使用了lParam中的重複計數;而在處理WM_KEYDOWN消息時卻不這麼作(避免有害的重複捲動)。對Backspace和Tab的處理由於使用了SendMessage函數而得到簡化,Backspace與Delete做法相仿,而Tab則如同輸入了若干個空格。

前面我已經提到過,在非WM_PAINT消息處理期間,如果要在窗口中繪製內容,則應該隱蔽光標。TYPER爲Delete鍵處理WM_KEYDOWN消息和爲字符鍵處理WM_CHAR消息時即是如此。在這兩種情況下,TYPER改變緩衝區中的內容,然後在窗口中繪製一個或者多個新字符。

雖然TYPER使用了與KEYVIEW2相同的做法以在字符集之間切換(就像使用者切換鍵盤佈局一樣),但對於遠東版的Windows,它還是不能正常工作。TYPER不允許使用兩倍寬度的字符。此問題將在第十七章討論,那時我們將詳細討論字體與文字輸出。

 
發佈了11 篇原創文章 · 獲贊 5 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章