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