1. 前言 WPF的本地化是個很常見的功能,我做過的WPF程式大部分都實現了本地化(不管最終有沒有用到)。通常本地化有以下幾點需求: 在程式啟動時根據 "CultureInfo.CurrentUICulture" 或配置項顯示對應語言的UI。 在程式運行時可以動態切換UI語言(無需重啟程式)。 製作 ...
1. 前言
WPF的本地化是個很常見的功能,我做過的WPF程式大部分都實現了本地化(不管最終有沒有用到)。通常本地化有以下幾點需求:
- 在程式啟動時根據CultureInfo.CurrentUICulture或配置項顯示對應語言的UI。
- 在程式運行時可以動態切換UI語言(無需重啟程式)。
- 製作對應不同語言的安裝包。
- 通過下載語言包實現多種語言的本地化。
其中只有第一點是必要的。
第二點最好也可以實現,很多時候切換語言只為了看看某個專業術語在英語中的原文是什麼,或者臨時列印個英文報表,平時使用還是用中文,用戶不想為了這點重啟程式。
第三點和第四點雖然很常見,但我從來沒實現過,畢竟文字資源(有時還有少量圖片)占用的空間不會太多,大部分WPF程式都沒有大到需要考慮安裝包大小,所有語言的資源全部打包進一個安裝包就可以了。
WPF本地化技術很成熟,也有幾種方案,微軟在MSDN給出了詳細的介紹WPF 全球化和本地化概述,還有一份古老的文檔WPF Localization Guidance,整整66頁,裡面詳細介紹了各種WPF本地化的機制。
本文只介紹兩種實現以上第1、2點需求的方案。
2. 使用資源詞典
2.1 基本原理
對WPF開發者來說,資源詞典肯定不會陌生。不過在資源詞典里使用string可能比較少。
<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,這樣沒法做到動態切換語言。
要使用資源詞典實現本地化,需要先創建所需語言的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;
這樣本地化的功能就完成了。
2.2 動態切換語言
其實上述方案已實現了動態切換語言。
XAML資源的引用原則是就近原則,這個就近不僅指VisualTree上的就近,還指時間上的就近。後添加進資源詞典的資源將替換之前的同名資源。使用DynamicResource而不是StaticResource,就是為了在資源被替換時能實時變更UI的顯示。
2.3 設計時支持
VisualStudio的XAML設計時支持對開發WPF程式至關重要,對本地化來說,設計時支持主要包含3部分:
- 在編寫XAML時可以得到資源的智能感知
- 有完整的設計視圖
- 在不同語言之間切換
使用資源詞典實現本地化,只需在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只是為了提高設計時體驗,沒有也能通過編譯。
2.4 在代碼里訪問資源
在代碼中訪問資源比較麻煩,需要知道資源的名稱,而且沒有智能感知,如果資源詞典由第三方類庫提供就會更麻煩。
var message = TryFindResource("SwitchLanguage") as string;
if (string.IsNullOrWhiteSpace(message) == false)
MessageBox.Show(message);
2.5 在代碼里替換資源
private void OnReplaceString(object sender, RoutedEventArgs e)
{
_totalReplace++;
string content = "Replace " + _totalReplace;
Resources["StringToReplace"] = content;
}
如上所示,在代碼中替換資源十分簡單,不過這種簡單也帶來了資源不可控的問題。
2.6 在程式集之間共用資源
上面有提過,在獲取第三方類庫中某個資源十分麻煩,不僅如此,連獲得第三方類庫中的資源詞典名稱都十分麻煩。我建議在類庫中定義如下的類,可以給開發者提供一些方便:
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);
}
2.7 總結
資源詞典是實現本地化的一種很常見的方式,它有如下優點:
- 簡單易用,而且容易理解。
- XAML語法簡單。
- 資源可以是除string以外的類型,如SolidColorBrush。
但這種方式的缺點也不少:
- 難以管理,一旦資源過多,重名、互相覆蓋、智能感知列表過長等問題將極大地影響開發,就連保證不同語言間資源詞典里的資源數量一致都很麻煩。
- 在程式集之間難以共用,引用很簡單,但由於沒有智能感知將很難使用,而且不同程式集之間的資源同名更難以跟蹤。
除此以外,在動態切換語言上還存在一些問題。下麵這段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>
3. 使用Resx資源文件
3.1 基本原理
比起資源詞典,我更喜歡使用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”的有效值。
這樣資源文件的實用性大打折扣。當然,這個方案也不支持動態切換語言。
3.2 動態切換語言
在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}}"/>
這樣語法複雜一些,但也有很多好處:
- 支持TypeConverter,這樣就可以使用除String以外的其它類型。
- 支持Binding的其它功能,如IValueConverter。
麻煩的是,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);
3.3 設計時支持
實現本地化的一個很麻煩的事情是如何在設計視圖看到各種語言下的效果。在使用資源詞典的方案中是通過在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"/>
3.4 在代碼里訪問資源
在代碼里訪問資源文件的資源十分簡單:
MessageBox.Show(Labels.SwitchLanguage);
3.5 在代碼里替換資源
資源文件要實現這個需求就一點都不有趣了,至少我從未在實際工作中做過。最大的難題是資源文件生成的類中的屬性是靜態屬性,而且只有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();
}
3.6 在程式集之間共用資源
只需要將資源文件的訪問修飾符改為public,無需其它操作就可以方便地在程式集之間共用資源。
3.7 管理資源文件
比起資源詞典,資源文件還有一個很大的優勢就是容易管理。Demo中只有一個名字Labels的資源文件,實際項目中可以按功能或模塊分別建立對應的資源文件,解決了資源詞典重名、互相覆蓋、智能感知列表過長等問題。另外我推薦使用VS的擴展程式ResXManager管理所有資源文件。
它可以在一個UI里管理所有語言的資源文件,極大地方便了資源文件的使用。
3.8 ReSharper支持
對Resx資源文件,ReSharper也提供了良好的支持。
當需要為某個資源修改Key時,可以按“資源文件名稱”+"."+"Key"來全局替換,通常這樣已經足夠放心。ReSharper更進一步,它提供了重命名功能。假設要將Labels的資源English重名為為Englishs,可以先在Labels.Designer.cs重命名,然後應用“Apply rename refactoring”選項:
這時所有引用,包括XAML都已應用新的名稱:
不過最後仍需自己動手在資源文件編輯器中修改Key。
除此之外,如果在XAML中使用了錯誤的Key,ReSharper也有錯誤提示:
在某些場合,ReShaper還可使用“Move To Resource”功能:
3.9 總結
使用Resx資源文件實現本地化有如下優點:
- 資源管理方便。
- 容易在代碼中使用。
- 容易在程式集之間共用。
- 支持TypeConverter,這樣就可以使用除String以外的其它類型。
- 支持Binding的其它功能,如IValueConverter。
- 相容性好,Silverlight及之後的XAML技術都可以使用。
- 第三方工具支持。
- 支持圖片、音頻等資源。
缺點如下:
- XAML語法相對複雜。
- 不能直接應用於TypeConverter不支持的類型,例如LinearGradientBrush。
雖然不能直接支持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>
4. 結語
這篇文章只介紹了本地化的入門知識,其它還有很多本地化的要點,如驗證信息中的本地化沒有涉及。另外,本地化還可以使用x:Uid方式或WPFLocalizeExtension等方式實現,這裡就不詳細介紹。
WPF 全球化和本地化概述里有介紹一些本地化的最佳做法,如UI上應該使用相對佈局而非絕對佈局、字體選擇等,這裡不再累贅。
需要註意的是上述兩種方案都不適用於CLR屬性,這也是為什麼我一直強調UIElement的屬性最好是依賴屬性的原因之一。
如有錯漏請指出。
5. 參考
WPF 全球化和本地化概述
Silverlight 部署和本地化
WPFLocalizationExtension
WPF Localization Guidance
XAML Resources
CultureInfo 類
Supported languages