【実務・中級編】DataGridViewのセル結合とカスタム描画:CellPaintingイベントを完全制御してエクセルライクな複雑な表レイアウトを再現する手法 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

DataGridViewの限界を超える:`CellPainting`完全制御によるエクセルライクな高速カスタム描画

業務システムのUIにおいて、エンドユーザーから最も要求される要件は何だろうか。答えはシンプルだ。「エクセルと同じように見やすく、直感的であれ」だ。

特に受発注管理や工程表、複雑なシフト表などの業務アプリケーションでは、単なるグリッド表示ではなく、「複数セルの視覚的な結合」「条件に応じた斜線描画」「アイコンとテキストの混在レイアウト」が強く求められる。

しかし、標準の `DataGridView` は、行単位・セル単位の独立した描画モデルを採用しているため、そのままではエクセルライクな表現など到底不可能である。素朴にコントロールを配置したり、不適切なイベントハンドリングを行ったりすれば、画面スクロールのたびに盛大な「ちらつき(フリッカ)」が発生し、実用に耐えないガラクタが出来上がる。

今回は、VB.NETの `CellPainting` イベントを極限まで掌握し、パフォーマンスを犠牲にせずにプロフェッショナルな表レイアウトを構築する極意を伝授する。

—

1. なぜ標準機能や安易な実装では失敗するのか?

多くの初中級プログラマが陥る罠が、`CellFormatting` や独自コントロールの乱用、あるいは無限の再描画命令(`Invalidate`)によるパフォーマンス崩壊だ。

罠1:無駄なレイアウト計算とイベントの多重発火

`DataGridView` はセルが画面に描画されるたびに `CellPainting` を発生させる。ここでデータベースへのアクセスや、重い文字列解析、不要なオブジェクト生成を行うと、スクロール速度が劇的に低下する。描画スレッド(UIスレッド)をブロックしてはならない。

罠2:ダブルバッファリングの欠如による「ちらつき」

Windows Formsのデフォルト描画は、背景の消去(`EraseBackground`)と実際の描画が分離しているため、再描画時に画面がチラつく。これを解決するには、コントロールのスタイルで `DoubleBuffered` を有効化するアーキテクチャ上の配慮が不可欠である。

—

2. アーキテクチャ設計:ちらつきなき高速描画の要件

今回の実装における設計方針は以下の3点に集約される。

1. 完全なダブルバッファリングの適用(継承コントロールによるプロパティ強制有効化)
2. `CellPainting` イベントのイベントキャンセルと完全自前描画(`e.Handled = True` の徹底)
3. データ構造と描画ロジックの分離(「結合すべき範囲」の事前計算)

それでは、実際のプロダクションコードを見ていこう。

—

3. 実装コード:エクセルライク制御クラス(VB.NET)

以下のコードは、コピー&ペーストでそのままプロジェクトに組み込み、即座に実務で使えるレベルに昇華させた堅牢な実装である。

ダブルバッファリング対応カスタム DataGridView

まずは、ちらつきを根絶するカスタムグリッドクラスを作成する。

Imports System.Windows.Forms

Namespace CustomControls
”’

”’ ちらつき(フリッカ)を完全排除した高速描画型 DataGridView
”’

Public Class ExDataGridView
Inherits DataGridView

Public Sub New()
‘ ダブルバッファリングを有効化し、描画を滑らかにする
Me.DoubleBuffered = True

‘ 行の自動リサイズ等によるパフォーマンス低下を防ぐ設定
Me.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.DisableResizing
Me.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing
End Sub

End Class
End Namespace

メインフォームでの `CellPainting` 完全制御実装

次に、特定のセルを結合風に描画し、斜線やカスタムパディングを適用するメインロジックを実装する。

Imports System.Drawing
Imports System.Windows.Forms
Imports CustomControls

Public Class MainForm
Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
‘ グリッドの初期設定
SetupGrid()
LoadDummyData()
End Sub

Private Sub SetupGrid()
‘ ExDataGridViewを動的生成して配置する場合の例(デザイナー配置でも可)
Dim exGrid As New ExDataGridView()
exGrid.Dock = Fill
exGrid.AllowUserToAddRows = False
exGrid.SelectionMode = DataGridViewSelectionMode.CellSelect

‘ カラム定義
exGrid.Columns.Add(“ColDate”, “日付”)
exGrid.Columns.Add(“ColCategory”, “カテゴリ”)
exGrid.Columns.Add(“ColItem”, “項目名”)
exGrid.Columns.Add(“ColStatus”, “ステータス”)

‘ イベントの紐付け
AddHandler exGrid.CellPainting, AddressOf ExGrid_CellPainting

Me.Controls.Add(exGrid)
End Sub

Private Sub LoadDummyData()
‘ 動作確認用のダダを投入(実際にはDBやファイルから取得)
Dim grid = DirectCast(Me.Controls(0), DataGridView)
grid.Rows.Add(“2023/10/01”, “開発部”, “要件定義”, “完了”)
grid.Rows.Add(“2023/10/01”, “開発部”, “基本設計”, “進行中”)
grid.Rows.Add(“2023/10/02”, “インフラ部”, “サーバ構築”, “未着手”)
End Sub

”’

”’ CellPaintingイベント:エクセルライクな結合・斜線・カスタム描画の総本山
”’

Private Sub ExGrid_CellPainting(sender As Object, e As DataGridViewCellPaintingEventArgs)
‘ ヘッダーや無効なインデックスは除外
If e.RowIndex < 0 OrElse e.ColumnIndex < 0 Then Return Dim grid = DirectCast(sender, DataGridView) ' --- 演出パターン1:特定条件(例: 1列目の同じ値)でセルの縦方向を結合風に見せる --- ' ※実務では事前に「結合グループマップ」を持たせるのが定石 If e.ColumnIndex = 0 AndAlso e.RowIndex > 0 Then
Dim currentValue = grid.Rows(e.RowIndex).Cells(e.ColumnIndex).Value
Dim prevValue = grid.Rows(e.RowIndex – 1).Cells(e.ColumnIndex).Value

If currentValue IsNot Nothing AndAlso currentValue.Equals(prevValue) Then
‘ 上のセルと値が同じ場合、上側の境界線を消去して「結合されている」ように見せる
e.AdvancedBorderStyle.Top = DataGridViewAdvancedCellBorderStyle.None
End If
End If

‘ — 演出パターン2:特定のセルに「斜線」を描画する(例: 休日や対象外セル) —
If e.ColumnIndex = 3 AndAlso e.RowIndex = 2 Then
‘ 標準の背景とセル枠を一旦描画
e.Paint(e.CellBounds, DataGridViewPaintParts.All And Not DataGridViewPaintParts.ContentForeground)

‘ 斜線を描画
Using pen As New Pen(Color.Red, 1.5F)
e.Graphics.DrawLine(pen, e.CellBounds.Left, e.CellBounds.Top, e.CellBounds.Right, e.CellBounds.Bottom)
End Using

‘ 必要であればテキストも上書き描画
If e.Value IsNot Nothing Then
TextRenderer.DrawText(e.Graphics, e.Value.ToString(), e.Font, e.CellBounds, e.ForeColor, TextFormatFlags.HorizontalCenter Or TextFormatFlags.VerticalCenter)
End If

‘ 描画処理を自前で完了したことを通知(超重要)
e.Handled = True
Return
End If

‘ — 演出パターン3:通常のセルだが、特定条件で背景色やパディングを動的変更する —
If e.ColumnIndex = 3 AndAlso e.Value?.ToString() = “完了” Then
‘ 背景を薄い緑にする
Using brush As New SolidBrush(Color.FromArgb(220, 255, 220))
e.Graphics.FillRectangle(brush, e.CellBounds)
End Using

‘ 枠線を描画
e.Paint(e.CellBounds, DataGridViewPaintParts.Border)

‘ テキストをカスタム描画(中央寄せなど)
TextRenderer.DrawText(e.Graphics, e.Value.ToString(), e.Font, e.CellBounds, Color.DarkGreen, TextFormatFlags.HorizontalCenter Or TextFormatFlags.VerticalCenter)

e.Handled = True
End If
End Sub
End Class

—

4. プロジェクトリーダーが教える!実務適用のチェックポイント

この実装を実際の業務アプリケーション(データベース連携やファイル出力)に組み込む際、以下のアーキテクチャ上の注意点を必ず遵守してほしい。

1. 描画ロジック内でDBやファイルI/Oを行わない

`CellPainting` は1秒間に何十回も呼び出される可能性がある。この中でSQLを発行したり、ファイルを読みに行ったりする愚行を犯せば、アプリは瞬時にフリーズする。
表示に必要なデータは、あらかじめメモリ上の構造体(`DataTable` や `List(Of T)`)に完全にロードし、インメモリで判定処理を完結させよ。

2. 仮想モード(Virtual Mode)との組み合わせ

数万件を超える巨大なデータを扱う場合、`DataSource` に直接バインドするのではなく、`VirtualMode = True` に設定し、`CellValueNeeded` イベントと今回の `CellPainting` を組み合わせるべきだ。メモリ消費量を極限まで抑えつつ、エクセルライクな表現を維持できる。

3. マウスヒットテスト(クリック判定)の同期

見た目上セルのボーダーを消して「結合」させても、データ構造上のセルは独立している。ユーザーが結合されたセル群のどこをクリックしたかによって、選択挙動やコンテキストメニューの挙動を制御したい場合は、`CellClick` イベント等で「どの論理セルを操作すべきか」の座標変換ロジックを別途用意する必要がある点を忘れてはならない。

—

5. おわりに

Visual Basic および .NET Framework / .NET Core における Windows Forms 開発は、レガシーな技術と揶揄されることがある。しかし、フレームワークの挙動を底の底まで理解し、GDI+やメッセージループの仕組みを掌握したエンジニアが手を入れたWindows Formsアプリは、Webアプリでは到達できない圧倒的な軽快さと、業務に最適化されたUXを提供できる。

「標準機能でできないから諦める」のではなく、`CellPainting` を支配し、要求されるすべてのUI要件をねじ伏せる。それこそが、現場を救うプロフェッショナルな業務自動化エンジニアの仕事である。

タイトルとURLをコピーしました