轻松从字符串表加载和格式化字符串






4.78/5 (33投票s)
两个类,可帮助您轻松地从字符串表中加载(和格式化)消息。
摘要
在准备应用程序以支持本地化时,最繁琐的步骤可能包括从源代码中提取字符串文字,并添加从资源字符串表加载这些字符串的代码。
添加提取字符串的代码并不困难。但由于它在应用程序中需要执行数百次,我们需要尽可能地简化代码。这可以节省我们数小时的枯燥工作。此外,生成的代码应尽可能易读。这为我们提供了使用最简单代码的第二个好理由。
本文介绍了两个虽小但非常有用的类,它们正是做到了这一点:以尽可能少且非侵入性的调用代码从字符串表中加载字符串文字。
它还涵盖了与本地化要求相关的几个字符串格式化注意事项。
引言
我为什么要使用字符串表?
在准备应用程序的本地化和翻译时,所有可翻译项(例如字符串)都必须存储在资源中而不是源代码中,以使翻译人员远离您的源代码。像 appTranslator 这样的本地化工具可以轻松操作您的应用程序资源,但您不希望它们玩弄您的源代码!
因此,所有需要翻译的字符串都必须存储在资源中。字符串表是适当的位置。
SetWindowText(_T("Weather Forecast")); // We must move this string to // the resource string table
注意:您可能希望将字符串存储在消息表中,它是一种替代资源,看起来很像字符串表。尽管最初设计用于更好的本地化支持,但它使用得较少。此外,Visual Studio 不为消息表提供自定义编辑器,使其吸引力大大降低。Daniel Lohmann 写了一篇关于消息表的精彩文章。
LoadString()
API:不够直接
以编程方式从字符串表中提取字符串涉及调用 LoadString()
API。使用原始 API 非常繁琐和无聊。由于大多数严肃的应用程序包含数百或数千个字符串文字,因此绝对需要一个尽可能易于使用和直接的包装器。
注意:此外,LoadString()
API 存在一个设计限制:它无法为您提供要加载字符串的大小,这使得正确分配缓冲区以加载字符串变得困难。
CString
在这方面很有帮助:它提供了自己的 LoadString()
包装器,在某种程度上简化了过程。下面的示例代码从字符串表中加载一个字符串,并使用它来设置当前窗口文本
CString strMyString; strMyString.LoadString(IDS_MYSTRING); // wrapping this call into VERIFY() // would be a good idea ! SetWindowText(strMyString); // Example of use of my string
它比原始 API 版本要好。但仍然太长了!
CMsg
:一行代码!
SetWindowText(CMsg(IDS_MYSTRING)); // This is straightforward!
CMsg
类是一个简单的 CString
包装器,它在其构造函数中接受一个字符串 ID。额外的技巧在于使用 C++ 的临时对象概念。(对象在函数调用的范围内创建、使用和销毁。)这样,您甚至不需要编写额外的代码行来声明对象。毕竟,您需要将字符串传递给函数(此处为:SetWindowText()
)。您在函数调用之前和之后都不需要它!
这就是您需要了解的关于使用 CMsg
的全部内容!由于它是一个 CString
,它具有 LPCTSTR
运算符,这意味着它可以在您通常使用字符串文字、原始字符串指针或 CString
的任何地方使用。
当然,如果字符串的使用不限于函数调用,您可以为其创建显式对象
CMsg strTitle(IDS_MYSTRING); // The string is loaded during the object // construction SetWindowText(strTitle); SomeOtherFunctionThatUsesTheString(strTitle);
更新!
Luis Barreira 在评论区提到 CString
有一个构造函数与 CMsg
的功能相同。只是它没有很好的文档说明
CString strTitle(MAKEINTRESOURCE(IDS_MYSTRING));
// Straightforward as well although not as compact
好主意,Luis!不过,请继续关注,因为我将介绍的第二个类(如下)甚至更有用。
顺便问一下,为什么起这么神秘的名字,叫 CMsg
?
为什么不是 CMessage
?或者 CStringEx
?或者其他更有意义的名字?嗯,再强调一下,我们的想法是您将在您的应用程序中数百甚至数千次地使用这个类。您可能希望有一个简短的名字,这意味着更少的打字。当然,如果您不喜欢它(或者它与您的某些代码产生名称冲突),您可以轻松地重命名该类。
如果字符串 ID 不正确怎么办?
在这种情况下,CMsg
对象包含字符串“???”,并且调试构建会 ASSERT。
CFMsg:一行代码的 sprintf() 包装器
很多时候,需要在将消息传递给函数之前对其进行格式化。既然我们谈论的是可翻译字符串,那么格式化消息也必须从字符串表中加载。让我们重新使用上面的示例,并设置一个更精细的窗口文本
CString strMyTitle, strMyTitleFormat; strMyTitleFormat.LoadString(IDS_MYTITLE); // "The weather today in %1" where // %1 is a placeholder for the city // name strMyTitle.FormatMessage(strMyTitleFormat, LPCTSTR(strCity)); SetWindowText(strMyTitle); // Example of use of my string
哎呀... 因为字符串中有一个可变参数,所以从一行变成了四行 :-(
幸运的是,CMsg
有一个姐妹类可以帮助我们处理格式化消息:CFMsg
。CFMsg
与 CMsg
非常相似。它的构造函数可以接受可变参数列表来格式化字符串(类似于 sprintf
)。
SetWindowText( CFMsg(IDS_MYTITLE, LPCTSTR(strCity)) ); // This is // straightforward!
现在,如果您觉得这样太紧凑,希望在 SetWindowText()
语句之外构建消息,当然可以将代码拆分
CFMsg csTitle(IDS_MYTITLE, LPCTSTR(strCity)); // "The weather today in %1" // where %1 is a placeholder // for the city name SetWindowText( csTitle );
实际上,上面段落的标题具有误导性。CFMsg
并不是 sprintf
包装器。它更像是一个 FormatMessage()
包装器。这会稍微影响格式说明符的编写方式,我们将在下面看到。
本地化要求格式化字符串参数带编号
格式消息很像 sprintf
,但对本地化更友好。它在格式化字符串中添加了参数编号。这对于确保翻译后的字符串格式正确非常重要,因为不同语言中的单词顺序通常不同。
例如:在法语中,形容词通常放在名词后面,与英语相反。
English | 快brown 狐狸跳过懒狗 |
French | Le rapiderenard brunsauta au-dessus du chien paresseux |
一个包含动物颜色和名称变量的格式化字符串只能通过使用这种参数编号进行翻译
English | 快%1 %2跳过懒狗 |
French | Le rapide%2 %1sauta au-dessus du chien paresseux |
格式规范:FormatMessage/CFMsg 与 sprintf
如果您习惯于 sprintf
格式说明符(您肯定是的!),请不要担心 FormatMessage()
和 CFMsg
中的语法更改。它们非常简单
%1
:第一个参数(字符串)。%2
:第二个参数(字符串)。%n
:第 n 个参数(字符串)。%1!d!
:第一个参数(十进制整数)。对于第 n 个参数,当您在sprintf
中使用%spec
时,请使用%n!spec!
符号。
FormatMessage()
的参数不限于字符串!
人们通常认为 FormatMessage()
的参数总是字符串。这是错误的。您可以使用任何类似 sprintf
的说明符。只需将说明符用感叹号括起来即可。例如:%d
变为 %1!d!
(将 1 替换为正确的参数编号)。
更新:嗯,并非 _所有_ 格式说明符都受支持:浮点说明符(e、E、f 和 g)不受支持。
如何调整现有字符串以利用 CFMsg?
这很简单。按当前顺序给参数编号。如果参数不是 %s
,请将说明符括在 in!
中。
例如:%s is %u years old
变为 %1 is %2!u! years old
。
即使您不使用字符串表,CFMsg 也是您的朋友
我们已经看到 CFMsg()
构造函数的第一个参数是字符串表中格式化字符串的 ID。实际上,这个构造函数有两个版本:一个接受字符串 ID,另一个接受字符串字面量。这意味着即使您不打算将字符串存储到字符串表中,也可以使用 CFMsg
。
SetWindowText(CFMsg(_T("The weather today in %1"), LPCTSTR(strCity)));
使用代码
只需将 Msg.h 和 Msg.cpp 添加到您的项目中。
在您将使用该类的 .cpp 文件中包含 Msg.h。我建议您在 stdafx.h 中包含 Msg.h,因为您很可能会在许多 .cpp 文件中使用 CMsg
和 CFMsg
。
将每个字符串文字存储在字符串表中(使用字符串编辑器),并在源代码中将其替换为CMsg(x)
,其中x
是您刚刚创建的字符串的ID。
演示项目
您可以在演示对话框项目中看到 CMsg
和 CFMsg
的实际应用。对话框的顶部演示了 CMsg
的使用。它加载并显示一个在组合框中指定编号的字符串。对话框的底部演示了 CFMsg
的使用。用户输入是格式化字符串的参数。
zip 文件包含 VC6 和 VC7 项目文件。zip 文件中附带的已编译 EXE 是使用 VC .NET 2003 编译的,因此需要 MFC71.dll。如果您使用的是 VC6,您可能需要重新编译该项目。
并非总是需要 CMsg
请注意,某些 MFC 函数的类成员,例如 AfxMessageBox()
,存在两种形式:一种接受 LPCTSTR
参数,另一种接受字符串 ID。在这种情况下,您甚至不需要 CMsg
。
AfxMessageBox(_T("Operation Failed."), MB_ICONERROR);
提取字符串后,代码变为
AfxMessageBox(IDS_ERROR, MB_ICONERROR); // No need to use CMsg here. // AfxMessageBox() will load the // string for us.
但是,如果字符串需要格式化,您可能希望继续使用 CFMsg
AfxMessageBox( CFMsg(IDS_ERROR, LPCTSTR(strError)), MB_ICONERROR);
准则
程序中使用了大量的字符串文字。其中大部分只使用一次。
常见的做法让我们认为导出到字符串表的字符串应该有一个符号标识符,而不仅仅是原始的数字 ID。然而,这种做法在与字符串表一起使用时会遇到限制:字符串太多,以至于为每个字符串找到一个易于使用、自说明且唯一的标识符很快就变成了一场噩梦。开发人员别无选择,只能创建一些复制字符串文字的标识符,并采用适应的语法(例如下划线代替空格)。这些标识符(非常)长,并且特别难以操作。
我建议您放弃此类字符串的标识符(源代码中多次使用的字符串除外!)。不要使用符号,而是在源代码中使用原始数字字符串 ID,并在行末尾以注释形式附加字符串的副本。
例如:
pWnd->SetWindowText("Please enter your name");
将文本提取到字符串表后,修改源代码如下:
pWnd->SetWindowText(4635); // Please enter your name
而不是
pWnd->SetWindowText(IDS_PLEASE_ENTER_YOUR_NAME);
我成功地将这种方法应用于数千个字符串文字。它更容易、编码更快,而且更具可读性。经验表明,当团队成员始终使用这种技术时,他们可以非常轻松地阅读彼此的代码。
当然,您自己的编码风格和实践可能有所不同。
结论
CMsg
和 CFMsg
并没有什么魔力。它们只是几行代码。但是,当需要从源代码中提取字符串文字并将其存储到字符串表时,它们极大地提高了生产力,这是为您的应用程序本地化做准备的强制性任务。