• 文章
  • WinAPI:擁抱 Unicode
釋出者:
2009年11月28日 (最後更新:2009年11月28日)

WinAPI:擁抱 Unicode

評分:4.2/5 (75票)
*****
第0節)引言
本文旨在介紹如何在WinAPI中擁抱Unicode。

我通常不鼓勵直接使用WinAPI進行程式設計,因為使用跨平臺的小部件庫,如wxWidgets、QT或任何其他庫通常會更好。但許多人仍然喜歡直接使用WinAPI……所以至少我應該引導他們走向正確的方向。此外,這裡的許多內容也適用於wx(可能也適用於QT,儘管我從未使用過QT,所以不能確定)。

我在格式化和校對這篇文章上沒有花太多功夫。所以對此表示歉意。儘管我的思路有些混亂,但我仍然認為這篇文章很好地傳達了我的想法。

Unicode萬歲!傳播愛!

第1節)UNICODE宏
UNICODE宏(以及/或者 _UNICODE宏——通常兩者都會用到)散佈在整個WinAPI中。它會重新定義一些型別和函式,使其使用char*字串(如果未定義)或wchar_t* Unicode字串(如果已定義)。

如果您使用MSVS,當您將專案設定更改為Unicode程式時,這些宏通常會在編譯器開始編譯之前自動定義。否則,您可以在包含Windows.h之前手動定義它們。

1
2
3
4
5
6
7
8
9
#ifndef UNICODE
#define UNICODE
#endif

#ifndef _UNICODE
#define _UNICODE
#endif

#include <Windows.h> 


您不需要#define其中任何一個就可以在程式中使用Unicode。它只是改變了一些型別,以便更容易地使用WinAPI的Unicode部分。

在本文的後續部分,“Unicode構建”是指定義了UNICODE和_UNICODE,而“ANSI構建”是指兩者都未定義。

第2節)LPSTR, LPCSTR, LPTSTR, LPCTSTR, LPWTFISALLTHIS
任何看過WinAPI的人可能都見過上述型別……但它們究竟是什麼?

不熟悉C/C++的程式設計師可能會認為它們是字串,就像std::string一樣。從文件和示例來看,確實可以這樣理解。而且由於WinAPI頁面似乎從不確切告訴您它們是什麼,所以這是一個合理的推論。

然而,事實並非如此。以上所有都是#define不同型別的*宏*。


現在您可能會看到“LPCTSTR”中的“STR”,但其餘的可能看起來像毫無意義的隨機字母組合。請放心,這其中是有規律可循的。

- 開頭的“LP”代表“Long Pointer”(長指標)。不深入探討長指標是什麼(或者說它過去是什麼,在現代計算中它的意義已不大),我們只能說這基本上是一個指標。這意味著LP告訴您,這種型別本身不是字串,而是指向字串(或者說C風格字串)的*指標*。

- “C”表示字串是常量

- “W”表示字串是寬字元(Unicode)

- “T”表示字串是TCHAR(見下文關於TCHAR的部分)

所以實際上,#defines如下:

1
2
3
4
5
6
7
8
#define  LPSTR          char*
#define  LPCSTR         const char*

#define  LPWSTR         wchar_t*
#define  LPWCSTR        const wchar_t*

#define  LPTSTR         TCHAR*
#define  LPCTSTR        const TCHAR* 


第3節)TCHAR, _T(), T(), TEXT()
TCHAR被#define為char或wchar_t,具體取決於是否定義了UNICODE宏。

透過正確使用TCHAR,您可以建立程式的ANSI和Unicode版本。您所要做的就是#define UNICODE如果您想要Unicode版本,或者不定義它如果您想要ANSI版本。

但這帶來了一個小問題。C++中的字串字面量可以有兩種形式,char或wchar_t。

1
2
const char*    a = "Foo";
const wchar_t* b = L"Bar";  // <-- note the L.  That makes it wide. 


編譯器不會自動檢測……所以像這樣的程式碼會產生編譯器錯誤:
1
2
const char*    a = L"Foo"; // <-- error, can't point char* to a wide string
const wchar_t* b = "Bar";  // <-- error, can't point wchar_t* to a non-wide string 


那麼這個呢?
 
const TCHAR*   c = "Foo";


請記住,TCHAR是char或wchar_t,具體取決於Unicode。所以上面的程式碼*只有在*您沒有構建Unicode時才會起作用。如果您正在構建Unicode,您會收到錯誤。

同樣,以下程式碼*除非*您正在構建Unicode,否則將不起作用:
 
const TCHAR*   c = L"Foo";


為了解決這個問題……WinAPI提供了一些其他宏,_T(), T(), 和 TEXT(),它們的作用相同。在Unicode構建中,它們會在字串字面量前加上L,使其成為寬字串,而在非Unicode構建中,它們什麼也不做。因此,它們將始終與TCHAR配合使用。

 
const TCHAR*   d = _T("foo");  // works in both Unicode and ANSI builds 



第4節)函式和結構名稱別名
許多Windows函式需要字串作為引數。但由於char和wchar_t字串是兩種截然不同的型別,所以同一個函式不能同時用於兩者。

以WinAPI函式“DeleteFile”為例,它接受一個引數。假設您想刪除“myfile.txt”。

 
DeleteFile( _T("myfile.txt") );  // notice _T because DeleteFile takes a LPC<b>T</b>STR 


這裡的訣竅是DeleteFile函式實際上並不存在!實際上有兩個不同的函式:

1
2
DeleteFileA( LPCSTR );  // ANSI version, taking a LPCSTR
DeleteFileW( LPCWSTR ); // Unicode version, taking LPCWSTR 


DeleteFile實際上是一個*宏*,根據是否為Unicode構建,它被定義為DeleteFileA或DeleteFileW。

因此……對於接受C風格字串的WinAPI函式……從某種意義上說,有3個不同的版本,每個版本接受不同型別的C字串:

1
2
3
DeleteFile   <-  Takes a TCHAR string (LPCTSTR)
DeleteFileA  <-  Takes a char string (LPCSTR)
DeleteFileW  <-  Takes a wchar_t string (LPCWSTR)


這幾乎適用於所有接受C字串作為引數的WinAPI函式。


但這還沒完!還有一些結構體也包含字串。例如,OPENFILENAME結構體包含各種C字串,用於檔案開啟對話方塊。正如您所料,該結構體也有3個版本:

1
2
3
OPENFILENAME  <-  has TCHAR strings
OPENFILENAMEA <-  has char strings
OPENFILENAMEW <-  has wchar_t strings


同樣……請注意,OPENFILENAME實際上*並不*存在,它只是根據構建情況被#define為其他兩個之一。

第5節)擁抱Unicode
那麼,在WinAPI中擁抱Unicode需要什麼?

對於大多數程式……不需要太多。只需遵循以下幾點即可:

-) 對於字元和C字串,使用TCHAR而不是char。
-) 使用std::basic_string<TCHAR>而不是std::string。您甚至可以typedef自己的tstring型別。
typedef std::basic_string<TCHAR> tstring; -) 不要使用std::string,因為它是一個char字串。
-) 將所有字串字面量放在_T()宏中。除非您正在處理WinAPI以外的庫。例如,標準庫函式如fstream的建構函式接受char*字串——所以不要將這些字串放在_T()宏中。實際上,如果您使用WinAPI,就不應該使用標準庫檔案I/O,因為標準庫不相容Unicode。
-) 不要使用標準庫C字串函式,如strcpy、strcat、sprintf等。這些函式都處理char——它們不處理wchar_t或TCHAR。或者,您可以使用“tstring”成員函式,以及Windows特定的TCHAR函式,如_tcscpy、_tcscat等。
-) *永遠不要* C風格地將C字串從一種型別轉換為另一種型別。C風格的轉換會掩蓋非常重要的編譯器錯誤。也請避免C++風格的轉換。基本上,如果您遇到字串型別錯誤——那是因為您做錯了。不要試圖透過轉換來解決問題。
-) 經常在ANSI構建和Unicode構建之間切換,以確保您的程式在這兩種模式下都能編譯。如果這樣做太麻煩,那就一直使用Unicode構建,而忽略ANSI構建。


對於您進行大量文字操作的其他程式,情況會更復雜一些……

-) 在讀寫檔案文字時要小心。不要為此使用TCHAR,因為它的size是可變的。如果您從檔案中讀取8位字元,請使用char;如果您讀取16位字元,請使用wchar_t。

-) 理想情況下,如果文字要寫入輸出檔案,您應該使用Unicode編碼,如UTF-8或UTF-16。但這超出了本文的範圍(或許以後會講到!)。

-) 如果您需要直接使用char或wchar_t(例如上述情況),請務必注意如何將這些字串移動到TCHAR字串。您通常需要逐個字元地複製字串,或者編寫自己的字串複製函式來完成。我不認為WinAPI有任何函式可以幫助處理這種情況,而且我知道標準庫也沒有。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// this function copies a char C string to a TCHAR C string:
void ustrcpy(TCHAR* dst, const char* src)
{
  while(*src)
  {
   *dst = *src;
   ++dst;
   ++src;
  }
  *dst = *src;
}

//---------
//  then, say you need to read a string from a file and put it in a text box with SetWindowText:

char str[500] = {0};            // note I'm using char because I specifically want 8-bit characers
ifstream myfile("myfile.txt");  // note no _T() macro because I'm dealing with std lib
                                //  ideally you'd open the file with WinAPI's CreateFile and read
                                //  that way because that is Unicode friendly.  However I'm trying
                                //  to keep this example simple
myfile >> str;      // read the string

TCHAR buffer[500];  // need to copy to a TCHAR buffer in order to give it to SetWindowText
ustrcpy( buffer, str );

// give it to WinAPI
SetWindowText( hMyTextBox, buffer );


更好的方法是為ustrcpy和類似函式建立模板函式,以便您可以與各種不同型別和大小的字串進行轉換。

1
2
3
4
5
template <typename T, typename TT>
void ustrcpy( T* dst, const TT* src )
{
  //.. same as above
}


或者……您可以避免使用WinAPI函式的TCHAR版本,而直接使用ANSI版本。這樣,Windows就會負責轉換。

1
2
3
4
5
6
char str[500] = {0};
myfile >> str;

 // note here we specifically call SetWindowTextA, not SetWindowText.
 // this is because we're giving a char string and not a TCHAR string.
SetWindowTextA( hMyTextBox, str );


還有更多內容嗎? ???