午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、Binding+ConverterでRGBとHSVの相互変換したかったけど、できなかったのでこうなった

できなかったので依存関係プロパティのFrameworkPropertyMetadataのなかの PropertyChangedCallbackで変換用のメソッドを実行、無限ループ防止にはフラグを使用

結果

結果
結果



環境

コード

2023WPF/2023041710_ARGBHSV/2023041710_ARGBHSV at main · gogowaten/2023WPF
github.com

<Window x:Class="_2023041710_ARGBHSV.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_2023041710_ARGBHSV"
        mc:Ignorable="d"
        Title="MainWindow" Height="400" Width="400">
    <Grid>
    <StackPanel>
      <StackPanel Orientation="Horizontal" Height="200">
        <Border x:Name="MyBorderColor" Width="200" Height="200"/>
        <Slider x:Name="MySliderA" Maximum="255" Minimum="0" SmallChange="1" LargeChange="32" Orientation="Vertical" Value="255"/>
        <Slider x:Name="MySliderR" Maximum="255" Minimum="0" SmallChange="1" LargeChange="32" Orientation="Vertical"/>
        <Slider x:Name="MySliderG" Maximum="255" Minimum="0" SmallChange="1" LargeChange="32" Orientation="Vertical"/>
        <Slider x:Name="MySliderB" Maximum="255" Minimum="0" SmallChange="1" LargeChange="32" Orientation="Vertical"/>
        <Slider x:Name="MySliderH" Maximum="360" Minimum="0" SmallChange="1" LargeChange="30" Orientation="Vertical"/>
        <Slider x:Name="MySliderS" Maximum="1" Minimum="0" SmallChange="0.01" LargeChange="0.1" Orientation="Vertical"/>
        <Slider x:Name="MySliderV" Maximum="1" Minimum="0" SmallChange="0.01" LargeChange="0.1" Orientation="Vertical"/>
        <Button x:Name="MyButton" Content="白" Click="MyButton_Click"/>
        <Button Content="test" Click="Button_Click"/>
      </StackPanel>
      <StackPanel>
        <TextBlock Text="{Binding MainColor, StringFormat=color \= {0}}"/>
        <TextBlock Text="{Binding A, StringFormat=a 000}"/>
        <TextBlock Text="{Binding R, StringFormat=r 000}"/>
        <TextBlock Text="{Binding G, StringFormat=g 000}"/>
        <TextBlock Text="{Binding B, StringFormat=b 000}"/>
        <TextBlock Text="{Binding H, StringFormat=h 000}"/>
        <TextBlock Text="{Binding S, StringFormat=s 0.00}"/>
        <TextBlock Text="{Binding V, StringFormat=v 0.00}"/>
      </StackPanel>
    </StackPanel>
  </Grid>
</Window>


MainWindow.xaml.cs

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

//RGBとHSVの相互変換
//依存関係プロパティは、ARGBがbyte型、HSVはdouble型
//RGBのどれかを変更したらHSVを再計算
//HSVのどれかを変更したらRGBを再計算
//このとき無限ループにならないようにフラグで判定
namespace _2023041710_ARGBHSV
{

    public partial class MainWindow : Window
    {
        #region 依存関係プロパティ

        /// <summary>
        /// 目的の色
        /// </summary>
        public Color MainColor
        {
            get { return (Color)GetValue(MainColorProperty); }
            set { SetValue(MainColorProperty, value); }
        }
        public static readonly DependencyProperty MainColorProperty =
            DependencyProperty.Register(nameof(MainColor), typeof(Color), typeof(MainWindow),
                new FrameworkPropertyMetadata(Color.FromArgb(0, 0, 0, 0),
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));


        public byte R
        {
            get { return (byte)GetValue(RProperty); }
            set { SetValue(RProperty, value); }
        }
        public static readonly DependencyProperty RProperty =
            DependencyProperty.Register(nameof(R), typeof(byte), typeof(MainWindow),
                new FrameworkPropertyMetadata(byte.MinValue,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    new PropertyChangedCallback(OnRGB)));


        public byte G
        {
            get { return (byte)GetValue(GProperty); }
            set { SetValue(GProperty, value); }
        }
        public static readonly DependencyProperty GProperty =
            DependencyProperty.Register(nameof(G), typeof(byte), typeof(MainWindow),
                new FrameworkPropertyMetadata(byte.MinValue,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    new PropertyChangedCallback(OnRGB)));

        public byte B
        {
            get { return (byte)GetValue(BProperty); }
            set { SetValue(BProperty, value); }
        }
        public static readonly DependencyProperty BProperty =
            DependencyProperty.Register(nameof(B), typeof(byte), typeof(MainWindow),
                new FrameworkPropertyMetadata(byte.MinValue,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    new PropertyChangedCallback(OnRGB)));

        public byte A
        {
            get { return (byte)GetValue(AProperty); }
            set { SetValue(AProperty, value); }
        }
        public static readonly DependencyProperty AProperty =
            DependencyProperty.Register(nameof(A), typeof(byte), typeof(MainWindow),
                new FrameworkPropertyMetadata(byte.MinValue,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public double H
        {
            get { return (double)GetValue(HProperty); }
            set { SetValue(HProperty, value); }
        }
        public static readonly DependencyProperty HProperty =
            DependencyProperty.Register(nameof(H), typeof(double), typeof(MainWindow),
                new FrameworkPropertyMetadata(0.0,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    new PropertyChangedCallback(OnHSV)));
        public double S
        {
            get { return (double)GetValue(SProperty); }
            set { SetValue(SProperty, value); }
        }
        public static readonly DependencyProperty SProperty =
            DependencyProperty.Register(nameof(S), typeof(double), typeof(MainWindow),
                new FrameworkPropertyMetadata(0.0,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    new PropertyChangedCallback(OnHSV)));

        public double V
        {
            get { return (double)GetValue(VProperty); }
            set { SetValue(VProperty, value); }
        }
        public static readonly DependencyProperty VProperty =
            DependencyProperty.Register(nameof(V), typeof(double), typeof(MainWindow),
                new FrameworkPropertyMetadata(0.0,
                    FrameworkPropertyMetadataOptions.AffectsRender |
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    new PropertyChangedCallback(OnHSV)));

        /// <summary>
        /// HSVを再計算、R、G、B変更時に使用
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnRGB(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is MainWindow mw)
            {
                if (mw.isHSVChangNow) return;
                mw.isRGBChangNow = true;
                (mw.H, mw.S, mw.V) = MathHSV.RGB2hsv(mw.R, mw.G, mw.B);
                mw.isRGBChangNow = false;
            }
        }

        /// <summary>
        /// RGBを再計算、H、S、V変更時に使用
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnHSV(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is MainWindow mw)
            {
                if (mw.isRGBChangNow) return;
                mw.isHSVChangNow = true;
                (mw.R, mw.G, mw.B) = MathHSV.Hsv2rgb(mw.H, mw.S, mw.V);
                mw.isHSVChangNow = false;
            }
        }
        #endregion 依存関係プロパティ

        //無限ループ防止用フラグ
        private bool isRGBChangNow;
        private bool isHSVChangNow;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;

            SetSliderBindings();

            SetMyBindings();
            Loaded += (s, e) => { SetMainColorWhite(); };
            //Loaded += (s, e) => { A = 255; };
        }


        private void SetMyBindings()
        {
            //目的の色(MainColor)はARGBとのBindingで生成
            MultiBinding mb = new();
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(AProperty) });
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(RProperty) });
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(GProperty) });
            mb.Bindings.Add(new Binding() { Source = this, Path = new PropertyPath(BProperty) });
            mb.Converter = new ConverterARGB2Color();
            SetBinding(MainColorProperty, mb);

            //色確認用のBorderの背景色をMainColorとBinding
            MyBorderColor.SetBinding(BackgroundProperty, new Binding()
            {
                Source = this,
                Path = new PropertyPath(MainColorProperty),
                Converter = new ConverterColor2Brush()
            });
        }



        private void SetSliderBindings()
        {
            MySliderA.SetBinding(Slider.ValueProperty, new Binding() { Source = this, Path = new PropertyPath(AProperty) });
            MySliderR.SetBinding(Slider.ValueProperty, new Binding() { Source = this, Path = new PropertyPath(RProperty) });
            MySliderG.SetBinding(Slider.ValueProperty, new Binding() { Source = this, Path = new PropertyPath(GProperty) });
            MySliderB.SetBinding(Slider.ValueProperty, new Binding() { Source = this, Path = new PropertyPath(BProperty) });
            MySliderH.SetBinding(Slider.ValueProperty, new Binding() { Source = this, Path = new PropertyPath(HProperty) });
            MySliderS.SetBinding(Slider.ValueProperty, new Binding() { Source = this, Path = new PropertyPath(SProperty) });
            MySliderV.SetBinding(Slider.ValueProperty, new Binding() { Source = this, Path = new PropertyPath(VProperty) });

        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var myc = MainColor;
            R = 200;
        }

        private void MyButton_Click(object sender, RoutedEventArgs e)
        {
            SetMainColorWhite();
        }
        private void SetMainColorWhite() => MainColor = Color.FromArgb(255, 255, 255, 255);
    }



    public class ConverterARGB2Color : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            byte a = (byte)values[0];
            byte r = (byte)values[1];
            byte g = (byte)values[2];
            byte b = (byte)values[3];
            return Color.FromArgb(a, r, g, b);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            Color cb = (Color)value;
            object[] result = new object[4];
            result[0] = cb.A;
            result[1] = cb.R;
            result[2] = cb.G;
            result[3] = cb.B;
            return result;
        }
    }

    public class ConverterColor2Brush : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            Color c = (Color)value;
            return new SolidColorBrush(c);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

依存関係プロパティの値変更時に何かのメソッドを実行したいときはSetの中じゃなくて、FrameworkPropertyMetadataのなかでPropertyChangedCallbackってのを使うみたい
実行できるメソッドには制約があってprivate static voidの必要あり?staticは必須だった

PropertyChangedCallback
これで
R、G、Bのどれかを変更したらH、S、Vを変更
H、S、Vのどれかを変更したらR、G、Bを変更
している
理想はこれをフラグ+メソッドじゃなくてBindingとConverterで行いたかったけどできなかった

HSV.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;

namespace _2023041710_ARGBHSV
{
    public struct RGB
    {
        public byte R { get; set; } = 0;
        public byte G { get; set; } = 0;
        public byte B { get; set; } = 0;
        public RGB(byte r, byte g, byte b)
        {
            R = r; G = g; B = b;
        }
        public RGB() { }
        public override string ToString()
        {
            return $"{R}, {G}, {B}";
            //return base.ToString();
        }
    }

    public struct HSV
    {
        //public double H, S, V;
        public double H { get; set; }
        public double S { get; set; }
        public double V { get; set; }

        public HSV(double h, double s, double v)
        {
            H = h;
            S = s;
            V = v;
        }
        public override string ToString()
        {
            return $"{H}, {S}, {V}";
        }

    }
    public class MathHSV
    {
        #region Color -> HSV

        /// <summary>
        /// Color(RGB)をHSV(円柱モデル)に変換、Hの値は0fから360f、SとVは0fから1f
        /// </summary>
        /// <param name="color"></param>
        /// <returns>HSV</returns>
        public static (double h, double s, double v) Color2HSV(Color color)
        {
            return RGB2hsv(color.R, color.G, color.B);
        }
        public static HSV Color2HSV2(Color color)
        {
            return Rgb2HSV(color.R, color.G, color.B);
        }

        #endregion Color -> HSV

        #region RGB -> HSV

        /// <summary>
        /// RGBをHSV(円柱モデル)に変換、RGBそれぞれの値を指定する
        /// </summary>
        /// <param name="r"></param>
        /// <param name="g"></param>
        /// <param name="b"></param>
        /// <returns>HSV</returns>
        public static (double h, double s, double v) RGB2hsv(byte r, byte g, byte b)
        {
            byte Max = Math.Max(r, Math.Max(g, b));
            byte Min = Math.Min(r, Math.Min(g, b));
            if (Max == 0) { return (360, 0, 0); }

            double chroma = Max - Min;
            double h;
            double s = chroma / Max;
            double v = Max / 255f;

            if (Max == Min) { h = 360f; }
            else if (Max == r)
            {
                h = 60f * (g - b) / chroma;
                if (h < 0) { h += 360f; }
            }
            else if (Max == g)
            {
                h = 60f * (b - r) / chroma + 120f;
            }
            else if (Max == b)
            {
                h = 60f * (r - g) / chroma + 240f;
            }
            else { h = 360f; }

            return (h, s, v);
        }
        public static HSV Rgb2HSV(byte r, byte g, byte b)
        {
            (double h, double s, double v) = RGB2hsv(r, g, b);
            return new HSV(h, s, v);
        }

        public static (double h, double s, double v) Rgb2hsv(double r, double g, double b)
        {
            return RGB2hsv(
                (byte)(Math.Round(r, MidpointRounding.AwayFromZero)),
                (byte)(Math.Round(g, MidpointRounding.AwayFromZero)),
                (byte)(Math.Round(b, MidpointRounding.AwayFromZero)));
        }
        public static HSV Rgb2HSV(double r, double g, double b)
        {
            return Rgb2HSV(
                (byte)(Math.Round(r, MidpointRounding.AwayFromZero)),
                (byte)(Math.Round(g, MidpointRounding.AwayFromZero)),
                (byte)(Math.Round(b, MidpointRounding.AwayFromZero)));
        }
        public static (double h, double s, double v) RGB2hsv(RGB rgb)
        {
            return RGB2hsv(rgb.R, rgb.G, rgb.B);
        }
        public static HSV RGB2HSV(RGB rgb)
        {
            return Rgb2HSV(rgb.R, rgb.G, rgb.B);
        }

        #endregion RGB -> HSV

        #region Color -> HSV(円錐モデル)

        //        プログラミング 第6弾(プログラム ) - Color Model:色をプログラムするブログ - Yahoo!ブログ
        //https://blogs.yahoo.co.jp/pspevolution7/17682985.html

        /// <summary>
        /// Color(RGB)をHSV(円錐モデル)に変換、Hの値は0fから360f、SとVは0fから1f
        /// </summary>
        /// <param name="color"></param>
        /// <returns></returns>
        public static (double h, double s, double v) Color2Hsv_ConicalModel(Color color)
        {
            byte R = color.R;
            byte G = color.G;
            byte B = color.B;
            byte Max = Math.Max(R, Math.Max(G, B));
            byte Min = Math.Min(R, Math.Min(G, B));
            if (Max == 0) { return (360f, 0f, 0f); }

            double chroma = Max - Min;
            double h;
            double s = chroma / 255f;//円錐モデル
            double v = Max / 255f;

            if (Max == Min) { h = 360f; }
            else if (Max == R)
            {
                h = 60f * (G - B) / chroma;
                if (h < 0) { h += 360f; }
            }
            else if (Max == G)
            {
                h = 60f * (B - R) / chroma + 120f;
            }
            else if (Max == B)
            {
                h = 60f * (R - G) / chroma + 240f;
            }
            else { h = 360f; }

            return (h, s, v);
        }
        public static HSV Color2HSV_ConicalModel(Color color)
        {
            (double h, double s, double v) = Color2Hsv_ConicalModel(color);
            return new HSV(h, s, v);
        }
        #endregion Color -> HSV(円錐モデル)

        #region HSV(円柱モデル) -> RGB、Color

        public static (byte r, byte g, byte b) Hsv2rgb(double h, double s, double v)
        {
            Color color = HSV2Color(h, s, v);
            return (color.R, color.G, color.B);
        }
        public static (byte r, byte g, byte b) HSV2rgb(HSV hsv)
        {
            Color color = HSV2Color(hsv.H, hsv.S, hsv.V);
            return (color.R, color.G, color.B);
        }
        public static RGB HSV2RGB(HSV hsv)
        {
            var (r, g, b) = HSV2rgb(hsv);
            return new RGB(r, g, b);
        }
        public static RGB Hsv2RGB(double h, double s, double v)
        {
            var (r, g, b) = Hsv2rgb(h, s, v);
            return new RGB(r, g, b);
        }

        #endregion HSV(円柱モデル) -> RGB

        #region HSV(円柱モデル) -> Color

        /// <summary>
        /// HSV(円柱モデル)をColorに変換
        /// </summary>
        /// <param name="hsv"></param>
        /// <returns>Color</returns>
        public static Color HSV2Color(double h, double s, double v)
        {
            h = h % 360f / 60f;
            double r = v, g = v, b = v;

            if (v == 0) { return Color.FromRgb(0, 0, 0); }

            int i = (int)Math.Floor(h);
            double d = h - i;
            if (h < 1)
            {
                g *= 1f - s * (1f - d);
                b *= 1f - s;
            }
            else if (h < 2)
            {
                r *= 1f - s * d;
                b *= 1f - s;
            }
            else if (h < 3)
            {
                r *= 1f - s;
                b *= 1f - s * (1f - d);
            }
            else if (h < 4)
            {
                r *= 1f - s;
                g *= 1f - s * d;
            }
            else if (h < 5)
            {
                r *= 1f - s * (1f - d);
                g *= 1f - s;
            }
            else// if (h < 6)
            {
                g *= 1f - s;
                b *= 1f - s * d;
            }

            //return Color.FromScRgb(1f,(float)r,(float)g,(float)b);
            return Color.FromRgb(
                (byte)Math.Round(r * 255f, MidpointRounding.AwayFromZero),
                (byte)Math.Round(g * 255f, MidpointRounding.AwayFromZero),
                (byte)Math.Round(b * 255f, MidpointRounding.AwayFromZero));
        }
        public static Color HSV2Color(HSV hsv)
        {
            return HSV2Color(hsv.H, hsv.S, hsv.V);
        }
        #endregion HSV(円柱モデル) -> Color

        #region HSV(円錐モデル) -> RGB

        /// <summary>
        /// RGBをHSV(円錐モデル)に変換、RGBそれぞれの値を指定する
        /// </summary>
        /// <param name="r"></param>
        /// <param name="g"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static (double h, double s, double v) RGB2hsv_ConicalModel(byte r, byte g, byte b)
        {
            return Color2Hsv_ConicalModel(Color.FromRgb(r, g, b));
        }
        public static HSV RGB2HSV_ConicalModel(byte r, byte g, byte b)
        {
            return Color2HSV_ConicalModel(Color.FromRgb(r, g, b));
        }
        #endregion HSV(円錐モデル) -> RGB

        #region HSV(円錐モデル) -> Color


        /// <summary>
        /// 円錐モデルのHSVをColorに変換
        /// </summary>
        /// <param name="hsv">円錐モデルのHSV</param>
        /// <returns></returns>
        public static Color HSV_ConicalModel2Color(double h, double s, double v)
        {
            double Max = v * 255f;
            double Min = (v - s) * 255f;
            double d = Max - Min;
            double r, g, b;
            if (h < 60)
            {
                r = Max;
                g = Min + d * h / 60f;
                b = Min;
            }
            else if (h < 120)
            {
                r = Min + d * (120f - h) / 60f;
                g = Max;
                b = Min;
            }
            else if (h < 180)
            {
                r = Min;
                g = Max;
                b = Min + d * (h - 120f) / 60f;
            }
            else if (h < 240)
            {
                r = Min;
                g = Min + d * (240f - h) / 60f;
                b = Max;
            }
            else if (h < 300)
            {
                r = Min + d * (h - 240f) / 60f;
                g = Min;
                b = Max;
            }
            else
            {
                r = Max;
                g = Min;
                b = Min + d * (360f - h) / 60f;
            }
            return Color.FromRgb(
                (byte)Math.Round(r, MidpointRounding.AwayFromZero),
                (byte)Math.Round(g, MidpointRounding.AwayFromZero),
                (byte)Math.Round(b, MidpointRounding.AwayFromZero));
        }

        public static (byte r, byte g, byte b) HSV_ConicalModel2RGB(double h, double s, double v)
        {
            Color color = HSV_ConicalModel2Color(h, s, v);
            return (color.R, color.G, color.B);
        }

        #endregion HSV(円錐モデル) -> HSV

        //public override string ToString()
        //{
        //    //return base.ToString();
        //    return $"{Hue}, {Saturation}, {Value}";
        //}
        //public string ToString100()
        //{
        //    return $"{Hue:000.00}, {Saturation * 100:000.00}, {Value * 100:000.00}";
        //}
    }
}

RGBとHSVの相互変換計算用クラス
長いけど今回使っているメソッドは2つだけ


感想

以前書いたときはSliderのValueChangedイベントのところで相互変換処理していたので、WPFっぽくないしSliderが主役になっていたけど、今回は各RGBHSVのプロパティ変更時に相互変換するようになったので自然な感じなった…きがする


関連記事

続きは3日後
gogowaten.hatenablog.com

前回のWPF記事は1週間前
gogowaten.hatenablog.com

5年前、これが前回の相互変換
gogowaten.hatenablog.com

7年前、このときは力技だった気がする
gogowaten.hatenablog.com