【極限の知見】DataGridViewのCellPainting完全制御:エクセルライクな高速カスタム描画とメモリ最適化の全手法
レガシーな業務アプリケーションの延命、あるいは新規開発において、ユーザーからの「エクセルと同じようにセルを結合して見やすくしてほしい」「特定条件でセルの背景を動的に変えたい」という要求は、避けて通れない呪縛のようなものである。
Windows Formsの `DataGridView` は標準で強力なグリッドコントロールだが、「データ構造としての結合」と「描画としての結合」が完全に分離されていないという構造的欠陥を抱えている。標準の `DataSource` バインドを維持したまま、安易にセル結合を行おうとすれば、スクロール時の致命的な「ちらつき(Flickering)」、メモリリーク、そして描画パフォーマンスの崩壊を引き起こす。
本稿では、VB.NETの `CellPainting` イベントを極限までハックし、GDI+の描画コンテキストを直接制御することで、数万件のレコードを抱えても滑らかに動作する「エクセルライクな複雑表レイアウト」を構築するプロフェッショナル向けアーキテクチャを提示する。
—
1. ちらつきの根源:GDI+描画のメカニズムとダブルバッファリングの強制
Windows Formsのコントロールは、デフォルトではOSのウィンドウメッセージ(`WM_PAINT`)を受け取るたびに描画処理を行う。特に `DataGridView` のような複雑なグリッドは、セルごとの境界線、背景、テキストが個別に描画されるため、スクロール時に強烈な画面のちらつきが発生する。
これを根絶するには、マネージドコードの枠組みを超え、Win32 APIレベルでのダブルバッファリング(画面の裏側で一度完成形を描画し、一括転送する手法)をコントロールのスタイルに強制適用しなければならない。
以下の拡張モジュール、またはカスタムコントロールを作成し、プロジェクトの基盤として組み込むこと。
Imports System.Windows.Forms
Imports System.Reflection
Public Module DataGridViewExtensions
”’
”’
الجمهور Sub EnableDoubleBuffering(ByVal dgv As DataGridView)
‘ リフレクションを用いて保護されたControlStylesプロパティ(DoubleBuffer)を強制有効化
Dim dgvType As Type = dgv.GetType()
Dim pi As PropertyInfo = dgvType.GetProperty(“DoubleBuffered”, BindingFlags.Instance Or BindingFlags.NonPublic)
If pi IsNot Nothing Then
pi.SetValue(dgv, True, Nothing)
End If
End Sub
End Module
この処理により、OSレベルでの描画バッファが確保され、`CellPainting` 内で行う高度なカスタム描画のオーバーヘッドを大幅に相殺できる。
—
2. CellPaintingイベントの完全制御:セル結合とカスタムレイアウトの実装
「セル結合」を表現する場合、データを実際に結合するのではなく、「描画をハイジャックする」のがプロの常道である。データを結合すると、ソートやフィルタリング機能が完全に破綻するためである。
ここでは、特定の条件(例:同一の「部門コード」を持つ連続したセル)を視覚的に統合しつつ、上部にアイコン、下部にテキストを配置する高度なレイアウトを実装する。
Imports System.Drawing
Imports System.Windows.Forms
Public Class ExcelLikeDataGridView
Inherits DataGridView
Protected Overrides Sub OnLoad(e As EventArgs)
MyBase.OnLoad(e)
‘ 自身の拡張メソッドを呼び出し
Me.EnableDoubleBuffering()
‘ 描画処理の競合を防ぐため、SelectionModeはセル単位ではなく行/列単位に最適化を推奨
Me.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 245, 245)
End Sub
”’
”’
Protected Overrides Sub OnCellPainting(e As DataGridViewCellPaintingEventArgs)
‘ ヘッダー行や無効なインデックスは標準描画にフォールバック
If e.RowIndex < 0 OrElse e.ColumnIndex < 0 Then
MyBase.OnCellPainting(e)
Return
End If
' 例として、第1カラム(ColumnIndex = 0)の特定の連続セルを結合描画するロジック
If e.ColumnIndex = 0 Then
' 描画の基本処理(背景の消去など)をキャンセルせず、自前で制御する場合はPaintPartsを指定
e.Paint(e.CellBounds, DataGridViewPaintParts.Background Or DataGridViewPaintParts.Focus)
Dim currentValue As Object = Me.Rows(e.RowIndex).Cells(e.ColumnIndex).Value
Dim prevValue As Object = If(e.RowIndex > 0, Me.Rows(e.RowIndex – 1).Cells(e.ColumnIndex).Value, Nothing)
‘ 上のセルと値が同じ場合、上のセルと「視覚的に」結合するため、テキストや枠線の描画をスキップする
If currentValue IsNot Nothing AndAlso currentValue.Equals(prevValue) Then
‘ 境界線のうち、上側のボーダーを描画しないことで結合感を演出
Using gridPen As New Pen(Me.GridColor)
‘ 下、右、左の境界線のみを描画
e.Graphics.DrawLine(gridPen, e.CellBounds.Left, e.CellBounds.Bottom – 1, e.CellBounds.Right, e.CellBounds.Bottom – 1)
e.Graphics.DrawLine(gridPen, e.CellBounds.Right – 1, e.CellBounds.Top, e.CellBounds.Right – 1, e.CellBounds.Bottom)
End Using
‘ イベント処理済みフラグを立てる
e.Handled = True
Return
End If
‘ 結合の「起点」となるセルの場合、下方向へ連続するセルを走査して結合高さを計算
Dim spanHeight As Integer = e.CellBounds.Height
Dim nextRowIndex As Integer = e.RowIndex + 1
While nextRowIndex < Me.Rows.Count Dim nextVal As Object = Me.Rows(nextRowIndex).Cells(e.ColumnIndex).Value If nextVal IsNot Nothing AndAlso nextVal.Equals(currentValue) Then spanHeight += Me.Rows(nextRowIndex).Height nextRowIndex += 1 Else Exit While End If End While ' 統合された領域全体の背景と境界線をカスタム描画 Dim combinedRect As New Rectangle(e.CellBounds.X, e.CellBounds.Y, e.CellBounds.Width, spanHeight) Using bgBrush As New SolidBrush(Me.CellStyles(e.RowIndex, e.ColumnIndex).BackColor) e.Graphics.FillRectangle(bgBrush, combinedRect) End Using ' アイコンとテキストの複合描画(エクセルライクなリッチレイアウト) ' 例:中央にアイコン、その下部にテキストを配置 Dim icon As Image = My.Resources.InfoIcon ' リソースからアイコンを取得 If icon IsNot Nothing Then Dim iconRect As New Rectangle(combinedRect.X + (combinedRect.Width - 16) / 2, combinedRect.Y + 8, 16, 16) e.Graphics.DrawImage(icon, iconRect) End If ' テキストの描画 Dim sf As New StringFormat() With { .Alignment = StringAlignment.Center, .LineAlignment = StringAlignment.Far } Using textBrush As New SolidBrush(Me.ForeColor) Dim textRect As New Rectangle(combinedRect.X, combinedRect.Y + 28, combinedRect.Width, combinedRect.Height - 32) e.Graphics.DrawString(CStr(currentValue), Me.Font, textBrush, textRect, sf) End Using ' 結合枠の外枠を描画 Using gridPen As New Pen(Me.GridColor) e.Graphics.DrawRectangle(gridPen, combinedRect.X, combinedRect.Y, combinedRect.Width - 1, combinedRect.Height - 1) End Using e.Handled = True Else ' 通常セルの描画 MyBase.OnCellPainting(e) End If End Sub End Class ---
3. メモリ管理の極意:GDI+リソースの枯渇を防ぐ防御的コーディング
シニアエンジニアであれば常識だが、`System.Drawing` 名前空間のクラス(`Pen`, `Brush`, `Font`, `Bitmap` など)の多くは `IDisposable` を実装しており、OS側のGDIリソース(ハンドル)を直接消費している。
`CellPainting` はグリッドが再描画されるたびに何百回、何千回と呼び出されるイベントである。この中で `New Pen(…)` や `New SolidBrush(…)` を `Using` ブロックなしで乱用したり、インスタンスを都度生成して放置すると、わずか数分のスクロールでガベージコレクション(GC)が追いつかなくなり、「OutOfMemoryException」または致命的なGDIハンドルリークによるアプリケーションの強制終了を招く。
対策:リソースの静的キャッシュ(Static Caching)
高頻度で描画に使用するペンやブラシは、クラスのフィールドとして事前生成(キャッシュ)し、イベント内では決して破棄・再生成しないアーキテクチャを採用する。
Public Class ExcelLikeDataGridView
Inherits DataGridView
‘ 描画用リソースの静的キャッシュ(メモリリークの完全防止と高速化)
Private Shared ReadOnly GridPen As New Pen(Color.FromArgb(200, 200, 200))
Private Shared ReadOnly HeaderBackBrush As New SolidBrush(Color.FromArgb(240, 240, 240))
Private Shared ReadOnly TextBrush As New SolidBrush(Color.FromArgb(50, 50, 50))
‘ コントロール破棄時にキャッシュを明示的に解放
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
GridPen.Dispose()
HeaderBackBrush.Dispose()
TextBrush.Dispose()
End If
MyBase.Dispose(disposing)
End Sub
‘ …以降、CellPainting内ではこれらのキャッシュされたインスタンスを使い回す…
End Class
この設計により、マネージドヒープへの負荷がゼロになり、GCの発生頻度が劇的に低下する。
—
4. レガシーシステム連携・VBAからの移行を見据えた設計思想
このような高度なカスタムコントロールを導入する最大の理由は、「旧来のMicrosoft AccessやVBA(Excel VBA)で構築されたレガシー業務システムのユーザー体験(UX)を損なわずに、堅牢な.NET環境へ移行するため」である。
VBAのユーザーはエクセルの柔軟なセルの振る舞いに慣れきっている。標準の無機質なグリッドを提示した瞬間に「使い物にならない」という現場の反発が生まれる。
しかし、本稿で示した `CellPainting` の完全制御とダブルバッファリングによる高速描画を組み合わせることで、開発者は「データ構造の健全性(RDBとの親和性)」と「UIの柔軟性(エクセルライクな見た目)」の二律背反を完璧に克服できる。
レガシーシステムの現代化(モダナイゼーション)を託されたチーフアーキテクトよ。安易なサードパーティ製高額コンポーネントに逃げることなく、VB.NETの底力を引き出したこの極限の描画制御を実装し、現場をうならせる強靭なシステムを構築せよ。
