【テクニカル・上級編】Windows FormsのDataGridViewにおけるセル結合(CellMerge)のエミュレーション:CellPaintingイベントを駆使したエクセルライクなヘッダー・明細の表現 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

Visual Illusion: DataGridViewにおける「セル結合」の真髄 —— CellPaintingによる描画制御とGDI+の極限活用

業務アプリケーションの要となるグリッドコントロール。その中でもWindows Formsの`DataGridView`は、その堅牢さと柔軟性から、長年レガシーシステムからモダンな業務ツールまで、あらゆる現場で重用されてきた。

しかし、現場の要求は常に残酷だ。「Excelのように、同じ値が続くセルを結合して見せろ」――。

標準の`DataGridView`には、セルを物理的に結合する機能など存在しない。データ構造は常に格子状(Grid)であり、インデックスに基づいた厳格なアクセスを前提としているからだ。ここで安易にデータ構造を書き換えようとするのは、アーキテクトとしての敗北を意味する。データは常に原子(Atomic)であるべきであり、結合はあくまで「視覚的な錯覚」として実装すべきなのだ。

本稿では、`CellPainting`イベントを制御下に置き、GDI+を直接操ることで、パフォーマンスを一切犠牲にすることなく「セル結合」を実現する、極限のテクニックを伝授する。

1. 設計思想:物理的な結合ではなく、視覚的な隠蔽

セル結合の実装において、初心者が陥る罠は「データの加工」である。結合するために値を空にする、あるいは親要素のインデックスを管理するといった設計は、ソート、フィルタリング、データバインディングといった`DataGridView`の強力な機能をすべて破壊する。

我々が目指すべきは、「データはそのまま、描画プロセスのみをハックする」というアプローチだ。

戦略の要諦

1. 比較ロジック: 現在のセルと、隣接するセル(上または下)の値を比較する。
2. 描画のスキップ: 値が同一である場合、境界線(Border)の描画を抑制し、テキストの描画をスキップする。
3. 描画の乗っ取り: `e.Handled = True` を宣言し、標準の描画ルーチンを完全にバイパスする。

2. 実装:CellPaintingによる高度な描画制御

以下に、垂直方向のセル結合(同じ値が続く場合に結合して見せる)を実現する、プロフェッショナル仕様のコードを示す。

Imports System.Drawing
Imports System.Windows.Forms

''' <summary>
''' 高度な描画制御を備えたDataGridView拡張クラス
''' </summary>
Public Class AdvancedDataGridView
    Inherits DataGridView

    Public Sub New()
        ' ダブルバッファリングを有効化し、描画時のちらつきを最小限に抑える
        ' これを行わないと、カスタム描画による描画負荷でスクロールが著しく低下する
        Me.DoubleBuffered = True
    End Sub

    ''' <summary>
    ''' セルの描画イベントをオーバーライドし、結合エミュレーションを行う
    ''' </summary>
    Protected Overrides Sub OnCellPainting(e As DataGridViewCellPaintingEventArgs)
        ' ヘッダーや、範囲外のインデックス、存在しないセルは無視する
        If e.RowIndex < 0 OrElse e.ColumnIndex < 0 Then
            MyBase.OnCellPainting(e)
            Return
        End If

        ' 特定の列(例: 0列目)に対してのみ結合処理を適用する
        ' 全列に適用すると、パフォーマンスの低下と意図しない挙動を招く
        If e.ColumnIndex = 0 AndAlso TypeOf e.Value Is String Then
            
            Dim currentValue As String = e.Value?.ToString()
            Dim isMergedWithAbove As Boolean = False
            Dim isMergedWithBelow As Boolean = False

            ' --- 1. 上のセルとの比較 ---
            If e.RowIndex > 0 Then
                Dim aboveValue = Me.Rows(e.RowIndex - 1).Cells(e.ColumnIndex).Value?.ToString()
                If currentValue = aboveValue Then
                    isMergedWithAbove = True
                End If
            End If

            ' --- 2. 下のセルとの比較 ---
            If e.RowIndex < Me.RowCount - 1 Then
                Dim belowValue = Me.Rows(e.RowIndex + 1).Cells(e.ColumnIndex).Value?.ToString()
                If currentValue = belowValue Then
                    isMergedWithBelow = True
                End If
            End If

            ' 結合の判定に基づき、描画処理をカスタマイズ
            ' ここでは「結合されている場合」の描画ロジックを記述する
            
            ' 背景の描画
            e.PaintBackground(e.CellBounds, True)

            ' 境界線の描画制御
            ' 上が結合されているなら、上部の境界線を引かない
            ' 下が結合されているなら、下部の境界線を引かない
            Dim rect As Rectangle = e.CellBounds
            
            ' 描画領域の調整(境界線を描画しないためのオフセット)
            If isMergedWithAbove Then
                rect.Y += 1 ' 上の境界線を隠すために少し下げる
                rect.Height -= 1
            End If
            If isMergedWithBelow Then
                rect.Height -= 1 ' 下の境界線を隠すために高さを削る
            End If

            ' セル枠線の描画(標準の描画を回避するため、必要な部分だけ手動で引く)
            ' 結合されていない箇所のみ、通常の境界線を描画する
            Using gridPen As New Pen(Me.GridColor)
                If Not isMergedWithAbove Then
                    e.Graphics.DrawLine(gridPen, rect.Left, rect.Top, rect.Right, rect.Top)
                End If
                If Not isMergedWithBelow Then
                    e.Graphics.DrawLine(gridPen, rect.Left, rect.Bottom, rect.Right, rect.Bottom)
                End If
                ' 左右の線は常に描画(または隣接列との兼ね合いで制御)
                e.Graphics.DrawLine(gridPen, rect.Left, rect.Top, rect.Left, rect.Bottom)
                e.Graphics.DrawLine(gridPen, rect.Right, rect.Top, rect.Right, rect.Bottom)
            End Using

            ' テキストの描画制御
            ' 上のセルと値が同じなら、テキストは描画しない(上のセルに任せる)
            If Not isMergedWithAbove Then
                ' テキストの配置(中央揃え等の調整)
                Dim textFormat As New StringFormat()
                textFormat.Alignment = StringAlignment.Center
                textFormat.LineAlignment = StringAlignment.Center
                
                ' 文字列の描画
                e.Graphics.DrawString(currentValue, e.CellStyle.Font, 
                                     New SolidBrush(e.CellStyle.ForeColor), 
                                     rect, textFormat)
            End If

            ' 描画完了を通知し、標準の描画ルーチンをスキップさせる
            e.Handled = True
            Return
        End If

        MyBase.OnCellPainting(e)
    End Sub
End Class

3. 伝説的アーキテクトの視点:パフォーマンスとメモリの極意

上記のコードは、一見すると完璧に見える。しかし、シニアエンジニアとして、私は以下の「見えないコスト」を指摘しなければならない。

GDI+ オブジェクトのライフサイクル管理

コード内で `Using gridPen As New Pen(…)` や `New SolidBrush(…)` を使用している。これは、描画のたびにオブジェクトを生成・破棄していることを意味する。`CellPainting` は、画面の更新(スクロールやリサイズ)のたびに、1秒間に数十回、あるいは数百回呼び出される。

この頻度で `New` を繰り返すと、マネージドヒープにゴミを撒き散らし、GC(ガベージコレクション)の頻発を招く。結果として、アプリケーションのスパイク(一瞬の停止)を引き起こす。

【最適化の鉄則】

  • 頻繁に使用する `Pen` や `Brush` は、クラスのフィールドとしてキャッシュするか、`SystemBrushes` や `SystemPens` を利用せよ。
  • もしカスタムカラーが必要なら、あらかじめ `Dictionary(Of Color, SolidBrush)` などに保持し、再利用する設計にせよ。

Windows API と描画の整合性

`DataGridView` は、内部的に Windows メッセージループ(`WM_PAINT`)に依存している。`e.Handled = True` を設定することは、OSに対して「この領域の描画責任は私が持った」と宣言することだ。

もし、`e.Graphics` に対して不適切な座標計算(例えば、境界線の幅を考慮しない計算)を行うと、スクロール時に「線が消える」「線が二重になる」といった、極めてデバッグしにくい描画の乱れが発生する。これは、GDI+ の座標系と、Windows のクライアント領域の座標系の解釈の齟齬から来るものだ。

レガシー環境への配慮

もし対象のシステムが .NET Framework 2.0/3.5 などの古い環境である場合、`DoubleBuffered` プロパティが公開されていない場合がある。その際は、リフレクションを用いて `Control.ControlStyles.OptimizedDoubleBuffer` を強制的に有効化するか、Windows API (`SendMessage`) を用いて `WS_EX_COMPOSITED` スタイルを適用するなどの、低レイヤーなアプローチが必要になる。

4. 結論

`DataGridView` におけるセル結合のエミュレーションは、単なる「見た目の変更」ではない。それは、「データ構造の整合性を守りつつ、いかにしてユーザーの認知を操作するか」という、フロントエンド・アーキテクチャの本質的な問いである。

1. データは分離せよ。
2. 描画は乗っ取れ。
3. リソースは使い回せ。

この三原則を遵守することで、あなたはレガシーな制約の中でも、モダンで洗練された、Excelに勝るとも劣らないユーザー体験を提供することができる。技術の深淵を覗く準備はできているか。

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