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 @   2324736194  阅读(1704)  评论(1编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示