[WPF]ExcelのようにDataGridのセルを数値に応じて色付けする

# [WPF]ExcelのようにDataGridのセルを数値に応じて色付けする

# はじめに

WPFでDataGridの各セルを数値に応じて色付けする方法についてまとめます。
Excelで言うところの条件付き書式ルールのような表示です。

下記画像はイメージです。
![excel_rule.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249816/5a9f435f-7737-33a1-1538-91d629ef8505.png)


# 全体構成

以下の流れで説明します。

1. ViewModelに二次元配列を用意する
2. DataGridを継承した自作コントロールを定義する
3. 自作コントロールに二次元配列をバインドする
4. バインドした二次元配列をConverterで加工する



### 1.ViewModelに二次元配列を用意する

各セルに表示する情報と色をまとめたクラスを作成し、二次元配列として定義します。

今回は色指定をViewModelで行いましたが、色指定のルールが単一であれば、Viewに色管理を任せた方がスッキリすると思います。

#### セルの表示情報クラス

```C#
class ColoredObject
{
    public object Object { get; }
    public Brush Foreground { get; }
    public Brush Background { get; }

    public ColoredObject(int value, int max)
    {
        Object = value;
        var b = (byte)Math.Min(value * 255 / max, 0xff);
        var f = (byte)~b;
        Background = new SolidColorBrush(Color.FromRgb(b, b, 0x00));
        Foreground = new SolidColorBrush(Color.FromRgb(f, f, f));
    }
}
```

#### ViewModelの2次元配列

初期値のGetSortedData()では、乱数により行列数を決めて、1から順にセル値を設定しています。（割愛）

```C#
class MainWindowViewModel : BindableBase
{
    private ColoredObject[,] _Array2d = GetSortedData();
    public ColoredObject[,] Array2d
    {
        get => _Array2d;
        private set => SetProperty(ref _Array2d, value);
    }
}
```



### 2.DataGridを継承した自作コントロールを定義する

ItemsSourceの更新に合わせてセル色も更新するため、DataGridを継承したコントロールを定義し、OnItemsSourceChanged() をoverrideします。

この辺りのBindingはxamlに押し込みたかったのですが、今の私には何ともできませんでした…　スマートな実装あればご教授下さいませ。

```C#
public class ColoredDataGrid : DataGrid
{
    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        base.OnItemsSourceChanged(oldValue, newValue);

        if (!(newValue is IEnumerable<ColoredObjectRow> newItems)) return;

        var col = newItems.First().ItemsSource.Count;
        this.Columns.Clear();
        for (int c = 0; c < col; c++)
        {
            var bindingTarget = $"{nameof(ColoredObjectRow.ItemsSource)}[{c}].";

            var style = new Style(typeof(TextBlock));

            style.Setters.Add(new Setter(TextBlock.BackgroundProperty,
                new Binding($"{bindingTarget}{nameof(ColoredObject.Background)}") { Mode = BindingMode.OneTime }));

            style.Setters.Add(new Setter(TextBlock.ForegroundProperty,
                new Binding($"{bindingTarget}{nameof(ColoredObject.Foreground)}") { Mode = BindingMode.OneTime }));

            this.Columns.Add(new DataGridTextColumn()
            {
                Binding = new Binding($"{bindingTarget}{nameof(ColoredObject.Object)}") { Mode = BindingMode.OneTime },
                ElementStyle = style,
            });
        }
    }
}
```



### 3.自作コントロールに二次元配列をバインドする

自作コントロールのItemsSourceにViewModelの二次元配列をBindingします。（Converterは後述）

ポイントは『AutoGenerateColumns="False"』で、Column追加をオートに任せず、コードビハインドにより自力で対応しています。

その他諸々のプロパティはええ感じに変更して下さい。

```xml

<v:ColoredDataGrid SnapsToDevicePixels="True" 
                   IsReadOnly="True"
                   CanUserReorderColumns="False"
                   CanUserSortColumns="False"
                   CanUserResizeRows="False"
                   CanUserResizeColumns="False"
                   SelectionMode="Extended"
                   SelectionUnit="Cell"
                   HeadersVisibility="None"
                   AutoGenerateColumns="False" 
                   ItemsSource="{Binding Array2d, Converter={StaticResource ColoredObjectBindingConverter}, Mode=OneWay}" />
```



### 4.バインドした二次元配列をConverterで加工する

自作コントロールでデータを扱うため、二次元配列を行単位にまとめます。

#### バインドデータの加工クラス

```C#
class ColoredObjectRow
{
    public IReadOnlyList<ColoredObject> ItemsSource { get; }

    public ColoredObjectRow(int r, ColoredObject[,] source)
    {
        int colLength = source.GetLength(1);
        var items = new List<ColoredObject>(colLength);
        for (var c = 0; c < items.Capacity; c++)
            items.Add(source[r, c]);
        ItemsSource = items;
    }
}
```

#### コンバータ

```C#
class ColoredObjectBindingConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is ColoredObject[,] data)
            return ConvertColoredObjectRows(data);
        return null;
    }

    private static IReadOnlyList<ColoredObjectRow> ConvertColoredObjectRows(ColoredObject[,] source)
    {
        var rowLength = source.GetLength(0);
        if (rowLength == 0) throw new ArgumentException(nameof(rowLength));

        var columnLength = source.GetLength(1);
        if (columnLength == 0) throw new ArgumentException(nameof(columnLength));

        var rows = new List<ColoredObjectRow>(rowLength);
        for (var r = 0; r < rows.Capacity; r++)
            rows.Add(new ColoredObjectRow(r, source));
        return rows;
    }
}
```


対応は以上です。



# 実行結果

Reflashボタンをクリックすると行列数がランダムに変化し、セルの値に応じて色が変わっています。

セル色は行列数により正規化しているので、常に左上(最小値)と右下(最大値)は同じ色になっています。

![app_result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249816/b6a0011b-1656-4017-449a-d8c3eafa1478.gif)


# まとめ

WPFのDataGridセルをExcelのように数値に応じて色付けする方法を紹介しました。

xamlのみで実装できなかった（コードビハインドを使っちゃた）のは少し悔しいのですが、やりたいことは実現できたので良しとします。



ソースコードはGithubにて公開しています。

[hsytkm/ColoredDataGridCell](<https://github.com/hsytkm/ColoredDataGridCell>)



# 環境

Visual Studio Community 2019
.NET Framework 4.6.1