[WPF]本地化入门

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([email protected]"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

6. 源码

LocalizationDemo

时间: 2024-10-12 16:16:51

[WPF]本地化入门的相关文章

[UWP]本地化入门

1. 前言 上一篇文章介绍了各种WPF本地化的入门知识,这篇文章介绍UWP本地化的入门知识. 2. 使用resw资源文件实现本地化 在以前的XAML平台,resx资源文件是一种很方便的本地化方案,但在UWP中微软又再次推荐x:Uid方案,默认的资源文件也变成resw资源文件.虽然后缀名只差了一个字母,但使用方式完全不同.最主要的区别是resw资源文件不会创建对应的Designer.cs类,这就导致本地化的实现方案完全不同. 2.1 在XAML中实现本地化 在XAML中实现本地化的过程很简单.首先

WPF快速入门系列(1)——WPF布局概览

一.引言 关于WPF早在一年前就已经看过<深入浅出WPF>这本书,当时看完之后由于没有做笔记,以至于我现在又重新捡起来并记录下学习的过程,本系列将是一个WPF快速入门系列,主要介绍WPF中主要的几个不同的特性,如依赖属性.命令.路由事件等. 在正式介绍之前,我还想分享下为什么我又要重新捡起来WPF呢?之前没有记录下来的原来主要是打算走互联网方向的,后面发现互联网方向经常加班,又累,有时候忙的连自己写了什么都不知道的,所以后面机缘巧合地进了一家外企,在外企不像互联网行业那样,比较清楚,有更多的时

WPF 从入门到进阶资料

Tutorials on WPF A Guided Tour of WPF by Josh Smith I wrote a series of introductory WPF articles on The Code Project.  The goal of those articles is to bring someone with no WPF experience up-to-speed enough so that (s)he can fully understand how th

DotNetCore 3.0 助力 WPF本地化

概览 随着我们的应用程序越来越受欢迎,我们的下一步将要开发多语言功能.方便越来越多的国家使用我们中国的应用程序, 基于 WPF 本地化,我们很多时候使用的是系统资源文件,可是动态切换本地化,就比较麻烦了. 有没有一种方法既可以适用系统的资源文件,又能方便快捷的切换本地化呢? 实现思路 现在我们将要实现的是基于 DotNetCore 3.0 以上版本 and WPF 桌面应用程序模块化的多语言功能. 动态切换多语言思路: 把所有模块的资源文件添加到字典集合. 将资源文件里的key,绑定到前台. 通

WPF快速入门系列(8)——MVVM快速入门

一.引言 在前面介绍了WPF一些核心的内容,其中包括WPF布局.依赖属性.路由事件.绑定.命令.资源样式和模板.然而,在WPF还衍生出了一种很好的编程框架,即WVVM,在Web端开发有MVC,在WPF客户端开发中有MVVM,其中VM就相当于MVC中C(Control).在Web端,微软开发了Asp.net MVC这样的MVC框架,同样在WPF领域,微软也开发了Prism这样的MVVM框架.Prism项目地址是:http://compositewpf.codeplex.com/SourceCont

WPF快速入门系列(4)——深入解析WPF绑定

一.引言 WPF绑定使得原本需要多行代码实现的功能,现在只需要简单的XAML代码就可以完成之前多行后台代码实现的功能.WPF绑定可以理解为一种关系,该关系告诉WPF从一个源对象提取一些信息,并将这些信息来设置目标对象的属性.目标属性总是依赖属性.然而,源对象可以是任何内容,可以是一个WPF元素.或ADO.NET数据对象或自定义的数据对象等.下面详细介绍了WPF绑定中的相关知识点. 二.绑定元素对象 2.1 如何实现绑定元素对象 这里首先介绍绑定最简单的情况——绑定元素对象,即数据源是一个WPF元

WPF快速入门系列(5)——深入解析WPF命令

一.引言 WPF命令相对来说是一个崭新的概念,因为命令对于之前的WinForm根本没有实现这个概念,但是这并不影响我们学习WPF命令,因为设计模式中有命令模式,关于命令模式可以参考我设计模式的博文:http://www.cnblogs.com/zhili/p/CommandPattern.html.命令模式的要旨在于把命令的发送者与命令的执行者之间的依赖关系分割开了.对此,WPF中的命令也是一样的,WPF命令使得命令源(即命令发送者,也称调用程序)和命令目标(即命令执行者,也称处理程序)分离.现

WPF快速入门系列(3)——深入解析WPF事件机制

一.引言 WPF除了创建了一个新的依赖属性系统之外,还用更高级的路由事件功能替换了普通的.NET事件. 路由事件是具有更强传播能力的事件——它可以在元素树上向上冒泡和向下隧道传播,并且沿着传播路径被事件处理程序处理.与依赖属性一样,可以使用传统的事件方式使用路由事件.尽管路由事件的使用方式与传统的事件一样,但是理解其工作原理还是相当重要的. 二.路由事件的详细介绍 对于.NET中的事件,大家应该在熟悉不过了.事件指的在某个事情发生时,由对象发送用于通知代码的消息.WPF中的路由事件允许事件可以被

WPF快速入门系列(2)——深入解析依赖属性

一.引言 感觉最近都颓废了,好久没有学习写博文了,出于负罪感,今天强烈逼迫自己开始更新WPF系列.尽管最近看到一篇WPF技术是否老矣的文章,但是还是不能阻止我系统学习WPF.今天继续分享WPF中一个最重要的知识点——依赖属性. 二.依赖属性的全面解析 听到依赖属性,自然联想到C#中属性的概念.C#中属性是抽象模型的核心部分,而依赖属性是专门基于WPF创建的.在WPF库实现中,依赖属性使用普通的C#属性进行了包装,使得我们可以通过和以前一样的方式来使用依赖属性,但我们必须明确,在WPF中我们大多数