WPF的本地化是个很常见的功能,我作过的WPF程序大部分都实现了本地化(无论最终有没有用到)。一般本地化有如下几点需求:html
其中只有第一点是必要的。
第二点最好也能够实现,不少时候切换语言只为了看看某个专业术语在英语中的原文是什么,或者临时打印个英文报表,平时使用仍是用中文,用户不想为了这点重启程序。
第三点和第四点虽然很常见,但我历来没实现过,毕竟文字资源(有时还有少许图片)占用的空间不会太多,大部分WPF程序都没有大到须要考虑安装包大小,全部语言的资源所有打包进一个安装包就能够了。git
WPF本地化技术很成熟,也有几种方案,微软在MSDN给出了详细的介绍WPF 全球化和本地化概述,还有一份古老的文档WPF Localization Guidance,整整66页,里面详细介绍了各类WPF本地化的机制。github
本文只介绍两种实现以上第一、2点需求的方案。express
对WPF开发者来讲,资源词典确定不会陌生。不过在资源词典里使用string可能比较少。windows
<Window x:Class="LocalizationDemoWpf.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:LocalizationDemoWpf" mc:Ignorable="d" xmlns:system="clr-namespace:System;assembly=mscorlib" Title="Window1" Height="300" Width="300"> <Window.Resources> <system:String x:Key="Chinese">中文</system:String> </Window.Resources> <Grid> <TextBlock Text="{DynamicResource Chinese}"/> </Grid> </Window>
如以上代码所示,在XAML中定义string资源须要先引入 xmlns:system="clr-namespace:System;assembly=mscorlib"
命名空间,以后再使用DynamicResource引用这个资源。不要使用StaticResource,这样无法作到动态切换语言。api
要使用资源词典实现本地化,须要先建立所需语言的xaml,我在DEMO中建立了en-us.xaml和zh-cn.xaml两个资源词典,里面的包含的资源结构一致(指数量和Key同样):编辑器
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:local="clr-namespace:LocalizationDemoWpf"> <system:String x:Key="SwitchLanguage">切换语言</system:String> <system:String x:Key="Chinese">中文</system:String> <system:String x:Key="English">英文</system:String> <system:String x:Key="Username">用户名</system:String> <system:String x:Key="Sex">性别</system:String> <system:String x:Key="Address">地址</system:String> <SolidColorBrush x:Key="Background" Color="#88FF0000"/> </ResourceDictionary>
在程序启动时根据CultureInfo.CurrentUICulture或配置项选择对应的资源词典,使用MergedDictionaries的方式加载到程序的资源集合中:工具
var culture = ReadCultureFromConfig(); var cultureInfo = new System.Globalization.CultureInfo(culture); Thread.CurrentThread.CurrentUICulture = cultureInfo; Thread.CurrentThread.CurrentCulture = cultureInfo; ResourceDictionary dictionary = new ResourceDictionary { Source = new Uri($@"Resources\{culture}.xaml", UriKind.RelativeOrAbsolute) }; Application.Current.Resources.MergedDictionaries[0] = dictionary;
这样本地化的功能就完成了。布局
其实上述方案已实现了动态切换语言。
XAML资源的引用原则是就近原则,这个就近不只指VisualTree上的就近,还指时间上的就近。后添加进资源词典的资源将替换以前的同名资源。使用DynamicResource而不是StaticResource,就是为了在资源被替换时能实时变动UI的显示。性能
VisualStudio的XAML设计时支持对开发WPF程序相当重要,对本地化来讲,设计时支持主要包含3部分:
使用资源词典实现本地化,只需在App.xaml中合并对应的资源词典便可得到完整的设计时支持。
<Application x:Class="LocalizationDemoWpf.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:LocalizationDemoWpf" xmlns:resource="clr-namespace:LocalizationDemoWpf.Resource;assembly=LocalizationDemoWpf.Resource" StartupUri="MainWindow.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/LocalizationDemoWpf;component/Resources/zh-cn.xaml"/> <!--<ResourceDictionary Source="/LocalizationDemoWpf;component/Resources/en-us.xaml"/>--> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
这段XAML只是为了提升设计时体验,没有也能经过编译。
在代码中访问资源比较麻烦,须要知道资源的名称,并且没有智能感知,若是资源词典由第三方类库提供就会更麻烦。
var message = TryFindResource("SwitchLanguage") as string; if (string.IsNullOrWhiteSpace(message) == false) MessageBox.Show(message);
private void OnReplaceString(object sender, RoutedEventArgs e) { _totalReplace++; string content = "Replace " + _totalReplace; Resources["StringToReplace"] = content; }
如上所示,在代码中替换资源十分简单,不过这种简单也带来了资源不可控的问题。
上面有提过,在获取第三方类库中某个资源十分麻烦,不只如此,连得到第三方类库中的资源词典名称都十分麻烦。我建议在类库中定义以下的类,能够给开发者提供一些方便:
public static class Resources { public static Uri EnglishResourceUri { get; } = new Uri("/LocalizationDemoWpf.Resource;component/Resource.en-us.xaml", UriKind.RelativeOrAbsolute); public static Uri ChineseResourceUri { get; } = new Uri("/LocalizationDemoWpf.Resource;component/Resource.zh-cn.xaml", UriKind.RelativeOrAbsolute); }
资源词典是实现本地化的一种很常见的方式,它有以下优势:
但这种方式的缺点也很多:
除此之外,在动态切换语言上还存在一些问题。下面这段XAML就无法作到动态切换语言:
<DataGrid Grid.Row="1" Margin="5"> <DataGrid.Columns> <DataGridTextColumn Header="{DynamicResource Username}"/> <DataGridTextColumn Header="{DynamicResource Sex}"/> <DataGridTextColumn Header="{DynamicResource Address}" Width="*"/> </DataGrid.Columns> </DataGrid>
在DataGridColumn的Header上作动态切换语言,须要写成DataTemplate的方式:
<DataGrid Grid.Row="2" Margin="5"> <DataGrid.Columns> <DataGridTextColumn > <DataGridTextColumn.HeaderTemplate> <DataTemplate > <TextBlock Text="{DynamicResource Username}"/ </DataTemplate> </DataGridTextColumn.HeaderTemplate> </DataGridTextColumn> <DataGridTextColumn > <DataGridTextColumn.HeaderTemplate> <DataTemplate > <TextBlock Text="{DynamicResource Sex}"/> </DataTemplate> </DataGridTextColumn.HeaderTemplate> </DataGridTextColumn> <DataGridTextColumn Width="*"> <DataGridTextColumn.HeaderTemplate> <DataTemplate > <TextBlock Text="{DynamicResource Address}"/> </DataTemplate> </DataGridTextColumn.HeaderTemplate> </DataGridTextColumn> </DataGrid.Columns> </DataGrid>
比起资源词典,我更喜欢使用Resx资源文件,不过这种方式语法复杂一些,并且也有很多小问题。
在VisualStudio中建立后缀名为resx的资源文件并打开,可在如下UI编辑资源文件的值(将访问修饰符改成public用起来方便些):
在修改资源文件的值后PublicResXFileCodeGenerator将自动建立对应的类并为每个键值添加以下代码:
/// <summary> /// 查找相似 Address 的本地化字符串。 /// </summary> public static string Address { get { return ResourceManager.GetString("Address", resourceCulture); } }
而后将这个资源文件复制粘贴一份,将名称改成“原名+.+对应的语言+.resx”的格式,而且将里面的值翻译成对应语言以下:
在UI上使用x:Static绑定到对应的资源:
<DataGridTextColumn Header="{x:Static local:Labels.Username}"/>
这样基本的本地化就完成了。不少控件库都是使用这种方式作本地化。除了字符串,resx资源文件还支持除字符串之外的资源,如图片、音频等。
可是这个方案只实现了最基本的本地化,并且最大的问题是只支持直接使用字符串,不支持TypeConverter,甚至也不支持除字符串之外的其它XAML内置类型(即Boolea,Char,Decimal,Single,Double,Int16,Int32,Int64,TimeSpan,Uri,Byte,Array等类型)。例如使用Label.resx中名为Background值为 #880000FF 的字符串为Grid.Background实现本地化:
Labels.designer.resx
/// <summary> /// 查找相似 #880000FF 的本地化字符串。 /// </summary> public static string Background { get { return ResourceManager.GetString("Background", resourceCulture); } }
MainWindow.xaml
<Grid Background="{x:Static local:Labels.Background}"/>
运行时报错:ArgumentException: “#88FF0000”不是属性“Background”的有效值。
这样资源文件的实用性大打折扣。固然,这个方案也不支持动态切换语言。
在Silverlight中已没有了x:Static的绑定方式,改成使用Binding实现本地化,这样虽然语法复杂一些,但更加实用。WPF固然也可使用这种方式。
首先, 建立一个类封装资源文件生成的类(在这个Demo中是Labels):
public class ApplicationResources { public ApplicationResources() { Labels = new Labels(); } public Labels Labels { get; set; } }
而后在App.xaml中将这个类做为资源添加到资源集合中,为了之后使用的语法简单些,我一般将Key取得很简单:
<Application x:Class="LocalizationDemoWpfUsingResource.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:LocalizationDemoWpfUsingResource" StartupUri="MainWindow.xaml"> <Application.Resources> <local:ApplicationResources x:Key="R" /> </Application.Resources> </Application>
最后在XAML中这样绑定:
<DataGridTextColumn Header="{Binding Labels.Username, Source={StaticResource R}}"/>
这样语法复杂一些,但也有不少好处:
麻烦的是,WPF彷佛不是很喜欢这种方式,VisualStudio会提示这种错误,毕竟资源文件中的属性都是static属性,不是实例成员。幸运的是编译一次这种错误提示就会消失。
将调用方式改成Binding之后就能够实现动态切换语言了。因为UI经过Binding获取资源文件的内容,能够经过INotifyPropertyChanged通知UI更新。将ApplicationResources 改造一下:
public class ApplicationResources : INotifyPropertyChanged { public static ApplicationResources Current { get; private set; } public ApplicationResources() { Current = this; Labels = new Labels(); } public Labels Labels { get; set; } public event PropertyChangedEventHandler PropertyChanged; public void ChangeCulture(System.Globalization.CultureInfo cultureInfo) { Thread.CurrentThread.CurrentUICulture = cultureInfo; Thread.CurrentThread.CurrentCulture = cultureInfo; Labels.Culture = cultureInfo; if (Current != null) Current.RaiseProoertyChanged(); } public void RaiseProoertyChanged() { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("")); } }
如今能够简单地切换语言了。
var culture = ReadCultureFromConfig(); var cultureInfo = new System.Globalization.CultureInfo(culture); ApplicationResources.Current.ChangeCulture(cultureInfo);
实现本地化的一个很麻烦的事情是如何在设计视图看到各类语言下的效果。在使用资源词典的方案中是经过在App.xaml中合并对应的资源词典:
<ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/LocalizationDemoWpf;component/Resources/zh-cn.xaml"/> <!--<ResourceDictionary Source="/LocalizationDemoWpf;component/Resources/en-us.xaml"/>--> </ResourceDictionary.MergedDictionaries>
在资源文件的方案中,须要在ApplicationResources中添加一个属性:
private string _language; /// <summary> /// 获取或设置 Language 的值 /// </summary> public string Language { get { return _language; } set { if (_language == value) return; _language = value; var cultureInfo = new CultureInfo(value); Thread.CurrentThread.CurrentUICulture = cultureInfo; Thread.CurrentThread.CurrentCulture = cultureInfo; Labels.Culture = cultureInfo; RaiseProoertyChanged(); } }
以后在App.xaml中就能够经过改变这个属性来改变设计时的UI的语言,在VS2017中连编译都不须要就能够改变设计视图的语言。
<local:ApplicationResources x:Key="R" Language="zh-CN"/>
在代码里访问资源文件的资源十分简单:
MessageBox.Show(Labels.SwitchLanguage);
资源文件要实现这个需求就一点都不有趣了,至少我从未在实际工做中作过。最大的难题是资源文件生成的类中的属性是静态属性,并且只有getter方法:
public static string StringToReplace { get { return ResourceManager.GetString("StringToReplace", resourceCulture); } }
咱们也能够建立一个派生类,强行替换对应的属性:
public class ExtendLabels : Labels { /// <summary> /// 获取或设置 StringToReplace 的值 /// </summary> public new string StringToReplace { get; set; } }
而后替换ApplicationResources中的Labels,而且触发PropertyChanged。不过这样会刷新全部UI上的字符串等资源,只为了替换一个字符资源代价有点大,幸亏通常来讲并不会太消耗性能。
private void OnReplaceString(object sender, RoutedEventArgs e) { _totalReplace++; string content = Labels.StringToReplace + " " + _totalReplace; if (_extendLabels == null) _extendLabels = new ExtendLabels(); _extendLabels.StringToReplace = content; ApplicationResources.Current.Labels = _extendLabels; ApplicationResources.Current.RaiseProoertyChanged(); }
只须要将资源文件的访问修饰符改成public,无需其它操做就能够方便地在程序集之间共享资源。
比起资源词典,资源文件还有一个很大的优点就是容易管理。Demo中只有一个名字Labels的资源文件,实际项目中能够按功能或模块分别创建对应的资源文件,解决了资源词典重名、互相覆盖、智能感知列表过长等问题。另外我推荐使用VS的扩展程序ResXManager管理全部资源文件。
它能够在一个UI里管理全部语言的资源文件,极大地方便了资源文件的使用。
对Resx资源文件,ReSharper也提供了良好的支持。
当须要为某个资源修改Key时,能够按“资源文件名称”+"."+"Key"来全局替换,一般这样已经足够放心。ReSharper更进一步,它提供了重命名功能。假设要将Labels的资源English重名为为Englishs,能够先在Labels.Designer.cs重命名,而后应用“Apply rename refactoring”选项:
这时全部引用,包括XAML都已应用新的名称:
不过最后仍需本身动手在资源文件编辑器中修改Key。
除此以外,若是在XAML中使用了错误的Key,ReSharper也有错误提示:
在某些场合,ReShaper还可以使用“Move To Resource”功能:
使用Resx资源文件实现本地化有以下优势:
缺点以下:
虽然不能直接支持LinearGradientBrush,但也不是彻底没有办法,只是复杂了许多,如分别对LinearGradientBrush的GradientStop作本地化:
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="Black" Offset="0"/> <GradientStop Color="{Binding Source={StaticResource R},Path=Labels.Background}" Offset="1"/> </LinearGradientBrush>
这篇文章只介绍了本地化的入门知识,其它还有不少本地化的要点,如验证信息中的本地化没有涉及。另外,本地化还可使用x:Uid方式或WPFLocalizeExtension等方式实现,这里就不详细介绍。
WPF 全球化和本地化概述里有介绍一些本地化的最佳作法,如UI上应该使用相对布局而非绝对布局、字体选择等,这里再也不累赘。
须要注意的是上述两种方案都不适用于CLR属性,这也是为何我一直强调UIElement的属性最好是依赖属性的缘由之一。
若有错漏请指出。
WPF 全球化和本地化概述
Silverlight 部署和本地化
WPFLocalizationExtension
WPF Localization Guidance
XAML Resources
CultureInfo 类
Supported languages