imani-cの日記

WPFスタイルとテンプレートの本をAmazonで出しました。ちょっととっつきにくいこれらについて知りたいお方は、覗いてみてください。「WPF スタイル」で検索すると、トップに出ます。gRPCの本もあります。「gRPC入門」で検索するとすぐ見つかります。

WPF - TextBlockの語順を変更

TextBlockに表示されるメッセージ内の語順を変更したいことが時々あります。わかりやすいのは分数の表現です。日本語では「5分の1」と書き、分母が先に来ます。英語では「1 of 5」と書き、分子が先に来ます。

分子と分母がバインド先のプロパティにある場合、語順の変化を吸収するには何らかの工夫が必要です。両方のTextBlockを用意しておき言語によって切り替えれば簡単に実現できますが、それではどちらのTextBlockを使うかを決める別のプロパティが必要になります。できれば、そのような面倒で間違いやすいものに頼らず、言語対応で変更される文字列だけで制御したいものです。

以下では、直感的にわかりやすいMultiBindingを使った方法を紹介します。

目次

本来やりたいこと(でもできない)

上に掲げた分数の例では、本来なら、以下のように書きたいのです。

<TextBlock>
    <TextBlock.Text>
        <MultiBinding StringFormat="{Binding FractionFormat}">  // StringFormatではBindingが機能しない。
            <Binding Path="Molecule"/>
            <Binding Path="Denominator"/>
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>
// バインドしたい書式文字列
public string FractionFormat { get; } = Fraction is {0} of {1}";

MultiBindingのStringFormatに書式文字列を収めたプロパティをバインドできれば、何もせずとも語順変更を実現できるのですが、StringFormatにはBindingを指定できません。つまり、上のような書き方をしてもダメなのです。

解決策

書式文字列FractionFormatをバインドできるのはMultiBindingの中身のBinding要素だけなので、そちらに移動してしまいます。

<Window x:Class="MultiBindExtension.MainWindow" ...
        xmlns:local="clr-namespace:MultiBindExtension" />

    <Window.Resources>
        <local:MultiBindFormatConverter x:Key="MyConverter"/>   // このコンバータがポイント。
    </Window.Resources>

    <TextBlock>
        <TextBlock.Text>
            <MultiBinding Converter="{StaticResource MyConverter}">
                <Binding Path="FractionFormat"/>        // 書式をこちらに移動した。
                <Binding Path="Molecule"/>
                <Binding Path="Denominator"/>
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>
</Window>

移動しただけだと書式制御にならないので、コンバーターMultiBindFormatConverterを用いて、書式制御させます。このコンバーターのソースは以下のとおりです。

// コンバーターのソースコード
namespace MultiBindExtension
{
    public class MultiBindFormatConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
            => string.Format((string)values[0], values.Skip(1).ToArray());

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
            => throw new NotSupportedException();
    }
}

string.Format()に与える書式文字列として最初のBindingから得られた文字列を用いています。

この方法なら、FractionFormatを変更すれば、表示もそれに合わせて自動的に変わります。プログラム実行中に言語を切り替える場合には便利です。

例外処理を全くしていませんが、コンバーターの使い方に誤りがあれば、デバッグ段階でstring.Format()が例外を起こしてくれますので、ここでは特に処理しなくて大丈夫だと思い省略しました。