C# DataGridView 行選択イベント 3 種 — SelectionChanged / CellEnter / CurrentCellChanged の使い分け早見表

みなさんこんにちは!ヒロポンです!!

朝、客先で席に着いた瞬間に「DataGridView の行を選択したら下のパネルに明細を表示してほしいんですけど」って肩を叩かれた経験、ないですか??

ん?普通に SelectionChanged で書けばよくない??俺も最初はそう思ってた。素直に SelectionChanged でハンドラ書いて、動作確認 OK、レビューも通って、本番投入。ここまでは普通の業務 SE の朝です。

数日後、業務側から「Ctrl+A で全選択した瞬間に画面が固まる」と連絡が来た。ログ見たら、1000 行選んだ瞬間にハンドラが 1000 回走って、明細パネルを 1000 回書き換えてた。マジか……。

DataGridView で「行を選んだ瞬間」を取るだけのことに、イベントが 3 つある。SelectionChanged / CellEnter / CurrentCellChanged。発火タイミングが微妙に違って、朝礼前のデバッグで一番踏みやすいやつです。

今回は、この 3 つを使い分けるための早見表と、コピペで動くコードを置いておきます。

目次

結論: 行選択 → 明細表示なら CurrentCellChanged + 起動時 guard

先に答えだけ言うと、「行を選んだ瞬間に下のパネルへ明細を流す」系の業務 UI なら、CurrentCellChanged + Form_Load 直後の null guard が一番安全です。

private void dgv_CurrentCellChanged(object sender, EventArgs e)
{
    // Form_Load 直後にも 1 回呼ばれるので、CurrentRow null チェック必須
    var row = dgv.CurrentRow;
    if (row == null || row.IsNewRow) return;

    var id = row.Cells["Id"].Value?.ToString();
    LoadDetailPanel(id);
}

SelectionChanged だと Ctrl+A で爆死、CellEnter だと編集モード進入でも発火する。地雷を両方避けられます。

なぜ 3 つもイベントがあるのか

そもそも DataGridView って Excel ライクな UI を意識して作られたコントロールです。「選択範囲」「カレント セル」「セル移動」の 3 つの状態概念 がそれぞれ独立して存在してて、それぞれにイベントが用意されてる、という建付け。

概念 何を表すか 対応イベント
選択範囲 (Selection) 複数行・複数セルを Ctrl/Shift で選んだ集合 SelectionChanged
カレント セル (CurrentCell) キーボード操作の起点となる「青枠の付いた 1 セル」 CurrentCellChanged
セルへの入場 (Enter) フォーカスがそのセルに移動した瞬間 CellEnter

業務系 UI で「行を選んだ瞬間に処理を走らせたい」と思う時、欲しいのはだいたい「カレント セルが移動した = 注目してる行が変わった」って意味なんですよね。だから CurrentCellChanged が正解になるケースが多い。

最短対処: 3 イベントの早見表 + コピペで動くコード

発火タイミングと用途を表にまとめます。朝礼前に上司へ説明する時、これ 1 枚で済むやつ。

DataGridView 行選択イベント 3 種の発火タイミング・用途・落とし穴の比較

3 パターンをそれぞれ最小コードで書くとこんな感じ。

パターン 1: SelectionChanged — 複数選択の集計に使う

private void dgv_SelectionChanged(object sender, EventArgs e)
{
    // 選択範囲のセル数を集計する用途には向く
    var selectedCount = dgv.SelectedRows.Count;
    lblStatus.Text = $"選択中: {selectedCount} 行";
}

落とし穴は、行を「明細表示」みたいな重い処理に使ったとき。Ctrl+A で 1000 行選択するとハンドラが 1000 回呼ばれて固まります。これ、俺がやらかしたやつ。

パターン 2: CellEnter — セル入場時の検証に使う

private void dgv_CellEnter(object sender, DataGridViewCellEventArgs e)
{
    // 「このセルに入ってきた瞬間」を取れる
    if (e.RowIndex < 0) return; // ヘッダー行ガード
    Debug.WriteLine($"CellEnter: Row={e.RowIndex}, Col={e.ColumnIndex}");
}

落とし穴: ユーザーがセル編集中に Tab キーで隣のセルへ動くと、また発火 します。「行が変わった時だけ動かしたい」という用途には不向き。

パターン 3: CurrentCellChanged — 行を選んだ瞬間の明細表示に使う (推奨)

private void dgv_CurrentCellChanged(object sender, EventArgs e)
{
    var row = dgv.CurrentRow;
    if (row == null || row.IsNewRow) return; // ★ Form_Load 直後の 1 回発火対策

    var id = row.Cells["Id"].Value?.ToString();
    if (string.IsNullOrEmpty(id)) return;

    LoadDetailPanel(id);
}

これで、行が変わった瞬間だけ明細パネルが更新されます。Ctrl+A で全選択しても 1 回しか走らない。いい感じ。

素朴に書いた版 → 推奨形への書き直し

冒頭で俺が踏んだやつを before/after で残しておきます。

SelectionChanged だけの素朴実装から CurrentCellChanged + 起動時 guard への書き直し

差分のポイントは 3 つ。①イベントを CurrentCellChanged に変える、②CurrentRow の null/IsNewRow チェック、③Cells["Id"].Value の null セーフキャスト。これだけで、全選択時の爆発と起動時の NullRef を両方塞げます。

ハマりポイント — 知らないと一晩飛ぶやつ 3 つ

⏱ 対処目安: 全部合わせて 30 分以内に潰せます。深夜デバッグで詰まる前にチェック。

SelectionChanged 全選択で大量発火 (⏱ 5 分)

Ctrl+A で全行選択した瞬間、ハンドラが行数分連続コール。明細表示・グラフ再描画・DB 検索を仕込んでると画面が固まります。俺は物流系の基幹で、業務側から「画面が固まりますって連絡が来てます」と報告を受けて気付いた。技術的には数行の書き換えで終わるんですが、業務側の信頼回復に翌朝の朝礼で 30 分使ったやつ。

CellEnter が編集モード中も呼ばれる (⏱ 10 分)

CellEnter は「フォーカスがセルに入った時」のイベント。ユーザーがセル編集中に Tab で隣のセルへ動くたびに発火します。「行が変わった時だけ走らせたい」と思って書くと、編集中もハンドラが走って予期しない挙動になる。CurrentCellChanged ならカレント セル単位の発火なので、編集進入では呼ばれません。

CurrentCellChanged が Form_Load 直後に 1 回発火する (⏱ 15 分)

これがいちばん意地悪な罠。DataGridView に DataSource を設定した瞬間、最初の 1 行がカレント セル扱いになるので CurrentCellChanged1 回呼ばれます。この時 dgv.CurrentRow が null になっているケースがあって、そのまま row.Cells["Id"].Value を触ると NullReferenceException で落ちる。冒頭の推奨コードに入れてある if (row == null || row.IsNewRow) return; がその対策です。

現場メモ: 障害対応で時間を溶かさないために

物流系の基幹で SelectionChanged で組んだ画面、結局 CurrentCellChanged + 起動時 guard に書き直して落ち着きました。書き換え自体は 5 分。

問題はその後。業務側に「修正が入ります、試してください」と説明する電話で 20 分。翌朝の朝礼で「ご迷惑おかけしました」と頭を下げる時間で 30 分。技術ミスを 5 分で直しても、業務側の信頼を取り戻すのには時間がかかるんですよね。

朝の検索でこの記事に辿り着いた人は、デバッガで止めて発火回数を見るより、まずイベントを CurrentCellChanged に差し替えるのが最短です。

まとめ

DataGridView の「行を選んだ瞬間」を取るイベントは 3 つあって、用途が全部違います。

  • 複数選択の集計SelectionChanged
  • セル入場時の検証CellEnter
  • 行を選んだ瞬間の明細表示CurrentCellChanged + 起動時 null guard

業務 SE が一番踏むのは 3 つ目の「明細表示」系。SelectionChanged で組んで全選択時に固まるパターン。これを朝礼までに直したいなら、イベントを差し替えて null guard を 2 行足すだけ。それで OK。

DataGridView は 1 つのコントロールでも、選択 / カレント セル / セル入場の 3 概念がそれぞれ独立して動いてる。歴史的経緯で似たイベントが並んでるだけなので、「どれが何を取るか」さえ覚えれば、朝礼前のデバッグで詰まらずに済みます。

……話ずれるけど、業務 SE が DataGridView 1 つでこれだけ詰まるって、現場あるあるなんですよね。同じ詰まりを面談で 30 秒で説明できるか、って観点でキャリア記事も置いてあるので、並行稼働や単価交渉の弾薬にしたい人はそっちもどうぞ。

よくある質問

Q1. SelectionChanged と CurrentCellChanged は同時に発火しますか?

A. マウスクリックで行を変えた時は、大抵 両方発火します。順序は CurrentCellChangedSelectionChanged。「行が変わった瞬間に 1 回だけ動かしたい」用途なら、どちらか片方にだけ処理を書けば十分です。

Q2. RowEnter ってイベントもあった気がするんですが、違いは?

A. RowEnter は「カレント セルの行が変わった瞬間」で、CurrentCellChanged とほぼ同じタイミング。差は微妙で、RowEnter の方が e.RowIndex を直接取れるので書き味が少しスッキリします。挙動は CurrentCellChanged と読み替えてもらって OK。

Q3. SelectionChanged を使いたい場面ってあるんですか?

A.「Ctrl/Shift で複数選択した結果を集計したい」系。たとえば「選択した行の金額合計をフッターに出す」みたいな用途は SelectionChanged が向きます。「単一行の明細表示」には向かない、という整理。

Q4. なぜ CurrentCellChanged は Form_Load 直後に 1 回発火するんですか?

A. DataGridView に DataSource を割り当てた時点で、内部的に最初のセルが「カレント セル」になります。「null → 最初のセル」への変化として CurrentCellChanged が発火する建付け。コンポーネントの仕様なので、ハンドラ側で null guard を入れるのが定石です。

Q5. CurrentRow と CurrentCell の違いは?

A. CurrentCell は「カレント セル」(列 × 行で 1 個のセル)、CurrentRow はそのセルが属する行 全体を返すプロパティ。行単位の明細表示には CurrentRow が直感的で書きやすいですが、内部的には CurrentCell.OwningRow と同じものを指してます。row.IsNewRow で新規行追加用のダミー行を弾けるので、合わせて使うのがおすすめ。

Q6. EnableHeadersVisualStyles とか他のプロパティとの相互作用はある?

A. 行選択イベントの発火タイミング自体には影響しません。発火順序や null 周りの挙動は DataSource の種類 (DataTable / List / BindingList) によって微妙に変わることがあるので、独自バインディングで詰まったら DataSource の差し替えで切り分けるのが早いです。基本は今回紹介した null guard と IsNewRow チェックでだいたい吸収できます。

余談: DataGridView の「行選択 = 行選択イベント」だと思ってた頃の話

新人の頃、行選択ってまさか 3 種類もあるとは思ってなくて、最初に Visual Studio のデザイナで DataGridView 選んで「Events タブ」を開いた時、Cell〜 だけで 30 個近くイベントが並んでて目眩がした。

「ぜんぶ調べてたら今日中に終わらない」と思って、結局 最初に出会った SelectionChanged で書いてレビュー出して、業務側から連絡が来てから「あ、複数あるのか」と気付いた、という流れ。あなたも経験あるでしょ?こういうやつ。

今思えば、朝の客先で「ちょっとこれ動かしたい」と言われた瞬間に、こういう発火タイミングの早見表 1 枚を持ってるかどうかで、朝礼までの 30 分の使い方が全然変わるんですよね。深夜の自宅でこの記事に辿り着いた人は、表だけスクショして翌朝の現場に持ち込んでもらって大丈夫です。

同じ罠を踏んだ業務 SE 仲間がいたら、どんどんシェア待ってるぜ!!

動作確認メモ: ここで紹介した DataGridView 行選択イベントの発火タイミング検証は、Mono container でコンパイル成功確認のみ実施しました。実機 (Windows + Visual Studio 2019 + .NET Framework 4.7.2) での実際の発火挙動 (Ctrl+A 全選択時の大量発火 / Form_Load 直後の 1 回発火) は、お手元の開発環境で別途お試しください。

関連記事

以上!


執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次