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 的做法。才有了通过代理的方式来关联视图和资源静态属性的方法。
代理类要求如下:
- 能够同步更新视图数据,也就是说需要实现接口 INotifyPropertyChanged 或有静态属性变化通知事件
- 具备和 Lang.cs 一样的资源属性名称,读取本身属性的值,指向的是 Lang 类中相同名称的静态属性
- 定义一个 CultureInfo 类型的属性 Culture,默认值和系统一致。
- 改变 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 这个类非常死板,不够灵活。每次添加一个资源属性,必须手动在里面创建一个相同的属性。
为了使代理类能够具备上述的优点。我们更新对代理类的要求,如下
- 能够同步更新视图数据
- 能够通过指定 Key 来获取与 Lang 类中相同的静态属性名对应的静态属性值。
- 改变 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"); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现