DataGridView:ボタン列の画像表示とクリックイベントの実装(サンプルあり)

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

DataGridView に「詳細」や「削除」、「検索」といったボタン列(DataGridViewButtonColumn)を追加した際、標準機能のままではボタンの上にテキストしか表示できません。ボタン内にアイコン画像を表示して、さらにクリックされたときに特定の処理を動かすというUIは、業務システムにおいて必須の要件です。

DataGridView のボタン列には直接画像を指定するプロパティはありませんが、CellPainting イベントを利用してボタンの上に画像を手動で重ねて描画し、CellContentClick イベントでクリック時のアクションを補足することで、綺麗に画像入りボタンとそのイベントハンドリングを実装できます。

今回は、プロジェクト内の特定フォルダに配置した 24×24 ピクセルの「検索アイコン(PNG)」をボタンに表示し、クリック時に MessageBox を表示するまでの完全な手順を解説します。

事前準備:画像ファイルの配置と設定

今回はプロジェクト内に配置した画像ファイルを直接読み込む方式をとります。

  1. プロジェクトの配下に Images という名前のフォルダを作成します。
  2. そのフォルダの中に、24×24 ピクセルの search.png という画像ファイルを配置します。
  3. 配置した search.png をクリックしてプロパティウィンドウを開き、「出力ディレクトリーにコピー」 の設定を 「新しい場合はコピーする」 (または「常にコピーする」)に変更します。

これにより、ビルド時に実行ファイル(.exe)と同じ階層へ自動的に Images/search.png が配置されるようになります。

実装コード

以下は、データの入った DataGridView に「検索」用の画像入りボタン列を追加し、ボタンがクリックされた際に対象行のデータをMessageBoxで表示するコード例です。

Form1.cs

Form1.cs
using System.Data;
namespace DataGridView_ImageButtonColumn
{
public partial class Form1 : Form
{
private DataTable _dataTable = new DataTable();
// 読み込んだ画像オブジェクトを使い回す(キャッシュする)ための変数
private Bitmap? _searchIcon;
public Form1()
{
InitializeComponent();
LoadFileIcon(); // 起動時に一度だけ画像を読み込む
InitializeDataGridView();
}
/// <summary>
/// 出力ディレクトリにコピーされたファイルから画像を読み込む
/// </summary>
private void LoadFileIcon()
{
try
{
// 実行ファイルがあるフォルダ(StartupPath)内の「Images/search.png」の絶対パスを結合
string imagePath = Path.Combine(Application.StartupPath, "Images", "search.png");
if (File.Exists(imagePath))
{
// ファイルから24x24のBitmapを生成してキャッシュ
_searchIcon = new Bitmap(imagePath);
}
else
{
MessageBox.Show($"画像ファイルが見つかりません。想定パス: {imagePath}",
"エラー", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
catch (Exception ex)
{
MessageBox.Show($"画像の読み込み中にエラーが発生しました:\n{ex.Message}",
"エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void InitializeDataGridView()
{
// 1. テストデータの準備
_dataTable.Columns.Add("Keywords", typeof(string));
_dataTable.Columns.Add("Target", typeof(string));
_dataTable.Rows.Add("C# 画面遷移", "Google");
_dataTable.Rows.Add("DataGridView 描画", "Bing");
dataGridView1.DataSource = _dataTable;
// 2. 検索ボタン列を手動で追加
var btnColumn = new DataGridViewButtonColumn();
btnColumn.Name = "SearchBtn";
btnColumn.HeaderText = "検索実行";
btnColumn.Text = ""; // ボタン上のテキストは空にして画像だけにする
btnColumn.UseColumnTextForButtonValue = true;
btnColumn.Width = 100; // 24x24のアイコンが余裕を持って収まる列幅
dataGridView1.Columns.Insert(0, btnColumn);
dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
// 24x24のアイコンが上下に見切れないよう、行高さを少し広めに設定(推奨: 32以上)
dataGridView1.RowTemplate.Height = 38;
// 3. セルの描画イベント(CellPainting)を購読
dataGridView1.CellPainting += DataGridView1_CellPainting;
// 4. ボタンのクリックイベント(CellContentClick)を購読
dataGridView1.CellContentClick += DataGridView1_CellContentClick;
}
/// <summary>
/// セルの描画が行われるときに呼び出されるイベント(画像の描画)
/// </summary>
private void DataGridView1_CellPainting(object? sender, DataGridViewCellPaintingEventArgs e)
{
// ヘッダー行や、目的のボタン列以外の列は処理対象外にする
if (e.RowIndex < 0 || e.ColumnIndex < 0) return;
if (dataGridView1.Columns[e.ColumnIndex].Name == "SearchBtn")
{
// A. ボタンの背景・枠線だけを通常通りシステムに描画させる(文字用のForegroundは除外)
e.Paint(e.CellBounds, DataGridViewPaintParts.All & ~DataGridViewPaintParts.ContentForeground);
// B. キャッシュしておいた 24x24 のアイコンを中央寄せで描画
if (_searchIcon != null)
{
// セルの幅・高さから、中央寄せになるX, Y座標を算出
int x = e.CellBounds.Left + (e.CellBounds.Width - _searchIcon.Width) / 2;
int y = e.CellBounds.Top + (e.CellBounds.Height - _searchIcon.Height) / 2;
e.Graphics.DrawImage(_searchIcon, x, y);
}
// C. システムによる標準のテキスト重ね描きなどをキャンセル
e.Handled = true;
}
}
/// <summary>
/// セルの中身(今回の場合はボタン)がクリックされたときに呼び出されるイベント
/// </summary>
private void DataGridView1_CellContentClick(object? sender, DataGridViewCellEventArgs e)
{
// 列ヘッダーのクリックや、目的のボタン列以外のクリックは無視する
if (e.RowIndex < 0 || e.ColumnIndex < 0) return;
if (dataGridView1.Columns[e.ColumnIndex].Name == "SearchBtn")
{
// クリックされた行のデータを取得
string keywords = dataGridView1.Rows[e.RowIndex].Cells["Keywords"].Value?.ToString() ?? "";
string target = dataGridView1.Rows[e.RowIndex].Cells["Target"].Value?.ToString() ?? "";
// イベントハンドリングの証明として、メッセージボックスを表示
MessageBox.Show($"「{target}」でキーワード「{keywords}」の検索を実行します。",
"検索イベント感知", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
base.OnFormClosed(e);
// ファイルから生成した画像リソースを明示的に解放
// これを怠るとWindowsによってファイルがロックされ続け、アプリ実行中に画像を上書き・削除できなくなります
_searchIcon?.Dispose();
}
}
}

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

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

実行結果

「検索実行」のボタン列に、用意しておいた検索用画像が表示されていることが分かります。また、この列上のボタンクリックで、イベントハンドリングによる MessageBox も表示されます。

実装の注意点

このUIとイベント制御をバグなく、かつ高速に動作させるために考慮されているプロ向けの設計ポイントが3つあります。

1. CellPainting 内での画像ロードによる「フリーズ・メモリひっ迫」の回避

CellPainting は、画面のスクロールやマウスカーソルの移動に伴って、1秒間に何十回・何百回と猛烈な頻度で連打されるイベントです。この中で毎回ファイルのディスク読み込み(File I/O)や新しい new Bitmap() を実行すると、描画処理が全く追いつかなくなり画面が激しくカクつく(フリーズする)原因になります。

// ❌ 画面がフリーズし、メモリがひっ迫するなコード例
private void DataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
// マウスが動くたびに毎回ディスクからファイルを読み直してBitmapを生成してしまう
var icon = new Bitmap(Path.Combine(Application.StartupPath, "Images", "search.png"));
e.Graphics.DrawImage(icon, x, y);
}

また、C#の Bitmap はWindowsの管理する「GDIオブジェクト」というリソースを大量に消費するため、一瞬でシステムの上限に達して OutOfMemoryException でアプリが強制終了します。 必ずサンプルコードのように、起動時に一回だけ読み込んでプライベート変数(フィールド)に保持(キャッシュ)し、イベント内ではそれを使い回す設計を徹底してください。

2. 正確なクリック判定には CellContentClick を使う

DataGridView には行やセルをクリックした際のイベントとして CellClick も用意されていますが、ボタン列の制御には CellContentClick を使用するのが最も確実です。

  • CellClick:ボタンの周辺にある「セルの僅かな余白(マージン)」をクリックしただけでもイベントが発火してしまう。
  • CellContentClick:セルの中身、つまり「配置されているボタンそのもの」がカチッと押されたときだけ正確に発火する。

この選定を行うだけで、「ボタンじゃない隙間を触ったのに処理が動いてしまった」というユーザーの誤操作やバグを防ぐことができます。

3. パス指定には Application.StartupPath を組み合わせる

単に new Bitmap("Images/search.png") とだけ記述すると、システムは「カレントディレクトリ(アプリが現在作業しているフォルダ)」を基準にファイルを探します。これは、アプリ内で「ファイルを開くダイアログ」などを使用して別のフォルダを選択した拍子に、作業フォルダの基準が勝手にズレてしまうという落とし穴があります。
そのため、業務システムでは必ず Application.StartupPath(実行ファイルのある絶対パス)を起点に Path.Combine で結合し、パスの基準線が絶対にブレないように固定するのが定石です。

まとめ

今回は、DataGridView のボタン列に画像(24x24の検索アイコン)をきれいに描画し、クリックイベントを正確にハンドリングする方法について解説しました。

CellPainting による土台描画と画像のキャッシュ処理を組み合わせることで、OS標準の立体感やクリック時の沈み込みアニメーションを活かしたまま、グラフィカルなUIが作れるようになります。

  • CellPaintingContentForeground 以外を先に描き、その上に画像を重ねる
  • 描画パフォーマンスを落とさないため、画像は起動時に一度だけ読み込んで使い回す
  • ボタンが本当に押されたことを保証するため、イベントには CellContentClick を使用する
  • フォーム終了時には _searchIcon.Dispose() を呼び出し、ファイルロックを適切に解除する

検索画面やデータ一覧画面の操作性を大幅に向上させるテクニックですので、洗練された業務画面を構築する際の参考になりましたら幸いです。


💬 コメント