windows核心编程-第二章 Unicode
第2章U n i c o d e
? ? 隨著M i c r o s o f t公司的Wi n d o w s操作系統在全世界日益廣泛的流行,對于軟件開發人員來說,將目標瞄準國際上的各個不同市場,已經成為一個越來越重要的問題。美國的軟件版本比國際版本提前6個月推向市場,這曾經是個司空見慣的現象。但是,由于各國對Wi n d o w s操作系統提供了越來越多的支持,因此就更加容易為國際市場生產各種應用軟件,從而縮短了軟件的美國版本與國際版本推出的時間間隔。
? ? Wi n d o w s操作系統始終不逾地提供各種支持,以幫助軟件開發人員進行應用程序的本地化工作。應用軟件可以從各種不同的函數中獲得特定國家的信息,并可觀察控制面板的設置,以確定用戶的首選項。Wi n d o w s甚至支持不同的字體,以適應應用的需要。
? ? 之所以將這一章放在本書的開頭,是因為考慮到 U n i c o d e是開發任何應用程序時要采用的基本步驟。本書的每一章中幾乎都要講到關于U n i c o d e的問題,而且書中給出的所有示例應用程序都是“用U n i c o d e實現的”。如果你為Microsoft Windows 2000或Microsoft Windows CE開發應用程序,你應該使用U n i c o d e進行開發。如果你為Microsoft Windows 98開發應用程序,你必須對某些問題作出決定。本章也要講述Windows 98的有關問題。
2.1 字符集
? ? 軟件的本地化要解決的真正問題,實際上就是如何來處理不同的字符集。多年來,許多人一直將文本串作為一系列單字節字符來進行編碼,并在結尾處放上一個零。對于我們來說,這已經成了習慣。當調用s t r l e n函數時,它在以0結尾的單字節字符數組中返回字符的數目。
? ? 問題是,有些文字和書寫規則(比如日文中的漢字就是個典型的例子)的字符集中的符號太多了,因此單字節(它提供的符號最多不能超過2 5 6個)是根本不敷使用的。為此出現了雙字節字符集(D B C S) ,以支持這些文字和書寫規則。
2.1.1 單字節與雙字節字符集
? ? 在雙字節字符集中,字符串中的每個字符可以包含一個字節或包含兩個字節。例如,日文中的漢字,如果第一個字符在0 x 8 1與0 x 9 F之間,或者在0 x E 0與0 x F C之間,那么就必須觀察下一個字節,才能確定字符串中的這個完整的字符。使用雙字節字符集,對于程序員來說簡直是個很大的難題,因為有些字符只有一個字節寬,而有些字符則是兩個字節寬。
? ? 如果只是調用s t r l e n函數,那么你無法真正了解字符串中究竟有多少字符,它只能告訴你到達結尾的0之前有多少個字節。A N S I的C運行期庫中沒有配備相應的函數,使你能夠對雙字節字符集進行操作。但是,Microsoft Visual C++的運行期庫卻包含許多函數,如_ m b s l e n ,它可以用來操作多字節(既包括單字節也包括雙字節)字符串。
? ? 為了幫助你對D B C S字符串進行操作,Wi n d o w s提供了下面的一組幫助函數(見表2 - 1 )。
? ? 前兩個函數CharNext和Char Prev允許前向或逆向遍歷DBCS字符串,方法是每次一個字符。第三個函數 IsDBCSLeadByte, 在字節返回到一個兩字字節符的第一個字節時將返回T R U E。
? ? 盡管這些函數使得我們對 D B C S的操作更容易,但還需要,一個更好的方法讓我們來看看U n i c o d e。
2.1.2 Unicode:寬字節字符集
? ? U n i c o d e是A p p l e和X e r o x公司于1 9 8 8年建立的一個技術標準。 1 9 9 1年,成立了一個集團機構負責U n i c o d e的開發和推廣應用。該集團由A p p l e、C o m p a q、H P、I B M、M i c r o s o f t、O r a c l e、Silicon Graphics, Inc.、S y b a s e、U n i s y s和X e r o x等公司組成(若要了解該集團的全部成員,請通過網址w w w. U n i c o d e . o rg查找) 。該集團負責維護U n i c o d e標準。U n i c o d e的完整描述可以參閱A d d i s o n We s l e y出版的《Unicode Standard》一書(該書可以通過網址w w w. U n i c o d e . o rg訂購) 。
? ? U n i c o d e提供了一種簡單而又一致的表示字符串的方法。U n i c o d e字符串中的所有字符都是1 6位的(兩個字節) 。它沒有專門的字節來指明下一個字節是屬于同一個字符的組成部分,還是一個新字符。這意味著你只需要對指針進行遞增或遞減,就可以遍歷字符串中的各個字符,不再需要調用C h a r N e x t、C h a r P r e v和I s D B C S L e a d B y t e之類的函數。由于U n i c o d e用一個1 6位的值來表示每個字符,因此總共可以得到65 000個字符,這樣,它就能夠對世界各國的書面文字中的所有字符進行編碼,遠遠超過了單字節字符集的2 5 6個字符的數目。
目前,已經為阿拉伯文、中文拼音、西里爾字母(俄文) 、希臘文、西伯萊文、日文、韓文和拉丁文(英文)字母定義了U n i c o d e代碼點 。這些字符集中還包含了大量的標點符號、數學符號、技術符號、箭頭、裝飾標志、區分標志和其他許多字符。如果將所有這些字母和符號加在一起,總計約達3 5 0 0 0個不同的代碼點,這樣,總計65 000多個代碼點中,大約還有一半可供將來擴充時使用。
? ? 這65 536個字符可以分成不同的區域。表2-2 顯示了這樣的區域的一部分以及分配給這些區域的字符。
? ? 目前尚未分配的代碼點大約還有29 000個,不過它們是保留供將來使用的。另外,大約有6 0 0 0個代碼點是保留供個人使用的。
2.2 為什么使用U n i c o d e
????當開發應用程序時,當然應該考慮利用 U n i c o d e的優點。即使現在你不打算對應用程序進行本地化,開發時將U n i c o d e放在心上,肯定可以簡化將來的代碼轉換工作。此外,U n i c o d e還
具備下列功能:
? 可以很容易地在不同語言之間進行數據交換。
? 使你能夠分配支持所有語言的單個二進制. e x e文件或D L L文件。
? 提高應用程序的運行效率(本章后面還要詳細介紹) 。
2.3 Windows 2000與U n i c o d e
????Windows 2000是使用U n i c o d e從頭進行開發的,用于創建窗口、顯示文本、進行字符串操作等的所有核心函數都需要U n i c o d e字符串。如果調用任何一個Wi n d o w s函數并給它傳遞一個A N S I字符串,那么系統首先要將字符串轉換成U n i c o d e,然后將U n i c o d e字符串傳遞給操作系統。如果希望函數返回A N S I字符串,系統就會首先將U n i c o d e字符串轉換成A N S I字符串,然后將結果返回給你的應用程序。所有這些轉換操作都是在你看不見的情況下發生的。當然,進行這些字符串的轉換需要占用系統的時間和內存。
????例如,如果調用C r e a t e Wi n d o w E x函數,并傳遞類名字和窗口標題文本的非U n i c o d e字符串,那么C r e a t e Wi n d o w E x必須分配內存塊(在你的進程的默認堆中) ,將非U n i c o d e字符串轉換成U n i c o d e字符串,并將結果存儲在分配到的內存塊中,然后調用U n i c o d e版本的C r e a t e Wi n d o w E x函數。
對于用字符串填入緩存的函數來說,系統必須首先將 U n i c o d e字符串轉換成非U n i c o d e字符串,然后你的應用程序才能處理該字符串。由于系統必須執行所有這些轉換操作,因此你的應用程序需要更多的內存,并且運行的速度比較慢。通過從頭開始用U n i c o d e來開發應用程序,就能夠使你的應用程序更加有效地運行。
2.4 Windows 98與U n i c o d e
? ? Windows 98不是一種全新的操作系統。它繼承了1 6位Wi n d o w s操作系統的特性,它不是用來處理U n i c o d e的。如果要增加對U n i c o d e的支持,其工作量非常大,因此在該產品的特性列表中沒有包括這個支持項目。由于這個原因,Windows 98像它的前任產品一樣,幾乎都是使用A N S I字符串來進行所有的內部操作的。
? ? 仍然可以編寫用于處理U n i c o d e字符和字符串的Wi n d o w s應用程序,不過,使用Wi n d o w s函數要難得多。例如,如果想要調用 C r e a t e Wi n d o w E x函數并將A N S I字符串傳遞給它,這個調用的速度非常快,不需要從你進程的默認堆棧中分配緩存,也不需要進行字符串轉換。但是,如果想要調用C r e a t e Wi n d o w E x函數并將U n i c o d e字符串傳遞給它,就必須明確分配緩存,并調用函數,以便執行從U n i c o d e到A N S I字符串的轉換操作。然后可以調用 C r e a t e Wi n d o w E x,傳遞A N S I字符串。當C r e a t e Wi n d o w E x函數返回時,就能釋放臨時緩存。這比使用Windows 2000上的U n i c o d e要麻煩得多。本章的后面要介紹如何在Windows 98下進行這些轉換。
? ? 雖然大多數U n i c o d e函數在Windows 98中不起任何作用,但是仍有少數U n i c o d e函數確實非常有用。這些函數是:
?
?
? ? 可惜的是,這些函數中有許多函數在Windows 98中會出現各種各樣的錯誤。有些函數無法使用某些字體,有些函數會破壞內存堆棧,有些函數會使打印機驅動程序崩潰,等等。如果要使用這些函數,必須對它們進行大量的測試。即使這樣,可能仍然無法解決問題。因此必須向用戶說明這些情況。
2.5 Windows CE與U n i c o d e
? ? Windows CE操作系統是為小型設備開發的,這些設備的內存很小,并且不帶磁盤存儲器。你可能認為,由于M i c r o s o f t公司的主要目標是建立一種盡可能小的操作系統,因此它會使用A N S I作為自己的字符集。但是M i c r o s o f t公司并非鼠目寸光,他們懂得,Windows CE的設備要在世界各地銷售,他們希望降低軟件開發成本,這樣就能更加容易地開發應用程序。為此,Windows CE本身就是使用U n i c o d e的一種操作系統。
? ? 但是,為了使Windows CE盡量做得小一些,M i c r o s o f t公司決定完全不支持ANSI Wi n d o w s函數。因此,如果要為Windows CE開發應用程序,必須懂得U n i c o d e,并且在整個應用程序中使用U n i c o d e。
2.6 需要注意的問題
? ? 下面讓我們進一步明確一下“M i c r o s o f t公司對U n i c o d e支持的情況”:
? ? ? Windows 2000既支持U n i c o d e,也支持A N S I,因此可以為任意一種開發應用程序。
? ? ? Windows 98只支持A N S I,只能為A N S I開發應用程序。
? ? ? Windows CE只支持U n i c o d e,只能為U n i c o d e開發應用程序。
? ? 雖然M i c r o s o f t公司試圖讓軟件開發人員能夠非常容易地開發在這3種平臺上運行的軟件,但是U n i c o d e與A N S I之間的差異使得事情變得困難起來,并且這種差異通常是我遇到的最大的問題之一。請不要誤解,M i c r o s o f t公司堅定地支持U n i c o d e,并且我也堅決鼓勵你使用它。不過你應該懂得,你可能遇到一些問題,需要一定的時間來解決這些問題。建議你盡可能使用U n i c o d e。如果運行Windows 98,那么只有在必要時才需轉換到A N S I。不過,還有另一個小問題你應該了解,那就是C O M。
2.7 對C O M的簡單說明
? ? 當M i c r o s o f t公司將C O M從1 6位Wi n d o w s轉換成Wi n 3 2時,公司作出了一個決定,即需要字符串的所有C O M接口方法都只能接受U n i c o d e字符串。這是個了不起的決定,因為C O M通常用于使不同的組件能夠互相進行通信,而U n i c o d e則是傳遞字符串的最佳手段。如果你為Windows 2000或Windows CE開發應用程序,并且也使用C O M,那么你將會如虎添翼。在你的整個源代碼中使用U n i c o d e,將使與操作系統進行通信和與C O M對象進行通信的操作變成一件輕而易舉的事情。
? ? 如果你為Windows 98開發應用程序,并且也使用C O M,那么將會遇到一些問題。C O M要求使用U n i c o d e字符串,而操作系統的大多數函數要求使用A N S I字符串。那是多么難辦的事情啊!我曾經從事過若干個項目的開發,在這些項目中,我編寫了許多代碼,僅僅是為了來回進行字符串的轉換。
2.8 如何編寫U n i c o d e源代碼
? ? M i c r o s o f t公司為U n i c o d e設計了Windows API,這樣,可以盡量減少對你的代碼的影響。實際上,你可以編寫單個源代碼文件,以便使用或者不使用U n i c o d e來對它進行編譯。只需要定義兩個宏(U N I C O D E和_ U N I C O D E) ,就可以修改然后重新編譯該源文件。
2.8.1 C運行期庫對U n i c o d e的支持
? ? 為了利用U n i c o d e字符串,定義了一些數據類型。標準的C頭文件S t r i n g . h已經作了修改,以便定義一個名字為w c h a r _ t的數據類型,它是一個U n i c o d e字符的數據類型:
? ? typedef unsigned short wchart_t;
? ? 例如,如果想要創建一個緩存,用于存放最多為 9 9個字符的U n i c o d e字符串和一個結尾為零的字符,可以使用下面這個語句:
? ? wchar_t szBuffer[100];
? ? 該語句創建了一個由 1 0 0個1 6位值組成的數組。當然,標準的C運行期字符串函數,如s t r c p y、s t r c h r和s t r c a t等,只能對A N S I字符串進行操作,不能正確地處理U n i c o d e字符串。因此,ANSI C也擁有一組補充函數。清單2 - 1顯示了一些標準的ANSI C字符串函數,后面是它們的等價U n i c o d e函數。
?
? ? 請注意,所有的U n i c o d e函數均以w c s開頭,w c s是寬字符串的英文縮寫。若要調用U n i c o d e函數,只需用前綴w c s來取代A N S I字符串函數的前綴s t r即可。
? ? 注意 大多數軟件開發人員可能已經不記得這樣一個非常重要的問題了,那就是
? ? M i c r o s o f t公司提供的C運行期庫與A N S I的標準C運行期庫是一致的。ANSI C規定,C運行期庫支持U n i c o d e字符和字符串。這意味著始終都可以調用C運行期函數,以便對U n i c o d e字符和字符串進行操作,即使是在Windows 98上運行,也可以調用這些函數。換句話說,w c s c a t、w c s l e n和w c s t o k等函數都能夠在Windows 98上很好地運行,這些都是必須關心的操作系統函數。
? ? 對于包含了對s t r函數或w c s函數進行顯式調用的代碼來說,無法非常容易地同時為A N S I和U n i c o d e對這些代碼進行編譯。本章前面說過,可以創建同時為A N S I和U n i c o d e進行編譯的單個源代碼文件。若要建立雙重功能,必須包含T C h a r. h文件,而不是包含S t r i n g . h文件。T C h a r. h文件的唯一作用是幫助創建A N S I / U n i c o d e通用源代碼文件。它包含你應該用在源代碼中的一組宏,而不應該直接調用s t r函數或者w c s函數。如果在編譯源代碼文件時定義了_ U N I C O D E,這些宏就會引用w c s這組函數。如果沒有定義_ U N I C O D E,那么這些宏將引用s t r這組宏。
? ? 例如,在T C h a r. h中有一個宏稱為_ t c s c p y。如果在包含該頭文件時沒有定義_ U N I C O D E ,那么_ t c s c p y就會擴展為A N S I的s t r c p y函數。但是如果定義了_UNICODE, _tcscpy將擴展為U n i c o d e的w c s c p y函數。擁有字符串參數的所有C運行期函數都在T C h a r. h文件中定義了一個通用宏。如果使用通用宏,而不是A N S I / U n i c o d e的特定函數名,就能夠順利地創建可以為 A N S I或U n i c o d e進行編譯的源代碼。
? ? 但是,除了使用這些宏之外,還有一些操作是必須進行的。 T C h a r. h文件包含了另外一些宏。若要定義一個A N S I / U n i c o d e通用的字符串數組,請使用下面的T C H A R數據類型。如果定義了_ U N I C O D E,T C H A R將聲明為下面的形式:
? ? typedef wchar_t TCHAR;
? ? 如果沒有定義_ U N I C O D E,則T C H A R將聲明為下面的形式:
? ? typedef char TCHAR;
? ? 使用該數據類型,可以像下面這樣分配一個字符串:
? ? TCHAR szString[100];
? ? 也可以創建對字符串的指針:
? ? TCHAR *szError = “Error”;
? ? 不過上面這行代碼存在一個問題。按照默認設置, M i c r o s o f t公司的C + +編譯器能夠編譯所有的字符串,就像它們是A N S I字符串,而不是U n i c o d e字符串。因此,如果沒有定義_ U N I C O D E,該編譯器將能正確地編譯這一行代碼。但是,如果定義了_ U N I C O D E,就會產生一個錯誤。若要生成一個U n i c o d e字符串而不是A N S I字符串,必須將該代碼行改寫為下面的樣子:
? ? TCHAR *szError = L”Error”;
? ? 字符串(literal string)前面的大寫字母L,用于告訴編譯器該字符串應該作為U n i c o d e字符串來編譯。當編譯器將字符串置于程序的數據部分中時,它在每個字符之間分散插入零字節。這種變更帶來的問題是,現在只有當定義了_ U N I C O D E時,程序才能成功地進行編譯。我們需要另一個宏,以便有選擇地在字符串的前面加上大寫字母L。這項工作由_ T E X T宏來完成,_ T E X T宏也在T C h a r. h文件中做了定義。如果定義了_ U N I C O D E,那么_ T E X T定義為下面的形式:
? ? #define_TEXT(x) L ## x
? ? 如果沒有定義_ U N I C O D E,_ T E X T將定義為
? ? #define _TEXT(x) x
? ? 使用該宏,可以改寫上面這行代碼,這樣,無論是否定義了 _ U N I C O D E宏,它都能夠正確地進行編譯。如下所示:
? ? TCHAR *szError = _TEXT(“Error”);
? ? _ T E X T宏也可以用于字符。例如,若要檢查一個字符串的第一個字符是否是大寫字母J,只需編寫下面的代碼即可:
?
2.8.2 Wi n d o w s定義的U n i c o d e數據類型
? ? Wi n d o w s頭文件定義了表2 - 3列出的數據類型。
? ? ?
? ? 這些數據類型是指U n i c o d e字符和字符串。Wi n d o w s頭文件也定義了A N S I / U n i c o d e通用數據類型P T S T R和P C T S T R。這些數據類型既可以指A N S I字符串,也可以指U n i c o d e字符串,這取決于當編譯程序模塊時是否定義了U N I C O D E宏。
? ? 請注意,這里的U N I C O D E宏沒有前置的下劃線。_ U N I C O D E宏用于C運行期頭文件,而U N I C O D E宏則用于Wi n d o w s頭文件。當編譯源代碼模塊時,通常必須同時定義這兩個宏。
2.8.3 Wi n d o w s中的U n i c o d e函數和A N S I函數
? ? 前面已經講過,有兩個函數稱為C r e a t e Wi n d o w E x,一個C r e a t e Wi n d o w E x接受U n i c o d e字符串,另一個C r e a t e Wi n d o w E x接受A N S I字符串。情況確實如此,不過,這兩個函數的原型實際上是下面的樣子:
?
? ? C r e a t e Wi n d o w E x W是接受U n i c o d e字符串的函數版本。函數名結尾處的大寫字母W是英文w i d e(寬)的縮寫。每個U n i c o d e字符的長度是1 6位,因此,它們常常稱為寬字符。C r e a t e Wi n d o w E x A的結尾處的大寫字母A表示該函數可以接受A N S I字符串。但是,在我們的代碼中,通常只包含了對 C r e a t e Wi n d o w E x的調用,而不是直接調用C r e a t e Wi n d o w E x W或者C r e a t e Wi n d o w E x A。在Wi n U s e r. h文件中,C r e a t e Wi n d o w E x實際上是定義為下面這種形式的一個宏:
?
? ? 當編譯源代碼模塊時, U N I C O D E 是否已經作了定義,將決定你調用的是哪個C r e a t e Wi n d o w E x版本。當轉用一個1 6位的Wi n d o w s應用程序時,你在編譯期間可能沒有定義U N I C O D E。對C r e a t e Wi n d o w E x函數的任何調用都會將該宏擴展為對C r e a t e Wi n d o w E x A的調用,即對C r e a t e Wi n d o w E x的A N S I版本的調用。由于1 6位Wi n d o w s只提供了C r e a t e Wi n d o w s E x的A N S I版本,因此可以比較容易地轉用它的應用程序。在Windows 2000下,M i c r o s o f t的C r e a t e Wi n d o w E x A源代碼只不過是一個形實替換程序層或翻譯層,用于分配內存,以便將A N S I字符串轉換成U n i c o d e字符串。該代碼然后調用C r e a t eWi n d o w E x W,并傳遞轉換后的字符串。當C r e a t e Wi n d o w E x W返回時,C r e a t e Wi n d o w E x A便釋放它的內存緩存,并將窗口句柄返回給你。
? ? 如果要創建其他軟件開發人員將要使用的動態鏈接庫( D L L) ,請考慮使用下面的方法。在D L L中提供兩個輸出函數。一個是A N S I版本,另一個是U n i c o d e版本。在A N S I版本中,只需要分配內存,執行必要的字符串轉換,并調用該函數的U n i c o d e版本(本章后面部分介紹這個進程) 。
? ? 在Windows 98下,M i c r o s o f t的C r e a t e Wi n d o w E x A源代碼是執行操作的函數。Windows 98提供了接受U n i c o d e參數的所有Wi n d o w s函數的進入點,但是這些函數并不將 U n i c o d e字符串轉換成A N S I字符串,它們只返回運行失敗的消息。調用G e t L a s t E r r o r將返回E R R O R _C A L L _ N O T _ I M P L E M E N T E D。這些函數中只有A N S I版本的函數才能正確地運行。如果編譯的代碼調用了任何寬字符函數,應用程序將無法在Windows 98下運行。Windows API中的某些函數,比如Wi n E x e c和O p e n F i l e等,只是為了實現與1 6位Wi n d o w s程序的向后兼容而存在,因此,應該避免使用。應該使用對C r e a t e P r o c e s s和C r e a t e F i l e函數的調用來取代對Wi n E x e c和O p e n F i l e函數的調用。從系統內部來講,老的函數完全可以調用新的函數。
? ? ?老的函數存在的一個大問題是,它們不接受 U n i c o d e字符串。當調用這些函數時,必須傳遞A N S I字符串。另一方面,所有新的和未過時的函數在Windows 2000中都同時擁有A N S I和U n i c o d e兩個版本。
2.8.4 Wi n d o w s字符串函數
????Wi n d o w s還提供了一組范圍很廣的字符串操作函數。這些函數與C運行期字符串函數(如s t r c p y和w c s c p y)很相似。但是該操作系統函數是操作系統的一個組成部分,操作系統的許多組件都使用這些函數,而不使用C運行期庫。建議最好使用操作系統函數,而不要使用C運行期字符串函數。這將有助于稍稍提高你的應用程序的運行性能,因為操作系統字符串函數常常被大型應用程序比如操作系統的外殼進程E x p l o r e r. e x e所使用。由于這些函數使用得很多,因此,在你的應用程序運行時,它們可能已經被裝入R A M。
????若要使用這些函數,系統必須運行 Windows 2000或Windows 98。如果安裝了I n t e r n e t Explorer 4.0或更新的版本,也可以在較早的Wi n d o w s版本中獲得這些函數。
在經典的操作系統函數樣式中,操作系統字符串函數名既包含大寫字母,也包含小寫字母,它的形式類似這個樣子:S t r C a t、S t r C h r、S t r C m p和S t r C p y等。若要使用這些函數,必須加上S h l WA p i . h頭文件。另外,如前所述,這些字符串函數既有A N S I版本,也有U n i c o d e版本,例如S t r C a t A和S t r C a t W。由于這些函數屬于操作系統函數,因此,當創建應用程序時,如果定義了U N I C O D E(不帶前置下劃線) ,那么它們的符號將擴展為寬字符版本。
2.9 成為符合A N S I和U n i c o d e的應用程序
? ? 即使你不打算立即使用U n i c o d e,最好也應該著手將你的應用程序轉換成符合U n i c o d e的應用程序。下面是應該遵循的一些基本原則:
? ? ? 將文本串視為字符數組,而不是c h a r s數組或字節數組。
? ? ? 將通用數據類型(如T C H A R和P T S T R)用于文本字符和字符串。
? ? ? 將顯式數據類型(如B Y T E和P B Y T E)用于字節、字節指針和數據緩存。
? ? ? 將T E X T宏用于原義字符和字符串。
? ? ? 執行全局性替換(例如用P T S T R替換P S T R) 。
? ? ? 修改字符串運算問題。例如函數通常希望你在字符中傳遞一個緩存的大小,而不是字節。
? ? 這意味著你不應該傳遞 s i z e o f ( s z B u ff e r ) ,而應該傳遞(s i z e o f ( s z B u ff e r ) / s i z e o f ( T C H A R )。另外,如果需要為字符串分配一個內存塊,并且擁有該字符串中的字符數目,那么請記住要按字節來分配內存。這就是說,應該調用malloc(nCharacters *sizeof(TCHAR)),而不是調用m a l l o c( n C h a r a c t e r s )。在上面所說的所有原則中,這是最難記住的一條原則,如果操作錯誤,編譯器將不發出任何警告。
? ? 當我為本書的第一版編寫示例程序時,我編寫的原始程序只能編譯為 A N S I程序。后來,當我開始撰寫本章的內容時,我想我應該鼓勵使用 U n i c o d e,并且打算創建一些示例程序,以便展示你可以非常容易地編寫既可以用U n i c o d e也可以用A N S I來編譯的程序。這時我發現最好的辦法是將本書的所有示例程序進行轉換,使它們都能夠用U n i c o d e和A N S I進行編譯。我用了大約4個小時將所有程序進行了轉換。考慮到我以前從來沒有這方面的轉換經驗,這個速度是相當不錯了。
2.9.1 Wi n d o w s字符串函數
? ? Wi n d o w s也提供了一組用于對U n i c o d e字符串進行操作的函數,表2 - 4對它們進行了描述。
?
? ? 這些函數是作為宏來實現的,這些宏既可以調用函數的 U n i c o d e版本,也可以調用函數的A N S I版本,這要根據編譯源代碼模塊時是否已經定義了U N I C O D E而定。例如,如果沒有定義U N I C O D E,l s t r c a t函數將擴展為l s t r c a t A。如果定義了U N I C O D E,l s t r c a t將擴展為l s t r c a t W。
? ? 有兩個字符串函數,即l s t r c m p和l s t r c m p i,它們的行為特性與等價的C運行期函數是不同的。C運行期函數s t r c m p、s t r c m p i、w c s c m p和w c s c m p i只是對字符串中的代碼點的值進行比較,這就是說,這些函數將忽略實際字符的含義,只是將第一個字符串中的每個字符的數值與第二個字符串中的字符的數值進行比較。而Wi n d o w s函數l s t r c m p和l s t r c m p i是作為對Wi n d o w s函數C o m p a r e S t r i n g的調用來實現的。
?
? ? 該函數對兩個 U n i c o d e字符串進行比較。C o m p a r e S t r i n g的第一個參數用于設定語言I D(L C I D) ,這是個3 2位值,用于標識一種特定的語言。C o m p a r e S t r i n g使用這個L C I D來比較這兩個字符串,方法是對照一種特定的語言來查看它們的字符的含義。這種操作方法比C運行期函數簡單地進行數值比較更有意義。
? ? 當l s t r c m p函數系列中的任何一個函數調用C o m p a r e S t r i n g時,該函數便將調用Wi n d o w s的G e t T h r e a d S t r i n g函數的結果作為第一個參數來傳遞:
? ? LCID GetThreadLocale();
? ? 每次創建一個線程時,它就被賦予一種語言。函數將返回該線程的當前語言設置。
? ? C o m p a r e S t r i n g的第二個參數用于標識一些標志,這些標志用來修改該函數比較兩個字符串時所用的方法。表2 - 5顯示了可以使用的標志。
?
? ? 當l s t r c m p調用C o m p a r e S t r i n g時,它傳遞0作為f d w S t y l e的參數。但是,當l s t r c m p i調用C o m p a r e S t r i n g時,它就傳遞N O R M _ I G N O R E C A S E。C o m p a r e S t r i n g的其余4個參數用于設定兩個字符串和它們各自的長度。如果為c c h 1參數傳遞- 1,那么該函數將認為p S t r i n g 1字符串是以0結尾,并計算該字符串的長度。對于p S t r i n g 2字符串來說,參數c c h 2的作用也是一樣。
? ? 其他C運行期函數沒有為U n i c o d e字符串的操作提供很好的支持。例如,t o l o w e r和t o u p p e r函數無法正確地轉換帶有重音符號的字符。為了彌補C運行期庫中的這些不足,必須調用下面這些Wi n d o w s函數,以便轉換U n i c o d e字符串的大小寫字母。這些函數也可以正確地用于A N S I字符串。
? ? 頭兩個函數:
? ? PTSTR CharLower(PTSTR pzsString);
和
? ? PTSTR CharUpper(PTSTR pszString);
? ? 既可以轉換單個字符,也可以轉換以 0結尾的整個字符串。若要轉換整個字符串,只需要傳遞字符串的地址即可。若要轉換單個字符,必須像下面這樣傳遞各個字符:
? ? TCHAR cLowerCaseChar = CharLower((PTSTR) szString[0]);
? ? 將單個字符轉換成一個P T S T R,便可調用該函數,將一個值傳遞給它,在這個值中,較低的1 6位包含了該字符,較高的1 6位包含0。當該函數看到較高位是0時,該函數就知道你想要轉換單個字符,而不是整個字符串。返回的值是個3 2位值,較低的1 6位中是已經轉換的字符。下面兩個函數與前面兩個函數很相似,差別在于它們用于轉換緩存中包含的字符(該緩存不必以0結尾) :
?
? ? 其他的C運行期函數,如i s a l p h a、i s l o w e r和i s u p p e r,返回一個值,指明某個字符是字母字符、小寫字母還是大寫字母。Windows API提供了一些函數,也能返回這些信息,但是Wi n d o w s函數也要考慮用戶在控制面板中指定的語言:
?
????p r i n t f函數家族是要介紹的最后一組C運行期函數。如果在定義了_ U N I C O D E的情況下編譯你的源代碼模塊,那么p r i n t f函數家族便希望所有字符和字符串參數代表U n i c o d e字符和字符串。但是,如果在沒有定義_ U N I C O D E的情況下編譯你的源代碼模塊,p r i n t f函數家族便希望傳遞給它的所有字符和字符串都是A N S I字符和字符串。
? ? M i c r o s o f t公司已經給C運行期的p r i n t f函數家族增加了一些特殊的域類型。其中有些域類型尚未被ANSI C采用。 新類型使你能夠很容易地對A N S I和U n i c o d e字符和字符串進行混合和匹配。操作系統的w s p r i n t f函數也得到了增強。下面是一些例子(請注意大寫S和小寫s的使用) :
?
2.9.2 資源
? ? 當資源編譯器對你的所有資源進行編譯時,輸出文件是資源的二進制文件。資源(字符串表、對話框模板和菜單等)中的字符串值總是寫作U n i c o d e字符串。在Windows 98和Wi n d o w s2 0 0 0下,如果應用程序沒有定義U N I C O D E宏,那么系統就會進行內部轉換。
? ? 例如,如果在編譯源代碼模塊時沒有定義 U N I C O D E,調用L o a d S t r i n g實際上就是調用L o a d S t r i n g A函數。這時L o a d S t r i n g A就從你的資源中讀取字符串,并將該字符串轉換成A N S I字符串。A N S I形式的字符串將從該函數返回給你的應用程序。
2.9.3 確定文本是A N S I文本還是U n i c o d e文本
? ? 到現在為止,U n i c o d e文本文件仍然非常少。實際上,M i c r o s o f t公司自己的大多數產品并沒有配備任何U n i c o d e文本文件。但是預計將來這種情況是會改變的(盡管這需要一個很長的過程) 。當然,Windows 2000的N o t e p a d (記事本)應用程序允許你既能打開U n i c o d e文件,也能打開A N S I文件,并且可以創建這些文件。圖2 - 1顯示了N o t e p a d的Save As(文件另存為)對話框。請注意可以用不同的方法來保存文本文件。
?
? ? 對于許多用來打開文本文件和處理這些文件的應用程序(如編譯器)來說,打開一個文件后,應用程序就能方便地確定該文本文件是包含A N S I字符還是U n i c o d e字符。I s Te x t U n i c o d e函數能夠幫助進行這種區分:
? ? DWORD IsTextUnicode(CONST PVOID pvBuffer ,int cb ,PINT pResult);
? ? 文本文件存在的問題是,它們的內容沒有嚴格和明確的規則,因此很難確定該文件是包含A N S I字符還是U n i c o d e字符。I s Te x t U n i c o d e使用一系列統計方法和定性方法,以便猜測緩存的
? ? 內容。由于這不是一種確切的科學方法,因此I s Te x t U n i c o d e有可能返回不正確的結果。
? ? 第一個參數p v B u ff e r用于標識要測試的緩存的地址。該數據是個無效指針,因為你不知道你擁有的是A N S I字符數組還是U n i c o d e字符數組。
? ? 第二個參數c b用于設定p v B u ff e r指向的字節數。同樣,由于你不知道緩存中放的是什么,因此c b是個字節數,而不是字符數。請注意,不必設定緩存的整個長度。當然,I s Te x t U n i c o d e
能夠測試的字節越多,得到的結果越準確。
? ? 第三個參數p R e s u l t是個整數的地址,必須在調用I s Te x t U n i c o d e之前對它進行初始化。對該
整數進行初始化后,就可以指明你要I s Te x t U n i c o d e執行哪些測試。也可以為該參數傳遞N U L L,
在這種情況下,I s Te x t U n i c o d e將執行它能夠進行的所有測試(詳細說明請參見Platform SDK文檔) 。
? ? 如果I s Te x t U n i c o d e認為緩存包含U n i c o d e文本,便返回T R U E,否則返回FA L S E。確實是這樣,盡管M i c r o s o f t將該函數的原型規定為返回D W O R D,但是它實際上返回一個布爾值。如果在p R e s u l t參數指向的整數中必須進行特定的測試,該函數就會在返回之前設定整數中的信息位,以反映每個測試的結果。
? ? Wi n d o w s 9 8 在Windows 98下,I s Te x t U n i c o d e函數沒有有用的實現代碼,它只是返回FA L S E。調用G e t L a s t E r r o r函數將返回E R R O R _ C A L L _ N O T _ I M P L E M E N T D。
第1 7章中的Flie Rev示例應用程序演示了I s TextUnicode函數的使用。
2.9.4 在U n i c o d e與A N S I之間轉換字符串
? ? Wi n d o w s函數M u l t i B y t e To Wi d e C h a r用于將多字節字符串轉換成寬字符串。下面顯示了M u l t i B y t e To Wi d e C h a r函數。
?
? ? u C o d e P a g e參數用于標識一個與多字節字符串相關的代碼頁號。d w F l a g s參數用于設定另一個控件,它可以用重音符號之類的區分標記來影響字符。這些標志通常并不使用,在d w F l a g s參數中傳遞0。p M u l t i B y t e S t r參數用于設定要轉換的字符串,c c h M u l t i B y t e參數用于指明該字符串的長度(按字節計算) 。如果為c c h M u l t i B y t e參數傳遞- 1,那么該函數用于確定源字符串的長度。
? ? 轉換后產生的U n i c o d e版本字符串將被寫入內存中的緩存,其地址由p Wi d e C h a r S t r參數指定。必須在c c h Wi d e C h a r參數中設定該緩存的最大值(以字符為計量單位) 。如果調用M u l t i B y t e To Wi d e C h a r,給c c h Wi d e C h a r參數傳遞0,那么該參數將不執行字符串的轉換,而是返回為使轉換取得成功所需要的緩存的值。一般來說,可以通過下列步驟將多字節字符串轉換成U n i c o d e等價字符串:
? ? 1) 調用M u l t i B y t e To Wi d e C h a r函數,為p Wi d e C h a r S t r參數傳遞N U L L,為c c h Wi d e C h a r參數傳遞0。
? ? 2) 分配足夠的內存塊,用于存放轉換后的U n i c o d e字符串。該內存塊的大小由前面對M u l t B y t e To Wi d e C h a r的調用返回。
? ? 3) 再次調用M u l t i B y t e To Wi d e C h a r,這次將緩存的地址作為p Wi d e C h a r S t r參數來傳遞,并傳遞第一次調用M u l t i B y t e To Wi d e C h a r時返回的緩存大小,作為c c h Wi d e c h a r參數。
? ? 4)?使用轉換后的字符串。
? ? 5) 釋放U n i c o d e字符串占用的內存塊。函數Wi d e C h a r To M u l t i B y t e將寬字符串轉換成等價的多字節字符串,如下所示:
?
? ? 該函數與M u l t i B i t e To Wi d e C h a r函數相似。同樣,u C o d e P a g e參數用于標識與新轉換的字符串相關的代碼頁。d w F l a g s則設定用于轉換的其他控件。這些標志能夠作用于帶有區分符號的字符和系統不能轉換的字符。通常不需要為字符串的轉換而擁有這種程度的控制手段,你將為d w F l a g s參數傳遞0。
? ? p Wi d e C h a r S t r參數用于設定要轉換的字符串的內存地址,c c h Wi d e C h a r參數用于指明該字符串的長度(用字符數來計量) 。如果你為c c h Wi d e C h a r參數傳遞- 1,那么該函數用于確定源字符串的長度。
? ? 轉換產生的多字節版本的字符串被寫入由p M u l t i B y t e S t r參數指明的緩存。必須在c c h M u l t i B y t e參數中設定該緩存的最大值(用字節來計量) 。如果傳遞0作為Wi d e C h a r To M u l t i B y t e函數的c c h M u l t i B y t e參數,那么該函數將返回目標緩存需要的大小值。通常可以使用將多字節字符串轉換成寬字節字符串時介紹的一系列類似的事件,將寬字節字符串轉換成多字節字符串。
? ? 你會發現,Wi d e C h a r To M u l t i B y t e函數接受的參數比M u l t i B y t e To Wi d e C h a r函數要多2個,即p D e f a u l t C h a r和p f U s e d D e f a u l t C h a r。只有當Wi d e C h a r To M u l t i B y t e函數遇到一個寬字節字符,而該字符在u C o d e P a g e參數標識的代碼頁中并沒有它的表示法時,Wi d e C h a r To M u l t i B y t e函數才使用這兩個參數。如果寬字節字符不能被轉換,該函數便使用p D e f a u l t C h a r參數指向的字符。如果該參數是N U L L(這是大多數情況下的參數值) ,那么該函數使用系統的默認字符。該默認字符通常是個問號。這對于文件名來說是危險的,因為問號是個通配符。
? ? p f U s e d D e f a u l t C h a r參數指向一個布爾變量,如果寬字符串中至少有一個字符不能轉換成等價多字節字符,那么函數就將該變量置為T R U E。如果所有字符均被成功地轉換,那么該函數就將該變量置為FA L S E。當函數返回以便檢查寬字節字符串是否被成功地轉換后,可以測試該變量。同樣,通常為該測試傳遞N U L L。
? ? 關于如何使用這些函數的詳細說明,請參見Platform SDK文檔。
? ? 如果使用這兩個函數,就可以很容易創建這些函數的 U n i c o d e版本和A N S I版本。例如,你可能有一個動態鏈接庫,它包含一個函數,能夠轉換字符串中的所有字符。可以像下面這樣編寫該函數的U n i c o d e版本:
?
ASCII版本
?
然后是提供一個對外接口:
?
總結
以上是生活随笔為你收集整理的windows核心编程-第二章 Unicode的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Intel汇编语言程序设计学习-第五章
- 下一篇: Windows核心编程 第三章 内核对象