やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Bindingで無限ループになってしまう

RGBとHSVの値を相互に変換してUIに表示するべく奮闘した。 散々調べたり試した結果、Bindingだと無限ループになってしまう。

Binding無限ループ

入手先

MEGA

一応動くが挙動がおかしい。

  • Sliderが動かせない
  • Sliderが不正な値になる

はじまり

まず、WPFではコントロールに対して直接値の代入はしないらしい。 Bindingを使ってやるのだとか。 BindingだのMVVMだのさっぱりわからないが、とにかくやってみた。

実装方法

方法 可能 備考
Event + 代入 Bindingではないけどいいの?
Binding × 一つのSourceとTargetしか設定できない
MultiBinding × 無限ループになる

無限ループ問題

Eventの場合は無限ループを回避できる。 値の代入前後でEventの削除と再追加をすることで。

でも、Bindingでの回避方法はわからなかった。 とりあえず、BindingとMultiBindingを駆使して無理やり実装する方法を考えてみた。 無限ループになってしまうが、こんなバカなことをしたという黒歴史を残しておく。

BindingにおけるSourceとTargetの関係づけ

種類 Source Target 備考
Binding
MultiBinding
複数のTargetにそれぞれ同一のSourceをBindingすることで解決できる
多対一+一対多の2段構えで解決できる

1対多の問題

1対多の場合、以下のような問題がある。

  • SourceもTargetもSetBindingできる型の要素である必要がある
  • 1となるSourceには全Targetのデータを取得できる内容を含める必要がある
  • Targetの数だけ専用のValueConverterを用意する必要がある

本当はデータクラスを自作して、それをSourceにしたい。 でも、そのデータクラスはSetBinding()できないから不可能。 そこで、SetBinding()できるクラスをデータの入れ物とする。 今回はTextBlockを用いた。

この問題は、そのまま多対多も持っている。

HSV⇔RGB相互変換するときのSourceとTarget

Source Target
R,G,B H,S,V
H,S,V R,G,B

Source(入力)が3つ、Target(出力)が3つ。すなわち多対多。

RだけではH,S,Vを算出できない。R,G,B3つ揃ってH,S,Vが算出できる。Sourceは3つ必要。 Rだけ変更してもH,S,Vの複数が変動しうる。Targetは3つ必要。

多対多の関係づけができない

WPFのBinding, MultiBindingでは多対多の関係づけができない。 1対1、1対多、しかできない。

そこで、Binding, MultiBindingを使って2段構えで多対多の関係づけをしてみた。 それがTextBlockを仲介する方法。

MultiBindingによる無理やりな実装

TextBlock仲介Binding

  • Slider[R,G,B]→TextBlockRGB→Slider[H,S,V]
  • Slider[H,S,V]→TextBlockHSV→Slider[R,G,B]

TextBlockにはそれぞれ、"[R],[G],[B],[H],[S],[V]"の値が入っている。 Slider[R,G,B]でRGB値を変更したときは、TextBlockRGBのほうにRGBHSV値を設定する。 Slider[H,S,V]でHSV値を変更したときは、TextBlockHSVのほうにRGBHSV値を設定する。 TextBlockRGBが変更されたら、Slider[H,S,V]のほうに値を設定する。 TextBlockHSVが変更されたら、Slider[R,G,B]のほうに値を設定する。

無限ループ問題

この方法は無限ループするため、思ったように動作しない。

イベント駆動と代入での方法なら、代入の前後にイベントの削除と再追加で無限ループを回避できる。 これと同じことができれば回避できると思うが…。

  • Slider[R,G,B]→TextBlockRGB→Slider[H,S,V]
  • Slider[H,S,V]→TextBlockHSV→Slider[R,G,B]

各要素の関連づけ

Binding

2種類のBindingを用いて以下のように関連づける。

  • MultiBinding
    • Slider[R,G,B]→TextBlockRGB
    • Slider[H,S,V]→TextBlockHSV
  • Binding
    • TextBlockRGB→Slider[H]
    • TextBlockRGB→Slider[S]
    • TextBlockRGB→Slider[V]
    • TextBlockHSV→Slider[R]
    • TextBlockHSV→Slider[G]
    • TextBlockHSV→Slider[B]

ValueConverter

SliderとTextの値を変換するためにValueConverterを間にはさんでいる。 以下のような入出力に値を変換するための処理である。

  • [R,G,B]→"[R],[G],[B],[H],[S],[V]"
  • "[R],[G],[B],[H],[S],[V]"→R

なので、実際は以下のようになる。

  • Slider[R,G,B]→(RgbConverter)→TextBlockRGB→(Converter[H,S,V])→Slider[H,S,V]
  • Slider[H,S,V]→(HsvConverter)→TextBlockHSV→(Converter[R,G,B])→Slider[R,G,B]

ValueConverterは以下の2種類8つが必要になる。

  • IMultiValueConverter
    • RgbToStrConverter [R,G,B]→"[R],[G],[B],[H],[S],[V]"
    • HsvToStrConverter [H,S,V]→"[R],[G],[B],[H],[S],[V]"
  • IValueConverter
    • StrToHConverter "[R],[G],[B],[H],[S],[V]"→H
    • StrToSConverter "[R],[G],[B],[H],[S],[V]"→S
    • StrToVConverter "[R],[G],[B],[H],[S],[V]"→V
    • StrToRConverter "[R],[G],[B],[H],[S],[V]"→R
    • StrToGConverter "[R],[G],[B],[H],[S],[V]"→G
    • StrToBConverter "[R],[G],[B],[H],[S],[V]"→B

各要素の実装

Slider[R,G,B]→TextBlockRGB

3つのSliderを入力として、1つのTextBlockへ出力する。 MultiBindingを用いる。

MultiBinding mbRgb = new MultiBinding();
mbRgb.Bindings.Add(new Binding() {
    Source = slider["R"],
    Path = new PropertyPath("Value"),
    Mode = BindingMode.OneWay,
    UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
});
mbRgb.Bindings.Add(new Binding() {
    Source = slider["G"],
    ...
});
mbRgb.Bindings.Add(new Binding() {
    Source = slider["B"],
    ...
});
mbRgb.NotifyOnSourceUpdated = true;
mbRgb.Converter = new RgbMultiValueConverter();
mbRgb.ConverterParameter = "mbRgb";
textBlockRgb.SetBinding(TextBlock.TextProperty, multiBindRgb);
RgbMultiValueConverter

3つのSliderから得たR,G,B値からH,S,Vを算出し、それら6つを文字列として返すクラス。 IMultiValueConverterを用いる。 OneWayなのでConvertBackは使わない。

public class RgbMultiValueConverter : IMultiValueConverter
{
    // [R,G,B] → "R,G,B,H,S,V"
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        double r = (double)values[0];
        double g = (double)values[1];
        double b = (double)values[2];
        var hsv = HsvRgbConverter.ToHsv((int)r, (int)g, (int)b);
        string hsvStr = string.Format(",{0},{1},{2}", (double)hsv.H, (double)hsv.S, (double)hsv.V);
        return string.Format("{0},{1},{2}", r, g, b) + hsvStr;
    }

    // "R,G,B,H,S,V" → [R,G,B]
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

TextBlockRGB→Slider[H,S,V]

1つのTextBlockを入力として、3つのSliderへ出力する。 これは、それぞれのSliderが、同じTextBlockを入力とすることで解決する。 以下はHSVのうちHのBindingである。

slider["H"].SetBinding(Slider.ValueProperty, new Binding() {
    Source = textBlockRgb,
    Path = new PropertyPath("Text"),
    Mode = BindingMode.OneWay,
    UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
    Converter = new TextBlockRgbToSliderHConverter(),
});

これを他のS,V,R,G,B,の分も用意する。

RgbMultiValueConverter

1つのTextBlockに含まれている文字列から、R,G,B,H,S,Vいずれか1つを返す。 以下はHSVのうちHのConverterである。 OneWayなのでConvertBackは使わない。

public class TextBlockRgbToSliderHConverter : IValueConverter
{
    // "R,G,B,H,S,V" → H
    public object Convert(object value, Type type, object parameter, CultureInfo culture)
    {
        return double.Parse(((string)value).Split(',')[3]);
    }

    // H → "R,G,B,H,S,V"
    public object ConvertBack(object value, Type type, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

これを他のS,V,R,G,B,の分も用意する。

所感

とてつもなくバカなことをしている気がしてならない。 根本的に何かを勘違いしているのか、知らないのか。