Prism を使用したComposite Application の多言語対応
PrismのWPFアプリを多言語対応する方法の例を書きたいと思います。多言語化ライブラリーとして WPFLocalizationExtension を使用し、.resx ファイルを使用した切り替えを行います。使用している環境は Win10, Visual Studio Community 2017, Prism.WPF V7.1.0.431, .NET4.5.2, DIコンテナとしてUnity が使われています。
WPFサンプルソフトの準備
今回使用するPrismのWPFサンプルは
github.com
の14番目である 14-UsingEventAggregator です。
14-UsingEventAggregatorをダウンロードして、VisualStudio で開き、Nuget で必要ライブラリーを復元してBuild&実行が可能な状態までもっていきます。
14-UsingEventAggregator は、UsingEventAggregator というメインのプロジェクトと ModuleA、ModuleB という2つのプラグイン用のクラスライブラリーがあります。
UsingEventAggregator の MainWindow には LeftRegion と RightRegion という2つの ContentControl エリアが設定されており、そこにModuleA の MessageView と ModuleB の MessageList がプラグインされることになります。
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <ContentControl prism:RegionManager.RegionName="LeftRegion" /> <ContentControl Grid.Column="1" prism:RegionManager.RegionName="RightRegion" /> </Grid></span>
プラグインしているのは UsingEventAggregator の App.xaml.cs に書かれている
protected override void ConfigureModuleCatalog (IModuleCatalog moduleCatalog) { moduleCatalog.AddModule<ModuleA.ModuleAModule>(); moduleCatalog.AddModule<ModuleB.ModuleBModule>(); }
になります。
ModuleA、ModuleBを名前指定でプラグインしていますが、特定のフォルダーを探索して組み込む場合には、サンプルソフトの 07-Modules – Directory にあるように、
protected override IModuleCatalog CreateModuleCatalog() { return new DirectoryModuleCatalog() { ModulePath = @".\Modules" }; }
とするとModulesフォルダーにあるDLLをプラグインするようになりますから、その時にはModuleA、ModuleBをBuildしたDLLをModulesフォルダーにコピーしておく必要があります。
プラグインされる場所は、それぞれのモジュールのModuleAModule.cs 、ModuleBModule.cs で regionManager に登録されています。
ModuleAModule.cs public void OnInitialized(IContainerProvider containerProvider) { var regionManager = containerProvider.Resolve<IRegionManager>(); regionManager.RegisterViewWithRegion ("LeftRegion", typeof(MessageView)); }
ModuleBModule.cs public void OnInitialized(IContainerProvider containerProvider) { var regionManager = containerProvider.Resolve<IRegionManager>(); regionManager.RegisterViewWithRegion ("RightRegion", typeof(MessageList)); }
左側に ModuleA、右側にModuleBがプラグインされ、SendMessageボタンを押すたびにModuleBのListボックスに文字が追加されていくというアプリケーションになります.
WPFLocalizationExtension の導入
.resxファイルを言語ごとに用意し、表示言語を切り替えるために、 WPFLocalizationExtensionを導入します。
Visual Studio 2017の NuGetの管理からWPFLocalizationExtension を UsingEventAggregator、ModuleA、ModuleB にインストールします。
github.com
resxファイルの追加
Propertiesに言語毎にResources ファイルを用意します。Porpertiesで右クリックし、追加、新しい項目からリソースファイルを追加します。その時のファイル名は言語名を含めたものします。日本語は Resources.ja-JP.resx、米語は Resources.en-US.resx となります。もともとあった Resources.resx は対応する言語のresxが無い時に使用されるので、通常は英語で書いておけばいいでしょう。
もともとのresxファイルを開いて、上部のタブにあるアクセス修飾子を Public にする必要があります。
例として、 UsingEventAggregator に resxファイルを追加し、次のような文字列を設定します。
ファイル 名前 値 Resources.resx TITLE Prism Unity Application(default) Resources.ja-JP.resx TITLE Prism Unity Application(ja-JP) Resources.en-US.resx TITLE Prism Unity Application(en-US)
それぞれのファイルのプロパティーの所はデフォルトのままで良いです。
ビルドアクション:埋め込みリソース
出力ディレクトリにコピー:コピーしない
ModuleA、ModuleB にも同様にして言語毎のresxファイルを作っておきます。ModuleA の resx にボタンの名称と、ModuleAからModuleBへ送る言葉を追加しておきます。
ファイル 名前 値 Resources.resx MESSAGE2SEND MESSAGE TO SEND SENDMESSAGE SEND MESSAGE Resources.ja-JP.resx MESSAGE2SEND 送信メッセージ SENDMESSAGE メッセージ送信 Resources.en-US.resx MESSAGE2SEND Message to Send SENDMESSAGE Send Message
ResX Manager
上記では1ファイル毎に文言を設定しましたが、ResX Manager という拡張機能を使用するとresxファイルを横断的に設定する事ができます。
Visual Studio 2017 の 拡張機能と更新プログラム から ResXManagerをダウンロードして組み込みます。インストールされると、ツールに Resx Manager が追加さされています。これを使用すると、エクセルファイルへの Export、inport 、外部サイトを使った翻訳機能もあります。
github.com
ResX Manager を起動すると、次のように複数の言語を同時に編集できます。
文字列のバインド
WPFLocalizationExtension を使用するために viewのXAMLに下記を追加します。
xmlns:lex="http://wpflocalizeextension.codeplex.com" lex:LocalizeDictionary.DesignCulture="en-US" lex:ResxLocalizationProvider.DefaultAssembly="ModuleA" lex:ResxLocalizationProvider.DefaultDictionary="Resources"
DesignCultureの値に応じて、デザイン時の表示言語が切り替えられます。
DefaultAssemblyにはそのプロジェクトのアッセンブリを指定します。上記はModuleAのものです。 UsingEventAggregator と ModuleB にも追加しておきましょう。
UsingEventAggregator のMainWindowでは次のようになります。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="UsingEventAggregator.Views.MainWindow" prism:ViewModelLocator.AutoWireViewModel="True" xmlns:lex="http://wpflocalizeextension.codeplex.com" lex:LocalizeDictionary.DesignCulture="en-US" lex:ResxLocalizationProvider.DefaultAssembly="UsingEventAggregator" lex:ResxLocalizationProvider.DefaultDictionary="Resources" Height="350" Width="500" MinWidth="500" MinHeight="350" Title="{lex:Loc TITLE}" >
上記では MainWindow のタイトルのバインドもしています。バインドは {lex:Loc 名前}で行う事ができます。
MainWindow に次のようなコンボボックスを追加しておくと、それだけで言語の選択ができます。
<ComboBox ItemsSource="{Binding MergedAvailableCultures, Source={x:Static lex:LocalizeDictionary.Instance}}" SelectedItem= "{Binding Culture, Source={x:Static lex:LocalizeDictionary.Instance}}" DisplayMemberPath="NativeName" />
ModuleA の MessageView のボタンの名前を MESSAGE2SEND にバインドします。
<Button Command="{Binding SendMessageCommand}" Content="{lex:Loc MESSAGE2SEND}" Margin="5"/>
ボタンが押された時には MessageViewの SendMessageCommand が呼ばれるので、そこの ModuleBへの転送文字列をresxから取り出した文字列に変更します。
その為に、値を取り出すstatic class を定義して置いて使用します。
//Resourcesから値を取り出すスタティッククラス public static class LocalizationProvider { public static T GetLocalizedValue<T>(string key) { return LocExtension.GetLocalizedValue<T> (Assembly.GetCallingAssembly().GetName().Name + ":Resources:" + key); } }
SendMessageに値を読み出す処理を追加します。
using WPFLocalizeExtension.Extensions; して
private void SendMessage() { //現在のcultureのresourcesを抜き出す Message = LocalizationProvider.GetLocalizedValue<string> ("MESSAGE2SEND"); _ea.GetEvent<MessageSentEvent>().Publish(Message); }
アプリの動作
これで、言語切り替えのコンボボックスで言語を選択すると、瞬時にバインドした文字列がその言語に切り替わります。(下記はregionの位置が分かるように、他にも少し修正しています。)
コードでresxから対応する言語の文字列を抜き出すのは、ログファイルへの書き込み言語の切り替え等で使えるでしょう。
まとめ
- WPFLocalizationExtensionを使う事で、モジュールを含めて簡単に言語切り替えができます。
- 言語毎のresxファイルの作成にはResXManagerを使うと便利です。
- コードでresxから文字列を抜き出すこともできます。
補足
1. View の InitializeComponent(); でエラーが出ることがあるかもしれません。その場合には、Resources.Designer.cs のコンストラクター
internal Resources() {
}
の internal を public にして build すると解決する事があります。
2. XAMLデザイナーを使用してコマンドのバインドを行う為には ViewModel に引数無しのコンストラクターが必要
3. コードでカルチャー切り替えを行う為に、カルチャリストを取得するのは下記。
ObservableCollection
4. バインドで key を省略すると 名前+"_Content" が選択される。
名前が MyButton で key が省略された場合({lex:Loc} は {lex:Loc MyButton_Content} と同じ。