运行时本地化
使用 Windows 资源在运行时本地化应用程序。
引言
这是莎士比亚的名言吗?有病毒请求我的许可才能安装吗?有快速的棕色狐狸跳过中国狗吗?大多数程序员都假装会说几种语言,编程语言,但却对卡纳达语(在印度某地使用)、中文普通话或德语的拼写规则一无所知。许多用户表现得好像他们懂英语,但这种知识通常仅限于单词:一、二、三……
为了调和这两类人,这里有一个库,可以在运行时本地化应用程序。系统文件夹中有超过 35MB 的文本可用,隐藏在菜单、对话框、消息和字符串表中。如果我们能将这些信息用我们的语言提取到一个程序中,那么在另一个程序中提取相同的信息用另一种语言也是可能的。
收集资源
收集
要使用或收集隐藏在文件中的资源,我们将文件加载为数据文件(在 Vista 中加载为映像),并确保我们的库会被正确关闭。
public class qResourceReader : SafeHandleZeroOrMinusOneIsInvalid
{
public qResourceReader(string fileName)
:base(true)
{
if (Environment.OSVersion.Version.Major > 5)
base.handle = LoadLibraryEx(filename, IntPtr.Zero, 0x20);
else
base.handle = LoadLibraryEx(filename, IntPtr.Zero, 0x01 | 0x02);
}
protected override bool ReleaseHandle()
{
return FreeLibrary(base.handle);
}
}
每个资源都由三个属性唯一标识:
- 类型:1(光标)/ 2(位图)/ 3(图标)/“Avi”/“MUI”/……
- 名称:1 / 2 / 3 /“Dialog_Open”/……
- 语言:1013(英语)/ 2052(中文)/……
类型和名称是字符串或无符号整数。Microsoft 使用一个简单的技巧来做到这一点:他们使用 IntPtr
,当这个 IntPtr
的值小于或等于 ushort.MaxValue
时,它指向一个数字;否则,它指向内存中的一个位置,可以使用 Marshal.PtrToString
函数从中读取字符串。
可以使用三个回调函数收集资源,每次揭示一个额外的特征。
public bool StartCollect()
{
//for each type
return EnumResourceTypes(base.handle,
new TypeDelegate(EnumTypes), new IntPtr.Zero);
}
//first callback
private bool EnumTypes(IntPtr hModule, IntPtr lpType, IntPtr lParam)
{
//for each name
return EnumResourceNames(hModule, lpType,
new NameDelegate(EnumNames), lParam);
}
//second callback
private bool EnumNames(IntPtr hModule, IntPtr lpType,
IntPtr lpName, IntPtr lParam)
{
//for each language
return EnumResourceLanguages(hModule, lpType, lpName,
new LanguageDelegate(EnumLanguages), lParam);
}
//third callback
private bool EnumLanguages(IntPtr hModule, IntPtr lpType,
IntPtr lpName, ushort langID, IntPtr lParam)
{
//Find the resource
IntPtr hRes = FindResource(hModule, lpType, lpName, langID);
//Load the resource
IntPtr hResData = LoadResource(hModule, hRes);
//Lock the resource
IntPtr result = LockResource(hResData);
//Todo : analyze the result
...
return true;
}
现在,IntPtr
结果是一个已锁定的资源,可以进行分析。
字符串
如果 lpType IntPtr
的值为 6,则结果是十六个有序的长度和文本对的块。使用一个简单的公式,我们可以计算每个字符串的 ID:((lpName
- 1) * 16) + 位置。
lpType : |
6 | 字符串 |
lpName : |
4 | (4-1) * 16 = 48 |
langId : |
1033 | English |
结果 | 7aaaaaaa6bbbbbb005ccccc4dddd3eee2ff1g01i2jj2kk3lll3mmm3nnn |
((4 - 1) * 16) + 0 = 48 : aaaaaaa (语言 英语) |
((4 - 1) * 16) + 1 = 49 : bbbbbb (语言 英语) |
((4 - 1) * 16) + 2 = 50 : |
((4 - 1) * 16) + 3 = 51 : |
((4 - 1) * 16) + 4 = 52 : ccccc (语言 英语) |
... |
消息表
消息存储在三元组中。第一个 DWORD
是三元组的数量。每个三元组中的第一个 DWORD
是起始编号 ID,第二个 DWORD
是结束编号 ID,最后一个 DWORD
是字符串的偏移量。
lpType : |
11 | 消息表 |
lpName : |
1 | 始终为 1 |
langId : |
1030 | Danish |
结果 | 3,11,15,22,17,17,99,20,21,144,14,1, aaaaaaa,12,1,bbbbbb.....Message17....Message20... |
3 个三元组的丹麦语消息 |
第一个三元组包含偏移量为 22 的消息 11、12、13、14 和 15 |
下一个三元组包含偏移量为 99 的消息 17 |
最后是偏移量为 144 的消息 20 和 21 |
14 个 Unicode 字节 aaaaaa |
12 个 Unicode 字节 bbbbb |
... |
菜单和弹出菜单
我们可以检查菜单或弹出菜单而不显示它。因为必须正确关闭句柄,所以我们使用 SafeHandle
。
public class qMenu : SafeHandleZeroOrMinusOneIsInvalid
{
public qMenu(qResourceReader reader, IntPtr result)
{
base.handle = LoadMenu(reader.DangerousGetHandle(), result);
}
protected override bool ReleaseHandle()
{
return DestroyMenu(this.handle))
}
}
现在,使用递归函数(有些菜单有子子子子子项),可以枚举所有项并唯一标识它们。
private Dictionary<uint, string> _sortdict = new Dictionary<uint, string>()
private void CollectAllIds(IntPtr ptr) //base.handle or submenu
{
int count = GetMenuItemCount(ptr);
if (count < 0)
return;
StringBuilder sb = new StringBuilder(500);
qMenuItemInfo inf = new qMenuItemInfo();
inf.cbSize = (uint)Marshal.SizeOf(inf.GetType());
inf.fMask = 0x02 | 0x04 | 0x40;
for (uint i = 0; i < count + 5; i++)
{
uint ui = GetMenuItemID(ptr, i);
if (GetMenuString(ptr, ui, sb, sb.Capacity, 0) > 0)
_sortdict.Add(ui, sb.ToString());
else
{
inf.cch = 0;
inf.dwTypeData = null;
if (GetMenuItemInfoW(ptr, i, true, ref inf))
{
if (inf.cch > 0)
{
inf.dwTypeData = new string(' ', (int)++inf.cch);
if (GetMenuItemInfoW(ptr, i, true, ref inf))
_sortdict.Add(--_counter, inf.dwTypeData);
}
if (inf.hSubMenu == IntPtr.Zero)
continue;
if (inf.hSubMenu.ToInt64() < Int32.MaxValue)
CollectAllIds(inf.hSubMenu);
}
else
CollectAllIds(GetSubMenu(ptr, i));
}
}
}
对话框
对话框对于查找复数很有趣。但是,不显示对话框就很难使用对话框函数。此外,在 Vista 中,您需要管理员权限才能使用某些对话框。因此,我们必须手动分析数据。
Unicode 允许代码("\r\n"
)和几乎所有内容作为合法字符,甚至是按钮(0x8000)或静态控件(0x8200)的标识符。因此,我们不能使用 Char.IsControl
函数来确定我们是在处理标识符还是文本。唯一确定的事情是:0x00 在字符串的末尾,0xFF 在字符串的开头。UnmanagedMemoryStream
可以向后读取结果。每次读取两个连续的 0 字节时,就可能是一个字符串的结束。每次读取两个连续的 255 值字节时,可能就遇到了新字符串的开始。对于实际的锯齿形代码,请参考提供的源代码。
提取资源
字符串和消息
Windows 提供了两个非常快速的函数:LoadString
和 FormatMessage
。
public bool TryFindStringResource(uint resourceId, out string result)
{
if (LoadStringW(base.handle, resourceId, sb, sb.Capacity) > 0)
{
result = sb.ToString();
return true;
}
result = null;
return false;
}
public bool TryFindMessageResource(uint resourceId,
ushort resourceLangId, out string result)
{
if (FormatMessageW(0xA00, base.handle, resourceId, resourceLangId,
sb, sb.Capacity, IntPtr.Zero) > 0)
{
result = sb.ToString().Trim(null);
return true;
}
result = null;
return false;
}
对话框和菜单
我们加载整个资源,然后对 Dictionary
调用 TryGetValue
来返回我们正在搜索的字符串。
public bool TryFindDialogString(uint dialogId, uint itemId, out string result)
{
IntPtr ptr = IntPtr.Zero;
int size = 0;
if (TryLockResource(new qResource(qResourceType.Dialogs, dialogId),
ref size, out ptr))
return new qDialog(ptr, size).Items.TryGetValue(itemId, out result);
result = null;
return false;
}
public bool TryFindMenuString(uint menuId, uint itemId, out string result)
{
IntPtr ptr = IntPtr.Zero;
int size = 0;
if (TryLockResource(new qResource(qResourceType.Menus, menuId),
ref size, out ptr))
return new qMenu(ptr).Items.TryGetValue(itemId, out result);
result = null;
return false;
}
位图 - 图标 - 光标
因为一张图片胜过千言万语,所以必须提供位图、图标和光标的提取。单个图标和光标使用 CreateIconFromResource
函数加载;其他图标和光标使用 LoadImage
函数加载。因为每个图像都必须正确关闭,所以再次使用 SafeHandle
。
本地化应用程序
对于每个目标 Windows 版本,我们在本地语言中搜索可能的候选者。
然后,我们提供一个链接到 qResourceReader
来提取资源。
qResourceReader _rr;
string s;
ToolStripMenuItem tsmi = new ToolStripMenuItem("For testing purpose only");
if (Environment.OSVersion.Version.Major > 5)
{
_rr = new qResourceReader("User32.dll");
if (_rr.TryFindStringResource(718, out s)
tsmi.Text = s;
}
else
{
_rr = new qResourceReader("Win32k.sys");
if (_rr.TryFindMessageResource(213, out s)
tsmi.Text = s;
}
如果需要打开不同的文件,我们只更改阅读器的文件名属性。也可以提取完整的菜单、对话框或字符串资源。
_rr.FileName = "hhctrl.ocx";
Dictionary<uint, string> hh = _rr.CollectMenuResources(6000);
if (hh.TryGetValue(4294967294, out s))
//"&File"
fileToolStripMenuItem.Text = s;
if (hh.TryGetValue(6002, out s))
//"E&xit"
exitToolStripMenuItem.Text = s;
关注点
本地化应用程序的一个更好的方法是在程序安装或修改期间将结果写入 XAML 文件。此时,我们知道是否真的需要本地化,我们可以请求管理员权限,和/或要求预装特定的应用程序。
在 Vista 中,文件 shell32.dll 在 24069 到 25065 之间的受损字符串资源中隐藏了大量有趣的信息。
24837, print; print out; printer; printers; printing; printner; ... ;uninstalls;
unistall; printen; afdrukken; druk; af; afdruk;
verwijder; verwijderen; ...; deactiveren; deactiveer;
历史
- 2008 年 1 月 22 日:初始版本。
- 2008 年 2 月 11 日:文本小幅更新。