第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 );
|
还有更多内容吗? ???