こんにちは!開発現場で日々、Windows FormsのUIと格闘している後輩エンジニアの皆さん。先輩の私です。
さて、業務アプリケーション開発において避けて通れないのが、そう、「DataGridView(グリッド)」ですね。エクセルっぽくデータを一覧表示・編集できる非常に強力なコントロールですが、標準で用意されている機能だけでは「日付を入力するのに、いちいち手打ちさせるなんてユーザーに優しくない!カレンダーからポチッと選ばせたい!」といった現場の切実な要求に応えられないことがあります。
標準の `DataGridViewComboBoxColumn` や `DataGridViewCheckBoxColumn` では物足りない。かと言って、セルの編集時に無理やり外部のDateTimePickerをフワッと重ねるようなやっつけ仕事は、スクロールしたときに位置がズレたり、フォーカスが迷子になったりと、バグの温床になります。
そこで今回は、プロのエンジニアなら絶対に押さえておきたい「DataGridViewのセルと列を完全に自作し、ドロップダウン付き日付ピッカーをグリッドの中に完全埋め込む手法」を、VB.NETを使って優しく、かつ骨太に解説していきます。
ここをクリアすれば、あなたもWindows FormsのUI設計において「一目置かれる存在」になれますよ。さあ、一緒に扉を開けましょう!
—
1. なぜ「カスタムセル・カスタム列」が必要なのか?
まず、このアーキテクチャの心構えからお話しします。
Windows FormsのDataGridViewは、MVC(Model-View-Controller)に近い洗練された描画・編集分離モデルで作られています。
- DataGridViewColumn(列): グリッド全体の列の定義や、セルのテンプレートを保持する。
- DataGridViewCell(セル): 各マスの「見た目(描画)」と「データ」を管理する。
- DataGridViewEditingControl(編集用コントロール): セルが編集状態になったときに、実際に上に被さるテキストボックスやコンボボックスなどの実体。
これら3つを正しく連携させないと、プロフェッショナルな挙動は手に入りません。今回は「日付ピッカー」をこの仕組みに完全に組み込みます。
—
2. 実装の全体像:作るべき3つのクラス
日付ピッカー付きカスタムグリッドを実現するためには、以下の3つのクラスを定義します。
1. `CalendarEditingControl` (編集コントロール本体:DateTimePickerを継承したもの)
2. `CalendarCell` (セル:描画と編集コントロールの結びつきを制御するもの)
3. `CalendarColumn` (列:グリッドに配置するためのラッパー)
難しそうに見えますか?大丈夫、一つひとつ紐解いていけば怖くありません。
—
3. ステップ・バイ・ステップ実装コード(VB.NET)
以下のコードは、そのままあなたのプロジェクト(VB.NETのWindows Formsアプリケーション)にコピー&ペーストして組み込める実用コードです。
Imports System
Imports System.Windows.Forms
Imports System.Drawing
Namespace CustomGridControls
‘ ==========================================
‘ 1. 編集コントロール(DateTimePickerを拡張)
‘ ==========================================
Friend Class CalendarEditingControl
Inherits DateTimePicker
Implements IDataGridViewEditingControl
Private reloadingValue As Boolean
Private gridControl As DataGridView
Public Sub New()
‘ 日付の表示形式を「年/月/日」に設定
Me.Format = DateTimePickerFormat.Short
End Sub
‘ IDataGridViewEditingControl の実装
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
Try
Me.Value = DateTime.Parse(CStr(value))
Catch
Me.Value = DateTime.Today
End Try
End If
End Set
End Property
Public Function GetEditingControlFormattedValue(ByVal context As DataGridViewDataErrorContexts) As Object Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
Return Me.Value.ToShortDateString()
End Function
Public Sub ApplyCellStyleToEditingControl(ByVal 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.RowIndex
End Get
Set(ByVal value As Integer)
Me.RowIndex = value
End Set
End Property
Public Function EditingControlWantsInputKey(ByVal key As Keys, ByVal dataGridViewWantsInputKey As Boolean) As Boolean Implements IDataGridViewEditingControl.EditingControlWantsInputKey
‘ 矢印キーなどをDateTimePicker内で完結させるための処理
Select Case key
Case Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Home, Keys.End
Return True
Case Else
Return False
End Select
End Function
Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
‘ 編集開始時の初期化処理(特に何もしなくてもOK)
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.gridControl
End Get
Set(ByVal value As DataGridView)
Me.gridControl = value
End Set
End Property
Public Property EditingControlValueChanged() As Boolean Implements IDataGridViewEditingControl.EditingControlValueChanged
Get
Return Me.reloadingValue
End Get
Set(ByVal value As Boolean)
Me.reloadingValue = value
End Set
End Property
Protected Overrides Sub OnValueChanged(ByVal eventargs As EventArgs)
‘ 値が変更されたことをグリッドに通知する
Me.reloadingValue = True
MyBase.OnValueChanged(eventargs)
If Me.gridControl IsNot Nothing Then
Me.gridControl.NotifyCurrentCellDirty(True)
End If
End Sub
End Class
‘ ==========================================
‘ 2. カスタムセル
‘ ==========================================
Public Class CalendarCell
Inherits DataGridViewTextBoxCell
Public Sub New()
MyBase.New()
End Sub
‘ 編集モードに入ったときに、先ほどのCalendarEditingControlを呼び出す
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
ctl.Value = Convert.ToDateTime(Me.Value)
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(DateTime)
End Get
End Property
End Class
‘ ==========================================
‘ 3. カスタム列
‘ ==========================================
Public Class CalendarColumn
Inherits DataGridViewColumn
Public Sub New()
‘ 列のテンプレートとして、自作のCalendarCellを割り当てる
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 value.GetType().IsAssignableFrom(GetType(CalendarCell)) Then
Throw New InvalidCastException(“CalendarCellを指定してください。”)
End If
MyBase.CellTemplate = value
End Set
End Property
End Class
End Namespace
—
4. このコードの「本質」とエンジニアの急所
上記のコードで、VB.NETのオブジェクト指向の妙と、Windows Formsの裏側の仕組みが少し見えてきたのではないでしょうか。ここでいくつか重要なポイントを解説します。
① `IDataGridViewEditingControl` インターフェイスの力
`CalendarEditingControl` が実装しているこのインターフェイスこそが、DataGridViewと外部コントロールを繋ぐ「魔法の契約書」です。
「今どんな値が入っているか(`EditingControlFormattedValue`)」や「どのセルの行にいるか(`EditingControlRowIndex`)」をグリッド側と安全にやり取りするためのメソッドが強制されます。これを守ることで、グリッドがスムーズに値を吸い上げてくれるのです。
② 陥りやすいエラーと罠:「型不一致(InvalidCastException)」
初心者が自作セルを作るときによくやるミスが、`ValueType` や `EditType` のオーバーライド忘れです。
例えば、セルの `ValueType` が `DateTime` であるにもかかわらず、バインドするデータソース側が文字列(String)やDBNullのままで上手く型変換のハンドリングができていないと、セルからフォーカスが外れた瞬間にクラッシュするか、値が消滅します。
今回のコードでは、`InitializeEditingControl` の中で `DBNull` や `Nothing` に対する防衛策(`DateTime.Today` をデフォルトにする)を入れているため、堅牢性が高まっています。
—
5. 実際のフォームでの使い方
作成したカスタム列は、通常のDataGridViewと同じようにデザイナー、もしくはコードから簡単に追加できます。
‘ フォームのロード時などにコードから追加する例
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
‘ 1. カレンダー列のインスタンスを作成
Dim col As New CustomGridControls.CalendarColumn()
col.Name = “BirthDateColumn”
col.HeaderText = “生年月日”
‘ 2. DataGridViewに追加
DataGridView1.Columns.Add(col)
‘ 3. テストデータを適当に流し込む
DataGridView1.Rows.Add(DateTime.Today)
End Sub
これだけで、グリッドのセルをクリックした瞬間に、あの使い慣れたカレンダー(DateTimePicker)がポップアップし、日付をスマートに入力できるようになります。ユーザー体験(UX)が劇的に向上すること間違いなしです。
—
おわりに
今回は、Windows FormsのDataGridViewにおけるカスタムセル・カスタム列の自社開発(ドロップダウン付き日付ピッカーの完全埋め込み)について解説しました。
一見するとコード量が多くて難しく感じるかもしれませんが、「列・セル・編集コントロール」の役割分担をしっかりと理解すれば、日付ピッカーだけでなく、ポップアップ電卓や、マスタ参照用の検索モーダル呼び出しボタンなど、あらゆるリッチUIをグリッド内に飼いならすことができるようになります。
ここをクリアしたあなたなら、もう「マクロや標準コントロールの枠に縛られるプログラマー」ではありません。ぜひ現場のアプリに取り入れて、まわりの開発メンバをアッと言わせてくださいね。
それでは、また次回の極限の知見でお会いしましょう!Happy Coding!
