しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

UWP でカラーピッカーを作りたい

ロゴツールを作る上で下記のようなカラーピッカーがほしくなったので作ってみることにしました

f:id:matatabi_ux:20150913195748p:plain

※ソース全体は下記を参照ください


まずは XAML から

<UserControl
    x:Class="UWPColorPickerSample.ColorPicker"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UWPColorPickerSample"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DataContext="{x:Bind ViewModel}"
    DataContextChanged="OnDataContextChanged"
    RequestedTheme="Dark"
    Height="186"
    Width="326">

    <UserControl.Resources>
        <ImageBrush x:Key="TransparentBtush"
                    ImageSource="ms-appx:///Assets/Images/light-transparent.png" 
                    Stretch="None"
                    AlignmentX="Left"
                    AlignmentY="Top"/>
    </UserControl.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition Width="16"/>
            <ColumnDefinition Width="16"/>
            <ColumnDefinition Width="144"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="8"/>
            <RowDefinition Height="150"/>
        </Grid.RowDefinitions>

        <TextBox Grid.Column="0"
                 Grid.ColumnSpan="2"
                 Grid.Row="0" 
                 Background="#FF101012"
                 BorderThickness="0"
                 InputScope="AlphanumericHalfWidth"
                 Text="{x:Bind ViewModel.Color, Mode=TwoWay, FallbackValue=#FFFF0000}"
                 FontSize="20"
                 IsTextPredictionEnabled="False" 
                 IsSpellCheckEnabled="False"/>

        <Grid Grid.Column="3"
              Grid.Row="0"
              Margin="20,0,0,0">
            <local:ImageTile Source="ms-appx:///Assets/Images/light-transparent.png"/>
            <Rectangle Fill="{x:Bind ViewModel.Color, Mode=OneWay, FallbackValue=#FFFF0000}"
                       Stroke="Black"
                       StrokeThickness="1"/>
        </Grid>

        <Grid Grid.Column="0"
              Grid.Row="2"
              Background="{x:Bind ViewModel.HueColor, Mode=OneWay, FallbackValue=#FFFF0000}">
            <Rectangle>
                <Rectangle.Fill>
                    <LinearGradientBrush StartPoint="0,0"
                                     EndPoint="1,0">
                        <GradientStop Offset="0" Color="White"/>
                        <GradientStop Offset="1" Color="#00FFFFFF"/>
                    </LinearGradientBrush>
                </Rectangle.Fill>
            </Rectangle>
            <Rectangle>
                <Rectangle.Fill>
                    <LinearGradientBrush StartPoint="0,0"
                                         EndPoint="0,1">
                        <GradientStop Offset="0" Color="#00000000"/>
                        <GradientStop Offset="1" Color="Black"/>
                    </LinearGradientBrush>
                </Rectangle.Fill>
            </Rectangle>
            <Canvas x:Name="pickerCanvas"
                    PointerPressed="OnPickerPressed"
                    Background="Transparent">
                <Canvas.Clip>
                    <RectangleGeometry Rect="0,0,150,150"/>
                </Canvas.Clip>
                <Grid Margin="-7,-7,0,0"
                      Canvas.Left="{x:Bind ViewModel.PickPointX, Mode=OneWay, FallbackValue=150}"
                      Canvas.Top="{x:Bind ViewModel.PickPointY, Mode=OneWay}">
                    <Ellipse Stroke="White"
                             StrokeThickness="3"
                             Width="14"
                             Height="14"
                             UseLayoutRounding="False"/>
                    <Ellipse Stroke="Black"
                             StrokeThickness="1"
                             Width="12"
                             Height="12"
                             UseLayoutRounding="False"/>
                </Grid>
            </Canvas>
        </Grid>

        <Rectangle x:Name="colorSpectrum"
                   Grid.Column="1"
                   Grid.Row="2"
                   Margin="1,0"
                   PointerPressed="OnHuePressed">
            <Rectangle.Fill>
                <LinearGradientBrush StartPoint="0,0"
                                     EndPoint="0,1">
                    <GradientStop Offset="0" Color="#FFFF0000"/>
                    <GradientStop Offset="0.2" Color="#FFFFFF00"/>
                    <GradientStop Offset="0.4" Color="#FF00FF00"/>
                    <GradientStop Offset="0.6" Color="#FF0000FF"/>
                    <GradientStop Offset="0.8" Color="#FFFF00FF"/>
                    <GradientStop Offset="1" Color="#FFFF0000"/>
                </LinearGradientBrush>
            </Rectangle.Fill>
        </Rectangle>

        <Canvas Grid.Column="2"
                Grid.Row="2">
            <Polygon Canvas.Top="{x:Bind ViewModel.ColorSpectrumPoint, Mode=OneWay}"
                     Points="8,-3 0,0 8,3"
                     Fill="White"/>
        </Canvas>

        <Grid Grid.Column="3"
              Grid.Row="2"
              VerticalAlignment="Bottom">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="16"/>
                <ColumnDefinition Width="128"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Column="0"
                       Grid.Row="0"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       Text="R"/>
            <TextBlock Grid.Column="0"
                       Grid.Row="1"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       Text="G"/>
            <TextBlock Grid.Column="0"
                       Grid.Row="2"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       Text="B"/>
            <TextBlock Grid.Column="0"
                       Grid.Row="3"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       Text="A"/>

            <Grid Grid.Column="1"
                  Grid.Row="0"
                  Margin="4,0,0,7">
                <TextBox Text="{x:Bind ViewModel.RedString, Mode=TwoWay, FallbackValue=255}"
                         BorderThickness="0"
                         InputScope="Number"
                         MaxLength="3"
                         FontSize="16"
                         Background="#FF101012" 
                         IsTextPredictionEnabled="False" 
                         IsSpellCheckEnabled="False"/>
                <Rectangle x:Name="red"
                           PointerPressed="OnRedPressed"
                           Height="8"
                           VerticalAlignment="Bottom">
                    <Rectangle.Fill>
                        <LinearGradientBrush StartPoint="0,0"
                                             EndPoint="1,0">
                            <GradientStop Offset="0" Color="{x:Bind ViewModel.RedStartColor, Mode=OneWay, FallbackValue=#FF000000}"/>
                            <GradientStop Offset="1" Color="{x:Bind ViewModel.RedEndColor, Mode=OneWay, FallbackValue=#FFFF0000}"/>
                        </LinearGradientBrush>
                    </Rectangle.Fill>
                </Rectangle>
            </Grid>

            <Grid Grid.Column="1"
                  Grid.Row="1"
                  Margin="4,0,0,7">
                <TextBox Text="{x:Bind ViewModel.GreenString, Mode=TwoWay, FallbackValue=0}"
                         BorderThickness="0"
                         InputScope="Number"
                         MaxLength="3"
                         FontSize="16"
                         Background="#FF101012"
                         IsTextPredictionEnabled="False" 
                         IsSpellCheckEnabled="False"/>
                <Rectangle x:Name="green"
                           PointerPressed="OnGreenPressed"
                           Height="8"
                           VerticalAlignment="Bottom">
                    <Rectangle.Fill>
                        <LinearGradientBrush StartPoint="0,0"
                                             EndPoint="1,0">
                            <GradientStop Offset="0" Color="{x:Bind ViewModel.GreenStartColor, Mode=OneWay, FallbackValue=#FFFF0000}"/>
                            <GradientStop Offset="1" Color="{x:Bind ViewModel.GreenEndColor, Mode=OneWay, FallbackValue=#FFFFFF00}"/>
                        </LinearGradientBrush>
                    </Rectangle.Fill>
                </Rectangle>
            </Grid>

            <Grid Grid.Column="1"
                  Grid.Row="2"
                  Margin="4,0,0,7">
                <TextBox Text="{x:Bind ViewModel.BlueString, Mode=TwoWay, FallbackValue=0}"
                         BorderThickness="0"
                         InputScope="Number"
                         MaxLength="3"
                         FontSize="16"
                         Background="#FF101012"
                         IsTextPredictionEnabled="False" 
                         IsSpellCheckEnabled="False"/>
                <Rectangle x:Name="blue"
                           PointerPressed="OnBluePressed"
                           Height="8"
                           VerticalAlignment="Bottom">
                    <Rectangle.Fill>
                        <LinearGradientBrush StartPoint="0,0"
                                             EndPoint="1,0">
                            <GradientStop Offset="0" Color="{x:Bind ViewModel.BlueStartColor, Mode=OneWay, FallbackValue=#FFFF0000}"/>
                            <GradientStop Offset="1" Color="{x:Bind ViewModel.BlueEndColor, Mode=OneWay, FallbackValue=#FFFF00FF}"/>
                        </LinearGradientBrush>
                    </Rectangle.Fill>
                </Rectangle>
            </Grid>

            <Grid Grid.Column="1"
                  Grid.Row="3"
                  Margin="4,0,0,2">
                <TextBox Text="{x:Bind ViewModel.AlphaString, Mode=TwoWay, FallbackValue=255}"
                         BorderThickness="0"
                         InputScope="Number"
                         MaxLength="3"
                         FontSize="16"
                         Background="#FF101012"
                         IsTextPredictionEnabled="False" 
                         IsSpellCheckEnabled="False"/>
                <local:ImageTile x:Name="alpha"
                                 Height="8"
                                 Source="ms-appx:///Assets/Images/light-transparent.png"
                                 VerticalAlignment="Bottom"
                                 PointerPressed="OnAlphaPressed"/>
                <Rectangle IsHitTestVisible="False"
                           Height="8"
                           VerticalAlignment="Bottom">
                    <Rectangle.Fill>
                        <LinearGradientBrush StartPoint="0,0"
                                             EndPoint="1,0">
                            <GradientStop Offset="0" Color="{x:Bind ViewModel.AlphaStartColor, Mode=OneWay, FallbackValue=#00FF0000}"/>
                            <GradientStop Offset="1" Color="{x:Bind ViewModel.AlphaEndColor, Mode=OneWay, FallbackValue=#FFFF0000}"/>
                        </LinearGradientBrush>
                    </Rectangle.Fill>
                </Rectangle>
            </Grid>

        </Grid>

    </Grid>
</UserControl>

ColorPicker という名前で UserControl を作って上記のような XAML をささっと組みます

色座標平面のグラデーションの部分は下記のようなイメージで、LinearGradientBrush を駆使して描画する必要があります

f:id:matatabi_ux:20150913201258p:plain

左から右へ白 → 原色(右側の色相スペクトラムで指定した色)のグラデを描画し、上から下へ原色 → 黒のグラデを描画します

コードビハインドは下記のような感じ

/// <summary>
/// Color picker settings view model
/// </summary>
public sealed partial class ColorPicker : UserControl
{
    /// <summary>
    /// View model
    /// </summary>
    public ColorPickerViewModel ViewModel { get; set; } = new ColorPickerViewModel();

    /// <summary>
    /// Constructor
    /// </summary>
    public ColorPicker()
    {
        this.InitializeComponent();
    }

    /// <summary>
    /// DataContext changed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event aruments</param>
    private void OnDataContextChanged(object sender, DataContextChangedEventArgs e)
    {
        this.ViewModel = this.DataContext as ColorPickerViewModel;
    }

    /// <summary>
    /// Picker area pointer pressed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event aruments</param>
    private void OnPickerPressed(object sender, PointerRoutedEventArgs e)
    {
        this.PickColor(e.GetCurrentPoint(this.pickerCanvas).Position);
        this.pickerCanvas.CapturePointer(e.Pointer);

        PointerEventHandler moved = null;
        moved = (s, args) =>
        {
            this.PickColor(args.GetCurrentPoint(this.pickerCanvas).Position);
        };
        PointerEventHandler released = null;
        released = (s, args) =>
        {
            this.pickerCanvas.ReleasePointerCapture(args.Pointer);
            this.PickColor(args.GetCurrentPoint(this.pickerCanvas).Position);
            this.pickerCanvas.PointerMoved -= moved;
            this.pickerCanvas.PointerReleased -= released;
        };

        this.pickerCanvas.PointerMoved += moved;
        this.pickerCanvas.PointerReleased += released;
    }

    /// <summary>
    /// Pick color
    /// </summary>
    /// <param name="point">pick point</param>
    private void PickColor(Point point)
    {
        var px = Math.Max(0d, point.X);
        px = Math.Min(this.pickerCanvas.ActualWidth, px);
        var py = Math.Max(0d, point.Y);
        py = Math.Min(this.pickerCanvas.ActualHeight, py);

        this.ViewModel.PickPointX = Math.Round(px, MidpointRounding.AwayFromZero);
        this.ViewModel.PickPointY = Math.Round(py, MidpointRounding.AwayFromZero);
        this.ViewModel.OnPickPointChanged();
    }

    /// <summary>
    /// Color spectrum area pointer pressed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event aruments</param>
    private void OnHuePressed(object sender, PointerRoutedEventArgs e)
    {
        this.ChangeHue(e.GetCurrentPoint(this.colorSpectrum).Position.Y);
        this.colorSpectrum.CapturePointer(e.Pointer);

        PointerEventHandler moved = null;
        moved = (s, args) =>
        {
            this.ChangeHue(args.GetCurrentPoint(this.colorSpectrum).Position.Y);
        };
        PointerEventHandler released = null;
        released = (s, args) =>
        {
            this.colorSpectrum.ReleasePointerCapture(args.Pointer);
            this.ChangeHue(args.GetCurrentPoint(this.colorSpectrum).Position.Y);
            this.colorSpectrum.PointerMoved -= moved;
            this.colorSpectrum.PointerReleased -= released;
        };
        this.colorSpectrum.PointerMoved += moved;
        this.colorSpectrum.PointerReleased += released;
    }

    /// <summary>
    /// Change color hue
    /// </summary>
    /// <param name="y">change point</param>
    private void ChangeHue(double y)
    {
        var py = Math.Max(0d, y);
        py = Math.Min(this.colorSpectrum.ActualHeight, py);

        this.ViewModel.ColorSpectrumPoint = Math.Round(py, MidpointRounding.AwayFromZero);
    }

    /// <summary>
    /// Red value bar pointer pressed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event aruments</param>
    private void OnRedPressed(object sender, PointerRoutedEventArgs e)
    {
        this.ViewModel.Red = this.ArrangeArgb(e.GetCurrentPoint(this.red).Position.X, this.red.ActualWidth);
        this.red.CapturePointer(e.Pointer);

        PointerEventHandler moved = null;
        moved = (s, args) =>
        {
            this.ViewModel.Red = this.ArrangeArgb(e.GetCurrentPoint(this.red).Position.X, this.red.ActualWidth);
        };
        PointerEventHandler released = null;
        released = (s, args) =>
        {
            this.red.ReleasePointerCapture(args.Pointer);
            this.ViewModel.Red = this.ArrangeArgb(e.GetCurrentPoint(this.red).Position.X, this.red.ActualWidth);
            this.red.PointerMoved -= moved;
            this.red.PointerReleased -= released;
        };
        this.red.PointerMoved += moved;
        this.red.PointerReleased += released;
    }

    /// <summary>
    /// Green value bar pointer pressed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event aruments</param>
    private void OnGreenPressed(object sender, PointerRoutedEventArgs e)
    {
        this.ViewModel.Green = this.ArrangeArgb(e.GetCurrentPoint(this.green).Position.X, this.green.ActualWidth);
        this.green.CapturePointer(e.Pointer);

        PointerEventHandler moved = null;
        moved = (s, args) =>
        {
            this.ViewModel.Green = this.ArrangeArgb(e.GetCurrentPoint(this.green).Position.X, this.green.ActualWidth);
        };
        PointerEventHandler released = null;
        released = (s, args) =>
        {
            this.green.ReleasePointerCapture(args.Pointer);
            this.ViewModel.Green = this.ArrangeArgb(e.GetCurrentPoint(this.green).Position.X, this.green.ActualWidth);
            this.green.PointerMoved -= moved;
            this.green.PointerReleased -= released;
        };
        this.green.PointerMoved += moved;
        this.green.PointerReleased += released;
    }

    /// <summary>
    /// Blue value bar pointer pressed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event aruments</param>
    private void OnBluePressed(object sender, PointerRoutedEventArgs e)
    {
        this.ViewModel.Blue = this.ArrangeArgb(e.GetCurrentPoint(this.blue).Position.X, this.blue.ActualWidth);
        this.blue.CapturePointer(e.Pointer);

        PointerEventHandler moved = null;
        moved = (s, args) =>
        {
            this.ViewModel.Blue = this.ArrangeArgb(e.GetCurrentPoint(this.blue).Position.X, this.blue.ActualWidth);
        };
        PointerEventHandler released = null;
        released = (s, args) =>
        {
            this.blue.ReleasePointerCapture(args.Pointer);
            this.ViewModel.Blue = this.ArrangeArgb(e.GetCurrentPoint(this.blue).Position.X, this.blue.ActualWidth);
            this.blue.PointerMoved -= moved;
            this.blue.PointerReleased -= released;
        };
        this.blue.PointerMoved += moved;
        this.blue.PointerReleased += released;
    }

    /// <summary>
    /// Alpha value bar pointer pressed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event aruments</param>
    private void OnAlphaPressed(object sender, PointerRoutedEventArgs e)
    {
        this.ViewModel.Alpha = this.ArrangeArgb(e.GetCurrentPoint(this.alpha).Position.X, this.alpha.ActualWidth);
        this.alpha.CapturePointer(e.Pointer);

        PointerEventHandler moved = null;
        moved = (s, args) =>
        {
            this.ViewModel.Alpha = this.ArrangeArgb(e.GetCurrentPoint(this.alpha).Position.X, this.alpha.ActualWidth);
        };
        PointerEventHandler released = null;
        released = (s, args) =>
        {
            this.alpha.ReleasePointerCapture(args.Pointer);
            this.ViewModel.Alpha = this.ArrangeArgb(e.GetCurrentPoint(this.alpha).Position.X, this.alpha.ActualWidth);
            this.alpha.PointerMoved -= moved;
            this.alpha.PointerReleased -= released;
        };
        this.alpha.PointerMoved += moved;
        this.alpha.PointerReleased += released;
    }

    /// <summary>
    /// Arrange color element value
    /// </summary>
    /// <param name="x">change point</param>
    /// <param name="max">change point max</param>
    private int ArrangeArgb(double x, double max)
    {
        var px = x * 255d / max;
        px = Math.Max(0d, px);
        px = Math.Min(255d, px);

        return (int)Math.Round(px, MidpointRounding.AwayFromZero);
    }
}

ドラッグでピッカー地点を移動できるようにだいたい下記のようなイベントハンドラを各場所に設定しています

    /// <summary>
    /// Picker area pointer pressed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event aruments</param>
    private void OnPickerPressed(object sender, PointerRoutedEventArgs e)
    {
        this.PickColor(e.GetCurrentPoint(this.pickerCanvas).Position);
        this.pickerCanvas.CapturePointer(e.Pointer);

        PointerEventHandler moved = null;
        moved = (s, args) =>
        {
            this.PickColor(args.GetCurrentPoint(this.pickerCanvas).Position);
        };
        PointerEventHandler released = null;
        released = (s, args) =>
        {
            this.pickerCanvas.ReleasePointerCapture(args.Pointer);
            this.PickColor(args.GetCurrentPoint(this.pickerCanvas).Position);
            this.pickerCanvas.PointerMoved -= moved;
            this.pickerCanvas.PointerReleased -= released;
        };

        this.pickerCanvas.PointerMoved += moved;
        this.pickerCanvas.PointerReleased += released;
    }

    /// <summary>
    /// Pick color
    /// </summary>
    /// <param name="point">pick point</param>
    private void PickColor(Point point)
    {
        var px = Math.Max(0d, point.X);
        px = Math.Min(this.pickerCanvas.ActualWidth, px);
        var py = Math.Max(0d, point.Y);
        py = Math.Min(this.pickerCanvas.ActualHeight, py);

        this.ViewModel.PickPointX = Math.Round(px, MidpointRounding.AwayFromZero);
        this.ViewModel.PickPointY = Math.Round(py, MidpointRounding.AwayFromZero);
        this.ViewModel.OnPickPointChanged();
    }

PointerPressed でカーソルをキャプチャ後、PointerMove 中に移動してピッカー座標を更新、PointerReleased があったらキャプチャ解除するような動作を追加しています

最後に ViewModel 側で座標に応じた色計算を行います

/// <summary>
/// Color picker ViewModel
/// </summary>
public partial class ColorPickerViewModel : BindableBase
{
    /// <summary>
    /// Picker height
    /// </summary>
    private static readonly double PickerHeight = 150d;

    /// <summary>
    /// Picker width
    /// </summary>
    private static readonly double PickerWidth = 150d;

    /// <summary>
    /// String color code to color converter
    /// </summary>
    private static readonly ColorConverter Converter = new ColorConverter();

    /// <summary>
    /// Constructor
    /// </summary>
    public ColorPickerViewModel()
    {
        this.color = "#FFFF0000";
        this.pickPointX = 150d;
        this.pickPointY = 0d;
        this.colorSpectrumPoint = 0d;
        this.UpdateColor(Colors.Red);
        this.UpdatePickPoint();
    }

    /// <summary>
    /// Update color properties
    /// </summary>
    /// <param name="color">new color</param>
    public void UpdateColor(Color color)
    {
        this.color = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", color.A, color.R, color.G, color.B);
        this.alpha = color.A;
        this.alphaString = this.alpha.ToString();
        this.red = color.R;
        this.redString = this.red.ToString();
        this.green = color.G;
        this.greenString = this.green.ToString();
        this.blue = color.B;
        this.blueString = this.blue.ToString();

        this.redStartColor = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", 0xff, 0, color.G, color.B);
        this.redEndColor = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", 0xff, 0xff, color.G, color.B);
        this.greenStartColor = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", 0xff, color.R, 0, color.B);
        this.greenEndColor = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", 0xff, color.R, 0xff, color.B);
        this.blueStartColor = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", 0xff, color.R, color.G, 0);
        this.blueEndColor = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", 0xff, color.R, color.G, 0xff);
        this.alphaStartColor = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", 0, color.R, color.G, color.B);
        this.alphaEndColor = string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", 0xff, color.R, color.G, color.B);

        var hsv = ToHSV(color);
        var h = FromHsv(hsv[0], 1f, 1f);
        this.hueColor = string.Format("#FF{0:X2}{1:X2}{2:X2}", h.R, h.G, h.B);

        this.OnPropertyChanged("Color");
        this.OnPropertyChanged("Red");
        this.OnPropertyChanged("RedString");
        this.OnPropertyChanged("Green");
        this.OnPropertyChanged("GreenString");
        this.OnPropertyChanged("Blue");
        this.OnPropertyChanged("BlueString");
        this.OnPropertyChanged("Alpha");
        this.OnPropertyChanged("AlphaString");
        this.OnPropertyChanged("RedStartColor");
        this.OnPropertyChanged("RedEndColor");
        this.OnPropertyChanged("GreenStartColor");
        this.OnPropertyChanged("GreenEndColor");
        this.OnPropertyChanged("BlueStartColor");
        this.OnPropertyChanged("BlueEndColor");
        this.OnPropertyChanged("AlphaStartColor");
        this.OnPropertyChanged("AlphaEndColor");
        this.OnPropertyChanged("HueColor");
    }

    /// <summary>
    /// Update pick color point
    /// </summary>
    public void UpdatePickPoint()
    {
        var hsv = ToHSV((Color)Converter.Convert(this.color, typeof(Color), null, null));
        this.pickPointX = PickerWidth * hsv[1];
        this.pickPointY = PickerHeight * (1 - hsv[2]);
        this.colorSpectrumPoint = PickerHeight * hsv[0] / 360f;

        this.OnPropertyChanged("PickPointX");
        this.OnPropertyChanged("PickPointY");
        this.OnPropertyChanged("ColorSpectrumPoint");
    }

    /// <summary>
    /// Convert rgb to hsv color
    /// </summary>
    /// <param name="color">rgb color</param>
    /// <returns>hsv color</returns>
    private static float[] ToHSV(Color color)
    {
        var rgb = new float[]
        {
            color.R / 255f, color.G / 255f, color.B / 255f
        };

        // RGB to HSV
        float max = rgb.Max();
        float min = rgb.Min();

        float h, s, v;
        if (max == min)
        {
            h = 0f;
        }
        else if (max == rgb[0])
        {
            h = (60f * (rgb[1] - rgb[2]) / (max - min) + 360f) % 360f;
        }
        else if (max == rgb[1])
        {
            h = 60f * (rgb[2] - rgb[0]) / (max - min) + 120f;
        }
        else
        {
            h = 60f * (rgb[0] - rgb[1]) / (max - min) + 240f;
        }

        if (max == 0d)
        {
            s = 0f;
        }
        else
        {
            s = (max - min) / max;
        }
        v = max;

        return new float[] { h, s, v };
    }

    /// <summary>
    /// Convert hsv to rgb color
    /// </summary>
    /// <param name="hue">hue</param>
    /// <param name="saturation">saturation</param>
    /// <param name="brightness">brightness</param>
    /// <returns>Color</returns>
    private static Color FromHsv(float hue, float saturation, float brightness)
    {
        if (saturation == 0)
        {
            var c = (byte)Math.Round(brightness * 255f, MidpointRounding.AwayFromZero);
            return ColorHelper.FromArgb(0xff, c, c, c);
        }

        var hi = ((int)(hue / 60f)) % 6;
        var f = hue / 60f - (int)(hue / 60d);
        var p = brightness * (1 - saturation);
        var q = brightness * (1 - f * saturation);
        var t = brightness * (1 - (1 - f) * saturation);

        float r, g, b;
        switch (hi)
        {
            case 0:
                r = brightness;
                g = t;
                b = p;
                break;

            case 1:
                r = q;
                g = brightness;
                b = p;
                break;

            case 2:
                r = p;
                g = brightness;
                b = t;
                break;

            case 3:
                r = p;
                g = q;
                b = brightness;
                break;

            case 4:
                r = t;
                g = p;
                b = brightness;
                break;

            case 5:
                r = brightness;
                g = p;
                b = q;
                break;

            default:
                throw new InvalidOperationException();
        }

        return ColorHelper.FromArgb(
            0xff,
            (byte)Math.Round(r * 255d),
            (byte)Math.Round(g * 255d),
            (byte)Math.Round(b * 255d));
    }

    /// <summary>
    /// Process in changing color
    /// </summary>
    partial void OnColorChanged()
    {
        this.UpdateColor((Color)Converter.Convert(this.color, typeof(Color), null, null));
        this.UpdatePickPoint();
    }

    /// <summary>
    /// Process in changing alpha channel string
    /// </summary>
    partial void OnAlphaStringChanged()
    {
        int parsed;
        if (int.TryParse(this.alphaString, out parsed))
        {
            this.Alpha = parsed;
        }
        else
        {
            this.AlphaString = this.alpha.ToString();
        }
    }

    /// <summary>
    /// Process in changing alpha channel
    /// </summary>
    partial void OnAlphaChanged()
    {
        var updated = (Color)Converter.Convert(this.color, typeof(Color), null, null);
        updated.A = (byte)Math.Max(0, this.alpha);
        updated.A = Math.Min((byte)0xff, updated.A);
        this.UpdateColor(updated);
        this.UpdatePickPoint();
    }

    /// <summary>
    /// Process in changing red string
    /// </summary>
    partial void OnRedStringChanged()
    {
        int parsed;
        if (int.TryParse(this.redString, out parsed))
        {
            this.Red = parsed;
        }
        else
        {
            this.RedString = this.red.ToString();
        }
    }

    /// <summary>
    /// Process in changing red
    /// </summary>
    partial void OnRedChanged()
    {
        var updated = (Color)Converter.Convert(this.color, typeof(Color), null, null);
        updated.R = (byte)Math.Max(0, this.red);
        updated.R = Math.Min((byte)0xff, updated.R);
        this.UpdateColor(updated);
        this.UpdatePickPoint();
    }

    /// <summary>
    /// Process in changing green string
    /// </summary>
    partial void OnGreenStringChanged()
    {
        int parsed;
        if (int.TryParse(this.greenString, out parsed))
        {
            this.Green = parsed;
        }
        else
        {
            this.GreenString = this.green.ToString();
        }
    }

    /// <summary>
    /// Process in changing green
    /// </summary>
    partial void OnGreenChanged()
    {
        var updated = (Color)Converter.Convert(this.color, typeof(Color), null, null);
        updated.G = (byte)Math.Max(0, this.green);
        updated.G = Math.Min((byte)0xff, updated.G);
        this.UpdateColor(updated);
        this.UpdatePickPoint();
    }

    /// <summary>
    /// Process in changing blue string
    /// </summary>
    partial void OnBlueStringChanged()
    {
        int parsed;
        if (int.TryParse(this.blueString, out parsed))
        {
            this.Blue = parsed;
        }
        else
        {
            this.BlueString = this.blue.ToString();
        }
    }

    /// <summary>
    /// Process in changing blue
    /// </summary>
    partial void OnBlueChanged()
    {
        var updated = (Color)Converter.Convert(this.color, typeof(Color), null, null);
        updated.B = (byte)Math.Max(0, this.blue);
        updated.B = Math.Min((byte)0xff, updated.B);
        this.UpdateColor(updated);
        this.UpdatePickPoint();
    }

    /// <summary>
    /// Process in changing color spectrum point
    /// </summary>
    partial void OnColorSpectrumPointChanged()
    {
        var old = (Color)Converter.Convert(this.color, typeof(Color), null, null);
        var hsv = ToHSV(old);
        hsv[0] = (float)(this.colorSpectrumPoint * 360f / PickerHeight);
        var updated = FromHsv(hsv[0], hsv[1], hsv[2]);
        this.UpdateColor(updated);
    }

    /// <summary>
    /// Process in changing color pick point
    /// </summary>
    public void OnPickPointChanged()
    {
        var old = (Color)Converter.Convert(this.color, typeof(Color), null, null);
        var hsv = ToHSV(old);
        var updated = FromHsv(hsv[0], (float)(this.pickPointX / PickerWidth), 1f - (float)(this.pickPointY / PickerHeight));
        this.UpdateColor(updated);
    }
}

色座標平面やその右の色相スペクトラムの指定座標から色への変換については RGB コードだと難しいので HSV(色相/彩度/明度)からの変換で行います

f:id:matatabi_ux:20150913201258p:plain

色相は 0~360 の角度で表され、彩度と明度は 0~1 で表されるので、RGB の 0~255 の範囲と合わせて調整します

結構面倒な実装が必要ですが、これまでのコードを実行してみた様子がこんな感じ(GIF なので色調が粗いです)

f:id:matatabi_ux:20150913214918g:plain

構造がわかると興味深いコントロールですね