65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2018年1月9日

CPOL

4分钟阅读

viewsIcon

20526

downloadIcon

176

如何在 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.exevbc.exe)。

对于我在 ReactOS 上使用的 Mono 构建环境(resgen.exe, mcs.exe),创建 *.resources 文件工作正常,但我未能成功地将它们嵌入到应用程序中。为了解决这个问题,我创建了一个非常简单的资源编译器。

创建编译器的想法受到了 Extended Strongly Typed Resource Generator by Dmytro Kryvko 这篇文章的启发。我的编译器也因此得名 - ResXFileClassGeneratorROSResXFileClassGenerator 类应用程序适用于 ReactOS)。非常简单意味着

  • 目前,它仅支持多语言文本资源和嵌入式位图资源。(但是,编译器的功能很容易扩展。)
  • 该编译器不是创建要包含在程序集中并在运行时解析的二进制资源,而是创建一个资源类,该类已包含已解析的资源,并需要与应用程序的 *.cs 文件一起编译。
  • 按语言选择资源值的过程或多或少是初步的。(这也可以轻易改进。)

使用资源编译器

编译器处理的 *.resx 文件使用 Visual Studio XML 资源文件语法。对于跨平台项目,*.resx 文件可以轻松地在项目之间复制。*.resx 文件的名称应结构化为 <namespace>[.<sub-namespace>].<class-name>[.<IETF-language-tag>].resx,并定义

  • 要创建的资源类的命名空间名称和类名,以及
  • 文本资源所针对的语言/区域性。

一个小的例子来说明。假设有两个资源文件。

  1. WinFormsDesigner.Properties.Resources.resx
  2. 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 上运行。

编译器

  1. 按指定的顺序解析资源文件,并使用 System.Xml.XmlDocument 类将结果存储在静态 ResourceManagerROS 类中;
  2. 并使用 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日:初版文章
© . All rights reserved.