• 文章
  • 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 );


还有更多内容吗? ???