RGBとHSVの値を相互に変換してUIに表示するべく奮闘した。 散々調べたり試した結果、Bindingだと無限ループになってしまう。
入手先
一応動くが挙動がおかしい。
- Sliderが動かせない
- Sliderが不正な値になる
はじまり
まず、WPFではコントロールに対して直接値の代入はしないらしい。 Bindingを使ってやるのだとか。 BindingだのMVVMだのさっぱりわからないが、とにかくやってみた。
実装方法
方法 | 可能 | 備考 |
---|---|---|
Event + 代入 | ○ | Bindingではないけどいいの? |
Binding | × | 一つのSourceとTargetしか設定できない |
MultiBinding | × | 無限ループになる |
無限ループ問題
Eventの場合は無限ループを回避できる。 値の代入前後でEventの削除と再追加をすることで。
でも、Bindingでの回避方法はわからなかった。 とりあえず、BindingとMultiBindingを駆使して無理やり実装する方法を考えてみた。 無限ループになってしまうが、こんなバカなことをしたという黒歴史を残しておく。
BindingにおけるSourceとTargetの関係づけ
種類 | Source | Target | 備考 |
---|---|---|---|
Binding | 1 | 1 | |
MultiBinding | 多 | 1 | |
? | 1 | 多 | 複数の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,の分も用意する。
所感
とてつもなくバカなことをしている気がしてならない。 根本的に何かを勘違いしているのか、知らないのか。