Windows Forms C#定石 - DataGridView - 値依存イメージ表示, ちらつき防止

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

**DataGridView については下記記事もあります**
- [Windows Forms C#定石 - DataGridView - EditMode, DropDown](https://qiita.com/chai0917/items/55af57dc17de0dd97bf0)
- [Windows Forms C#定石 - DataGridView - ReadOnly, Disable相当](https://qiita.com/chai0917/items/525634fb35cf4efc0b40)
- [Windows Forms C#定石 - DataGridView - DataTable](https://qiita.com/chai0917/items/dbf8e6480d6383480f8e)


### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8

::: note
記載したソースコードは .NET 8 ベースとしています。
.NET Framework 4.8 の場合は、コメントで記載している null 許容参照型の明示 `?` を削除してください。
:::

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::


## 値依存イメージ表示

[Dev Q&A: DataGridView | Microsoft Learn](https://learn.microsoft.com/en-us/archive/msdn-magazine/2006/january/dev-q-a-datagridview) で、Figure 5 Custom Image Column for the DataGridView として記載されている手法があります。
この手法だと、DataGridViewImageCell 派生クラス（ CustomImageCell ）に固定イメージを設定しているので、異なるイメージを利用する場合には、別クラスを作成しなければなりません。
そこで、DataGridViewImageColumn 派生クラスにイメージを保持して、イメージを設定可能としています。

```CSharp
// 数値に関係づけられたイメージを表示する DataGridViewCell
public class DataGridViewBindImageColumn : DataGridViewImageColumn
{
  // 初期値を設定しておかないと、意図した動作にならない
  public Image[] BindImages { get; set; } = new Image[] {
                 SystemIcons.Application.ToBitmap() };

  public DataGridViewBindImageColumn()
  {
    this.CellTemplate = new DataGridViewBindImageCell();
    this.ValueType = typeof(int);
    this.ImageLayout = DataGridViewImageCellLayout.Zoom;
    this.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
  }
}
public class DataGridViewBindImageCell : DataGridViewImageCell
{
  public DataGridViewBindImageCell()
  {
    this.ValueType = typeof(int);
  }

  // .NET Framework 時 object? の ? 不要
  protected override object? GetFormattedValue(
    object value, int rowIndex, ref DataGridViewCellStyle cellStyle, 
    TypeConverter valueTypeConverter, 
    TypeConverter formattedValueTypeConverter,
    DataGridViewDataErrorContexts context)
  {
    if (this.OwningColumn is DataGridViewBindImageColumn column
     && column.BindImages != null
     && Value is int index
     && index >= 0 && index < column.BindImages.Length)
    {
      return column.BindImages[index];
    }
    return null;
  }
  // 行追加時の既定値
  public override object DefaultNewRowValue => -1;
}
```

上記の利用方法を記載します。

```CSharp
// DataGridView 設定
dataGridView1.AllowUserToResizeRows = false;
dataGridView1.AutoGenerateColumns = false;
dataGridView1.MultiSelect = false;            // 複数選択無効
dataGridView1.ColumnHeadersVisible = true;    // 列ヘッダ表示
dataGridView1.RowHeadersVisible = false;      // 行ヘッダ非表示
dataGridView1.AllowUserToDeleteRows = false;  // 削除キーで削除を無効
dataGridView1.AllowUserToAddRows = false;     // 末尾行での行追加を無効
dataGridView1.ScrollBars = ScrollBars.Both;
dataGridView1.ColumnHeadersHeight = 28;

dataGridView1.Columns.AddRange(new DataGridViewColumn[]
{
  new DataGridViewTextBoxColumn { Name = "ID", Width = 30, ReadOnly = true },
  new DataGridViewBindImageColumn { Name = "Default", Width = 50 },
  new DataGridViewBindImageColumn { Name = "Status", Width = 50,
        BindImages = new Image[] {
          SystemIcons.Information.ToBitmap(),
          SystemIcons.Question.ToBitmap(),
          SystemIcons.Warning.ToBitmap(),
          SystemIcons.Error.ToBitmap() } }
});

// テストデータ
dataGridView1.Rows.Clear();
for (int no = 0; no < 5; no++)
{
  DataGridViewRow row = new DataGridViewRow();
  row.CreateCells(dataGridView1);
  row.Height = 30;

  row.Cells[0].Value = no;
  row.Cells[1].Value = no;
  row.Cells[2].Value = no;
  dataGridView1.Rows.Add(row);
}
```

![DataGirdView-Image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/e35a840d-d60a-4807-8e78-e155e4a1c1da.png)


## ちらつき防止
### DoubleBuffered
> [［C#］DataGridViewのDoubleBufferedを有効にする](https://propg.ee-mall.info/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0/c/c-datagridview%E3%81%AEdoublebuffered%E3%82%92%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B/)
> [方法: フォームとコントロールのダブル バッファリングでグラフィックスのちらつきを軽減する](https://learn.microsoft.com/ja-jp/dotnet/desktop/winforms/advanced/how-to-reduce-graphics-flicker-with-double-buffering-for-forms-and-controls)

DataGridView を使ってみると、表示が重いことから、ちらつきが気になることがあります。
この表示の重さは、DoubleBuffered プロパティを有効にすることによって、改善が期待できます。
DataGridView では、DoubleBuffered プロパティが protected となってしまっているので直接アクセスできませんが、リフレクションを使用して設定することが可能です。

```CSharp
using System.Reflection;
```
```CSharp
this.Load += Form1_Load;
```
```CSharp
// .NET Framework 時 object? の ? 不要
private void Form1_Load(object? sender, EventArgs e)
{
  // デザイナで DataGridView dataGridView1, dataGridView2 を配置
  var info = typeof(DataGridView).GetProperty("DoubleBuffered",
               BindingFlags.Instance | BindingFlags.NonPublic);
  info?.SetValue(dataGridView1, true, null);
  info?.SetValue(dataGridView2, true, null);
}
```

::: note
下記手法で Form 全体をダブルバッファリングさせることもできますが、複雑な UI を描画する際にはパフォーマンスに影響を与える可能性があります。

```CSharp
// 対象 Form クラスに追加
protected override CreateParams CreateParams
{
  get
  {
    CreateParams cp = base.CreateParams;
    cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED 追加
    return cp;
  }
}
```
:::

### 再描画の一時停止

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.suspendlayout

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.resumelayout

DataGirdView のデフォルト動作では、行追加などの操作毎に DataGridView の再描画が発生して、画面がちらついてしまうことがあります。 
この対策としては、SuspendLayout と ResumeLayout を使用して再描画を一時停止、再開することで、ちらつきを抑えることができます。

```CSharp
// デザイナで DataGridView dataGridView1 を配置
// 一時停止
Cursor cursor = this.Cursor;
this.Cursor = Cursors.WaitCursor;
dataGridView1.SuspendLayout();

// TODO - データ更新とソート

// 再開
dataGridView1.ResumeLayout();
this.Cursor = cursor;
```