VB.NET/WinForms の DataGridView には専用の DataGridViewDateTimeColumn がありません。とはいえ、正しい設定と少しの実装で、ヘッダークリックによる「文字列ソート」ではなく、実用的な「日時ソート」を高精度に実現できます。この記事では .NET Framework 4.8 を前提に、最小構成から入力 UI の強化(DateTimePicker 埋め込み)、バインド時の注意点、パフォーマンスやバリデーションまで、現場でそのまま使えるレシピとして詳解します。
DataGridView で日付/時刻列が難しい理由
DataGridView は列の型に応じてソート方法を切り替えます。セルの Value が String なら辞書式(文字列)で、DateTime なら時系列で比較されます。つまり「見た目の書式」ではなく「保持している値の型」がソートの成否を左右します。専用列が無い=実現できない、ではありません。
最小構成:TextBox 列+型指定だけで“日時ソート”にする
専用列が不要なケースは多く、次の 3 ステップで十分です。
- DataGridViewTextBoxColumn を追加
ValueType = GetType(DateTime)を設定(最重要)- 表示書式を
DefaultCellStyle.Format = "yyyy/MM/dd HH:mm"などに指定
' 最小構成(非バインドでもバインドでも有効)
Dim col As New DataGridViewTextBoxColumn()
col.Name = "StartTime"
col.HeaderText = "開始日時"
col.ValueType = GetType(DateTime) ' ★ここが重要:日時として保持
col.DefaultCellStyle.Format = "yyyy/MM/dd HH:mm"
col.SortMode = DataGridViewColumnSortMode.Automatic
DataGridView1.Columns.Add(col)
これだけで、セルが DateTime を保持するようになり、ヘッダークリックの標準ソートが“日時順”で動作します。書式は見た目のための設定で、ソートは ValueType に依存する点が肝です。
よくある勘違い
- 書式だけ(
Formatの指定)ではソートは「文字列比較」のままです。 - 列の
ValueTypeをDateTimeにしても、セルにStringを代入してしまうと文字列ソートになります(入れる値の型も守る)。
解決策の要点まとめ(チートシート)
| 要素 | 具体的な設定/実装ポイント | 効果 |
|---|---|---|
| ソートを正しく行う最小構成 | 1. DataGridViewTextBoxColumn を追加2. ValueType = GetType(DateTime) を設定3. DefaultCellStyle.Format = "yyyy/MM/dd HH:mm" など希望の書式を指定 | セルの保持値が DateTime になり、標準のクリックソートが「日時順」で働く |
| データバインディング時 | DataTable の列型を DateTime にする(推奨) BindingSource → DataTable 構成ならヘッダークリックで DataView によるソートが有効 List(Of T)/BindingList(Of T) は既定で並べ替え非対応(後述の回避策を適用) | 型情報に基づいた正しいソートが自動で動作 |
| 編集 UI をリッチに | 公式サンプルをベースに DateTimePicker をホストするカスタム列/セル(例:DataGridViewDateTimePickerColumn)を作成 | カレンダーやスピンで確実に日時入力。保持値は DateTime のまま |
| バリデーション | CellValidating で DateTime.TryParse/TryParseExact を用い、無効値を弾く | 入力ミスを防止し、ソート整合性を維持 |
バインド構成ごとの“正しい並べ替え”実装
DataTable を使う(最も簡単で堅牢)
DataTable の列型を GetType(DateTime) にするだけで、ヘッダークリックにより DataView.Sort が内部的に設定され、時系列ソートになります。
' DataTable を作ってバインド
Dim dt As New DataTable()
dt.Columns.Add("ID", GetType(Integer))
dt.Columns.Add("開始日時", GetType(DateTime))
dt.Columns.Add("件名", GetType(String))
dt.Rows.Add(1, #2025-01-05 09:00#, "A")
dt.Rows.Add(2, #2025-10-02 14:30#, "B")
dt.Rows.Add(3, #2025-03-15 08:15#, "C")
Dim bs As New BindingSource() With {.DataSource = dt}
DataGridView1.AutoGenerateColumns = False
' 列定義(書式は見た目のみ、値は DateTime のまま)
Dim colTime As New DataGridViewTextBoxColumn() With {
.DataPropertyName = "開始日時",
.HeaderText = "開始日時",
.ValueType = GetType(DateTime),
.DefaultCellStyle = New DataGridViewCellStyle() With {.Format = "yyyy/MM/dd HH:mm"},
.SortMode = DataGridViewColumnSortMode.Automatic
}
Dim colId As New DataGridViewTextBoxColumn() With {.DataPropertyName="ID", .HeaderText="ID"}
Dim colSubject As New DataGridViewTextBoxColumn() With {.DataPropertyName="件名", .HeaderText="件名"}
DataGridView1.Columns.AddRange(colId, colTime, colSubject)
DataGridView1.DataSource = bs
この構成では、Null 値は DBNull.Value として扱われます。表示や比較で問題になる場合は NullValue を指定します。
colTime.DefaultCellStyle.NullValue = "" ' 空表示にする等
List(Of T)/BindingList(Of T) を使う場合
BindingList(Of T) は既定では並べ替えに非対応です。ヘッダークリックしても変化がない場合は下記のいずれかを選びます。
- SortableBindingList の実装を導入(IBindingList の Sort メンバーを実装)
- BindingSource に並べ替え済みの新しいコレクションを差し替える(LINQ の
OrderBy→ 再バインド) - 非バインドに切り替え、
SortCompareイベントでカスタムソート(後述)
コレクションを差し替える例:
Dim list As New BindingList(Of Item)(GetItems())
Dim bs As New BindingSource() With {.DataSource = list}
DataGridView1.DataSource = bs
' ヘッダークリックなどのトリガで再バインド
Dim asc As Boolean = True
AddHandler DataGridView1.ColumnHeaderMouseClick,
Sub(_, e)
If DataGridView1.Columns(e.ColumnIndex).DataPropertyName = NameOf(Item.StartTime) Then
Dim ordered = If(asc,
list.OrderBy(Function(x) x.StartTime).ToList(),
list.OrderByDescending(Function(x) x.StartTime).ToList())
asc = Not asc
bs.DataSource = New BindingList(Of Item)(ordered)
End If
End Sub
Public Class Item
Public Property Id As Integer
Public Property StartTime As DateTime ' ← プロパティ型が DateTime であること
Public Property Subject As String
End Class
編集 UI を強化:DateTimePicker をセルにホストする
入力の確実性や体験を高めたい場合、編集時だけ DateTimePicker を表示するカスタム列/セルを導入します。セルの値そのものは DateTime なので、ソートの仕組みはそのまま活かせます。
カスタム列/セル(VB.NET)
' DataGridViewDateTimePickerColumn.vb
Public Class DataGridViewDateTimePickerColumn
Inherits DataGridViewColumn
Public Sub New()
MyBase.New(New DataGridViewDateTimePickerCell())
End Sub
Public Overrides Property CellTemplate As DataGridViewCell
Get
Return MyBase.CellTemplate
End Get
Set(value As DataGridViewCell)
If value IsNot Nothing AndAlso Not TypeOf value Is DataGridViewDateTimePickerCell Then
Throw New InvalidCastException("DataGridViewDateTimePickerCell が必要です。")
End If
MyBase.CellTemplate = value
End Set
End Property
End Class
Public Class DataGridViewDateTimePickerCell
Inherits DataGridViewTextBoxCell
Public Sub New()
Me.Style.Format = "yyyy/MM/dd HH:mm"
End Sub
Public Overrides ReadOnly Property EditType As Type
Get
Return GetType(DateTimePickerEditingControl)
End Get
End Property
Public Overrides ReadOnly Property ValueType As Type
Get
Return GetType(DateTime)
End Get
End Property
Public Overrides ReadOnly Property DefaultNewRowValue As Object
Get
Return DateTime.Now
End Get
End Property
Public Overrides Sub InitializeEditingControl(rowIndex As Integer, initialFormattedValue As Object, dataGridViewCellStyle As DataGridViewCellStyle)
MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
Dim ctl = TryCast(DataGridView.EditingControl, DateTimePickerEditingControl)
If ctl IsNot Nothing Then
ctl.Format = DateTimePickerFormat.Custom
ctl.CustomFormat = If(String.IsNullOrEmpty(dataGridViewCellStyle.Format), "yyyy/MM/dd HH:mm", dataGridViewCellStyle.Format)
ctl.Value = If(TypeOf Me.Value Is DateTime, CType(Me.Value, DateTime), DateTime.Now)
End If
End Sub
End Class
' IDataGridViewEditingControl を実装する編集用コントロール
Public Class DateTimePickerEditingControl
Inherits DateTimePicker
Implements IDataGridViewEditingControl
Private dataGridViewCtrl As DataGridView
Private valueIsChanged As Boolean
Public Sub New()
Me.Format = DateTimePickerFormat.Custom
Me.CustomFormat = "yyyy/MM/dd HH:mm"
Me.ShowUpDown = False
End Sub
Public Property EditingControlFormattedValue As Object Implements IDataGridViewEditingControl.EditingControlFormattedValue
Get
Return Me.Value.ToString(Me.CustomFormat)
End Get
Set(value As Object)
If TypeOf value Is String Then
Dim dt As DateTime
If DateTime.TryParse(CStr(value), dt) Then
Me.Value = dt
End If
End If
End Set
End Property
Public Function GetEditingControlFormattedValue(context As DataGridViewDataErrorContexts) As Object Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
Return EditingControlFormattedValue
End Function
Public Sub ApplyCellStyleToEditingControl(cellStyle As DataGridViewCellStyle) Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
Me.Font = cellStyle.Font
Me.CalendarForeColor = cellStyle.ForeColor
Me.CalendarMonthBackground = cellStyle.BackColor
End Sub
Public Property EditingControlRowIndex As Integer Implements IDataGridViewEditingControl.EditingControlRowIndex
Public Function EditingControlWantsInputKey(keyData As Keys, dataGridViewWantsInputKey As Boolean) As Boolean Implements IDataGridViewEditingControl.EditingControlWantsInputKey
' 日付ピッカーの操作に必要なキーは自コントロールで処理
Select Case (keyData And Keys.KeyCode)
Case Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Home, Keys.End, Keys.PageDown, Keys.PageUp
Return True
End Select
Return Not dataGridViewWantsInputKey
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 dataGridViewCtrl
End Get
Set(value As DataGridView)
dataGridViewCtrl = value
End Set
End Property
Public Property EditingControlValueChanged As Boolean Implements IDataGridViewEditingControl.EditingControlValueChanged
Get
Return valueIsChanged
End Get
Set(value As Boolean)
valueIsChanged = value
End Set
End Property
Public ReadOnly Property EditingPanelCursor As Cursor Implements IDataGridViewEditingControl.EditingPanelCursor
Get
Return MyBase.Cursor
End Get
End Property
Protected Overrides Sub OnValueChanged(eventargs As EventArgs)
MyBase.OnValueChanged(eventargs)
valueIsChanged = True
If dataGridViewCtrl IsNot Nothing Then
dataGridViewCtrl.NotifyCurrentCellDirty(True)
End If
End Sub
End Class
使い方は簡単で、通常の列と置き換えるだけです。
Dim dtCol As New DataGridViewDateTimePickerColumn() With {
.DataPropertyName = "開始日時",
.HeaderText = "開始日時"
}
DataGridView1.Columns.Add(dtCol)
この方式は “編集時だけコントロールを生成する” ため、数千行規模でもオーバーヘッドを抑えられます。
CellValidating で堅牢に:入力バリデーション
ユーザーが自由入力できる構成では、CellValidating で TryParse/TryParseExact を行い、失敗時はエラー表示して編集をキャンセルします。
Private Sub DataGridView1_CellValidating(sender As Object, e As DataGridViewCellValidatingEventArgs) _
Handles DataGridView1.CellValidating
Dim dgv = DirectCast(sender, DataGridView)
Dim col = dgv.Columns(e.ColumnIndex)
If col.ValueType Is GetType(DateTime) Then
If String.IsNullOrWhiteSpace(CStr(e.FormattedValue)) Then
' 空許可:必要に応じて DBNull / Nothing をセット
dgv.Rows(e.RowIndex).Cells(e.ColumnIndex).Value = DBNull.Value
Return
End If
Dim dt As DateTime
Dim ok As Boolean =
DateTime.TryParseExact(CStr(e.FormattedValue),
{"yyyy/M/d H:m", "yyyy/MM/dd HH:mm", "yyyy/MM/dd"},
Globalization.CultureInfo.CurrentCulture,
Globalization.DateTimeStyles.None,
dt)
If Not ok Then
dgv.Rows(e.RowIndex).ErrorText = "日時の形式が正しくありません。"
e.Cancel = True
Else
dgv.Rows(e.RowIndex).Cells(e.ColumnIndex).Value = dt
dgv.Rows(e.RowIndex).ErrorText = ""
End If
End If
End Sub
Null 値・既定値・範囲チェック
- Null 表示:
DefaultCellStyle.NullValueに空文字やハイフンを指定。 - 既定値:
DefaultNewRowValueをオーバーライド、または行追加時に値をセット。 - 範囲:
MinDate/MaxDateをDateTimePicker側で制限可能。
colTime.DefaultCellStyle.NullValue = "-"
' DateTimePicker の範囲
' (編集コントロールの InitializeEditingControl で設定しても良い)
SortCompare で“好きな順”に並べ替える(非バインド時)
非バインド(DataSource = Nothing)のときは、SortCompare イベントで自由度の高い比較ロジックを差し込めます。例として、Null は常に最後に回しつつ日時で比較:
Private Sub DataGridView1_SortCompare(sender As Object, e As DataGridViewSortCompareEventArgs) _
Handles DataGridView1.SortCompare
Dim isDateCol = (DataGridView1.Columns(e.Column.Index).ValueType Is GetType(DateTime))
If Not isDateCol Then Return
Dim a = If(TryCast(e.CellValue1, DateTime?), Nothing)
Dim b = If(TryCast(e.CellValue2, DateTime?), Nothing)
If Not a.HasValue AndAlso Not b.HasValue Then
e.SortResult = 0 : e.Handled = True : Return
ElseIf Not a.HasValue Then
e.SortResult = 1 : e.Handled = True : Return ' Null を後ろへ
ElseIf Not b.HasValue Then
e.SortResult = -1 : e.Handled = True : Return
End If
e.SortResult = DateTime.Compare(a.Value, b.Value)
e.Handled = True
End Sub
バインドしている場合は SortCompare は呼ばれません。DataTable なら DataView の Sort が、ソート対応コレクションならコレクション側の並べ替えが使われます。
表示書式とカルチャ(ロケール)
- 表示書式(
DefaultCellStyle.Format)はToString(format, CultureInfo)に準じます。 - 年・月・日のゼロ詰めや 24 時間表記を保証したい場合は
"yyyy/MM/dd HH:mm:ss"のような固定書式を明示。 - ユーザー環境のロケールで表示を変えたいなら、
Format = "g"など標準書式を使うか、CellFormattingでIFormatProviderを切り替え。
Private Sub DataGridView1_CellFormatting(sender As Object, e As DataGridViewCellFormattingEventArgs) _
Handles DataGridView1.CellFormatting
Dim col = DataGridView1.Columns(e.ColumnIndex)
If col.ValueType Is GetType(DateTime) AndAlso e.Value IsNot Nothing Then
Dim dt = CType(e.Value, DateTime)
e.Value = dt.ToString("yyyy/MM/dd HH:mm", Globalization.CultureInfo.InvariantCulture)
e.FormattingApplied = True
End If
End Sub
時刻の種類(DateTime.Kind)とタイムゾーン
DateTime の Kind(Local/Utc/Unspecified)は、比較そのものには影響しませんが、表示や他システムとの連携でズレの原因になります。保存は UTC、表示はローカル、といった方針を決め、必要に応じて DateTime.SpecifyKind や ToLocalTime()/ToUniversalTime() を利用します。
パフォーマンス最適化の実践ポイント
- 編集時のみコントロールを生成:上記カスタムセルは編集時だけ
DateTimePickerを作るため軽い。 - 仮想モード(
VirtualMode = True)+必要データだけ供給:行数が非常に多い場合に有効。 - カラムの
SortModeを明示(Automatic/Programmatic/NotSortable)。 - 大量更新は
DataGridView.SuspendLayout()でレイアウト負荷を軽減。
“文字列で保持するとソート順が壊れる” を再現して理解する
例えば「2025/01/05」と「2025/10/02」を文字列比較すると、“1” と “10” の辞書式で逆転しがちです。保持を DateTime にするだけで解決します。
' NG:文字列で保持
DataGridView1.Rows.Add("2025/10/02 14:30") ' 10月
DataGridView1.Rows.Add("2025/01/05 09:00") ' 1月
' OK:DateTime で保持
DataGridView1.Rows.Add(#2025-10-02 14:30#) ' 10月
DataGridView1.Rows.Add(#2025-01-05 09:00#) ' 1月
エラー処理(DataError)
無効な値が入ったときの例外を握りつぶさず、原因をユーザーに伝えると UX が向上します。
Private Sub DataGridView1_DataError(sender As Object, e As DataGridViewDataErrorEventArgs) _
Handles DataGridView1.DataError
e.Cancel = True
DataGridView1.Rows(e.RowIndex).ErrorText = "値を解析できませんでした。入力形式を確認してください。"
End Sub
実用サンプル:最小×実用のフォームコード
以下は「DataTable+日時列+ヘッダークリックで時系列ソート+DateTimePicker 編集」の一体サンプルです。
Public Class Form1
Private bs As BindingSource
Private dt As DataTable
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.Width = 900
Me.Height = 500
Dim dgv As New DataGridView() With {
.Dock = DockStyle.Fill,
.AllowUserToAddRows = True,
.AllowUserToDeleteRows = True,
.AutoGenerateColumns = False
}
Me.Controls.Add(dgv)
' DataTable 準備
dt = New DataTable()
dt.Columns.Add("ID", GetType(Integer))
dt.Columns.Add("開始日時", GetType(DateTime))
dt.Columns.Add("件名", GetType(String))
dt.Rows.Add(1, DateTime.Now.AddHours(-1), "朝会")
dt.Rows.Add(2, DateTime.Now.AddDays(1).Date.AddHours(14), "設計レビュー")
dt.Rows.Add(3, DBNull.Value, "日時未定")
bs = New BindingSource() With {.DataSource = dt}
' 列定義:ID
Dim colId As New DataGridViewTextBoxColumn() With {
.DataPropertyName = "ID",
.HeaderText = "ID",
.SortMode = DataGridViewColumnSortMode.Automatic
}
' 列定義:開始日時(DateTimePicker ホスト)
Dim colTime As New DataGridViewDateTimePickerColumn() With {
.DataPropertyName = "開始日時",
.HeaderText = "開始日時"
}
colTime.DefaultCellStyle.Format = "yyyy/MM/dd HH:mm"
colTime.DefaultCellStyle.NullValue = ""
' 列定義:件名
Dim colSubject As New DataGridViewTextBoxColumn() With {
.DataPropertyName = "件名",
.HeaderText = "件名",
.SortMode = DataGridViewColumnSortMode.Automatic
}
dgv.Columns.AddRange(colId, colTime, colSubject)
dgv.DataSource = bs
' バリデーション
AddHandler dgv.CellValidating, AddressOf DataGridView1_CellValidating
AddHandler dgv.DataError, AddressOf DataGridView1_DataError
End Sub
End Class
.NET 6/8 でも同じ考え方でOK
WinForms には現在も「専用の DateTime 列」はありません。列の ValueType を DateTime にし、表示は書式で整え、必要なら DateTimePicker をホストするという方針は、.NET Framework 4.8 でも .NET 6/8 でも同様に有効です。
チェックリスト:本番投入前に必ず確認
- 列の
ValueTypeがDateTimeになっている - セルに代入する値が本当に
DateTime(文字列を代入していない) - 表示書式(
DefaultCellStyle.Format)は要件に合致している - Null 値のポリシー(表示、ソート時の位置、保存形式)を決めている
- バインド構成に応じた並べ替え戦略(DataTable なら自動、List/BindingList なら代替策)を整備
- 編集 UI(DateTimePicker)の挙動、範囲、カルチャを確認
- 大量データ時のパフォーマンスと例外処理(
DataError)を確認
まとめ
DataGridView に DataGridViewDateTimeColumn はなくても、「列は TextBox で、保持型を DateTime にする」だけで正しい「日時ソート」は達成できます。さらに、DateTimePicker を編集時にホストすれば入力品質と体験も向上。バインド構成(DataTable/List/BindingList)ごとのソートの仕組みを理解し、バリデーションや Null 取り扱い、書式・カルチャ、パフォーマンスのポイントを押さえれば、実運用に耐える日付/時刻列を簡潔な実装で用意できます。今日から“文字列ソートの罠”とはお別れです。

コメント