四、关于字符串、字符集那些事
和现代Windows操作系统一样,COM也使用
UTF-16 LE字符集的16位
wchar_t字符串(文档中称之为
LPCOLESTR或
LPCWSTR)。C/C++对wchar_t字符串也有支持,比如C的wcslen、wcscpy等函数、C++的wstring字符串对象等,WinAPI也有lstrlenW、lstrcpyW等函数可以处理它们。
由于字符串都是基于wchar_t的,因此COM和WinAPI之间可以正常交换字符串。所以在Win32程序中使用COM并不需要进行转码。但是对于C/C++的控制台输入输出函数(如scanf、printf)来说,使用wchar_t却容易出问题。由于它们是基于char的,wchar_t需要在内部转换成char才能正常输出。输出不正常的主要原因是VC运行库默认总是使用"C"这种区域设定(即将Unicode的00-FF段直接对应到单字节,大于FF的直接丢弃)进行转码。解决这个问题通常有三个方法:
第一个方法是使用
setlocale(LC_ALL, ".OCP");和
locale::global(locale(".OCP"));重置一下区域设定。但是C语言中对于宽字符的输入输出,需要使用特殊的函数,其中许多带有下划线(如_getws_s),不太美观。并且不支持UTF-8(setlocale(LC_ALL, ".65001")返回NULL)。
第二个方法是不使用VC运行时的转码机制,改用WinAPI转码。Windows有两个WinAPI可以实现wchar_t和char之间的转码:MultiByteToWideChar和WideCharToMultiByte。它们不受VC运行库的限制,但是使用较为麻烦,这里不考虑。
第三个方法是使用ATL为我们定义了两个伪函数
CW2A和
CA2W。它们其实是转码中间层,我们调用的是它的构造函数。由于它们实际上也是调用的WinAPI转码,因此同样不用受VC运行库的限制。CW2A的作用是将wchar_t字符串(Wide)转换为临时char字符串(ANSI),而CA2W则是将char字符串(ANSI)转换为临时wchar_t字符串(Wide)。使用它们要包含
atlbase.h。
CW2A和CA2W的使用方法很简单:
1.将char字符串传入CA2W,可得到临时wchar_t字符串(用于COM调用)
wcscpy(my_wide_string, CA2W("我是普通char字符串")); // 使用C式wchar_t[]字符串保存
wstring my_wide_string2 = CA2W("我是普通char字符串"); // 使用C++式wstring字符串保存
wstring my_wide_string3 = CA2W("我是普通char字符串", 950); // 使用C++式wstring字符串保存,char使用Big5字符集(950)
hr = pISpVoice->Speak(CA2W("我是普通char字符串"), SPF_DEFAULT, NULL); // 在COM中直接使用
2.将wchar_t字符串传入CW2A,可得到临时char字符串(用于C/C++的输入输出)
strcpy(my_ansi_string, CW2A(L"我是wchar_t字符串")); // 使用C式char[]字符串保存
string my_ansi_string2 = CW2A(L"我是wchar_t字符串"); // 使用C++式string字符串保存
string my_ansi_string3 = CW2A(L"我是wchar_t字符串", 65001); // 使用C++式string字符串保存,char使用UTF-8字符集(65001)
printf("字符串:%s\n", (char*)CW2A(L"我是wchar_t字符串", 1)); // 输出字符串,OEM代码页,这里要手动转换为(char *)
CW2A和CA2W有个可选参数,可指定代码页,其中有一些系统指定的特殊代码页:
0 当前ANSI代码页CP_ACP(
记事本编码)(简体中文936,繁体中文950,英文1252)(默认值)
1 当前OEM代码页CP_OEMCP(
命令提示符编码)(简体中文936,繁体中文950,英文437)
65000 UTF-7代码页CP_UTF7(不常用)
65001 UTF-8代码页CP_UTF8(
常用国际编码)
有些文章可能提到可以用wchar_t *或char *变量来接收传过来的字符串,这是错误的。C++中通过直接调用构造函数创建的对象,
生命期仅限本条语句。CA2W和CW2A对于128个字符以内的字符串是分配在栈上的,即使析构了字符串有时还可以从栈中读出。但是对于特别长的字符串则会分配在堆上,用char *或wchar_t *接收的只是地址而已,CW2A或CA2W的析构函数已经调用,为字符串所分配的空间已释放,再访问它肯定会崩溃。
在这里我们实现一个可以交互式阅读文本的程序,在命令提示符(OEM代码页)输入语句,即可让Windows的语音引擎为我们读出来。
首先我们要使用gets_s读入字符串(使用起来比gets差不多,但能正确判断缓冲区大小):
char strinput[200] = "";
gets_s(strinput); // 从控制台读入字符串
然后我们将pISpVoice->Speak第一个参数L"电脑在说话!"替换为CA2W(strinput, 1)即可:
hr = pISpVoice->Speak(CA2W(strinput, 1), SPF_DEFAULT, NULL);
ValidateHR(hr);
为了能够重复输入,我们将上述两个过程套进while(true)循环里,并判断是否输入了exit,若有则退出:
if (strcmpi(strinput, "exit") == 0) break; // 如果输入了exit则退出
最终代码如下。现在,你可以让系统读出你输入的任意字符串了。[s::lol]
<code class="lang-cpp">#include <stdio.h>
#include <stdlib.h>
#include <windows.h> // 使用COM组件需要包含windows.h
#include <sapi.h> // 使用ISpVoice语音组件需要包含sapi.h
#include <atlbase.h> // 使用CComPtr<t>和CA2W需要包含atlbase.h
#define ValidateHR(hr) \
if (FAILED(hr)) { \
printf("HRESULT错误:%p,在%s第%d行\n", hr, __FILE__, __LINE__); \
exit(1); \
}
int main()
{
HRESULT hr = CoInitialize(0); // 初始化COM环境,参数保留,必须为0
ValidateHR(hr);
{
// 建立ISpVoice接口实例(组件ID为CLSID_SpVoice)
CComPtr<ispvoice> pISpVoice;
hr = pISpVoice.CoCreateInstance(CLSID_SpVoice); // 省略了后两个参数:NULL, CLSCTX_ALL
ValidateHR(hr);
while (true)
{
char strinput[200] = "";
gets_s(strinput); // 从控制台读入字符串(OEM代码页)
if (strcmpi(strinput, "exit") == 0) // 如果输入了exit则退出
break;
// 调用ISpVoice的Speak成员函数(使用CA2W转码)
hr = pISpVoice->Speak(CA2W(strinput, 1), SPF_DEFAULT, NULL);
ValidateHR(hr);
}
} // pISpVoice在作用域边界会自动释放
CoUninitialize(); // 卸载COM环境
return 0;
}</ispvoice></t></atlbase.h></sapi.h></windows.h></stdlib.h></stdio.h></code>