适用于非 Windows 系统的 .NET *.resx 文件的一个非常简单的资源编译器





5.00/5 (1投票)
如何在 ReactOS(以及 Linux 等其他非 Windows 操作系统)上为 GUI 应用程序提供 .NET 兼容 *.resx 文件中的多语言资源
引言
本文基于提示 Introduction to C# on ReactOS 及其后续文章 Introduction to System.Windows.Forms on ReactOS with C#。在 ReactOS(以及可能是其他非 Windows 操作系统,如 Linux)上,无法运行 Visual Studio,因此 *.resx 文件无法轻松包含到 .NET 应用程序中(例如,到 System.Windows.Forms
应用程序中)。虽然 Visual Studio 会自动编译 *.resx 文件并将其嵌入到应用程序中,但非 Visual Studio 的开发环境必须手动完成此操作。
背景
要编译 *.resx 文件并将其嵌入到应用程序中,需要
- 调用 resgen.exe 从资源文件(*.resx 文件)创建二进制资源(*.resources 文件),以及
- 使用 /resource:<filename> 编译器选项调用编译器(csc.exe 或 vbc.exe)。
对于我在 ReactOS 上使用的 Mono 构建环境(resgen.exe, mcs.exe),创建 *.resources 文件工作正常,但我未能成功地将它们嵌入到应用程序中。为了解决这个问题,我创建了一个非常简单的资源编译器。
创建编译器的想法受到了 Extended Strongly Typed Resource Generator by Dmytro Kryvko 这篇文章的启发。我的编译器也因此得名 - ResXFileClassGeneratorROS
(ResXFileClassGenerator
类应用程序适用于 ReactOS)。非常简单意味着
- 目前,它仅支持多语言文本资源和嵌入式位图资源。(但是,编译器的功能很容易扩展。)
- 该编译器不是创建要包含在程序集中并在运行时解析的二进制资源,而是创建一个资源类,该类已包含已解析的资源,并需要与应用程序的 *.cs 文件一起编译。
- 按语言选择资源值的过程或多或少是初步的。(这也可以轻易改进。)
使用资源编译器
编译器处理的 *.resx 文件使用 Visual Studio XML 资源文件语法。对于跨平台项目,*.resx 文件可以轻松地在项目之间复制。*.resx 文件的名称应结构化为 <namespace>[.<sub-namespace>].<class-name>[.<IETF-language-tag>].resx
,并定义
- 要创建的资源类的命名空间名称和类名,以及
- 文本资源所针对的语言/区域性。
一个小的例子来说明。假设有两个资源文件。
- WinFormsDesigner.Properties.Resources.resx
- WinFormsDesigner.Properties.Resources.de.resx
第一个文件定义了命名空间 WinFormsDesigner.Properties
、资源类名 Resources
和标准的(回退)语言/区域性资源。
第二个文件定义了针对德语的备用语言文本资源。
WinFormsDesigner.Properties.Resources.resx 如下所示:
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
...
-->
<xsd:schema ...
...
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Drawing" name="System.Drawing" />
<data name="ImageExit16x16" type="System.Drawing.Bitmap, System.Drawing"
mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAQ1JREFUOE+lk6ES
gzAQRNtPQtYiK5G1kZWR2MhKZG1kZWUtsjK/kE9AXndTAhfKDMwU5g2Q7G1u7o6jiBz+umhAXNPIl3rC
nmv5ocbaSDo8Bz9vTiQEkXe/DXTeORhV8+kDAoeH3w/05qQNXk8Z7t1+oFcGdTo53qwM6uZ3ZrlOfWEQ
fSexNYlCvPKddNCXBkg/XJsJbZLf9X6EvjAIrZUeVdVok+Ue9bMB+r00WMtAmxQGHJbgkAGGiBQ1YGvV
nTXUTxkkAwxSf1kEo1Vci2yxNsEa9aYa54AGsXMSDIq4E+pLA7QlWANgssnYRp2BR1Ujh4nzsAV03qIG
2YA/VP7Dvs8qwSL9gCAGEsZ9AB8mrjl1sCJ5AAAAAElFTkSuQmCC
</value>
</data>
...
<data name="MenuTopLevelItemEdit" xml:space="preserve">
<value>Edit</value>
<comment>user interface text</comment>
</data>
...
</root>
它显示了一个用于嵌入式位图资源的 <data>...</data>
标签示例,以及另一个用于文本资源的示例。
WinFormsDesigner.Properties.Resources.de.resx 覆盖了德语的文本资源
...
<data name="MenuTopLevelItemEdit" xml:space="preserve">
<value>Bearbeiten</value>
<comment>user interface text</comment>
</data>
...
可以通过 ResXFileClassGeneratorROS.exe /?
获取资源编译器的命令行语法。
为了编译两个示例文件到一个资源类,我为 Notepad++ 扩展 NppExec
使用了以下命令序列:
SET LOCAL RESSRC1=.\WinFormsDesigner.Properties.Resources.resx
SET LOCAL RESSRC2=.\WinFormsDesigner.Properties.Resources.de.resx
SET LOCAL RESTGT=.\WinFormsDesigner.Properties.Resources.cs
..\ResXFileClassGeneratorROS\ResXFileClassGeneratorROS.exe "$(RESSRC1)","$(RESSRC2)" "$(RESTGT)"
Using the Code
完整的源代码可作为 Visual Studio 2010 项目下载。该资源编译器可以在 Windows 上的 Visual Studio(目标是 .NET Framework 客户端配置文件)上构建,并可以在 ReactOS 上运行。
编译器
- 按指定的顺序解析资源文件,并使用
System.Xml.
XmlDocument
类将结果存储在静态ResourceManagerROS
类中; - 并使用
System.IO.StreamWriter
类写入所需的资源类。
解析器的第一步是确定和检查资源数据属性
xmlDocument = new XmlDocument();
xmlDocument.Load(path);
foreach (XmlNode node in xmlDocument.DocumentElement.ChildNodes)
{
if (node.Name == "data")
{
ResXInfo.DataType vt = ResXInfo.DataType.String;
// Determine resource data attributes.
XmlAttribute nameAttr = node.Attributes["name"];
XmlAttribute mimetypeAttr = node.Attributes["mimetype"];
// Check resource data type.
var resourceDataType = node.Attributes["type"];
if (resourceDataType != null && resourceDataType.InnerText.Contains("Bitmap"))
{
if (mimetypeAttr.InnerText == "application/x-microsoft.net.object.bytearray.base64")
vt = ResXInfo.DataType.BitmapBase64;
else
{
Console.WriteLine("ERROR: Bitmap '" + nameAttr.InnerText +
"' unsupported image data format. Skip this resource.");
continue;
}
}
else if (resourceDataType != null)
{
Console.WriteLine("ERROR: Unknown resource type '" + resourceDataType +
"'. Skip this resource.");
continue;
}
// Fallback (no 'type' attribute provided) is string resource.
目前,只接受“Bitmap
”(使用“application/x-microsoft.net.object.bytearray.base64
”编码)和 string
资源 - 但这很容易扩展(在 if (resourceDataType != ...)
和 else
块之间)。
解析器的第二步是处理资源数据值,并将结果注册到 static
ResourceManagerROS
类
// Determine resource data value.
XmlNode valueNode = null;
foreach (XmlNode childNode in node.ChildNodes)
if (childNode.Name == "value")
valueNode = childNode;
string value;
// Check resource name and data value.
if (nameAttr == null)
{
Console.WriteLine("ERROR: Resource value without name. Skip this resource.");
continue;
}
if (valueNode == null)
{
Console.WriteLine("ERROR: Resource value without name. Skip this resource.");
continue;
}
// Process bitmap Base64 coded data.
if (vt == ResXInfo.DataType.BitmapBase64)
{
value = valueNode.InnerText.Replace("\r", "").Replace
("\n", "").Replace("\t", "").Replace(" ", "");
if (value != null && value is string)
{
byte[] imageData = Convert.FromBase64String(value as string);
if (imageData != null && imageData.Length > 0)
{
System.Drawing.Bitmap bmp = null;
using (var ms = new System.IO.MemoryStream(imageData))
{
bmp = new System.Drawing.Bitmap(ms);
}
if (bmp == null)
{
Console.WriteLine("ERROR: Bitmap '" + nameAttr.InnerText +
"' unable to create bitmap from image data. Skip this resource.");
continue;
}
}
else
{
Console.WriteLine("ERROR: Bitmap '" + nameAttr.InnerText +
"' with empty image data. Skip this resource.");
continue;
}
}
else
{
Console.WriteLine("ERROR: Bitmap '" + nameAttr.InnerText +
"' without image data. Skip this resource.");
continue;
}
}
// Fallback (no 'type' attribute provided) is string resource.
else
value = valueNode.InnerText;
// Register resource.
ResXInfo entry = null;
if (string.IsNullOrWhiteSpace(ieftLanguageTag))
{
if (ResourceManagerROS.ContainsKey(nameAttr.Value))
{
Console.WriteLine("ERROR: Resource name '" + nameAttr.Value +
"' already in use. Skip this resource.");
continue;
}
else
{
entry = new ResXInfo(vt, value);
ResourceManagerROS.Add(nameAttr.Value, entry);
}
}
else
{
entry = ResourceManagerROS.GetResXInfo(nameAttr.Value);
if (entry == null)
{
Console.WriteLine("ERROR: Resource name '" + nameAttr.Value +
"' must exist to add a language specific value. Skip this resource.");
continue;
}
else
entry.AddLanguageValue(ieftLanguageTag, value);
}
目前,资源值处理仅限于“Bitmap
”(使用“application/x-microsoft.net.object.bytearray.base64
”编码)和 string
资源 - 但这也很容易改进(在 if (vt == ...)
和 else
块之间)。
资源注册区分默认语言(回退)资源(不提供 ieftLanguageTag
)和创建新的资源 entry
,以及备用语言资源(提供 ieftLanguageTag
)并扩展现有资源 entry
。
写入器生成资源类体……
using (System.IO.StreamWriter classWriter = System.IO.File.CreateText(targetFile))
{
classWriter.WriteLine("//-----------------------------------------------------------------------");
classWriter.WriteLine("// <auto-generated>");
classWriter.WriteLine("// This code was generated by a tool.");
classWriter.WriteLine("//");
classWriter.WriteLine("// Changes to this file may cause incorrect behavior and will be lost");
classWriter.WriteLine("// if the code is regenerated.");
classWriter.WriteLine("// </auto-generated>");
classWriter.WriteLine("//-----------------------------------------------------------------------");
classWriter.WriteLine("");
classWriter.WriteLine("namespace " + targetNamespace);
classWriter.WriteLine("{");
classWriter.WriteLine(" using System;");
classWriter.WriteLine(" using System.Globalization;");
classWriter.WriteLine("");
classWriter.WriteLine(" /// <summary>A strongly-typed resource
/// class for looking up localized");
classWriter.WriteLine(" /// resources.</summary>");
classWriter.WriteLine(" internal class " + targetClassName);
classWriter.WriteLine(" {");
classWriter.WriteLine(" private static CultureInfo resourceCulture;");
classWriter.WriteLine("");
classWriter.WriteLine(" /// <summary>Override the current culture
/// for all resource lookups");
classWriter.WriteLine(" /// using this strongly typed resource class.</summary>");
classWriter.WriteLine(" internal static CultureInfo Culture");
classWriter.WriteLine(" {");
classWriter.WriteLine(" get { return resourceCulture; }");
classWriter.WriteLine(" set { resourceCulture = value;}");
classWriter.WriteLine(" }");
...
classWriter.WriteLine(" }");
classWriter.Write("}");
classWriter.Close();
}
……并遍历注册到 static
ResourceManagerROS
类中的所有资源 entries
,以写入资源类属性。
var resourceEnumerator = System.Resources.ResourceManagerROS.GetEnumerator();
while (resourceEnumerator.MoveNext())
{
var k = resourceEnumerator.Current.Key;
var t = resourceEnumerator.Current.Value.ValueType;
var v = resourceEnumerator.Current.Value.DefaultValue;
classWriter.WriteLine("");
if (t == System.Resources.ResXInfo.DataType.BitmapBase64)
{
classWriter.WriteLine(" /// <summary>Buffer the
/// bitmap similar to " + k + ".</summary>");
classWriter.WriteLine(" private static System.Drawing.Bitmap _" + k + ";");
classWriter.WriteLine("");
classWriter.WriteLine(" /// <summary>Look up a bitmap
/// similar to " + k + ".</summary>");
classWriter.WriteLine(" internal static System.Drawing.Bitmap " + k);
classWriter.WriteLine(" {");
classWriter.WriteLine(" get");
classWriter.WriteLine(" {");
classWriter.WriteLine(" if (_" + k + " != null)");
classWriter.WriteLine(" return _" + k + ";");
classWriter.WriteLine("");
classWriter.WriteLine(" using (var ms = new System.IO.MemoryStream(
Convert.FromBase64String(\"" + v + "\")))");
classWriter.WriteLine(" {");
classWriter.WriteLine(" _" + k + " = new System.Drawing.Bitmap(ms);");
classWriter.WriteLine(" }");
classWriter.WriteLine(" return _" + k + ";");
classWriter.WriteLine(" }");
classWriter.WriteLine(" }");
}
else
{
classWriter.WriteLine(" /// <summary>Look up a localized string similar to " +
k + ".</summary>");
classWriter.WriteLine(" internal static string " + k);
classWriter.WriteLine(" {");
classWriter.WriteLine(" get");
classWriter.WriteLine(" {");
if (resourceEnumerator.Current.Value.CountLanguages > 0)
{
classWriter.WriteLine("string fullCulture = (resourceCulture != null ? " +
"resourceCulture.IetfLanguageTag : " +
"CultureInfo.CurrentUICulture.IetfLanguageTag);");
classWriter.WriteLine(" string baseCulture = " +
"fullCulture.Split(new char[] {'-'})[0];");
for (int conutAlternativeLanguages = 0;
conutAlternativeLanguages < resourceEnumerator.Current.Value.CountLanguages;
conutAlternativeLanguages++)
{
string currentIeftLanguageTag = resourceEnumerator.Current.Value.
GetLanguageValue(conutAlternativeLanguages).IeftLanguageTag;
string currentIeftLanguageVal = resourceEnumerator.Current.Value.
GetLanguageValue(conutAlternativeLanguages).Value.ToString();
classWriter.WriteLine("");
classWriter.WriteLine
(" if(\"" + currentIeftLanguageTag +
"\" == fullCulture)");
classWriter.WriteLine
(" return \"" + currentIeftLanguageVal + "\";");
classWriter.WriteLine
(" if(\"" + currentIeftLanguageTag +
"\".StartsWith(baseCulture))");
classWriter.WriteLine
(" return \"" + currentIeftLanguageVal + "\";");
}
classWriter.WriteLine
(" return \"" + v + "\";");
}
else
{
classWriter.WriteLine
(" return \"" + v + "\";");
}
classWriter.WriteLine(" }");
classWriter.WriteLine(" }");
}
}
当前的实现仅为 string
资源提供备用语言。语言选择实现了一个非常简单的回退机制,该机制具有局限性。假设有以下资源的
- 任何默认(回退)语言,例如
en
, - 奥地利德语
de-AT
- 瑞士德语
de-CH
以及命令行如下:
ResXFileClassGeneratorROS.exe .\WinFormsDesigner.Properties.Resources.resx,
.\WinFormsDesigner.Properties.Resources.de-AT.resx,
.\WinFormsDesigner.Properties.Resources.de-CH.resx .\WinFormsDesigner.Properties.Resources.cs
回退机制将生成为:
if("de-AT" == fullCulture)
return "Kiste";
if("de-AT".StartsWith(baseCulture))
return "Kiste";
if("de-CH" == fullCulture)
return "Chaschta";
if("de-CH".StartsWith(baseCulture))
return "Chaschta";
return "Box";
并且 de-CH
的资源 string
将永远不会被返回,因为if("de-AT".StartsWith(baseCulture))
在 if("de-CH" == fullCulture)
达到之前就已经匹配了。可以通过对语言进行分组并减少使用 StartsWith(baseCulture)
的检查次数来克服此局限性。
关注点
我想了解在 ReactOS 上实际存在哪些用于创建 .NET GUI 应用程序的内容。这个编译器允许我在 Windows 和 ReactOS 上进行同步的 Windows Forms 应用程序开发。
此外,我认为该编译器也可用于其他非 Windows 平台。
历史
- 2018年1月9日:初版文章