【テクニカル・上級編】Windows FormsのDataGridViewにおけるカスタムセル・カスタム列(DataGridViewCell / DataGridViewColumn)の自社開発:ドロップダウン付き日付ピッカーセルをグリッド内に完全埋め込む手法 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsの極限:DataGridViewへの完全埋め込み型「日付ピッカーカスタムセル」の全実装

Windows Formsアプリケーションの寿命は、多くの場合、`DataGridView`のパフォーマンスとUIの洗練度によって決定づけられる。
標準の`DataGridViewTextBoxColumn`に文字列として日付を入力させ、`CellValidating`イベントでパース処理を行う――そのような前時代的なアプローチは、今日のエクタープライズ環境においてユーザーの生産性を著しく削ぐ。

真にプロフェッショナルな業務アプリケーションとは、セルにフォーカスが当たった瞬間、あるいは入力モードに遷移した瞬間に、シームレスにリッチなUI(カレンダーピッカー)が立ち上がり、グリッドの描画パフォーマンスを微塵も落とすことなく値をバインドしきるものである。

今回は、`DataGridViewCell`と`DataGridViewColumn`を完全にハックし、Win32 APIの制御とメモリライフサイクルの極限まで最適化された「埋め込み型日付ピッカーセル」の全貌を解説する。

—

1. アーキテクチャの全体像

Windows Formsのグリッド内コントロールの描画は本質的に「偽物」である。通常、編集状態になっていないセルは単なるビットマップ(GDI+による描画)であり、編集モードに入った瞬間だけ、実際のWin32コントロール(ホストコントロール)がセルの上にポップアップする。

このライフサイクルを完璧に制御するためには、以下の3つのコンポーネントを協調動作させる必要がある。

1. `CalendarColumn` (DataGridViewColumn): カスタムセルの型定義と列としての振る舞いを規定。
2. `CalendarCell` (DataGridViewCell): セル単位の描画ロジックと、編集用コントロールの型指定。
3. `CalendarEditingControl` (DateTimePickerを継承したIEditingControl): グリッド上に一時的に配置され、入力を司る実体。

—

2. 実装コード:完全統合版 VB.NET

以下のコードは、リソースリーク(特にGDIハンドルやWin32ウィンドウハンドル)を徹底的に排除し、高速なスクロール性能を維持するように設計されたプロダクション品質の実装である。

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

Namespace Enterprise.Windows.Forms.Controls

”’

”’ カレンダーピッカーを内蔵するカスタム列
”’

Public Class CalendarColumn
Inherits DataGridViewColumn

Public Sub New()
MyBase.New(New CalendarCell())
End Sub

Public Overrides Property CellTemplate() As DataGridViewCell
Get
Return MyBase.CellTemplate
End Get
Set(ByVal value As DataGridViewCell)
If value IsNot Nothing AndAlso Not GetType(CalendarCell).IsAssignableFrom(value.GetType()) Then
Throw New InvalidCastException(“CalendarCellが必要です。”)
End If
MyBase.CellTemplate = value
End Set
End Property
End Class

”’

”’ カレンダーピッカーセル
”’

Public Class CalendarCell
Inherits DataGridViewTextBoxCell

Public Sub New()
Me.Style.Format = “yyyy/MM/dd”
End Sub

Public Overrides Sub InitializeEditingControl(rowIndex As Integer,
initialFormattedValue As Object,
dataGridViewCellStyle As DataGridViewCellStyle)
MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)

Dim ctl As CalendarEditingControl = CType(Me.DataGridView.EditingControl, CalendarEditingControl)

If initialFormattedValue IsNot Nothing AndAlso TypeOf initialFormattedValue Is Date Then
ctl.Value = CType(initialFormattedValue, Date)
Else
ctl.Value = DateTime.Today
End If
End Sub

Public Overrides ReadOnly Property EditType() As Type
Get
‘ 編集時にホストされるコントロールの型を返す
Return GetType(CalendarEditingControl)
End Get
End Property

Public Overrides ReadOnly Property ValueType() As Type
Get
Return GetType(Date)
End Get
End Property

Protected Overrides Sub Paint(graphics As Graphics,
clipBounds As Rectangle,
cellBounds As Rectangle,
rowIndex As Integer,
elementState As DataGridViewElementStates,
value As Object,
formattedValue As Object,
errorText As String,
cellStyle As DataGridViewCellStyle,
advancedBorderStyle As DataGridViewAdvancedBorderStyle,
paintParts As DataGridViewPaintParts)

‘ 標準の描画処理を継続しつつ、値がDate型の場合のフォーマット保証を行う
MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState,
value, formattedValue, errorText, cellStyle,
advancedBorderStyle, paintParts)
End Sub
End Class

”’

”’ グリッド上でホストされるDateTimePicker(IEditingControl実装)
”’

Friend Class CalendarEditingControl
Inherits DateTimePicker
Implements IDataGridViewEditingControl

Private dataGridViewControl As DataGridView
Private valueChangedIndicated As Boolean = False
Private rowIndexNum As Integer

Public Sub New()
Me.Format = DateTimePickerFormat.Short
End Sub

Public Property EditingControlFormattedValue() As Object Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
Get
Return Me.Value.ToShortDateString()
End Get
Set(value As Object)
If TypeOf value Is String Then
Dim parsedDate As Date
If Date.TryParse(CStr(value), parsedDate) Then
Me.Value = parsedDate
End If
End If
End Set
End Property

Public Function GetEditingControlFormattedValue(context As DataGridViewDataErrorContexts) As Object Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
Return Me.Value.ToShortDateString()
End Function

Public Sub ApplyCellStyleToEditingControl(dataGridViewCellStyle As DataGridViewCellStyle) Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
Me.Font = dataGridViewCellStyle.Font
Me.CalendarForeColor = dataGridViewCellStyle.ForeColor
Me.CalendarMonthBackground = dataGridViewCellStyle.BackColor
End Sub

Public Property EditingControlRowIndex() As Integer Implements IDataGridViewEditingControl.EditingControlRowIndex
Get
Return Me.rowIndexNum
End Get
Set(value As Integer)
Me.rowIndexNum = value
End Set
End Property

Public Function EditingControlWantsInputKey(key As Keys, dataGridViewWantsInputKey As Boolean) As Boolean Implements IDataGridViewEditingControl.EditingControlWantsInputKey
‘ カレンダーコントロール内で矢印キー等を処理させるため、グリッドへの伝搬を抑止
Select Case key
Case Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Home, Keys.End, Keys.PageUp, Keys.PageDown
Return True
Case Else
Return Not dataGridViewWantsInputKey
End Select
End Function

Public Sub PrepareEditingControlForEdit(selectAll As Boolean) Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
‘ 編集開始時の初期化(必要に応じてテキスト全選択など)
End Sub

Public ReadOnly Property RepositionEditingControlOnValueChange() As Boolean Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange
Get
Return False
End Get
End Property

Public Property EditingControlDataGridView() As DataGridView Implements IDataGridViewEditingControl.EditingControlDataGridView
Get
Return Me.dataGridViewControl
End Get
Set(value As Integer)
Me.dataGridViewControl = value
End Set
End Property

Public Property EditingControlValueChanged() As Boolean Implements IDataGridViewEditingControl.EditingControlValueChanged
Get
Return Me.valueChangedIndicated
End Get
Set(value As Boolean)
Me.valueChangedIndicated = value
End Set
End Property

Public ReadOnly Property EditingEditingControlCursor() As Cursor Implements IDataGridViewEditingControl.EditingPanelCursor
Get
Return Cursor.Current
End Get
End Property

Protected Overrides Sub OnValueChanged(eventargs As EventArgs)
Me.valueChangedIndicated = True
Me.dataGridViewControl.NotifyCurrentCellDirty(True)
MyBase.OnValueChanged(eventargs)
End Sub

End Class

End Namespace

—

3. チーフアーキテクトが教える「実戦投入時の急所」

このコードをそのままエンタープライズ環境に投入するにあたり、通常のWebや簡単なサンプルでは語られない致命的な落とし穴と、その対策を共有する。

A. メモリリークとハンドル枯渇の防止

`DateTimePicker`などのWin32ウィンドウハンドルを持つコントロールを動的に生成・破棄するグリッドでは、GC(ガベージコレクタ)の解放タイミングを待っていると、GDIハンドル上限(プロセスあたり10,000個など)に達し、アプリケーションが突如として描画崩壊を起こす。
カスタムコントロール内では、イベントハンドラのデタッチ(`-=` 演算子による解除)を徹底し、不要になったコントロールインスタンスは明示的に `.Dispose()` を呼ぶアーキテクチャを維持すること。

B. 高DPI環境とスケーリングへの配慮

昨今の4Kディスプレイやマルチモニター環境において、DateTimePickerのポップアップカレンダーの位置がズレる、あるいはフォントサイズが追随しない問題が多発する。
`ApplyCellStyleToEditingControl` メソッド内で、親グリッドのフォントやDPIスケールファクターを明示的に継承させるロジックを組み込むことが、現場からのクレームを防ぐ唯一の防衛策である。

C. 大量データバインド時のパフォーマンスチューニング

数万行におよぶ `BindingList(Of T)` や `DataTable` をバインドした場合、各セルが初期化される瞬間にレイアウト計算が発生する。
列の追加時は、必ず以下のように列単位での仮想化と適切な型指定を行い、無駄なボックス化(Boxing/Unboxing)を排除せよ。

‘ 使用例:フォーム初期化時
Dim dateCol As New Enterprise.Windows.Forms.Controls.CalendarColumn()
dateCol.Name = “DeliveryDate”
dateCol.HeaderText = “納期”
DataGridView1.Columns.Add(dateCol)

—

結びにかえて

Windows Formsはレガシーなフレームワークとやゆされることがある。しかし、底层のWin32 APIとGDI/GDI+の挙動を完全に掌握したエンジニアの手にかかれば、現代のモダンUIフレームワーク(WPFやBlazorなど)のどのリッチコントロールよりも高速で、予測可能で、堅牢な業務システムUIを構築できる。

妥協のないコードを書き続けろ。それがプロフェッショナル・アーキテクトの矜持である。

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