WPF 运行时本地化






4.93/5 (50投票s)
具有运行时更新和 Visual Studio 设计时支持的 WPF 本地化解决方案。
更新 (2018): 已更新至 VS2017,示例还包含一个卫星程序集,以展示此方法如何 扩展以管理更复杂的设置。
更新: 在 Kingsley Moore 下方的评论非常有帮助,使得解决方案变得更加简单易于添加到应用程序后,本文已重写。
目录
引言
本文是又一次尝试,旨在以一种简单且易于维护的方式解决 WPF 应用程序本地化的问题。
在这种情况下,我还希望能够
- 在运行时切换文化 - 自动更新所有本地化元素
- 使用现有的资源文件结构(*.resx 文件),这些文件可以在 Visual Studio 中维护
- 为 Expression Blend(以及其他 XAML 设计应用程序)中的本地化元素保留设计时支持
示例应用程序中有一些本地化字符串来演示其工作原理,但由于演示本地化字符串的方式只有这么多,因此我保持了简短。
本文最初提供的实现需要一个包装器类来包装属性集,手动或自动生成包装器类都很麻烦。此更新消除了对该包装器的需求,从而使其更易于使用。
背景
关于这个主题,已经有许多其他文章,包括 使用 Locbaml 本地化 WPF 应用程序,其中涵盖了本地化 XAML 文件的不同方法,每种方法都有其优缺点。该文章中的第一个方法(不使用 LocBaml.exe 进行定向本地化)给了我启发,但为了触发运行时自动更新(针对所有元素,即使是现有窗口),我必须偏离该方法。
另一个早期项目是 WPF 运行时多语言,它确实为运行时自动更新提供了解决方案,但在我看来,这增加了语言资源文件管理的复杂性。通过在本文中维护对现有*.resx文件的支持,我们仍然可以使用现有应用程序轻松地将应用程序翻译成新的文化。
我强烈建议阅读并理解这两篇文章,那里有很多信息,以及对我在其他项目中非常有帮助的有用技巧。
Using the Code
使用 ObjectDataProvider 自动更新
为了在更改当前文化时实现自动更新,我利用了 ObjectDataProvider 的特性。MSDN 页面中的一个关键信息是
"当您想用另一个对象替换当前绑定源对象,并让所有相关的绑定都得到更新时,此类也很有用。"
所以,我们只需要替换(或刷新)ObjectDataProvider
对象实例,并且 ODP 上的任何绑定都将自动更新。
这是此解决方案得到改进的地方。虽然可以直接绑定到自动生成的 RESX 设计器类(Resources.Designer.cs)的属性,但我们还需要为 ODP 获取该类的实例。所有 ResXFileCodeGenerator
(默认和自定义)都将 Resources
构造函数标记为 internal,这意味着它只能从同一程序集内部访问。(这也意味着构造函数无法从 XAML 访问 - 即使用 ODP ObjectType
。)为了解决这个问题,我们可以使用 ODP 上的 MethodName
属性。(另一种选择可能是扩展现有的自定义 ResXFileCodeGenerator
以将构造函数标记为 public
,但这没有必要)。。。
<ObjectDataProvider x:Key="Resources"
ObjectType="{x:Type cultures:CultureResources}"
MethodName="GetResourceInstance"/>
... 使用方法
public Properties.Resources GetResourceInstance()
{
return new Properties.Resources();
}
使用 MethodName
意味着 ODP 将成为方法返回的对象,允许我们绑定 Resources
类的实例。我们可以创建此实例,因为对内部构造函数的调用来自同一程序集内部,而不是直接来自 XAML。效果非常好。
此功能生效的一个限制是 Resources
类必须是 public
的,因为我们无法通过 public
方法返回 internal 类的实例(这会产生一个恼人的编译器错误)。这意味着我们可以使用 Visual Studio 2005 & 2008 中工作的 扩展强类型资源生成器[^],或者使用 Visual Studio 2008 附带的 PublicResXFileCodeGenerator
工具。我喜欢扩展代码生成器,因为它还可以生成非常有用的字符串格式化方法作为奖励。
public static void ChangeCulture(CultureInfo culture)
{
//remain on the current culture if the desired culture cannot be found
// - otherwise it would revert to the default resources set,
// which may or may not be desired.
if (pSupportedCultures.Contains(culture))
{
Properties.Resources.Culture = culture;
ResourceProvider.Refresh();
}
else
Debug.WriteLine("Culture [{0}] not available", culture);
}
更新当前文化非常简单,我在 CultureResources
类中添加了一个方法,该方法会更新当前资源的 Culture 并触发 ObjectDataProvider
的更新,使其调用 GetResourceInstance
方法,更新 ODP 的 ObjectInstance
,从而刷新 ODP 上的任何绑定 - 这些绑定会更新为新的资源值。
设计时支持
在设计时,Properties.Resources.Culture
最初被设置为项目中的中性语言,或者如果未设置中性语言,则设置为当前线程的 Culture。无论哪种情况,任何绑定都将默认使用默认资源文件(Resources.resx)中的字符串。
添加本地化字符串
所有需要本地化的字符串都需要定义在所有资源文件中才能进行本地化,因此通常在设置好默认Resources.resx文件后添加更多文化会更容易。否则,您需要将每个新字符串添加到所有现有的 RESX 文件中。
然后,我们可以将绑定添加到所需的 UI 元素
<Label x:Name="labelCultureName"
Content="{Binding Path=LabelCultureName, Source={StaticResource Resources}}"/>
如果我们已将此资源字符串添加到默认Resources RESX 文件中,重新编译项目后,此默认字符串值现在应显示在设计器中,当然在运行应用程序时也会显示。
如果您发现为默认值以外的Resources文件添加的字符串似乎总是显示默认值,请检查每个 RESX 文件中的资源字符串名称是否正确。如果存在绑定错误,则 Binding 中设置的 Path 与任何 RESX 文件中的字符串都不匹配,甚至无法像前一种情况那样回退到默认 RESX 值。
添加更多文化
向项目中添加另一个文化的简单方法是,在 Visual Studio 中复制粘贴默认的Resources.resx文件以创建新文件。从 MSDN CultureInfo 参考页上的列表中选择一个合适的文化代码。在扩展名中添加文化代码,例如 Resources.Fr-fr.resx,Visual Studio 将在构建应用程序时使用它来创建本地化 DLL。现在您有了一个新的 RESX 文件,可以更改新文化的资源值,就完成了。
枚举可用文化
为该项目添加了多种文化后,可以演示用于枚举我们已实现的文化的代码。我已将其动态化,以避免在添加新文化时需要重新构建应用程序。对于已安装的现有副本,您只需创建一个名为新文化名称的文件夹,并将新命名正确的资源 DLL 放入其中。重新启动应用程序即可使用(或者,如果您是从应用程序内部导入文化,则可以修改此方法以再次搜索安装目录)。
Debug.WriteLine("Get Installed cultures:");
CultureInfo tCulture = new CultureInfo("");
foreach (string dir in Directory.GetDirectories(Application.StartupPath))
{
try
{
//see if this directory corresponds to a valid culture name
DirectoryInfo dirinfo = new DirectoryInfo(dir);
tCulture = CultureInfo.GetCultureInfo(dirinfo.Name);
//determine if a resources DLL exists in this directory that
//matches the executable name
if (dirinfo.GetFiles(Path.GetFileNameWithoutExtension
(Application.ExecutablePath) + ".resources.dll").Length > 0)
{
pSupportedCultures.Add(tCulture);
Debug.WriteLine(string.Format(" Found Culture: {0} [{1}]",
tCulture.DisplayName, tCulture.Name));
}
}
//ignore any ArgumentExceptions generated for non-culture
//directories in the bin folder
catch { }
}
以上是检查应用程序bin目录中与文化名称匹配的文件夹的一种相对快捷的方法。CultureInfo.GetCultureInfo
方法将在字符串参数与任何已定义的 CultureInfo
类型不匹配时按预期失败。
UserControl 的设计时解决方案
在我看来,UserControl
以一种微妙的方式出现了一个问题。如果需要本地化的属性在 UserControl 外部可访问(作为依赖属性添加到代码隐藏文件中),那么没有问题,您可以按上述方式进行本地化。但是,如果需要本地化的属性不可外部访问,例如 Label Content
属性,那么解决方案会有点棘手。
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WPFLocalize.UserControl2"
x:Name="UserControl">
<Grid x:Name="LayoutRoot">
<Label x:Name="labelUserControl2"
Content="{Binding Path=LabelCultureName,
Source={StaticResource Resources}}"/>
</Grid>
</UserControl>
当您在 UserControl 中的标签上添加绑定(如上所示)时,它将在运行时以及设计时(例如在 Blend 中)自行加载时正确渲染。不幸的是,当您加载包含该 UserControl
的窗口时,它将无法正确渲染。(这似乎只在 Expression Blend 中是问题,Visual Studio 2008 设计器在这种情况下会正确渲染)。
我理解当将 UserControl
加载为 Window 的子项时出现的问题是,设计器会创建该控件的实例,然后将其添加到 Window。在运行时可用的资源不存在,因为实例不是从 Window 内部创建的,因此上述绑定失败,控件无法渲染。在尝试解决此情况的许多失败之后,我最终想出了以下方法
public UserControl2()
{
#if DEBUG
//only perform the following fix if we are in the designer
// - the default ctor is not executed when editing the usercontrol,
// but is executed when usercontrol has been added to a window/page
// NB. The Visual Studio designer might return null for Application.Current
// http://msdn.microsoft.com/en-us/library/bb546934.aspx
if (DesignerProperties.GetIsInDesignMode(this) && Application.Current != null)
{
Uri resourceLocater =
new System.Uri("/WPFLocalize;component/ResourceDictionary1.xaml",
UriKind.Relative);
ResourceDictionary dictionary =
(ResourceDictionary)Application.LoadComponent(resourceLocater);
//add the resourcedictionary containing our Resources ODP to
//App.Current (which is the Designer / Blend)
if (!Application.Current.Resources.MergedDictionaries.Contains(dictionary))
Application.Current.Resources.MergedDictionaries.Add(dictionary);
}
#endif
this.InitializeComponent();
}
使用 DesignerProperties.GetIsInDesignMode()
意味着此代码仅在设计时执行,而它所做的只是将包含我们的 Resources ObjectDataProvider
的 ResourceDictionary
添加到设计器本身,以便在 UserControl
初始化时可用。这实际上将是 ODP 的第二个实例,这在运行时是不好的(因为只有包含在 App.xaml 中的第一个实例会被更新),但在设计时是没问题的,因为我们不会更新文化。问题解决了。
限制
在此示例中,我使用的是 WPF Bindings,它需要依赖属性来绑定。在某些情况下,您可能需要访问这些属性,但添加绑定不合适或不容易实现。例如,当您想直接从代码访问本地化值时。为了在这种情况下保持自动更新正常工作,您可以挂钩 ObjectDataProvider DataChanged
事件的处理程序,该事件在我们更新 ODP 后触发。因此,当在 eventhandler
中重新获取值时,更新的资源值就可用了。或者,您可以确保在知道 ODP 已更新后重新获取本地化值,差别不大。
ResourceCultureProvider.DataChanged +=
new EventHandler(ResourceCultureProvider_DataChanged);
void ResourceCultureProvider_DataChanged(object sender, EventArgs e)
{
Debug.WriteLine
("ObjectDataProvider.DataChanged event. New culture [{0}]",
CurrentResourceCulture.LabelCultureName));
}
历史
- 2008 年 1 月 - 首次发布
- 2008 年 5 月 - 重写,消除了对包装器类的需求
- 2010 年 2 月 - 更新了链接
- 2018 年 6 月 - 更新了 VS2017 的示例