業務アプリを VB.NET で作っていると、いつかは同じ要望が来ます。「この画面の内容を Excel に出してほしい」。定番の答えは Microsoft.Office.Interop.Excel を使ったループです。開発機では動きます。ところが、Office の入っていないお客様の PC で止まり、出力のあとに EXCEL.EXE が残り、郵便番号の先頭のゼロが消えます。
この記事では、そのボタンを ReoGrid で作ります。ReoGrid は .xlsx ファイルを自分で書き出すので、Office の有無にかかわらずどの PC でも出力できます。コードは貼り付けて使える 1 つのモジュールで、単純なループが見落とす点も処理しています。掲載したコードはすべて Option Strict On でビルドし、実際の DataGridView で動かして、出力したファイルを Excel で開いて確かめました。
DataTable にバインドした DataGridView。社内用の ID 列は非表示で、得意先 の列はユーザーがドラッグして先頭に移しています。
0600001 がそのまま残ります。プロジェクトに合う ReoGrid を選ぶ
| プロジェクト | 使うもの | パッケージ |
|---|---|---|
| .NET 10 | ReoGrid V5 | unvell.ReoGrid.One |
| .NET Framework 4.8、または .NET 8(Windows) | ReoGrid V4 | unvell.ReoGrid4 |
VB.NET の WinForms アプリは今も .NET Framework が多く、その場合は V4 を使います。V4 のモジュールはこちら。どちらも UNVELL のパッケージフィードからインストールします(V5 の手順、V4 の手順)。30 日間のトライアルキーで、どちらも試せます。
DataGridView の出力で押さえるべき点
Rows と Columns を回すだけなら 10 行で書けそうに見えます。実際には、次の点に気を配る必要があります。
| 落とし穴 | 起きること | このモジュールの対応 |
|---|---|---|
| 一番下の「新規行」 | 最終行で NullReferenceException、またはファイルに空行が入る | IsNewRow の行を飛ばす |
| 編集中のセル | ツールバーのボタンはフォーカスを奪わないので、最後の入力が抜ける | 最初に grid.EndEdit() を呼ぶ |
| 非表示の列 | 社内用の ID がお客様に送るファイルに載る | Visible の列だけを使う |
| 並べ替えた列 | ファイルの列順が画面と違う | DisplayIndex の順に並べる |
.ToString() で書いた数値 | 緑の三角だらけになり、SUM が 0 を返す | セルの Value をそのまま渡す |
| 先頭がゼロのコード | Interop で代入すると、文字列 0600001 が数値 600001 になる | 文字列は文字列のまま |
DBNull | DBNull という文字が入る、または例外 | 空のセルにする |
| 書式のない日付 | セルに 46335 と表示される | 列の ValueType から日付の書式を付ける |
| .NET の書式文字列 | N0 は Excel の書式コードではなく、Excel が受け付けない | #,##0 に変換する |
出力モジュール — ReoGrid V5
.vbproj のプロジェクトにパッケージを追加します。
dotnet add package unvell.ReoGrid.One
アプリケーションの起動時に、ライセンスキーを 1 回だけ適用します。キーが無いと、保存のときに ReoGridLicenseException が発生します。
Imports unvell.ReoGrid.Core.License
ReoGridLicense.SetLicense(myLicenseKey)
続いて、次のモジュールを追加します。
Imports System.Windows.Forms
Imports unvell.ReoGrid.Core
Imports unvell.ReoGrid.Core.Style
Imports unvell.ReoGrid.IO.Excel
Public Module DataGridViewExport
Public Sub SaveAsXlsx(grid As DataGridView, path As String,
Optional selectedOnly As Boolean = False)
' 入力中のセルを確定する
grid.EndEdit()
' ユーザーに見えている列を、見えている順に
Dim columns = grid.Columns.Cast(Of DataGridViewColumn)().
Where(Function(col) col.Visible).
OrderBy(Function(col) col.DisplayIndex).
ToList()
' 画面の順の行。一番下の空の「新規行」は除く
Dim source = If(selectedOnly,
grid.SelectedRows.Cast(Of DataGridViewRow)(),
grid.Rows.Cast(Of DataGridViewRow)())
Dim rows = source.
Where(Function(row) row.Visible AndAlso Not row.IsNewRow).
OrderBy(Function(row) row.Index).
ToList()
Dim book As New Workbook()
Dim ws = book.AddWorksheet("Sheet1")
' 見出し行。列幅は画面の幅を使う
For c = 0 To columns.Count - 1
ws.SetText(0, c, columns(c).HeaderText)
ws.Columns.SetSize(c, columns(c).Width)
Next
ws.SetRowStyle(0, New StyleRecord With {.Bold = True, .BackgroundColor = &HFFDDEBF7UI})
' データ。セルの値をそのまま渡すので、数値は数値のまま
For r = 0 To rows.Count - 1
For c = 0 To columns.Count - 1
Dim value = rows(r).Cells(columns(c).Index).Value
ws.SetObjectValue(r + 1, c, If(value Is DBNull.Value, Nothing, value))
Next
Next
' 数値と日付の書式
If rows.Count > 0 Then
For c = 0 To columns.Count - 1
Dim code = ExcelFormatOf(columns(c))
If code IsNot Nothing Then
ws.SetNumberFormat(New RangePosition(1, c, rows.Count, 1), code)
End If
Next
End If
XlsxWriter.Write(book, path)
End Sub
' DataGridView の書式文字列は .NET のものなので、Excel の書式コードに変換する
Private Function ExcelFormatOf(col As DataGridViewColumn) As String
Select Case col.DefaultCellStyle.Format
Case "N0" : Return "#,##0"
Case "N2" : Return "#,##0.00"
Case "C0" : Return "¥#,##0"
Case "P1" : Return "0.0%"
Case "yyyy/MM/dd HH:mm" : Return "yyyy/mm/dd hh:mm"
End Select
If col.ValueType Is GetType(Date) Then Return "yyyy/mm/dd"
Return Nothing
End Function
End Module
並べ替えや絞り込みをしたグリッドも、そのまま出力できます。列見出しのクリックや DataView の RowFilter は Rows そのものを変えるので、ファイルは画面のとおりになります。row.Visible = False で隠した行も出力から外れます。
ボタンから呼び出す
Imports System.IO
Private Sub ExportButton_Click(sender As Object, e As EventArgs) Handles ExportButton.Click
Using dlg As New SaveFileDialog With {.Filter = "Excel ブック|*.xlsx", .FileName = "受注一覧.xlsx"}
If dlg.ShowDialog(Me) <> DialogResult.OK Then Return
Try
SaveAsXlsx(DataGridView1, dlg.FileName)
Catch ex As IOException
MessageBox.Show("ファイルが別のプログラムで開かれています。閉じてからやり直してください。")
End Try
End Using
End Sub
この Catch は見た目以上に大切です。出力の失敗で一番多いのは、先週のファイルを Excel で開いたままにしているケースです。保存すると IOException が発生し、既存のファイルは元のまま残ります。
書式文字列を変換する
DefaultCellStyle.Format に入っているのは .NET の書式文字列で、Excel には別の書式コードがあります。見た目は似ていても意味が違うので、モジュールの中で対応させています。
DefaultCellStyle.Format | Excel の書式コード | 1234.5 の表示 |
|---|---|---|
N0 | #,##0 | 1,235 |
N2 | #,##0.00 | 1,234.50 |
C0 | ¥#,##0 | ¥1,235 |
P1(0.1234 の場合) | 0.0% | 12.3% |
yyyy/MM/dd HH:mm(日付の場合) | yyyy/mm/dd hh:mm | 2026/11/09 14:05 |
アプリで使っている書式の数だけ行を足してください。知っておきたい点が 2 つあります。
- 画面の
C0は PC の地域設定に従いますが、Excel の書式コードは固定です。ファイルはどの PC でも同じ記号で表示されます。 - 日付の書式には列の型が必要です。バインドした列にはデータから
ValueTypeが入りますが、バインドしないグリッドではcol.ValueType = GetType(Date)を自分で指定してください。指定しないと日付のシリアル値が表示されます。数値も同じで、ValueTypeの無い列ではユーザーの入力が文字列のまま残ります。col.ValueType = GetType(Decimal)を指定すると数値になります。
その他の書式コードは Excel の表示形式コードを読み解く をご覧ください。
選択した行だけを出力する
SaveAsXlsx(DataGridView1, dlg.FileName, selectedOnly:=True)
SelectedRows は、最後に選択した行から順に並びます。行 0、1、3 の順に選択すると、列挙の順序は 3、1、0 です。モジュールの OrderBy(Function(row) row.Index) が画面の順に戻すので、ファイルは上から順に並びます。SelectedRows に入るのは行全体を選択した行だけなので、SelectionMode は FullRowSelect が向いています。
仕上げ
XlsxWriter.Write の直前に次の行を足すと、そのまま渡せるシートになります。
' スクロールしても見出しが残るようにして、フィルターボタンを付ける
ws.View.FrozenRows = 1
ws.CreateAutoFilter(New RangePosition(0, 0, rows.Count + 1, columns.Count))
' C 列の合計。データの下に 1 行あけて置く
Dim total = rows.Count + 2
ws.SetText(total, 0, "合計")
ws.SetFormula(total, 2, $"SUM(C2:C{rows.Count + 1})")
ws.SetNumberFormat(total, 2, "#,##0")
ws.SetRowStyle(total, New StyleRecord With {.Bold = True})
合計の上の空行は残してください。Excel はフィルター範囲のすぐ下の行を範囲の一部として扱うので、そこに合計を置くとデータと一緒に絞り込まれ、並べ替えられてしまいます。
同じブックは PDF にもそのまま出力できます。ファイルの先頭に Imports unvell.ReoGrid.IO.Pdf を加えてください。
PdfExporter.Export(book, "受注一覧.pdf")
大きなグリッドもそのまま出力できます。バインドした DataGridView の 10 万行 × 6 列を、デスクトップ PC で V5 は 2〜3 秒、V4 は 3〜4 秒で出力でき、Excel でもすべての行を確認できました。
.NET Framework 4.8 では V4 で同じことをする
V4 のパッケージは unvell.ReoGrid4 で、.NET Framework 4.8 と .NET 8(Windows)で動きます。ライセンスの呼び出しは同じで、Imports unvell.ReoGrid.License を使います。行と列の選び方は V5 とまったく同じで、書き出しの部分だけが変わります。
Imports System.Windows.Forms
Imports unvell.ReoGrid
Imports unvell.ReoGrid.DataFormat
Imports unvell.ReoGrid.Graphics
Imports unvell.ReoGrid.IO
Public Module DataGridViewExport
Public Sub SaveAsXlsx(grid As DataGridView, path As String,
Optional selectedOnly As Boolean = False)
grid.EndEdit()
Dim columns = grid.Columns.Cast(Of DataGridViewColumn)().
Where(Function(col) col.Visible).
OrderBy(Function(col) col.DisplayIndex).
ToList()
Dim source = If(selectedOnly,
grid.SelectedRows.Cast(Of DataGridViewRow)(),
grid.Rows.Cast(Of DataGridViewRow)())
Dim rows = source.
Where(Function(row) row.Visible AndAlso Not row.IsNewRow).
OrderBy(Function(row) row.Index).
ToList()
Dim book = ReoGridControl.CreateMemoryWorkbook()
Dim ws = book.Worksheets(0)
ws.Resize(rows.Count + 1, columns.Count)
For c = 0 To columns.Count - 1
ws(0, c) = columns(c).HeaderText
ws.SetColumnsWidth(c, 1, CUShort(columns(c).Width))
Next
ws.SetRangeStyles(0, 0, 1, columns.Count, New WorksheetRangeStyle With {
.Flag = PlainStyleFlag.FontStyleBold Or PlainStyleFlag.BackColor,
.Bold = True,
.BackColor = SolidColor.FromArgb(255, 221, 235, 247)
})
' 書式を先に設定する。文字列の列の先頭のゼロを保つため
If rows.Count > 0 Then
For c = 0 To columns.Count - 1
ApplyFormat(ws, New RangePosition(1, c, rows.Count, 1), columns(c))
Next
End If
For r = 0 To rows.Count - 1
For c = 0 To columns.Count - 1
Dim value = rows(r).Cells(columns(c).Index).Value
ws(r + 1, c) = If(value Is DBNull.Value, Nothing, value)
Next
Next
book.Save(path, FileFormat.Excel2007)
End Sub
Private Sub ApplyFormat(ws As Worksheet, range As RangePosition, col As DataGridViewColumn)
Select Case col.DefaultCellStyle.Format
Case "N0"
ws.SetRangeDataFormat(range, CellDataFormatFlag.Number,
New NumberDataFormatter.NumberFormatArgs With {.DecimalPlaces = 0, .UseSeparator = True})
Return
Case "N2"
ws.SetRangeDataFormat(range, CellDataFormatFlag.Number,
New NumberDataFormatter.NumberFormatArgs With {.DecimalPlaces = 2, .UseSeparator = True})
Return
Case "C0"
ws.SetRangeDataFormat(range, CellDataFormatFlag.Currency,
New CurrencyDataFormatter.CurrencyFormatArgs With {.PrefixSymbol = "¥", .DecimalPlaces = 0, .UseSeparator = True})
Return
End Select
If col.ValueType Is GetType(Date) Then
ws.SetRangeDataFormat(range, CellDataFormatFlag.DateTime,
New DateTimeDataFormatter.DateTimeFormatArgs With {.Format = "yyyy/MM/dd"})
ElseIf col.ValueType Is GetType(String) Then
ws.SetRangeDataFormat(range, CellDataFormatFlag.Text)
End If
End Sub
End Module
2 つのループの順序には意味があります。V4 は数値に見える文字列を数値として読むので、文字列の列にはデータを入れる前に書式を設定します。これで 0600001 が 0600001 のまま残ります。
見出しの固定は ws.FreezeToCell(1, 0) です。合計を入れるときは ws.Resize(rows.Count + 2, columns.Count) でシートを 1 行広げ、数式を文字列で代入します(ws(rows.Count + 1, 2) = "=SUM(C2:C5)")。
V4 での現時点の制限
Booleanの列は、TrueまたはFalseという文字列で保存します。- V4.6 では、同じシートの左側に小数桁の同じ数値列があると、通貨の列の通貨記号が失われます。小数桁を変えるか、通貨の列を先に置いてください。
yyyy/MM/ddのパターンは Excel 標準の短い日付形式で保存され、PC の地域設定に従います。日本語の環境では2026/11/2と表示されます。yyyy-MM-ddなどの他のパターンは指定どおりに保存します。
V5 では、いずれの場合も本物の TRUE / FALSE を書き出し、通貨記号も日付の書式コードも指定どおりに保存します。
Interop から移行する理由
Interop は本物の Excel を操作する仕組みなので、Excel にまつわる条件をすべて引き受けることになります。
| Office Interop | ReoGrid | |
|---|---|---|
| ユーザーの PC の Office | 必須 | Office なしで動作 |
0600001 のような文字列 | セルを先に文字列書式にしない限り、数値 600001 に変換される | 入力どおりに保存 |
| 出力のあと | COM オブジェクトの解放漏れで EXCEL.EXE が残ることがある | アプリの中で完結する |
| サービスやタスクスケジューラ上 | Microsoft のサポート対象外 | 画面なしで動作 |
アプリの Excel 機能がこのボタンだけなら、上のモジュールで Office への依存をなくせます。保存の前にユーザーがシートを編集したい場合は、ReoGridControl を DataGridView の代わりに置けます。数式・フィルター・印刷まで最初から備えています。
まとめ
- 列は
VisibleとDisplayIndexで選び、IsNewRowの行は飛ばす。最初にgrid.EndEdit()を呼ばないと最後の編集が抜ける .ToString()ではなくcell.Valueを渡すと、数値も日付もそのままの型で届くDefaultCellStyle.Formatは .NET の文字列なので、N0などは Excel の書式コードに変換する- 選択行の出力は
SelectedRowsをIndexで並べ直すと画面の順になる - Excel で開いたままのファイルに備えて
IOExceptionを捕まえる - .NET 10 なら V5、.NET Framework 4.8 なら V4。モジュールはほぼ同じ形で書ける
関連情報
- VB.NET で使う — プロジェクトの準備と、C# サンプルを VB に読み替えるポイント
- DataTable の内容をシートに表示する — グリッドを通さずにデータを出力するとき
- XLSX 入出力 — 保存・読み込みの対応範囲
- VB.NET で Excel ファイルを読み込み・書き出し・表示する
- Excel の表示形式コードを読み解く
- 「CSV を Excel で開いたら郵便番号の 0 が消えた」を自社アプリで防ぐ
- C# で表計算シートを PDF にする