WPF Toolkit PropertyGrid 表示名やカテゴリー名のローカライズ (国際化/多言語対応)

WPF Toolkit PropertyGrid 表示名やカテゴリー名のローカライズ (国際化/多言語対応)

こんにちは、kenzauros です。

Xceed Software 社の Extended WPF Toolkit に含まれる PropertyGrid は自作アプリなどでプロパティーを設定する UI を提供するために非常に有用です。

Extended WPF Toolkit はライセンス的にも Microsoft Public License (MS-PL) で提供されるため、使いやすいです。

ただし、この PropertyGrid 、公式 Wiki もそこそこ充実しているのですが、やはり凝った使い方をするにはオンラインの断片的な情報をつなぎ合わせるしかありません。

その中でも一筋縄で行かなかったのが、表示名やカテゴリー名のローカライズ、つまり国際化対応です。

概要

やりたいこと

localization for wpf toolkit propertygrid 1

左のように英語表記だったものを右のような日本語表記に変えたいのです。

WPF の国際化対応について

WPF アプリケーションのローカライズに関しては、この記事では割愛しますので、 MS か先達の記事を参照ください。

プロジェクトに Resources.resx Resources.ja-JP.resx が配置済み、ソースコードから Resources.リソース名 で文字列が取得できるようになっているものとします。

PropertyGrid の表示制御とローカライズ

PropertyGrid の表示制御に必要な属性

PropertyGrid で要素の表示を制御するために必要な属性は主に下記の通りです。

属性 指定対象 用途 国際化の要否
CategoryAttribute プロパティー カテゴリー名の設定 要
DisplayNameAttribute プロパティー プロパティーの表示名 要
DescriptionAttribute プロパティー プロパティーの説明 要
PropertyOrderAttribute プロパティー プロパティーの並び順指定 不要
BrowsableAttribute プロパティー プロパティーの非表示指定 不要
CategoryOrderAttribute クラス カテゴリーの並び順指定 要

※ CategoryOrderAttribute のみ Xceed.Wpf.Toolkit PropertyGrid の独自属性です。

通常、それぞれの属性の指定は下記のようになります。

[CategoryOrder("General", 1)]
internal class AwesomeClass {

    [Category("General"), PropertyOrder(1)]
    [DisplayName("Your Name")]
    [Description("Input your name in English")]
    public string Name { get; set; }

    [Category("General"), PropertyOrder(2)]
    [DisplayName("Your Age")]
    [Description("Input your age")]
    public int Age { get; set; }

}

Browsable や PropertyOrder はその性質上、多言語化を考慮する必要はありません。 CategoryOrder はカテゴリー名自体が多言語化されるため、並び順指定に使うカテゴリー名も国際化に合わせて変えてやる必要があります。

ということで、前述の表で “要” となっている 4 つのクラスについて多言語化を行います。

以下、詳細を説明しますが、ソースコードは Gist にも置いていますので、参照ください。

Extended WPF Toolkit PropertyGrid ローカライズ (国際化/多言語対応) - Gist

Resources のラッパークラス作成

Visual Studio が生成する Resources クラスは Resources.MyText のような感じで静的にリソース名を指定するしかできません。 Resources.Get("MyText") のようにキーを指定してリソースを取得できないのです。

そのため、まず Resources クラスからキー指定で文字列を取得する静的なラッパークラスを作成します。本来ならパーシャルクラスで定義したいところですが、クラスに partial がついていないため、しかたなく別クラスにしています。

名前はなんでもいいのですが、ここでは LocalizedResources としました。

internal static class LocalizedResources
{
    readonly static ResourceManager _ResourceManager = new ResourceManager(typeof(Resources));

    public static string GetString(string resourceKey)
    {
        return _ResourceManager.GetString(resourceKey) ?? resourceKey;
    }
}

中身は ResourceManager.GetString() をプロキシしているだけの単純なユーティリティークラスです。

この LocalizedResources.GetString() でリソースからローカライズされた文字列が取得できるので、各属性クラスを継承したクラスからこれを利用します。

ローカライズに対応した属性クラスの作成

LocalizedResources ができてしまえば、継承クラスの実装は極めてシンプルです。

internal class LocalizedCategoryAttribute : CategoryAttribute
{
    public LocalizedCategoryAttribute(string resourceKey)
        : base(LocalizedResources.GetString(resourceKey)) { }
}

internal class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    public LocalizedDisplayNameAttribute(string resourceKey)
        : base(LocalizedResources.GetString(resourceKey)) { }
}

internal class LocalizedDescriptionAttribute : DescriptionAttribute
{
    public LocalizedDescriptionAttribute(string resourceKey)
        : base(LocalizedResources.GetString(resourceKey)) { }
}

internal class LocalizedCategoryOrderAttribute : CategoryOrderAttribute
{
    public LocalizedCategoryOrderAttribute(string resourceKey, int order)
        : base(LocalizedResources.GetString(resourceKey), order) { }
}

つまり継承クラスのコンストラクターで国際化済みの文字列を取得し、もともとの属性クラスのコンストラクターに渡して初期化しているだけです。

ローカライズ済み属性の適用

新しい属性クラスができたら、既存の属性指定を Localized なんたらに置き換えます。当然ながら、引数には Resources.resx で定義したリソース名を指定してください。

[LocalizedCategoryOrder("Category_General", 1)]
internal class AwesomeClass {

    [LocalizedCategory("Category_General"), PropertyOrder(1)]
    [LocalizedDisplayName("Property_Name_DisplayName")]
    [LocalizedDescription("Property_Name_Description")]
    public string Name { get; set; }

    [LocalizedCategory("Category_General"), PropertyOrder(2)]
    [LocalizedDisplayName("Property_Age_DisplayName")]
    [LocalizedDescription("Property_Age_Description")]
    public int Age { get; set; }

}

これで Resources.ja-JP.resx にも Category_General などを定義すれば、無事 PropertyGrid で日本語化ができているはずです。

またこの Localized なんたらなクラスを応用すれば、多言語化だけでなく、もっと柔軟に Category や Description を変化させることもできるでしょう。

関連情報

先人たちの取り組み

このローカライズのためのアプローチは、 10 年以上前からさまざまな人が取り組んでいます。

最も役にたったのは下記の Brian Lagunas 氏の 2015 年の記事でした。

継承クラスで ResourceManager からリソースを取得してくるというアイディアがグッドです。国際化の例が日本語なのも好印象です(笑)

余談ですが、彼は Infragistics 社の Senior Product Owner みたいです。 Infragistics 社にとって日本も重要なマーケットであることがうかがい知れます。

彼は各クラスのメンバーをオーバーライドして実装していたのですが、それぞれオーバーライドするメンバーが異なるので、少々煩雑なイメージがありました。

そこで ResourceManager の部分を別クラスに切り出し、各継承クラスはコンストラクターのみオーバーライドするだけで実現できるようにしたのが、今回の記事です。

しかし今あらためて見返すと他の参照記事に似たようなアイディアがありましたね… 自分で実装してみないとわからないものです😭

DisplayAttribute というのもある

ちなみに表示名だけでよければ、リソースを使用できる DisplayAttribute という属性クラスもあります。

DisplayAttribute は下記のように利用します。

[Display(ResourceType = typeof(Resources), Name = "Property_Name_DisplayName")]

これでもたしかに表示名が国際化できました。

あと、実はこの DisplayAttribute には Description プロパティーや GroupName プロパティーなどもあります。

[Display(ResourceType = typeof(Resources), Name = "Property_Name_DisplayName", Description = "Property_Name_Description", GroupName = "Category_General")]

こんな感じで指定すれば、全部リソースからイイ感じにとってきてくれる…ことが期待されます。が、残念ながら私の環境では GroupName などを指定すると PropertyGrid の表示が途中で止まってしまい、動作しませんでした。

WPF Toolkit の PropertyGrid が対応していないだけなのかもしれませんが、ソースを追う気力はなかったので、諦めました。残念です。

まぁ、今回の記事のソースコードは非常にシンプルですので、現状ではベターな解決法だと思います。

kenzauros