WPF - 全球化 - 多语言处理

创建 .resx 资源文件 —— 语言包

此解决方案中语言包使用的是 .resx 资源文件,一般接触过 Windows Form 的开发人员应该都知道。

如果,不知道什么是 .resx 资源文件。请参考 Resources in .NET apps

语言包的名称为 Lang.resx,创建不同语言的资源文件的格式为 Lang.语言的英文简写.resx。 

创建 .resx 资源文件时,需要进行特殊设置。

  • 创建默认(中文)包时,访问修饰符需要设置为:Public 
  • 创建其它语言包时,访问修饰符需要设置为:无代码生成

 示例:

  • (中文包)Lang.resx

  

  • (英文包)Lang.en.resx

 

绑定语言包的静态属性

打开 Lang.Designer.cs 文件,你会看见 Lang 的类。这个类是由很多静态属性组成的,我们现在需要在视图上关联这些静态属性。

Lang
/// <summary>
///   一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Lang
{

    private static global::System.Resources.ResourceManager resourceMan;

    private static global::System.Globalization.CultureInfo resourceCulture;

    [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
    internal Lang()
    {
    }

    /// <summary>
    ///   返回此类使用的缓存的 ResourceManager 实例。
    /// </summary>
    [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
    public static global::System.Resources.ResourceManager ResourceManager
    {
        get
        {
            if (object.ReferenceEquals(resourceMan, null))
            {
                global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MetroDemo.Langs.Lang", typeof(Lang).Assembly);
                resourceMan = temp;
            }
            return resourceMan;
        }
    }

    /// <summary>
    ///   重写当前线程的 CurrentUICulture 属性,对
    ///   使用此强类型资源类的所有资源查找执行重写。
    /// </summary>
    [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
    public static global::System.Globalization.CultureInfo Culture
    {
        get
        {
            return resourceCulture;
        }
        set
        {
            resourceCulture = value;
        }
    }

    /// <summary>
    ///   查找类似 按钮 的本地化字符串。
    /// </summary>
    public static string Controls_Button
    {
        get
        {
            return ResourceManager.GetString("Controls.Button", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 日历视图 的本地化字符串。
    /// </summary>
    public static string Controls_CalendarView
    {
        get
        {
            return ResourceManager.GetString("Controls.CalendarView", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 复选框 的本地化字符串。
    /// </summary>
    public static string Controls_CheckBox
    {
        get
        {
            return ResourceManager.GetString("Controls.CheckBox", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 组合框 的本地化字符串。
    /// </summary>
    public static string Controls_ComboBox
    {
        get
        {
            return ResourceManager.GetString("Controls.ComboBox", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 标签 的本地化字符串。
    /// </summary>
    public static string Controls_Lable
    {
        get
        {
            return ResourceManager.GetString("Controls.Lable", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 列表框 的本地化字符串。
    /// </summary>
    public static string Controls_ListBox
    {
        get
        {
            return ResourceManager.GetString("Controls.ListBox", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 密码框 的本地化字符串。
    /// </summary>
    public static string Controls_PasswordBox
    {
        get
        {
            return ResourceManager.GetString("Controls.PasswordBox", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 进度条 的本地化字符串。
    /// </summary>
    public static string Controls_ProgressBar
    {
        get
        {
            return ResourceManager.GetString("Controls.ProgressBar", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 单选按钮 的本地化字符串。
    /// </summary>
    public static string Controls_RadioButton
    {
        get
        {
            return ResourceManager.GetString("Controls.RadioButton", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 滚动查看器 的本地化字符串。
    /// </summary>
    public static string Controls_ScrollViewer
    {
        get
        {
            return ResourceManager.GetString("Controls.ScrollViewer", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 滑动块 的本地化字符串。
    /// </summary>
    public static string Controls_Slider
    {
        get
        {
            return ResourceManager.GetString("Controls.Slider", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 选项卡 的本地化字符串。
    /// </summary>
    public static string Controls_TabControl
    {
        get
        {
            return ResourceManager.GetString("Controls.TabControl", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 文本块 的本地化字符串。
    /// </summary>
    public static string Controls_TextBlock
    {
        get
        {
            return ResourceManager.GetString("Controls.TextBlock", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 文本框 的本地化字符串。
    /// </summary>
    public static string Controls_TextBox
    {
        get
        {
            return ResourceManager.GetString("Controls.TextBox", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 开关按钮 的本地化字符串。
    /// </summary>
    public static string Controls_ToggleButton
    {
        get
        {
            return ResourceManager.GetString("Controls.ToggleButton", resourceCulture);
        }
    }

    /// <summary>
    ///   查找类似 树视图 的本地化字符串。
    /// </summary>
    public static string Controls_TreeView
    {
        get
        {
            return ResourceManager.GetString("Controls.TreeView", resourceCulture);
        }
    }
}

绑定语言包的静态属性

Xaml
<UserControl x:Class="MetroDemo.Views.ButtonView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:demo="http://github.com/MetroDemo"
             prism:ViewModelLocator.AutoWireViewModel="True" 
             mc:Ignorable="d" Style="{StaticResource Controls.UserControl.Demo}"
             Height="450" Width="400">
    <Button Content="{x:Static demo:Lang.Controls_Button}"/>
</UserControl>

使用 StaticExtension 绑定的语言包是无法做到动态切换的,也就是说。如果,用户想不关闭应用程序的前提下。手动切换语言,界面上的数据是无法改变的。

主要原因有 StaticExtension 仅读取一次数据,即便后台数据发送变化,视图层也不会同步更新。

为了解决视图层不同步更新的问题,我们采用 Binding 的方式绑定数据。

Xaml
<UserControl x:Class="MetroDemo.Views.ButtonView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:demo="http://github.com/MetroDemo"
             prism:ViewModelLocator.AutoWireViewModel="True" 
             mc:Ignorable="d" Style="{StaticResource Controls.UserControl.Demo}"
             Height="450" Width="400">
    <Button Content="{Binding Path=(demo:Lang.Controls_Button)}"/>
</UserControl>

虽然,我们使用了 Binding 绑定数据。但是,Lang 类没有静态属性变化通知事件,还是无法同步数据变化。

为了解决上述问题,我们可以手动给 Lang 类添加这个事件。但是,我们需要注意的是 Lang.Designer.cs 这个文件是由 VS 自动生成的。

每次用户编辑 Lang.resx 资源文件内的资源,Lang.Designer.cs 就会生成一个新的 Lang 类。

所以,我们想在 Lang 类中添加静态属性变化通知事件的方式就行不通。

后来,我参考了项目 HandyControl 的做法。才有了通过代理的方式来关联视图和资源静态属性的方法。

代理类要求如下:

  1. 能够同步更新视图数据,也就是说需要实现接口 INotifyPropertyChanged 或有静态属性变化通知事件
  2. 具备和 Lang.cs 一样的资源属性名称,读取本身属性的值,指向的是 Lang 类中相同名称的静态属性
  3. 定义一个 CultureInfo 类型的属性 Culture,默认值和系统一致。
  4. 改变 Culture 时,会通知视图同步数据
LangProvider
public class LangProvider : INotifyPropertyChanged
{
    public string Controls_Button => Lang.Controls_Button;

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public void SetCulture(CultureInfo culture)
    {
        Lang.Culture = culture;
        OnPropertyChanged(nameof(Controls_Button));
    }
}

LangProvider 这个类非常死板,不够灵活。每次添加一个资源属性,必须手动在里面创建一个相同的属性。

为了使代理类能够具备上述的优点。我们更新对代理类的要求,如下

  1. 能够同步更新视图数据
  2. 能够通过指定 Key 来获取与 Lang 类中相同的静态属性名对应的静态属性值。
  3. 改变 CultureInfo 语言区域时,通知视图同步数据
LangProvider
public class LangProvider : IRaisePropertyChanged
{
    private ResourceManager lang;
    private CultureInfo culture;
    private Type langType;

    public Type LangType
    {
        get => langType;
        set
        {
            var changed = this.SetValue(ref langType, value);
            if (changed)
            {
                lang = GetLang(langType.Assembly);
            }
        }
    }

    public CultureInfo Culture
    {
        get => culture;
        set
        {
            var changed = this.SetValue(ref culture, value);
            if (changed)
            {
                // NPN_This 是常量字符串,值 = "Item[]"
                var args = new PropertyChangedEventArgs(ComponentModelExt.NPN_This);
                RaisePropertyChanged(args);
            }
        }
    }

    public string this[string key]
    {
        get
        {
            if (null == lang)
            {
                return default;
            }

            var name = key.Replace('_', '.');
            var value = null == culture ? lang.GetString(name) : lang.GetString(name, culture);
            return value;
        }
    }

    private ResourceManager GetLang(Assembly assembly)
    {
        var names = assembly.GetManifestResourceNames();
        var lang = default(ResourceManager);
        foreach (var name in names)
        {
            if (!name.ToLower().EndsWith(".resources"))
                continue;

            var resource = IO.Path.GetFileNameWithoutExtension(name);
            var resourceName = resource.Split('.').Last();
            if (resourceName.ToLower() == "lang")
            {
                if (null == lang)
                {
                    lang = new ResourceManager(resource, assembly);
                    continue;
                }

                throw new NotImplementedException("一个程序集中只允许添加一个语言包");
            }
        }

        return lang;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged(PropertyChangedEventArgs e)
    {
        PropertyChanged?.Invoke(this, e);
    }
}
Xaml
<UserControl x:Class="MetroDemo.Views.ButtonView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:demo="http://github.com/MetroDemo"
             prism:ViewModelLocator.AutoWireViewModel="True" 
             mc:Ignorable="d" Style="{StaticResource Controls.UserControl.Demo}"
             Height="450" Width="400">
    <UserControl.Resources>
      <LangProvider x:Key="Lang" LangType="{x:Type demo:Lang}"/>
    </UserControl.Resources>
    <StackPanel>
        <Button 
            Click="ButtonBase_OnClick"
            Content="{Binding Source={StaticResource Lang},Path=[Controls_Button]}" />
    </StackPanel>
</UserControl>

通过改变属性 LangProvider.Culture 来触发更新机制

后台代码
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    if (TryFindResource("Lang") is System.Windows.Resources.LangProvider provider)
    {
        provider.Culture = new CultureInfo("en");
    }
}

以我的编码习惯来说,这种方案的缺点是我需要到打开资源包,复制里面的资源名称到绑定的路径中,这样做很不方便。

我还是比较喜欢用 VS 中智能提示来找属性名,比较快捷明了。不用害怕少复制字母这种情况发生。

于是,就有了下面这个特殊的扩展标记类。

它的主要作用是

  • 能够触发 VS 中的智能提示,找到 Lang 对应的静态属性名
  • 自动实例化应用程序 LangProvider,也是就默认值
  • 也可以指定 LangProvider
  • 自动绑定 LangProvider 对应的属性 this[]
LangExtension
public class LangExtension : MarkupExtension
{
    private static LangProvider defaultProvider;
    private LangProvider provider;

    public static LangProvider DefaultProvider
    {
        get
        {
            if (null == defaultProvider)
            {
                defaultProvider = new LangProvider()
                {
                    LangType = Application.Current.GetType()
                };
            }
            return defaultProvider;
        }
        set
        {
            defaultProvider = value;
        }
    }

    /// <summary>
    /// 表示语言包对象的静态属性名
    /// </summary>
    public StaticExtension Key { get; set; }

    public LangProvider Provider
    {
        get
        {
            if (null == provider)
            {
                provider = DefaultProvider;
            }
            return provider;
        }
        set => provider = value;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (null == Key)
        {
            return this;
        }

        var binding = new Binding();
        binding.Source = Provider;
        binding.Path = new PropertyPath($"[{Key.Member}]");
        return binding.ProvideValue(serviceProvider);
    }
}
Xaml
<UserControl x:Class="MetroDemo.Views.ButtonView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:demo="http://github.com/MetroDemo"
             prism:ViewModelLocator.AutoWireViewModel="True" 
             mc:Ignorable="d" Style="{StaticResource Controls.UserControl.Demo}"
             Height="450" Width="400">
    <StackPanel>
        <Button 
            Click="ButtonBase_OnClick"
            Content="{Lang Key={x:Static demo:Lang.Controls_Button}}" />
    </StackPanel>
</UserControl>
后台代码
public partial class ButtonView : UserControl
{
    public ButtonView()
    {
        InitializeComponent();
        // 如果,你是在主程序中创建的语言包,则此代码可以省略。
        // 当前演示代码项目,语言包放在了单独的类库中,所以,需要指定默认语言代理
        LangExtension.DefaultProvider = new System.Windows.Resources.LangProvider()
        {
            LangType = typeof(Lang)
        };
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        LangExtension.DefaultProvider.Culture = new CultureInfo("en");
    }
}

 

参考:

HandyControl

posted @ 2021-08-10 15:51  2324736194  阅读(1492)  评论(1编辑  收藏  举报