C#のColor関連の便利拡張メソッド＋α 24選（HSV色空間への変換も）

C# の色関連で便利な拡張メソッド＋αの一覧です。
なおこの記事では単にColorといった場合、System.Windows.Media.Color(WPF)を表しているとします。
一部の使用方法例にある色つきの四角形は後述するInline Color Picker拡張機能で表示しています。

# メソッド一覧
## System.Windows.Media(WPF)とSystem.Drawing.Color(WinForms)の変換

中身がほとんど同じなのに、名前空間の違う2つのColorの相互変換です。

```csharp
public static System.Windows.Media.Color ToMediaColor(this System.Drawing.Color dColor) =>
    System.Windows.Media.Color.FromArgb(dColor.A, dColor.R, dColor.G, dColor.B);

public static System.Drawing.Color ToDrawingColor(this System.Windows.Media.Color mColor) =>
    System.Drawing.Color.FromArgb(mColor.A, mColor.R, mColor.G, mColor.B);
```

## 文字列変換

文字列からColorへ変換するメソッドです。
`"#ADFF2F"`や`"Cyan"`といった文字列からColorに変換します。
標準ライブラリの変換メソッド`ColorConverter.ConvertFromString`は返り値がobjectで使いづらいため、それをColorに変換します。
また変換失敗時に、例外ではなくnullかColorの初期値(透明色`"#00000000"`)を返す2つのメソッドです。

```csharp
public static Color ToColorOrDefault(this string code) => ToColorOrNull(code) ?? default;

public static Color? ToColorOrNull(this string code)
{
    try
    {
        return (Color)ColorConverter.ConvertFromString(code);
    }
    catch (FormatException _)
    {
        return null;
    }
}
```

## 定義済み色一覧
`Colors`の定義済み色一覧を取得します。
なぜか標準ライブラリには無いのでリフレクションを使用します。

```csharp
public static IReadOnlyList<string> KnownColorNames { get; } =
    typeof(Colors)
        .GetProperties(BindingFlags.Public | BindingFlags.Static)
        .Select(info => (info.Name))
        .ToArray();

public static IReadOnlyList<Color> KnownColors { get; } =
    KnownColorNames
    .Select(n => n.ToColorOrDefault())
    .ToArray();
```

## 定義済み色かどうか
Colorが定義済みかどうか判定するメソッドと、定義済みならその名称を取得するメソッドです。
実は定義済み色には同色で別名の色が含まれています。そのため、名前順で後にくる方の色名はこのメソッドでは無視されます。

| 色名1   | 色名2（無視) | コード    |
| ------- | ------------ | --------- |
| Aqua    | Cyan         | #00FFFF |
| Fuchsia | Magenta      | #FF00FF |



```csharp
public static IReadOnlyDictionary<Color, string> KnownColorDictionary { get; } = CreateKnownColorDictonary();
private static IReadOnlyDictionary<Color, string> CreateKnownColorDictonary()
{
    var dict = new Dictionary<Color, string>(KnownColorNames.Count());

    foreach (string name in KnownColorNames)
    {
        //Cyan == Aqua, Magenta == Fuchsia
        var color = name.ToColorOrDefault();
        if (!dict.ContainsKey(color))
            dict.Add(color, name);
    }
    return dict;
}

public static bool IsKnownColor(this Color c) => KnownColorDictionary.ContainsKey(c);

public static string ToKnownColorName(this Color c) =>
    c.IsKnownColor()
        ? KnownColorDictionary[c]
        : null;
```

使用方法例です。（C# Interactive）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/8ffde518-d4ef-0e3d-eaff-9489055f4a93.png)


## Freeze化

Colorだけに限定されるわけではないですが、Freezableオブジェクトを `Freeze()`して返すだけのメソッドです。
パフォーマンス上Freezeできるオブジェクトは基本的にFreezeして返したほうがよいです。

```csharp
public static T WithFreeze<T>(this T freezableObj) where T : Freezable
{
    freezableObj.Freeze();
    return freezableObj;
}
```

## Brush変換

XAML上では`Background="Red"`と書けるので意識しませんが、本来WPFではコントロールの色指定はColor直接ではなく、Brushを使用します。単色の場合はSolidColorBrushを使用するので、それへの変換メソッドです。

```csharp
public static SolidColorBrush ToSolidColorBrush(this Color mColor, bool isFreeze = false) =>
    isFreeze
    ? new SolidColorBrush(mColor)
    : new SolidColorBrush(mColor).WithFreeze();
```

## 正規化RGBの取得
Colorの`R`, `G`, `B`プロパティはbyte型(0~255)ですが、それをdouble型(0~1)に変換します。
また、正規化したRGB値からColorを作成します。

```csharp
public static double NormalizedRed(this Color color) => Normalize(color.R);
public static double NormalizedGreen(this Color color) => Normalize(color.G);
public static double NormalizedBlue(this Color color) => Normalize(color.B);
private static double Normalize(byte value) => value / 255d;

private static byte DeNormalize(double value) =>
    value > 1 ? (byte)0xFF
    : value < 0 ? (byte)0x00
    : (byte)(value * 255d);

public static Color NormalizedRgbToColor(double nR, double nG, double nB) =>
    Color.FromRgb(
        r: DeNormalize(nR),
        g: DeNormalize(nG),
        b: DeNormalize(nB));
```

## HSB(HSV)色空間との変換
Hue(色相)・Saturation(彩度)・Brightness(明度)で色を表現するHSB色空間との変換メソッドです。
Brightnessの代わりにValueを使ってHSV色空間とも言われますが、中身は同じです。


```csharp
public struct HsbColor
{
    private double _hue;
    /// <summary>
    /// 色相 0~360
    /// </summary>
    public double Hue
    {
        get => _hue;
        set => _hue = ColorExtension.ChopIn360(value);
    }

    private double saturation;
    /// <summary>
    /// 彩度 0~1
    /// </summary>
    public double Saturation
    {
        get => saturation;
        set => saturation = Math.Max(0, Math.Min(value, 1));
    }

    private double brightness;
    /// <summary>
    /// 明度 0~1 (Value)
    /// </summary>
    public double Brightness
    {
        get => brightness;
        set => brightness = Math.Max(0, Math.Min(value, 1));
    }

    public static HsbColor FromHSB(double hue, double saturation, double brightness) =>
        new HsbColor() { Hue = hue, Saturation = saturation, Brightness = brightness };

    public override string ToString() => string.Format($"H:{Hue:F0}, S:{Saturation:F2}, B:{Brightness:F2}");
}

public static class ColorExtension
{
    public static double ChopIn360(double degreeAngle) =>
        degreeAngle > 360 ? degreeAngle % 360
        : degreeAngle < 0 ? (degreeAngle % 360) + 360
        : degreeAngle;
}

public static class HsbColorExt
{
    public static HsbColor ToHsb(this Color source)
    {
        double nR = source.NormalizedRed();
        double nG = source.NormalizedGreen();
        double nB = source.NormalizedBlue();

        double[] nRGBs = new[] { nR, nG, nB };
        double max = nRGBs.Max();
        double min = nRGBs.Min();

        double diff = max - min;

        return new HsbColor()
        {
            Hue = max == min ? 0
                : max == nR ? 60d * (nG - nB) / diff
                : max == nG ? (60d * (nB - nR) / diff) + 120d
                : (60d * (nR - nG) / diff) + 240d,
            Saturation = max == 0
                ? 0
                : diff / max,
            Brightness = max,
        };
    }

    public static Color ToRgb(this HsbColor source)
    {
        double max = source.Brightness;
        double min = max * (1 - source.Saturation);
        int hueZone = (int)(source.Hue / 60d);
        double f = source.Hue / 60d - hueZone;
        double x0 = max * (1 - f * source.Saturation);
        double x1 = max * (1 - (1 - f) * source.Saturation);

        var (nR, nG, nB) = hueZone switch
        {
            0 => (max, x1, min),
            1 => (x0, max, min),
            2 => (min, max, x1),
            3 => (min, x0, max),
            4 => (x1, min, max),
            _ => (max, min, x0),
        };

        return ColorExtension.NormalizedRgbToColor(nR, nG, nB);
    }
```

使用方法例です。（C# Interactive）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/bcfc5b0d-fb74-5be1-9038-a2ff31a3fc8b.png)


## 色相・彩度・明度の個別取得
なぜかSystem.Drawing.Colorにはあった、RGB ColorからのHSBの各パラメータの個別取得メソッドです。
上記のHSB色構造体への変換を経由して取得します。

```csharp
public static double GetHue(this Color c) =>
    c.ToHsb().Hue;
public static double GetSaturation(this Color c) =>
    c.ToHsb().Saturation;
public static double GetBrightness(this Color c) =>
    c.ToHsb().Brightness;
```

## 色相・彩度・明度の変更した色
色相・彩度・明度を変更した色を計算して返すメソッドです。
ある色をベースに明度・彩度を維持して、120°ずつ色相を変更した3色が欲しい、といった時に便利です。

```csharp
public static Color AddHue(this Color c, double deltaH)
{
    var hsb = c.ToHsb();
    hsb.Hue += deltaH;
    return hsb.ToRgb();
}
public static Color AddSaturation(this Color c, double deltaS)
{
    var hsb = c.ToHsb();
    hsb.Saturation += deltaS;
    return hsb.ToRgb();
}
public static Color AddBrightness(this Color c, double deltaB)
{
    var hsb = c.ToHsb();
    hsb.Brightness += deltaB;
    return hsb.ToRgb();
}
```

使用方法例です。（C# Interactive）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/aacdfb2d-0a9e-f99e-962b-844f42339de5.png)

# 参考


https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.color?view=netframework-4.8

http://sourcechord.hatenablog.com/entry/20130710/1373476676

https://www.rapidtables.com/convert/color/hsv-to-rgb.html

# おまけ コード上に色を表示してくれるVisualStuido拡張機能

[Inline Color Picker -marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=NikolaMSFT.InlineColorPicker&ssr=false#overview)

こんな感じで色コードまたは色名を書くとその横に小さく色を表示してくれる拡張機能です。C#でもXAMLでも、C# Interactiveウインドウでも表示されます。
コード上で色指定する時に便利。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/5eb45db8-3185-e086-98c1-9b1a8f1f15b3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/a07ae17a-59df-e009-802c-653049e68b22.png)

# 環境

VisualStudio2019
.NET Framework 4.8
C#8