2012年4月19日木曜日

wpf : 色選択コントロールを作成

wpfには色選択ダイアログがないようです。 参照を追加してwpfじゃない方の色選択ダイアログ(System.Windows.Forms.ColorDialog)を使えばいいのかもしれませんが、ちょっと欲しいのと違ったので自作してみました。

ColorDialogもコレもHLS色空間から色を選択できます。 というわけで見た目は似てますが、ちょっとだけ違います。 ColorDialogは輝度を変えても色相-彩度の選択領域が不変になってます。 それに対して、自作した色選択コントロールは輝度に応じて色相-彩度の選択領域も変化します。 それが良いのか、かえって使いづらくなったのは不明。 その実装のせいでちょっと重くなってしまいましたしねぇ。 開発環境のPCはiCore7なので気にする程の重さじゃないんですが、非力なノートマシンとかで使い物になるかは試してません。

とりあえず特徴とか制約とかはこんな感じ。

  • RGB色空間およびHLS色空間のパラメータで色を選択できる。
  • htmlで色を指定するときのようなテキスト表記(#123456やlightgreenなど)で色を選択できる。
  • 色選択「ダイアログ」ではなく色選択「コントロール」として作ったので、微妙に使い方が広がった。
  • 選択色が変わったらRgbColorChangedイベントで通知される。
  • RgbColorプロパティはバインドできる。
  • その他のプロパティ(R、G、BとかHueとか)は面倒なのでバインド不可。 (他のコントロールからはバインドできるけど、Rプロパティにバインドを追加とかはできない。)
  • RGB⇔HLSの変換コードは適当なので、丸め誤差あり。 HLSは感覚的なものだからこの用途では問題ないはず。 ただ、変換コードをコピー&ペーストして使いまわすときは要テスト。
  • RGB⇔HLSの変換はwikipedia(en)のページ"HSL and HSV"をなんとなく眺めながらコーディング。 その部分は最適化などとは無縁。
  • 選択領域にWriteableBitmapを使っているのでコンパイル時にunsafeオプションが必要。
  • 等倍表示でのみ動作確認。 LayoutTransformとかしても動くように作った気はするけど、ノーチェックなのでTransformしたら誤動作するかも。

コードはこんな感じ。 まずはColorSelectControl.xaml。

<UserControl
        x:Class="UserControls.ColorSelectControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:appCtrls="clr-namespace:UserControls"
        mc:Ignorable="d" 
        d:DesignHeight="420" d:DesignWidth="440"
        Loaded="OnLoaded"
>
    <StackPanel Name="basePanel">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="16"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Column="0" Grid.Row="0" VerticalAlignment="Center" Margin="2">S</TextBlock>
            <Border
                    Grid.Column="1"
                    Grid.Row="0"
                    BorderThickness="1"
                    BorderBrush="Black"
            >
                <Border BorderThickness="1" BorderBrush="White">
                    <Canvas
                        Width="360"
                        Height="256"
                    >
                        <Canvas.Clip>
                            <RectangleGeometry Rect="0,0,360,256"/>
                        </Canvas.Clip>
                        <Image
                                Name="selectArea"
                                Width="360"
                                Height="256"
                                Stretch="Fill"
                                MouseLeftButtonDown="OnMouseDownInSelectArea"
                                MouseMove="OnMouseMoveInSelectArea"
                        />
                        <Canvas
                                Name="selectAreaCursor"
                                Canvas.Left="{Binding Hue}"
                                Canvas.Bottom="{Binding Saturation}"
                                IsHitTestVisible="False"
                        >
                            <Line X1="-10" Y1="0" X2="-6" Y2="0" StrokeThickness="2" Stroke="Black" Opacity="0.7"/>
                            <Line X1="6" Y1="0" X2="10" Y2="0" StrokeThickness="2" Stroke="Black" Opacity="0.7"/>
                            <Line X1="0" Y1="-10" X2="0" Y2="-6" StrokeThickness="2" Stroke="Black" Opacity="0.7"/>
                            <Line X1="0" Y1="6" X2="0" Y2="10" StrokeThickness="2" Stroke="Black" Opacity="0.7"/>
                            <Line X1="-8" Y1="-8" X2="-5" Y2="-5" StrokeThickness="2" Stroke="White" Opacity="0.7"/>
                            <Line X1="5" Y1="-5" X2="8" Y2="-8" StrokeThickness="2" Stroke="White" Opacity="0.7"/>
                            <Line X1="-8" Y1="8" X2="-5" Y2="5" StrokeThickness="2" Stroke="White" Opacity="0.7"/>
                            <Line X1="5" Y1="5" X2="8" Y2="8" StrokeThickness="2" Stroke="White" Opacity="0.7"/>
                        </Canvas>
                    </Canvas>
                </Border>
            </Border>
            <Border
                    Grid.Column="3"
                    Grid.Row="0"
                    BorderThickness="1"
                    BorderBrush="Black"
            >
                <Border BorderThickness="1" BorderBrush="White">
                    <Canvas Width="16" Height="256">
                        <Image
                                Name="selectBar"
                                Width="16"
                                Height="256"
                                Stretch="Fill"
                                MouseLeftButtonDown="OnMouseDownInSelectBar"
                                MouseLeftButtonUp="OnMouseUpInSelectBar"
                                MouseMove="OnMouseMoveInSelectBar"
                        />
                        <Thumb
                                Name="thumbSelectBar"
                                Canvas.Left="16"
                                DragDelta="OnSelectBarThumbDragDelta"
                                Height="16"
                        >
                            <Thumb.Template>
                                <ControlTemplate>
                                    <!-- ホットスポットは左端の真ん中 -->
                                    <Canvas>
                                        <Polygon
                                                Canvas.Top="8"
                                                Points="0,0 6,-6 6,6"
                                                Fill="Black"
                                                Stroke="Black"
                                                StrokeThickness="3"
                                        />
                                        <Polygon
                                                Canvas.Top="8"
                                                Points="0,0 6,-6 6,6"
                                                Stroke="White"
                                                StrokeThickness="1"
                                        />
                                    </Canvas>
                                </ControlTemplate>
                            </Thumb.Template>
                        </Thumb>
                    </Canvas>
                </Border>
            </Border>
            <TextBlock Grid.Column="1" Grid.Row="1" HorizontalAlignment="Center" Margin="2">H</TextBlock>
            <TextBlock Grid.Column="3" Grid.Row="1" HorizontalAlignment="Center" Margin="2">L</TextBlock>
        </Grid>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="16"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="8"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
            </Grid.RowDefinitions>
            <StackPanel
                    Grid.Column="0"
                    Grid.Row="0"
                    Grid.ColumnSpan="2"
                    Orientation="Horizontal"
            >
                <Border
                        BorderThickness="1"
                        BorderBrush="Black"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                >
                    <Border BorderThickness="1" BorderBrush="White">
                        <Rectangle Width="32" Height="32">
                            <Rectangle.Fill>
                                <SolidColorBrush Color="{Binding RgbColor}"/>
                            </Rectangle.Fill>
                        </Rectangle>
                    </Border>
                </Border>
                <Label VerticalAlignment="Bottom">← 選択色</Label>
            </StackPanel>
            <StackPanel
                    Grid.Column="2"
                    Grid.Row="0"
                    Grid.ColumnSpan="3"
                    Orientation="Horizontal"
                    HorizontalAlignment="Right"
            >
                <Label VerticalAlignment="Center">テキスト表記</Label>
                <TextBox
                        Name="textBoxTextualRepresentation"
                        Width="100"
                        VerticalAlignment="Center"
                        GotFocus="OnTextBoxTextualRepresentationGotFocus"
                >
                    <TextBox.CommandBindings>
                        <CommandBinding
                                Command="{x:Static appCtrls:ColorSelectControl.UpdateFromTextualRepresentationCommand}"
                                Executed="OnUpdateFromTextualRepresentationCommand"
                        />
                    </TextBox.CommandBindings>
                    <TextBox.InputBindings>
                        <KeyBinding Key="Enter" Command="{x:Static appCtrls:ColorSelectControl.UpdateFromTextualRepresentationCommand}"/>
                    </TextBox.InputBindings>
                </TextBox>
                <Button VerticalAlignment="Center" Click="OnUpdateFromTextualRepresentationButtonClick">↓</Button>
            </StackPanel>
            <Label Grid.Column="0" Grid.Row="2">H : 色相</Label>
            <appCtrls:NumericBox
                    Grid.Column="1"
                    Grid.Row="2"
                    Width="140"
                    TextBoxWidth="40"
                    ValueMin="0"
                    ValueMax="359"
                    LargeChange="10"
                    Value="{Binding Hue}"
            />
            <Label Grid.Column="0" Grid.Row="3">S : 彩度</Label>
            <appCtrls:NumericBox
                    Grid.Column="1"
                    Grid.Row="3"
                    Width="140"
                    TextBoxWidth="40"
                    ValueMin="0"
                    ValueMax="255"
                    LargeChange="10"
                    Value="{Binding Saturation}"
            />
            <Label Grid.Column="0" Grid.Row="4">L : 輝度</Label>
            <appCtrls:NumericBox
                    Grid.Column="1"
                    Grid.Row="4"
                    Width="140"
                    TextBoxWidth="40"
                    ValueMin="0"
                    ValueMax="255"
                    LargeChange="10"
                    Value="{Binding Lightness}"
            />
            <Label Grid.Column="3" Grid.Row="2">R : 赤</Label>
            <appCtrls:NumericBox
                    Grid.Column="4"
                    Grid.Row="2"
                    Width="140"
                    TextBoxWidth="40"
                    ValueMin="0"
                    ValueMax="255"
                    LargeChange="10"
                    Value="{Binding R}"
            />
            <Label Grid.Column="3" Grid.Row="3">G : 緑</Label>
            <appCtrls:NumericBox
                    Grid.Column="4"
                    Grid.Row="3"
                    Width="140"
                    TextBoxWidth="40"
                    ValueMin="0"
                    ValueMax="255"
                    LargeChange="10"
                    Value="{Binding G}"
            />
            <Label Grid.Column="3" Grid.Row="4">B : 青</Label>
            <appCtrls:NumericBox
                    Grid.Column="4"
                    Grid.Row="4"
                    Width="140"
                    TextBoxWidth="40"
                    ValueMin="0"
                    ValueMax="255"
                    LargeChange="10"
                    Value="{Binding B}"
            />
        </Grid>
    </StackPanel>
</UserControl>

ColorSelectControl.xaml.cs。

using System;
using System.ComponentModel;
using System.Media;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Diagnostics;

namespace UserControls
{
    public partial class ColorSelectControl : UserControl, INotifyPropertyChanged
    {
        public readonly static RoutedCommand UpdateFromTextualRepresentationCommand = new RoutedCommand();

        // Hueの1週を何分割するか? 度数法にあわせて360とする。
        public const int HueCircumference = 360;

        // SaturationとLightnessの分割数。
        // 概念的には0.0~1.0のところを整数値0~BitsPerPixel-1で表す。
        public const int BitsPerPixel = 256;
        private const int _BitsPerPixel_1 = BitsPerPixel - 1;

        public static readonly DependencyProperty RgbColorProperty = DependencyProperty.Register(
                "RgbColor",
                typeof(Color),
                typeof(ColorSelectControl),
                new FrameworkPropertyMetadata(
                        new Color(),
                        FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                        new PropertyChangedCallback(ColorSelectControl.OnRgbColorChanged)
                )
        );

        public Color RgbColor
        {
            get { return (Color)GetValue(RgbColorProperty); }
            set { SetValue(RgbColorProperty, value); }
        }

        private static void OnRgbColorChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            ColorSelectControl thisCtrl = (ColorSelectControl)obj;

            Color newColor = (Color)args.NewValue;
            newColor.A = 0xff;
            thisCtrl.R = newColor.R;
            thisCtrl.G = newColor.G;
            thisCtrl.B = newColor.B;

            RoutedPropertyChangedEventArgs<Color> evt = new RoutedPropertyChangedEventArgs<Color>(
                    (Color)args.OldValue,
                    newColor,
                    RgbColorChangedEvent
            );
            thisCtrl.OnRgbColorChanged(evt);
        }

        public static readonly RoutedEvent RgbColorChangedEvent = EventManager.RegisterRoutedEvent(
                "RgbColorChanged",
                RoutingStrategy.Bubble,
                typeof(RoutedPropertyChangedEventHandler<Color>),
                typeof(ColorSelectControl)
        );

        public event RoutedPropertyChangedEventHandler<Color> RgbColorChanged
        {
            add { AddHandler(RgbColorChangedEvent, value); }
            remove { RemoveHandler(RgbColorChangedEvent, value); }
        }

        protected virtual void OnRgbColorChanged(RoutedPropertyChangedEventArgs<Color> args)
        {
            RaiseEvent(args);
        }

        WriteableBitmap _selectBarBmp;
        WriteableBitmap _selectAreaBmp;

        // ↑のWriteableBitmapを描きかえるべきか?
        // 1つのプロパティを更新したときに何度もWriteableBitmapの描きかえが呼ばれるコードがある。
        // WriteableBitmapの描きかえは重いので、1度の描きかえで済むようにするためのフラグ。
        // UpdateBmpメソッド参照。
        private bool _shouldUpdateBmp;

        private byte _r;
        public byte R
        {
            get
            {
                return _r;
            }
            set
            {
                if (_r == value)
                    return;

                _r = value;
                NotifyPropertyChanged("R");

                Color c = RgbColor;
                c.R = _r;
                RgbColor = c;

                UpdateTextualRepresentation();
                UpdateHsl();
            }
        }

        private byte _g;
        public byte G
        {
            get
            {
                return _g;
            }
            set
            {
                if (_g == value)
                    return;

                _g = value;
                NotifyPropertyChanged("G");

                Color c = RgbColor;
                c.G = _g;
                RgbColor = c;

                UpdateTextualRepresentation();
                UpdateHsl();
            }
        }

        private byte _b;
        public byte B
        {
            get
            {
                return _b;
            }
            set
            {
                if (_b == value)
                    return;

                _b = value;
                NotifyPropertyChanged("B");

                Color c = RgbColor;
                c.B = _b;
                RgbColor = c;

                UpdateTextualRepresentation();
                UpdateHsl();
            }
        }

        private int _hue;
        public int Hue
        {
            get
            {
                return _hue;
            }
            set
            {
                if (_hue == value)
                    return;

                _hue = value;
                NotifyPropertyChanged("Hue");
                UpdateRgb();
                UpdateBmp();
            }
        }

        private int _saturation;
        public int Saturation
        {
            get
            {
                return _saturation;
            }
            set
            {
                if (_saturation == value)
                    return;

                _saturation = value;
                NotifyPropertyChanged("Saturation");
                UpdateRgb();
                UpdateBmp();
            }
        }

        private int _lightness;
        public int Lightness
        {
            get
            {
                return _lightness;
            }
            set
            {
                if (_lightness == value)
                    return;

                _lightness = value;
                NotifyPropertyChanged("Lightness");
                UpdateRgb();
                UpdateBmp();
                UpdateThumbSelectBarFromLightness();
            }
        }

        public ColorSelectControl()
        {
            InitializeComponent();

            _selectAreaBmp = new WriteableBitmap(HueCircumference, BitsPerPixel, 96, 96, PixelFormats.Bgr32, null);
            _selectBarBmp = new WriteableBitmap(1, BitsPerPixel, 96, 96, PixelFormats.Bgr32, null);

            selectArea.Source = _selectAreaBmp;
            selectBar.Source = _selectBarBmp;

            basePanel.DataContext = this;
        }

        private void UpdateHsl()
        {
            ToHsl(_r, _g, _b, out _hue, out _saturation, out _lightness);
            NotifyPropertyChanged("Hue");
            NotifyPropertyChanged("Saturation");
            NotifyPropertyChanged("Lightness");

            UpdateBmp();
            UpdateThumbSelectBarFromLightness();
        }

        private void UpdateRgb()
        {
            Color c = FromHsl(_hue, _saturation, _lightness);
            _r = c.R;
            _g = c.G;
            _b = c.B;
            NotifyPropertyChanged("R");
            NotifyPropertyChanged("G");
            NotifyPropertyChanged("B");
            RgbColor = c;
            UpdateTextualRepresentation();
        }

        private void UpdateBmp()
        {
            Dispatcher.BeginInvoke(new Action(() =>
            {
                if (_shouldUpdateBmp)
                {
                    _shouldUpdateBmp = false;
                    UpdateSelectArea();
                    UpdateSelectBar();
                }
            }));

            _shouldUpdateBmp = true;
        }

        private void UpdateTextualRepresentation()
        {
            textBoxTextualRepresentation.Text =
                "#" +
                _r.ToString("X2") +
                _g.ToString("X2") +
                _b.ToString("X2");
        }

        private void UpdateFromTextualRepresentation()
        {
            string text = textBoxTextualRepresentation.Text;
            if (text.ToLower() == "transparent")
            {
                SystemSounds.Beep.Play();
                return;
            }

            try
            {
                RgbColor = (Color)ColorConverter.ConvertFromString(text);
            }
            catch (NullReferenceException)
            {
                SystemSounds.Beep.Play();
            }
            catch (FormatException)
            {
                SystemSounds.Beep.Play();
            }
        }

        private void UpdateHsFromMouseCursor(MouseEventArgs evt)
        {
            if (evt.LeftButton == MouseButtonState.Released)
                return;

            double hueRangeOnImage = selectArea.ActualWidth;
            double saturationRangeOnImage = selectArea.ActualHeight;

            Point p = evt.GetPosition(selectArea);
            int x = (int)(p.X * HueCircumference / hueRangeOnImage);
            if (HueCircumference <= x)
                x = HueCircumference - 1;
            int y = (int)((BitsPerPixel - p.Y) * _BitsPerPixel_1 / saturationRangeOnImage);
            if (BitsPerPixel <= y)
                y = _BitsPerPixel_1;

            Hue = x;
            Saturation = y;

            evt.Handled = true;
        }

        private void UpdateLightnessFromMouseCursor(MouseEventArgs evt)
        {
            if (evt.LeftButton == MouseButtonState.Released)
                return;

            double rangeOnImage = selectBar.ActualHeight;

            Point p = evt.GetPosition(selectBar);
            int y = (int)((BitsPerPixel - p.Y) * _BitsPerPixel_1 / rangeOnImage);
            if (y < 0)
                y = 0;
            if (BitsPerPixel <= y)
                y = _BitsPerPixel_1;

            Lightness = y;

            evt.Handled = true;
        }

        // Lightnessプロパティが変更されたときのThumbSelectBarの移動
        private void UpdateThumbSelectBarFromLightness()
        {
            // 座標が2度設定されるのを防ぐため、ThumbSelectBarがドラック中はそのまま返す
            if (thumbSelectBar.IsDragging)
                return;

            double thumbHotspotY = thumbSelectBar.ActualHeight / 2.0;
            double rangeOnImage = selectBar.ActualHeight;
            double bottom = _lightness * rangeOnImage / _BitsPerPixel_1 - thumbHotspotY;
            Canvas.SetBottom(thumbSelectBar, bottom);
        }

        // ThumbSelectBarがユーザーにドラッグされたときのThumbの移動とLightnessの設定
        private void UpdateThumbSelectBarFromDragDelta(DragDeltaEventArgs evt)
        {
            double rangeOnImage = selectBar.ActualHeight;
            double thumbHotspotY = thumbSelectBar.ActualHeight / 2.0;

            double bottom = Canvas.GetBottom(thumbSelectBar);
            bottom -= evt.VerticalChange;
            if (bottom < -thumbHotspotY)
                bottom = -thumbHotspotY;
            if (rangeOnImage - thumbHotspotY < bottom)
                bottom = rangeOnImage - thumbHotspotY;

            Canvas.SetBottom(thumbSelectBar, bottom);

            Lightness = (int)((bottom + thumbHotspotY) * _BitsPerPixel_1 / rangeOnImage);

            evt.Handled = true;
        }

        private void UpdateSelectArea()
        {
            _selectAreaBmp.Lock();
            int width = HueCircumference;
            int height = BitsPerPixel;
            Int32Rect dirtyRect = new Int32Rect(0, 0, width, height);

            int lightness = _lightness;

            unsafe
            {
                int stride = _selectAreaBmp.BackBufferStride / sizeof(Pixel);
                Pixel* p = ((Pixel*)_selectAreaBmp.BackBuffer) + stride * _BitsPerPixel_1;

                for (int saturation = 0; saturation < height; saturation++)
                {
                    for (int hue = 0; hue < width; hue++)
                    {
                        Color c = FromHsl(hue, saturation, lightness);
                        p[hue].A = 0xff;
                        p[hue].R = c.R;
                        p[hue].G = c.G;
                        p[hue].B = c.B;
                    }
                    p -= stride;
                }
            }

            _selectAreaBmp.AddDirtyRect(dirtyRect);
            _selectAreaBmp.Unlock();
        }

        private void UpdateSelectBar()
        {
            _selectBarBmp.Lock();
            int height = _selectBarBmp.PixelHeight;
            Int32Rect dirtyRect = new Int32Rect(0, 0, 1, height);

            int hue = _hue;
            int saturation = _saturation;

            unsafe
            {
                int stride = _selectBarBmp.BackBufferStride / sizeof(Pixel);
                Pixel* p = (Pixel*)_selectBarBmp.BackBuffer + stride * _BitsPerPixel_1;

                for (int lightness = 0; lightness < BitsPerPixel; lightness++)
                {
                    Color c = FromHsl(hue, saturation, lightness);
                    p->A = 0xff;
                    p->R = c.R;
                    p->G = c.G;
                    p->B = c.B;

                    p -= stride;
                }
            }

            _selectBarBmp.AddDirtyRect(dirtyRect);
            _selectBarBmp.Unlock();
        }

        public void ToHsl(byte r, byte g, byte b, out int hue, out int saturation, out int lightness)
        {
            double max = Math.Max(Math.Max(r, g), b) / (double)_BitsPerPixel_1;
            double min = Math.Min(Math.Min(r, g), b) / (double)_BitsPerPixel_1;
            double c = max - min;

            int denominator = 2 * r - g - b;
            double h =
                    (denominator != 0
                    ? Math.Atan2(Math.Sqrt(3.0) * (g - b), denominator) * HueCircumference / (Math.PI * 2.0)
                    : 0);
            double l = (max + min) / 2.0;
            double s = (c == 0 ? 0 : c / (1.0 - Math.Abs(2.0 * l - 1.0)));

            hue = (int)h;
            if (hue < 0)
                hue += HueCircumference;
            saturation = (int)(s * _BitsPerPixel_1);
            lightness = (int)(l * _BitsPerPixel_1);
        }

        public Color FromHsl(int hue, int saturation, int lightness)
        {
            /*
            Debug.Assert(0 <= hue && hue < HueCircumference);
            Debug.Assert(0 <= saturation && saturation < BitsPerPixel);
            Debug.Assert(0 <= lightness && lightness < BitsPerPixel);
            */
            while (hue < 0)
                hue += HueCircumference;
            while (HueCircumference <= hue)
                hue -= HueCircumference;
            if (saturation < 0)
                saturation = 0;
            if (BitsPerPixel <= saturation)
                saturation = _BitsPerPixel_1;
            if (lightness < 0)
                lightness = 0;
            if (BitsPerPixel <= lightness)
                lightness = _BitsPerPixel_1;

            Color res = new Color();
            res.A = 255;

            double sn = (double)saturation / (double)_BitsPerPixel_1;
            double ln = (double)lightness / (double)_BitsPerPixel_1;

            double c = (1.0 - Math.Abs(2.0 * ln - 1.0)) * sn;
            double regionH = (double)(hue * 6) / (double)HueCircumference;
            double x = c * (1.0 - Math.Abs(regionH % 2.0 - 1.0));
            double m = ln - c / 2.0;

            byte byteC = (byte)((c + m) * _BitsPerPixel_1);
            byte byteX = (byte)((x + m) * _BitsPerPixel_1);
            byte byteM = (byte)(m * _BitsPerPixel_1);

            int region = (int)regionH;
            switch (region)
            {
            case 0: res.R = byteC; res.G = byteX; res.B = byteM; break;
            case 1: res.R = byteX; res.G = byteC; res.B = byteM; break;
            case 2: res.R = byteM; res.G = byteC; res.B = byteX; break;
            case 3: res.R = byteM; res.G = byteX; res.B = byteC; break;
            case 4: res.R = byteX; res.G = byteM; res.B = byteC; break;
            case 5: res.R = byteC; res.G = byteM; res.B = byteX; break;
            default:
                throw new ArgumentException("Hue value is illegal.");
            }

            return res;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            UpdateThumbSelectBarFromLightness(); // Load前にも呼ばれるが、そのときはコントロールのサイズが確定していないため位置が不正になる。
        }

        private void OnUpdateFromTextualRepresentationCommand(object sender, ExecutedRoutedEventArgs e)
        {
            UpdateFromTextualRepresentation();
        }

        private void OnUpdateFromTextualRepresentationButtonClick(object sender, RoutedEventArgs e)
        {
            UpdateFromTextualRepresentation();
        }

        private void OnTextBoxTextualRepresentationGotFocus(object sender, RoutedEventArgs e)
        {
            Dispatcher.BeginInvoke((Action)(() => textBoxTextualRepresentation.SelectAll()));
        }

        private void OnMouseMoveInSelectArea(object sender, MouseEventArgs evt)
        {
            UpdateHsFromMouseCursor(evt);
        }

        private void OnMouseDownInSelectArea(object sender, MouseButtonEventArgs evt)
        {
            UpdateHsFromMouseCursor(evt);
        }

        private void OnMouseMoveInSelectBar(object sender, MouseEventArgs evt)
        {
            UpdateLightnessFromMouseCursor(evt);
        }

        private void OnMouseDownInSelectBar(object sender, MouseButtonEventArgs evt)
        {
            selectBar.CaptureMouse();
            UpdateLightnessFromMouseCursor(evt);
        }

        private void OnMouseUpInSelectBar(object sender, MouseButtonEventArgs evt)
        {
            selectBar.ReleaseMouseCapture();
        }

        private void OnSelectBarThumbDragDelta(object sender, DragDeltaEventArgs evt)
        {
            UpdateThumbSelectBarFromDragDelta(evt);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void NotifyPropertyChanged(string property)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }
    }
}

WriteableBitmapに書き込むときに使っているPixel構造体はこんな感じ。

using System.Runtime.InteropServices;

namespace UserControls
{
    [StructLayout(LayoutKind.Explicit)]
    struct Pixel
    {
        [FieldOffset(0)]
        public System.UInt32 C;

        [FieldOffset(0)]
        public byte B;
        [FieldOffset(1)]
        public byte G;
        [FieldOffset(2)]
        public byte R;
        [FieldOffset(3)]
        public byte A;
    }
}

C言語で言うところの共用体のような使い方ですね。

あと、前に作ったNumericBoxコントロールが必要です。 NumericBox.xamlとNumericBox.xaml.csとNumericBoxValidationRule.csをコピーしてください。

実は、NumericBoxはこの色選択コントロールの部品に必要だから作ったんですよね。 やっと目的達成ですよ。 「ColorDialogの代わりをチャッチャと作ろう」ってだけだったはずなのに、こんなにダラダラ長いコードになってしまった。

以下、使用例です。 コントロールとして使う場合はこんな感じ。 MainWindow.xamlは、

<Window x:Class="ColorSelectControlTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:appCtrls="clr-namespace:UserControls"
        Title="パネルのサンプル"
        Width="460"
        Height="524"
>
    <Grid Margin="4">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Border Grid.ColumnSpan="3" BorderBrush="Gray" BorderThickness="2" CornerRadius="4">
            <appCtrls:ColorSelectControl
                    x:Name="colorSelectControl"
                    Margin="4"
                    RgbColor="{Binding ElementName=brush1, Path=Color}"
                    RgbColorChanged="OnSelectedColorChanged"
            />
        </Border>
        <Label Grid.Column="0" Grid.Row="1">バインド</Label>
        <Rectangle Grid.Column="1" Grid.Row="1" Width="48" Height="24" HorizontalAlignment="Left">
            <Rectangle.Fill>
                <SolidColorBrush x:Name="brush1" Color="Green"/>
            </Rectangle.Fill>
        </Rectangle>
        <Label Grid.Column="0" Grid.Row="2">イベント</Label>
        <Rectangle Grid.Column="1" Grid.Row="2" Width="48" Height="24" HorizontalAlignment="Left">
            <Rectangle.Fill>
                <SolidColorBrush x:Name="brush2"/>
            </Rectangle.Fill>
        </Rectangle>
        <Button
                Grid.Column="2"
                Grid.Row="2"
                Content="ダイアログで選択"
                Click="OnShowDialogButtonClick"
        />
    </Grid>
</Window>

wpfなのでダラダラ長いけど、ColorSelectControl要素の部分は短めで済んでいます。

コードの中身は、1つ目のRectangleは色選択コントロールの選択色とバインドしてます。 2つ目のRectangleはRgbColorChangedイベントで変えます。

MainWindow.xaml.csは、

using System.Windows;
using System.Windows.Media;

namespace ColorSelectControlTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void OnSelectedColorChanged(object sender, RoutedPropertyChangedEventArgs<Color> evt)
        {
            brush2.Color = evt.NewValue;
        }

        private void OnShowDialogButtonClick(object sender, RoutedEventArgs e)
        {
            ColorSelectWindow colorSelectWindow = new ColorSelectWindow();
            colorSelectWindow.Owner = this;
            colorSelectWindow.RgbColor = brush1.Color;
            if (colorSelectWindow.ShowDialog() ?? false)
            {
                brush1.Color = colorSelectWindow.RgbColor;
            }
        }
    }
}

OnShowDialogButtonClickには色選択ダイアログを呼び出すコードが書かれています。 色選択ダイアログは、Windowに色選択コントロールを載せて作りました。 ColorSelectWindow.xamlは、

<Window x:Class="ColorSelectControlTest.ColorSelectWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:appCtrls="clr-namespace:UserControls"
        Title="ダイアログのサンプル"
        Width="480"
        Height="520"
        ShowInTaskbar="False"
        WindowStyle="ToolWindow"
>
    <DockPanel Margin="4">
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right">
            <Button IsDefault="True" Width="40" Margin="10,0" Click="OnOkButtonClick">OK</Button>
            <Button IsCancel="True">Cancel</Button>
        </StackPanel>
        <appCtrls:ColorSelectControl x:Name="colorSelectControl"/>
    </DockPanel>
</Window>

ColorSelectWindow.xaml.csは、

using System.Windows;
using System.Windows.Media;

namespace ColorSelectControlTest
{
    public partial class ColorSelectWindow : Window
    {
        public Color RgbColor
        {
            get
            {
                return colorSelectControl.RgbColor;
            }
            set
            {
                colorSelectControl.RgbColor = value;
            }
        }

        public ColorSelectWindow()
        {
            InitializeComponent();
        }

        private void OnOkButtonClick(object sender, RoutedEventArgs e)
        {
            DialogResult = true;
        }
    }
}