VB.NET WinFormsのDataGridViewで日付・時刻列を正しくソートする実装大全(DateTimePicker対応)

VB.NET/WinForms の DataGridView には専用の DataGridViewDateTimeColumn がありません。とはいえ、正しい設定と少しの実装で、ヘッダークリックによる「文字列ソート」ではなく、実用的な「日時ソート」を高精度に実現できます。この記事では .NET Framework 4.8 を前提に、最小構成から入力 UI の強化(DateTimePicker 埋め込み)、バインド時の注意点、パフォーマンスやバリデーションまで、現場でそのまま使えるレシピとして詳解します。

目次

DataGridView で日付/時刻列が難しい理由

DataGridView は列の型に応じてソート方法を切り替えます。セルの Value が String なら辞書式(文字列)で、DateTime なら時系列で比較されます。つまり「見た目の書式」ではなく「保持している値の型」がソートの成否を左右します。専用列が無い=実現できない、ではありません。

最小構成:TextBox 列+型指定だけで“日時ソート”にする

専用列が不要なケースは多く、次の 3 ステップで十分です。

  1. DataGridViewTextBoxColumn を追加
  2. ValueType = GetType(DateTime) を設定(最重要)
  3. 表示書式を 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 取り扱い、書式・カルチャ、パフォーマンスのポイントを押さえれば、実運用に耐える日付/時刻列を簡潔な実装で用意できます。今日から“文字列ソートの罠”とはお別れです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次