第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
|
那麼這個呢?
請記住,TCHAR是char或wchar_t,具體取決於Unicode。所以上面的程式碼*只有在*您沒有構建Unicode時才會起作用。如果您正在構建Unicode,您會收到錯誤。
同樣,以下程式碼*除非*您正在構建Unicode,否則將不起作用:
為了解決這個問題……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 );
|
還有更多內容嗎? ???