6. 鍵盤

Post date: 2012/3/23 上午 05:37:07

6. 鍵盤

在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的不同組合,這些組合可以產生帶有音調標記的小寫或者大寫,例如,à、á、â、Ä、或 Å。

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

按鍵訊息

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

表6-1

通常「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鍵的功能,所以您無需攔截這些訊息。您的視窗訊息處理程式將最後收到關於這些按鍵結果(如功能表選擇)的其他訊息。如果您想在自己的視窗訊息處理程式中加上攔截系統按鍵的程式碼(如本章後面的 KEYVIEW1KEYVIEW2 程式所作的那樣),那麼在處理這些訊息之後再傳送到DefWindowProc,Windows就仍然可以將它們用於通常的目的。

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

case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_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

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

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

表6-3

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

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

表6-4

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

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

表6-5

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

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

表6-6

Windows用VK_LWIN和VK_RWIN鍵打開「開始」功能表或者(在以前的版本中)啟動「工作管理員程式」。這兩個都可以用於登錄或登出Windows(只在Microsoft Windows NT中有效),或者登錄或登出網路(在Windows for Applications中)。應用程式能夠通過顯示輔助資訊或者當成捷徑鍵看待來處理application鍵。

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

表6-7

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

表6-8

另外,還定義了一些其他虛擬鍵碼,但它們只用於非標準鍵盤上的鍵,或者通常在大型主機終端機上使用的鍵。查看/ 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所示。

重複計數

重複計數是該訊息所表示的按鍵次數,大多數情況下,重複計數設定為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。除了兩個之外:

圖6-1 lParam變數的6個按鍵訊息欄位

  • 如果活動視窗最小化了,則它沒有輸入焦點。這時候所有的按鍵都會產生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函式,您就能獲得此資訊。例如:

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

傳回的值低位元被設為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功能鍵為止:

不要這麼做!這將讓程式當死(除非在執行此敘述之前早就從訊息佇列中接收到了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代碼的方法:

iState = GetKeyState (VK_SHIFT) ;

iState = GetKeyState (VK_CAPITAL) ;

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

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 ;

至此,您已經有了大概觀念了吧。我們的目標是為捲動列添加鍵盤介面,並且也正在這麼做。通過把捲動訊息發送到視窗訊息處理程式,我們實作了用游標移動鍵進行捲動列的功能。現在您知道在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_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訊息(及其他三種字元訊息)說明的字元代碼將是:

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

如果hwnd的視窗訊息處理程式獲得Unicode訊息,那麼變數fUnicode將為TRUE,這表示視窗是用RegisterClassW註冊的視窗類別。

訊息順序

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

(TCHAR) wParam

fUnicode = IsWindowUnicode (hwnd) ;

表6-10

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

表6-11

Shift鍵本身不產生字元訊息。

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

表6-12

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

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

表6-13

最右列給出了在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訊息參數中六個欄位的狀態。

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

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

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

外語鍵盤問題

如果您執行美國英語版本的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和字元 á、â、¢、ä 和 å。在這裏有些字元不能正確顯示。難道您不應該得到希臘字母表中的字母嗎?

現在切換到俄語鍵盤並重新輸入『abcde』。現在您得到WM_CHAR訊息和字元代碼0xF4、0xE8、0xF1、0xE2和0xF3,以及字元 ô、è、ñ、â 和 ó。而且,還是有些字母不能正常顯示。您應從斯拉夫字母表中得到這些字母。

問題在於:您已經切換鍵盤以產生不同的字元代碼,但您還沒有將此切換通知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中得到的字元代碼相同。但現在顯示的字元是 t、b、y、d 和 e。這些確實是小寫的希臘字母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中的一樣。不過,顯示的字元卻是 a、i、n、s 和 ϊ,而不是正確的 á、é、í、ó 和 ú。

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

您還可安裝俄語版的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總結了所有的情況:

SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

SetBkMode (hdc, TRANSPARENT) ;

圖6-2 KEYVIEW1的螢幕顯示

表6-14

在檔案名稱中,「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所示。

在本字元集中(與本章其他部分一樣),您將看到一些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的系統字體。

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

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

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

在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所示。

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

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

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

圖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時,這些字元代碼才能與 t、b、y、d 和 e 相對應。

如果您安裝了俄語鍵盤佈局並敲入『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字元代碼與希臘字母 t、b、y、d 和 e 相對應。不過,所有這五個字元都顯示為方塊!這是因為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代碼表示 á,如果來自希臘語鍵盤則代表 a,如果來自俄語鍵盤則代表 s。在Unicode版本程式中,16位元字元代碼的含義很明確:a字元是0x00E1,a 字元是0x03B1,而 s 字元是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中定義的下列值之一。我已給出注釋,指出和這些字元集相關的內碼表:

圖6-5 美國版Windows中的SYSTEM_FIXED_FONT

圖6-3 美國版Windows中的OEM_FIXED_FONT

圖6-4 美國版Windows中的SYSTEM_FONT

圖6-6 希臘版Windows中的SYSTEM_FONT

圖6-7 俄語版Windows中的SYSTEM_FONT

圖6-8 日語版Windows中的SYSTEM_FONT

為什麼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不允許使用兩倍寬度的字元。此問題將在 第十七章 討論,那時我們將詳細討論字體與文字輸出。