WinFormsのDataGridViewで検索文字列に一致する行を選択し、先頭のヒット行へスクロールする処理が、行数が増えると急に遅くなることがあります。本記事では、遅さの原因を分解し、二重走査の排除、LINQでの一致行Index取得、データソース側検索など、実務で使える高速化パターンをVB.NET中心に解説します。
DataGridViewの検索・選択・スクロールが遅くなる典型パターン
「0列目に検索文字列が含まれる行を全部選択し、先頭の選択行へスクロールする」要件はシンプルですが、行数が多いDataGridViewでは実装の仕方で体感速度が大きく変わります。特に遅くなりやすいのは次の流れです。
- 全行を走査して一致行を
row.Selected = Trueにする - その後で
DataGridView.SelectedRowsを走査し、最小Index(先頭の選択行)を探す FirstDisplayedScrollingRowIndexに最小Indexをセットしてスクロールする
この実装が重くなりやすい理由は、単純に「行を探す」よりも、選択状態の管理とUI更新にコストが寄っているからです。
| 処理 | コストが増えやすいポイント | 結果として起きること |
|---|---|---|
| 一致行を次々選択する | 選択状態の更新、再描画、SelectionChangedなどのイベント発火 | 一致件数が多いほどUIが固まったように見える |
| SelectedRowsを走査する | SelectedRowsコレクションの構築・並び替え・取得が内部的に重くなることがある | 「先頭の選択行を探す」だけのはずが想像以上に時間がかかる |
| セル値の比較 | 毎行 ToString() / ToLowerInvariant() を呼ぶと割り当てが増える | CPUとGC負荷が増えて検索が伸びる |
つまり、改善の方向性は大きく3つです。
- 二重走査をやめる(先頭ヒットIndexを最初の走査中に確定させる)
- 比較処理を軽くする(文字列変換・小文字化の回数を減らす)
- 大量選択そのものを避ける/見せ方を変える(先頭だけ強調、フィルター、ナビゲーション)
最短で効く改善:二重走査をやめて1回のループで完結させる
「全行走査 → SelectedRows走査」という二重走査は、行数が多いほど効いてきます。最も手堅い改善は、最初の走査中に先頭ヒット行のIndexを保持して、2回目のループを消すことです。
VB.NET(Option Strict On対応)1パス実装
Private Sub SelectMatchesAndScrollFirst(dgv As DataGridView, keyword As String)
If dgv Is Nothing Then Throw New ArgumentNullException(NameOf(dgv))
dgv.ClearSelection()
Dim kw As String = If(keyword, String.Empty).Trim()
If kw.Length = 0 Then Return
Dim firstHit As Integer = -1
' UI更新が重いケースでは、最低限のレイアウト更新に抑える
dgv.SuspendLayout()
Try
For Each row As DataGridViewRow In dgv.Rows
' 末尾の新規行(AllowUserToAddRows=True)を除外
If row.IsNewRow Then Continue For
' 値がNothingのケースに備える
Dim text As String = Convert.ToString(row.Cells(0).Value)
' .NET Frameworkでも使える:IndexOf + OrdinalIgnoreCase
If (Not String.IsNullOrEmpty(text)) AndAlso
text.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0 Then
row.Selected = True
' 先頭ヒットを最初の1回で確定
If firstHit = -1 Then
firstHit = row.Index
End If
End If
Next
Finally
dgv.ResumeLayout()
End Try
' スクロールはヒットがあるときだけ
If firstHit >= 0 AndAlso firstHit < dgv.RowCount Then
' 行が非表示(Visible=False)だと例外になることがあるため、必要ならVisibleも確認
If dgv.Rows(firstHit).Visible Then
dgv.FirstDisplayedScrollingRowIndex = firstHit
End If
' 先頭ヒット行をアクティブにしてユーザーの視線を誘導
dgv.CurrentCell = dgv.Rows(firstHit).Cells(0)
End If
End Sub
ポイントは次の通りです。
- SelectedRowsを後から走査しない:先頭ヒットを
firstHitに保持して終了 - 比較はIndexOfで大文字小文字を無視:毎行の
ToLowerInvariant()を避ける - Null安全:
Convert.ToStringならNothingを空文字として扱える - IsNewRowを除外:末尾の新規行で無駄な判定や例外原因を作らない
| 方式 | 走査回数 | 先頭ヒット確定 | 体感速度 | 実装難易度 |
|---|---|---|---|---|
| 二重走査(Rows→SelectedRows) | 2回以上になりがち | 後段で確定 | 遅くなりやすい | 低 |
| 1パス(Rows走査中にfirstHit保持) | 1回 | 走査中に確定 | 速くなりやすい | 低 |
実務ではまずこの1パス化を入れるだけで「先頭の選択行を探す部分が重い」問題が解消するケースが多いです。
受理回答の形:LINQで一致行Indexをまとめて取得してから選択・スクロールする
コードを簡潔にしつつ、先頭ヒットIndexを確実に取得したいなら、LINQで一致行の行Indexをリスト化し、それを選択とスクロールに使う方法が定番です。ポイントは「SelectedRowsを後から歩かない」ことと、「比較コストの高い処理を毎行繰り返さない」ことです。
VB.NET(Option Strict On)LINQ版:Castで明示キャストする
DataGridView.Rows はLINQで扱うと型推論が崩れて Object 扱いになりやすく、Option Strict Onだと暗黙変換が禁止されてエラーになります。そこで Cast(Of DataGridViewRow)() を挟んで型を確定させます。
Private Sub SelectByLinqAndScrollFirst(dgv As DataGridView, keyword As String)
dgv.ClearSelection()
Dim kw As String = If(keyword, String.Empty).Trim()
If kw.Length = 0 Then Return
' 一致行のIndexをまとめて取得(Option Strict OnならCast必須)
Dim hitIndexes As List(Of Integer) =
dgv.Rows.Cast(Of DataGridViewRow)().
Where(Function(r) Not r.IsNewRow).
Where(Function(r)
Dim text As String = Convert.ToString(r.Cells(0).Value)
Return (Not String.IsNullOrEmpty(text)) AndAlso
text.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0
End Function).
Select(Function(r) r.Index).
ToList()
' 選択(一致件数が多い場合、ここ自体が重くなる点は後述)
For Each i In hitIndexes
dgv.Rows(i).Selected = True
Next
' 先頭ヒットへスクロール
If hitIndexes.Count > 0 Then
Dim firstHit As Integer = hitIndexes(0)
If dgv.Rows(firstHit).Visible Then
dgv.FirstDisplayedScrollingRowIndex = firstHit
End If
dgv.CurrentCell = dgv.Rows(firstHit).Cells(0)
End If
End Sub
LINQ版のメリットは「一致判定」「Index抽出」「先頭Indexの取得」が一連の流れで書ける点です。処理としては1回の列挙でIndexを集め、最後に選択とスクロールを行います。
| 観点 | 1パスFor版 | LINQ版 |
|---|---|---|
| 可読性 | 手続き的で追いやすい | 短くまとまりやすい |
| 速度 | 最速になりやすい(余計な中間リストがない) | 中間リスト生成はあるが、二重走査を避ければ十分速いことが多い |
| Option Strict On | 型を明示すれば問題なし | Cast(Of DataGridViewRow)() がないと躓きやすい |
Option Strict Onでのつまずきポイント
「Rowsに対してWhereを書いたらコンパイルエラーになる」という状況は、Rows がジェネリックではなく、LINQが IEnumerable として受けてしまい、要素型が Object と解釈されることが原因です。次の表のように、Castで要素型を確定させるのが最短です。
| 書き方 | Option Strict On | 補足 |
|---|---|---|
dgv.Rows.Where(Function(r) ...) | NGになりやすい | rがObject扱いになり、Cellsなどのメンバー参照で詰まる |
dgv.Rows.Cast(Of DataGridViewRow)().Where(...) | OK | 要素型がDataGridViewRowに固定される |
dgv.Rows.OfType(Of DataGridViewRow)().Where(...) | OK | 型が混在する場合に安全(通常はCastで十分) |
比較処理を軽くする:ToLowerInvariant/ToString連発をやめる
行数が増えるほど、比較の前処理が効いてきます。よくある遅い書き方は、毎行こういう処理をしてしまうパターンです。
- セル値を毎回
ToString()(または暗黙的にToString) - 毎回
ToLowerInvariant()で小文字化 Containsで部分一致
この形は「文字列の生成」「小文字化で新しい文字列ができる」のが積み重なり、行数が多いとGCの影響も出やすくなります。おすすめは、検索語は1回だけ整形し、比較はIndexOf(StringComparison)で行う形です。
| やりがち | おすすめ | 理由 |
|---|---|---|
cellText.ToLowerInvariant().Contains(kwLower) | cellText.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0 | 小文字化の新規文字列を作らずに比較できる |
毎行 keyword.ToLowerInvariant() | 事前に kw = keyword.Trim() だけ | 検索語の前処理は1回で十分 |
row.Cells(0).Value.ToString() | Convert.ToString(row.Cells(0).Value) | Nothingに強く、Nullチェックが簡潔 |
.NET 5+ / .NET 6+(新しめのWinForms)の場合
もしプロジェクトが .NET 5+ / .NET 6+ 以降なら、Contains に StringComparison を渡せるため、より読みやすく書けます(ただし、.NET Frameworkではこのオーバーロードは使えないため注意)。
' .NET 5+ の例(.NET Frameworkではコンパイル不可)
If text.Contains(kw, StringComparison.OrdinalIgnoreCase) Then
' ...
End If
「先頭へスクロール」は速いのに「大量行の選択」が遅い場合の対処
検索処理そのものを最適化しても、ヒット件数が多いと「大量行の選択」自体が重いことがあります。DataGridViewはUIコントロールなので、選択行が増えるほど描画やイベントが増え、ユーザー体感としては「処理が遅い」に直結します。
このケースでは、要件を少しだけ現実寄りにすると一気に快適になります。
先頭一致だけ選択してスクロールする(最も体感改善が大きい)
「まずは先頭一致へ飛べれば十分」という業務画面は多いです。全件選択をやめるだけで、ほぼ確実に軽くなります。
Private Sub FocusFirstMatchOnly(dgv As DataGridView, keyword As String)
dgv.ClearSelection()
Dim kw As String = If(keyword, String.Empty).Trim()
If kw.Length = 0 Then Return
Dim firstHit As Integer = -1
For Each row As DataGridViewRow In dgv.Rows
If row.IsNewRow Then Continue For
Dim text As String = Convert.ToString(row.Cells(0).Value)
If (Not String.IsNullOrEmpty(text)) AndAlso
text.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0 Then
firstHit = row.Index
Exit For
End If
Next
If firstHit >= 0 Then
dgv.Rows(firstHit).Selected = True
dgv.CurrentCell = dgv.Rows(firstHit).Cells(0)
dgv.FirstDisplayedScrollingRowIndex = firstHit
End If
End Sub
「先頭一致へ移動」だけなら、比較回数も選択回数も最小で済みます。検索ボックスに入力するたびに動かすようなUI(インクリメンタルサーチ)にも向きます。
一致箇所を「次へ」「前へ」で巡回する(大量選択を避けつつ探しやすい)
全件を同時に選択しなくても、ヒット位置をリスト化しておけばユーザーは十分に辿れます。例えば、検索時に一致行Indexを保持しておき、ボタンで順送りするイメージです。
' フォームのフィールドなどに保持
Private _hitIndexes As List(Of Integer) = New List(Of Integer)
Private _hitPointer As Integer = -1
Private Sub BuildHitIndex(dgv As DataGridView, keyword As String)
Dim kw As String = If(keyword, String.Empty).Trim()
_hitIndexes =
dgv.Rows.Cast(Of DataGridViewRow)().
Where(Function(r) Not r.IsNewRow).
Where(Function(r)
Dim text As String = Convert.ToString(r.Cells(0).Value)
Return (Not String.IsNullOrEmpty(text)) AndAlso
text.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0
End Function).
Select(Function(r) r.Index).
ToList()
_hitPointer = If(_hitIndexes.Count > 0, 0, -1)
End Sub
Private Sub MoveToHit(dgv As DataGridView, pointer As Integer)
If pointer < 0 OrElse pointer >= _hitIndexes.Count Then Return
Dim idx As Integer = _hitIndexes(pointer)
dgv.ClearSelection()
dgv.Rows(idx).Selected = True
dgv.CurrentCell = dgv.Rows(idx).Cells(0)
dgv.FirstDisplayedScrollingRowIndex = idx
End Sub
この方式なら、行数が多くても「選択は常に1行」なので軽く、かつユーザーはヒットを迷わず辿れます。ヒット件数のラベル表示(例:3 / 28)を併用するとさらに親切です。
SelectionChangedなどのイベントが重い場合は「検索中は無視」する
意外と見落とされがちですが、DataGridViewの選択変更イベントで別処理(詳細表示更新、DB参照、ログ出力など)をしていると、行を選ぶたびにその処理が走って一気に遅くなります。検索処理中だけイベント側を早期Returnするだけで改善することがあります。
Private _isSearching As Boolean = False
Private Sub dgv_SelectionChanged(sender As Object, e As EventArgs) Handles dgv.SelectionChanged
If _isSearching Then Return
' ここに重い処理があると、複数行選択で地獄になりがち
' 例:詳細パネルの更新、別クエリ発行など
End Sub
Private Sub SearchWithEventGuard()
_isSearching = True
Try
SelectMatchesAndScrollFirst(dgv, txtKeyword.Text)
Finally
_isSearching = False
End Try
End Sub
「検索処理は速いはずなのに固まる」という場合、UIイベントがボトルネックになっていることがよくあります。
データソース側で検索・フィルターする(大量データなら最も効く)
DataGridViewのセルを直接なめて比較するのは、あくまで「表示のためのUI」を使って検索している状態です。行数が多いなら、データソース側で検索したほうが合理的です。
| データソース | 向いている手法 | メリット | 注意点 |
|---|---|---|---|
| DataTable / DataView | RowFilter / BindingSource.Filter | 表示行自体を絞れるので選択・スクロールも軽い | Filter文字列のエスケープが必要 |
| BindingList(Of T) | 検索用の前処理(小文字キー保持)+LINQ検索 | 比較コストを下げられる | フィルター機能は標準で弱い(別リストへ差し替え等が必要) |
| DB(件数が非常に多い) | SQL側でLIKE検索して結果だけ表示 | UIに大量データを持ち込まない | 検索体験(インクリメンタル等)は設計が必要 |
DataTable + BindingSourceで「表示を絞る」例
DataGridViewが BindingSource 経由で DataTable にバインドされているなら、Filterで表示行を絞るのが強力です。ヒット行が数十件に減れば、選択やスクロールも一気に軽くなります。
Private Sub ApplyFilterByKeyword(bs As BindingSource, keyword As String)
Dim kw As String = If(keyword, String.Empty).Trim()
If kw.Length = 0 Then
bs.RemoveFilter()
Return
End If
' Filter/RowFilterはシングルクォートがエスケープ必要
Dim safe As String = kw.Replace("'", "''")
' 例:列名が Code の場合(部分一致)
bs.Filter = String.Format("Code LIKE '%{0}%'", safe)
End Sub
この方式なら、スクロールは「先頭行(Index 0)」へ行くだけで済みます。さらに、ユーザーも「検索結果だけが表示される」ため迷いにくく、UIとして自然です。
前処理で比較コストを下げる(BindingList(Of T)など)
オブジェクトリストをバインドしている場合は、表示用プロパティとは別に検索用の正規化済みキーを用意すると効きます。例えば、0列目に表示している値の小文字化を都度行うのではなく、モデル側に保持しておきます。
' 例:表示対象のモデル
Public Class ItemRow
Public Property Code As String
' 検索用に正規化した値を保持(初期化時・更新時にだけ作る)
Public ReadOnly Property CodeKey As String
Get
Return If(Code, String.Empty).ToLowerInvariant()
End Get
End Property
End Class
検索時は CodeKey を使えば、毎行の ToLowerInvariant() を避けられます。特に「同じデータに対して何度も検索する」画面では差が出ます。
例外・落とし穴:Null、範囲外、非表示行、そして“イベント地獄”
高速化と同じくらい重要なのが、ハマりやすい例外や仕様の把握です。次のような点を押さえておくと、保守が楽になります。
| 症状 | 原因 | 対策 |
|---|---|---|
| NullReferenceExceptionが出る | Cells(0).Value がNothing | Convert.ToString を使う/Nullチェックを入れる |
| FirstDisplayedScrollingRowIndexで例外 | ヒットなし/範囲外Index/非表示行 | ヒット時のみ設定し、0~RowCount-1 と Visible を確認 |
| 末尾の空行が引っかかる | AllowUserToAddRowsの新規行 | row.IsNewRow をスキップ |
| 検索のたびに画面が固まる | 大量行選択+SelectionChanged等で重い処理 | 検索中はイベント処理を抑止/先頭一致だけ選択/フィルターに寄せる |
| ソート後に“期待した行”に行けない | 表示順とデータ順の混同 | 表示に合わせるならrow.Index、データ側ならDataBoundItemを基準に考える |
特に「SelectionChangedで別処理をしている」ケースは盲点になりやすいので、処理が遅いときはイベントハンドラも含めて疑うのが近道です。
実務でのおすすめパターンまとめ
最後に、要件別に「どの手法を選ぶべきか」を整理します。迷ったら、まずは1パス化(SelectedRows二重走査の排除)を入れ、それでも辛ければ「大量選択をやめる」か「データソース側へ寄せる」の順で検討すると失敗しにくいです。
| やりたいこと | おすすめ | 理由 |
|---|---|---|
| 一致行を全部選択し、先頭へスクロール | 1パスFor版(firstHit保持) | 最小の変更で効果が出やすい |
| コードを短く読みやすくしたい | LINQでIndexリスト化(Cast必須) | 先頭ヒットの取得が明確で、SelectedRows走査が不要 |
| ヒット件数が多くてUIが重い | 先頭一致だけ選択/次へ前へ方式 | 選択行数を抑えると体感が劇的に改善 |
| データが多すぎて根本的に遅い | データソース側で検索・フィルター | UIに重い処理を背負わせない設計にできる |
DataGridViewは便利ですが、「大量データの検索」は設計次第でいくらでも重くなります。二重走査の排除と比較処理の最適化は即効性が高く、さらに必要なら“選択の見せ方”や“検索の場所”を変えることで、行数が増えても快適な検索体験を作れます。

コメント