DataGridView:ヘッダーにチェックボックスを配置して全行選択・解除(サンプルあり)

📋 目次(クリックで展開)

DataGridView で一覧画面を作成する際、先頭列へ「行選択用のチェックボックス列」を配置し、任意の行をまとめて処理できるようにしたいケースは多くあります。さらに、列ヘッダーをクリックすることで「全行を一括で選択・選択解除」できる機能は、実務の業務システムにおいて求められることが多いです。

しかし、標準の DataGridViewCheckBoxColumn を追加しただけでは、列ヘッダー部分にチェックボックスを配置するプロパティは用意されていません。

また、ただ単純に行をループして値を書き換えるだけでは、「手動でチェックした行だけ、ヘッダーで全解除したはずなのに画面上チェックが残ってしまう」という描画バグに直面します。

今回は、この描画の事象を回避し、データ行の選択状況に応じてヘッダーが「部分選択状態(TriState)」に切り替わる、一括選択機能の実装手順を解説します。

ヘッダーの全選択・解除を実現する仕組み

この機能を実装する流れは、大きく分けて以下の4つのステップになります。

  • チェックボックス列の追加とヘッダーのカスタム描画(CellPainting:先頭列に DataGridViewCheckBoxColumn を手動で追加します。ヘッダーセル(RowIndex == -1)の描画タイミングにおいて、OS標準のチェックボックスのデザインを出力できる CheckBoxRenderer.DrawCheckBox を使い、現在の全選択状態に応じたチェックボックスを手動描画します。
  • ヘッダークリック時の全行書き換えと編集終了(CellClick:ヘッダーセルがクリックされたら、現在編集中のセルの状態を強制終了(EndEdit した上で、全行のチェックボックスの値を一括で書き換えます。
  • データ変更時の自動スキャン(CellValueChanged:ユーザーがデータ行のチェックを切り替えたら全行をスキャンし、「すべて選択ならON」、「すべて未選択ならOFF」、「一部だけ選択なら部分選択(Mixed)」へとヘッダーの状態を切り替えます。
  • 値の即時確定(CurrentCellDirtyStateChanged:チェックボックスを操作した際、クリックした瞬間に値を確定させて連動をスムーズにします。

実装コード

画面に DataGridView(dataGridView1)が配置されているものとします。以下が部分選択対応および残像バグ対策を施したコード例です。

Form1.cs

Form1.cs
using System.Data;
using System.Windows.Forms.VisualStyles; // CheckBoxRenderer を使うために必要
namespace DataGridView_HeaderCheckBox;
public partial class Form1 : Form
{
private DataTable _dataTable = new DataTable();
// ヘッダーの状態を管理する変数(初期値は未チェック)
private CheckBoxState _headerState = CheckBoxState.UncheckedNormal;
// 一括更新中の「イベント連打」を防ぐための制御フラグ
private bool _isUpdatingAllRows = false;
public Form1()
{
InitializeComponent();
InitializeData();
InitializeDataGridView();
}
private void InitializeData()
{
// 表示用デモデータの準備
_dataTable.Columns.Add("ID", typeof(string));
_dataTable.Columns.Add("Country", typeof(string));
_dataTable.Columns.Add("City", typeof(string));
_dataTable.Rows.Add("B-6000", "Belgium", "Charleroi");
_dataTable.Rows.Add("04876", "Brazil", "Campinas");
_dataTable.Rows.Add("08737", "Brazil", "Resende");
_dataTable.Rows.Add("T2F 8M4", "Canada", "Tsawwassen");
}
private void InitializeDataGridView()
{
// 1. データソースをバインドする前に、チェックボックス列を手動で先頭に追加
var chkColumn = new DataGridViewCheckBoxColumn();
chkColumn.Name = "SelectChk";
chkColumn.HeaderText = ""; // ヘッダーテキストは空にして、後から画像を描画します
chkColumn.Width = 45;
chkColumn.Resizable = DataGridViewTriState.False;
dataGridView1.Columns.Add(chkColumn);
// データのバインド
dataGridView1.DataSource = _dataTable;
dataGridView1.AllowUserToAddRows = false;
// 2. 必要なイベントを購読
dataGridView1.CellPainting += DataGridView1_CellPainting;
dataGridView1.CellClick += DataGridView1_CellClick;
dataGridView1.CurrentCellDirtyStateChanged += DataGridView1_CurrentCellDirtyStateChanged;
dataGridView1.CellValueChanged += DataGridView1_CellValueChanged;
}
/// <summary>
/// セルの描画イベント(ヘッダーにチェックボックスを描画する)
/// </summary>
private void DataGridView1_CellPainting(object? sender, DataGridViewCellPaintingEventArgs e)
{
// 先頭列(ColumnIndex == 0)かつ ヘッダー行(RowIndex == -1)のときのみカスタム描画
if (e.RowIndex == -1 && e.ColumnIndex == 0)
{
e.PaintBackground(e.CellBounds, true);
Size checkBoxSize = CheckBoxRenderer.GetGlyphSize(e.Graphics, CheckBoxState.UncheckedNormal);
int x = e.CellBounds.X + (e.CellBounds.Width - checkBoxSize.Width) / 2;
int y = e.CellBounds.Y + (e.CellBounds.Height - checkBoxSize.Height) / 2;
Point drawPoint = new Point(x, y);
// 現在の _headerState(Checked/Unchecked/Mixed)に基づいて描画
CheckBoxRenderer.DrawCheckBox(e.Graphics, drawPoint, _headerState);
e.Handled = true;
}
}
/// <summary>
/// セルまたはヘッダーがクリックされたときのイベント
/// </summary>
private void DataGridView1_CellClick(object? sender, DataGridViewCellEventArgs e)
{
// 先頭列のヘッダーがクリックされたかチェック
if (e.RowIndex == -1 && e.ColumnIndex == 0)
{
// 現在編集中のデータ行があれば、その編集状態を強制終了させて確定する
dataGridView1.EndEdit();
// 現在「全選択」状態であれば次は「全解除」、それ以外(未選択・部分選択)なら次は「全選択」にする
bool nextCheckValue = (_headerState != CheckBoxState.CheckedNormal);
// イベント連打防止フラグを立てる
_isUpdatingAllRows = true;
// 3. 全行をループして、チェックボックスの値を一括で書き換える
foreach (DataGridViewRow row in dataGridView1.Rows)
{
if (!row.IsNewRow)
{
row.Cells["SelectChk"].Value = nextCheckValue;
}
}
_isUpdatingAllRows = false;
// ヘッダーの状態を更新
_headerState = nextCheckValue ? CheckBoxState.CheckedNormal : CheckBoxState.UncheckedNormal;
// ヘッダーだけでなく、データ行全体の見た目を確実に同期させるためグリッド全体を再描画
dataGridView1.Invalidate();
}
}
/// <summary>
/// チェックボックスの値が確定したときに呼び出されるイベント
/// </summary>
private void DataGridView1_CellValueChanged(object? sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0 && e.ColumnIndex == 0 && !_isUpdatingAllRows)
{
UpdateHeaderCheckBoxState();
}
}
/// <summary>
/// データ行のチェック状態をスキャンし、ヘッダーの状態を決定するメソッド
/// </summary>
private void UpdateHeaderCheckBoxState()
{
int checkedCount = 0;
int dataRowCount = 0;
foreach (DataGridViewRow row in dataGridView1.Rows)
{
if (!row.IsNewRow)
{
dataRowCount++;
if (row.Cells["SelectChk"].Value is bool b && b)
{
checkedCount++;
}
}
}
// チェック数に応じて、ヘッダーの状態(3つのステータス)を割り出す
if (checkedCount == 0)
{
_headerState = CheckBoxState.UncheckedNormal; // 全て未選択
}
else if (checkedCount == dataRowCount)
{
_headerState = CheckBoxState.CheckedNormal; // 全て選択
}
else
{
_headerState = CheckBoxState.MixedNormal; // 一部のみ選択(部分選択状態)
}
dataGridView1.InvalidateCell(0, -1);
}
private void DataGridView1_CurrentCellDirtyStateChanged(object? sender, EventArgs e)
{
if (dataGridView1.CurrentCell is DataGridViewCheckBoxCell)
{
dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
}

サンプルアプリケーション

DataGridView_HeaderCheckBox.zip
※ サンプルアプリケーションは、.NET 10 をターゲットに作成しています。

一部の行を選択すると、列ヘッダーは部分選択状態になります。

また、全ての行を選択すると、列ヘッダーのチェックボックスも全選択(チェック)状態になります。

実装のポイント

1. 手動チェック行の残像を防ぐ2つの対策

データ行を手動でチェックした後にヘッダーで一括操作を行うと、手動操作した行だけ「見た目のチェックが切り替わらない(別の行を選ぶと直る)」という事象が発生します。これは以下の2つの対策で防ぐことができます。

  • dataGridView1.EndEdit() の実行:ユーザーがチェックを入れたセルは、現在フォーカスを持っている「編集中のアクティブセル」になっています。この状態のまま裏で値を変更しても、セルが古い編集画面を抱え込んでしまい、見た目の表示が更新されません。ループ前に編集モードを強制終了させることで、セルの状態を通常に戻します。
  • dataGridView1.Invalidate() による全体再描画:ヘッダー部分だけの再描画(InvalidateCell)では、アクティブだったデータ行の描画更新が漏れてしまうことがあります。一括操作の後はグリッド全体に再描画を指示(Invalidate)することで、すべての行の表示をデータと同期します。

2. 一括書き換え時の「フリーズ」を防ぐフラグ制御

ヘッダーがクリックされた際、コード内のループ処理によって全行のチェックボックス(Value)を上書きします。DataGridView の仕様として、Value が書き換わると行数分の CellValueChanged イベントが発生します。

もし制御フラグ(_isUpdatingAllRows)を用意していないと、「1行目の値を書き換える ➡️ イベントが発生して全行を再スキャンする ➡️ 2行目を書き換える ➡️ また全行を再スキャンする……」 というループが発生し、データ件数が多い場合に画面がフリーズします。一括処理の前後をフラグで挟み、処理中の無駄な全行スキャンを回避します。

3. 部分選択(Mixed)状態を表現する CheckBoxState.MixedNormal

CheckBoxRenderer.DrawCheckBox メソッドには、第3引数に CheckBoxState.MixedNormal を指定することができます。これを使用することで、Windows 10 や 11 などの OS のビジュアルスタイルに準拠した「四角い塗りつぶしマーク」の部分選択インジケーターを描画することができます。

まとめ

今回は、DataGridView のヘッダーに一括選択用のチェックボックスを配置し、部分選択(TriState)への対応と、アクティブ行の残像バグを解消する実装手順について解説しました。

データ変更のライフサイクルと DataGridView の描画タイミングをコントロールすることで、ユーザーがストレスを感じない一括操作 UI を構築できます。

  • ヘッダーでの一括処理の前には、dataGridView1.EndEdit() でカレントセルの編集状態を終わらせる
  • 値の一括変更後は dataGridView1.Invalidate() を呼び、グリッド全体の描画を同期する
  • CellValueChanged による自動スキャン時は、_isUpdatingAllRows フラグで多重ループによるフリーズを防ぐ
  • 一部選択時は CheckBoxState.MixedNormal を使用し、直感的な部分選択状態を表現する

実務のシステム開発で役立つテクニックです。参考になりましたら幸いです。


💬 コメント