【Windows Forms極限optimization】DataGridViewに「本物の」カレンダーポップアップをねじ込む:カスタムセル・列完全実装ガイド
Windows Formsアプリケーションの設計において、避けて通れない最大の難所――それが `DataGridView` だ。
「セル内で日付を選ばせたいだけなのに、なぜ標準の `DataGridViewDateTimePickerColumn` は存在しないのか?」
業務システムの開発現場で、この愚痴を何度耳にしたことか。標準で提供されるのは粗末なテキストボックスか、自由度の低いコンボボックスのみ。サードパーティ製コントロールに頼るのも手だが、ライセンスコストや将来的な保守リスクを考えると、自前で極限までチューニングされたカスタムコントロールを保持しておくことが、シニアエンジニアとしての最大の武器となる。
今回は、`DataGridViewCell` と `DataGridViewColumn` を完全に掌握し、グリッドのパフォーマンスを微塵も落とすことなく、滑らかに動作するカレンダーポップアップ付きカスタム日付セルをVB.NETで実装する手法を授けよう。
—
1. なぜ「力技のコントロール配置」では破綻するのか?
素人がやりがちな悪手は、セルが編集状態に入ったときに `DateTimePicker` を `DataGridView.Controls` に動的に `Add` し、フォーカスを制御するというアプローチだ。
この方法では、以下の致命的なバグが必ず発生する。
- 描画のラグとちらつき (Flickering): スクロールするたびにコントロールの生成・破棄・再配置が走り、UI线程が悲鳴を上げる。
- クリッピングの不具合: グリッドの端で日付ピッカーが途切れたり、ヘッダー領域からはみ出したりする。
- フォーカスロスト地獄: ポップアップしたカレンダーをクリックした瞬間に「セルの編集抜け」と判定され、値が確定せずに消滅する。
プロフェッショナルが取るべきアプローチはこれだ:
「普段は単なる文字列を描画し、編集時のみホストとなる単一の `DateTimePicker` を画面全体で使い回す(EditingControlの継承)」
—
2. アーキテクチャの全体像
今回作成するカスタム実装は、以下の3つのピースで構成される。
1. `CalendarEditingControl`: 実際の編集時にセル上に一時配置される、`DateTimePicker` を継承したコントロール。
2. `CalendarCell`: セルの描画と、編集コントロールのホスト管理を行う `DataGridViewCell` の継承クラス。
3. `CalendarColumn`: グリッドの列として振る舞い、初期設定をカプセル化する `DataGridViewColumn` の継承クラス。
この分離されたライフサイクルを理解することが、バグのないUI設計の第一歩だ。
—
3. プロダクションコード実装
以下のコードは、そのままプロジェクトに組み込んで即座に実戦投入できるレベルまで練り上げられた、堅牢なVB.NETコードである。
① 編集用コントロール (`CalendarEditingControl.vb`)
Imports System.Windows.Forms
”’
”’
Public Class CalendarEditingControl
Inherits DateTimePicker
Implements IDataGridViewEditingControl
Private gridAsDataGridView As DataGridView
Private valueIsChangedAs Boolean = False
Private rowIndexAs Integer
Public Sub New()
‘ 日付形式を適切に設定
Me.Format = DateTimePickerFormat.Short
End Sub
Region “IDataGridViewEditingControl 実装”
”’
”’
Public Property EditingControlDataGridView() As DataGridView Implements IDataGridViewEditingControl.EditingControlDataGridView
Get
Return Me.gridAsDataGridView
End Get
Set(ByVal value As DataGridView)
Me.gridAsDataGridView = value
End Set
End Property
”’
”’
Public Property EditingControlFormattedValue() As Object Implements IDataGridViewEditingControl.EditingControlFormattedValue
Get
Return Me.Value.ToShortDateString()
End Get
Set(ByVal value As Object)
If TypeOf value is String Then
Dim parsedDate As Date
If Date.TryParse(CStr(value), parsedDate) Then
Me.Value = parsedDate
Else
Me.Value = DateTime.Today
End If
End If
End Set
End Property
”’
”’
Public Property EditingControlRowIndex() As Integer Implements IDataGridViewEditingControl.EditingControlRowIndex
Get
Return Me.rowIndex
End Get
Set(ByVal value As Integer)
Me.rowIndex = value
End Set
End Property
”’
”’
Public Property EditingControlValueChanged() As Boolean Implements IDataGridViewEditingControl.EditingControlValueChanged
Get
Return Me.valueIsChanged
End Get
Set(ByVal value As Boolean)
Me.valueIsChanged = value
End Set
End Property
”’
”’
Public ReadOnly Property EditingPanelCursor() As Cursor Implements IDataGridViewEditingControl.EditingPanelCursor
Get
Return Cursors.Default
End Get
End Property
”’
”’
Public ReadOnly Property RepositionEditingControlOnValueChange() As Boolean Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange
Get
Return False
End Get
End Property
”’
”’
Public Sub ApplyCellStyleToEditingControl(ByVal dataGridViewCellStyle As DataGridViewCellStyle) Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
Me.Font = dataGridViewCellStyle.Font
If dataGridViewCellStyle.ForeColor Then
Me.ForeColor = dataGridViewCellStyle.ForeColor
End If
If dataGridViewCellStyle.BackColor Then
Me.BackColor = dataGridViewCellStyle.BackColor
End If
End Sub
”’
”’
Public Function EditingControlWantsInputKey(ByVal keyData As Keys, ByVal dataGridViewWantsInputKey As Boolean) As Boolean Implements IDataGridViewEditingControl.EditingControlWantsInputKey
Select Case (keyData And Keys.KeyCode)
Case Keys.Escape, 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 Function GetEditingControlFormattedValue(ByVal context As DataGridViewDataErrorContexts) As Object Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
Return Me.Value.ToShortDateString()
End Function
”’
”’
Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
‘ 特殊な処理は不要
End Sub
End Region
”’
”’
Protected Overrides Sub OnValueChanged(ByVal eventargs As EventArgs)
Me.valueIsChanged = True
If Me.gridAsDataGridView IsNot Nothing Then
Me.gridAsDataGridView.NotifyCurrentCellDirty(True)
End If
MyBase.OnValueChanged(eventargs)
End Sub
End Class
② カスタムセル (`CalendarCell.vb`)
Imports System.Windows.Forms
”’
”’
Public Class CalendarCell
Inherits DataGridViewTextBoxCell
Public Sub New()
MyBase.New()
‘ デフォルトで短い日付形式を設定
Me.Style.Format = “yyyy/MM/dd”
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
”’
”’
Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer, ByVal initialFormattedValue As Object, ByVal dataGridViewCellStyle As DataGridViewCellStyle)
MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
Dim ctl As CalendarEditingControl = CType(Me.DataGridView.EditingControl, CalendarEditingControl)
If Me.Value Is Nothing OrElse TypeOf Me.Value Is DBNull Then
ctl.Value = DateTime.Today
Else
‘ 安全な型キャスト
Dim parsedDate As Date
If Date.TryParse(Me.Value.ToString(), parsedDate) Then
ctl.Value = parsedDate
Else
ctl.Value = DateTime.Today
End If
End If
End Sub
End Class
③ カスタム列 (`CalendarColumn.vb`)
Imports System.Windows.Forms
”’
”’
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 TypeOf value Is CalendarCell Then
Throw New InvalidCastException(“CalendarCell型を指定してください。”)
End If
MyBase.CellTemplate = value
End Set
End Property
End Class
—
4. 実務における運用上の注意点とデータベース連携の罠
このカスタム列を実際の業務システム(SQL ServerやOracle、あるいはSQLiteなどのローカルDB)へ組み込む際、以下の2点を見落とすと確実に炎上する。
1. DBNull と DateTime の暗黙的変換の排除
データベースから取得したデータが `DBNull.Value` である場合、VB.NETの厳密型チェック(`Option Strict On`)環境下では、そのままセルにバインドするとクラッシュする。
必ずプロパティ設定時やデータテーブルへの書き戻し時に、以下のような防衛的コードを挟むこと。
‘ データベースから取得した値をDataGridViewにバインドする際の例
Dim cellValue As Object = row(“TargetDate”)
If cellValue Is DBNull.Value Then
dataGridView1.Rows(i).Cells(“colDate”).Value = DBNull.Value
Else
dataGridView1.Rows(i).Cells(“colDate”).Value = Convert.ToDateTime(cellValue)
End If
2. グリッド全体のパフォーマンス維持
何千行もの大量データを扱う画面で、全セルがこのカレンダーコントロールのインスタンス化オーバーヘッドを背負うわけではない。前述の通り、「編集時のみ1つのコントロールが使い回される(ホスティング)」 アーキテクチャになっているため、メモリリークや描画遅延の心配は無用だ。安心してマスターデータ一覧などに投入してほしい。
—
5. 結び:チーフアーキテクトからの提言
業務アプリケーションの価値は、「ユーザーの入力ストレスをいかに極限までゼロに近づけるか」にかかっている。
標準機能の不足を言い訳にして「テキストボックスに `20231012` と手打ちさせてバリデーションではじく」ような前時代的なUIを設計するのは、もう終わりにしよう。
今回提供したコードベースをあなたのソリューションに組み込むことで、プロフェッショナルな洗練されたモダンUIと、揺るぎない堅牢性を同時に手に入れてほしい。妥協なきエンジニアリングの健闘を祈る。
