2018-01-13

VB.NETとC#からのExcel出力

検索すればあちこちで見つかる VB.Net および C# からの Excel 出力ですが、意外と以下の条件を満たすものがないようですので、サンプルコードを掲載しておきます。

条件

  1. Excel 終了時にきちんと COM の開放が行われ、EXCEL.EXE が終了する。
  2. 高速化(2次元配列を使用)している。
  3. 書式設定が行われている。
  4. 遅延バインディング

参考

Excelファイルを C# と VB.NET で読み込む “正しい” 方法 - Qiita

はじめに “Excel C#” や “Excel VB.NET” でググった新人プログラマが、古い情報や間違った情報で茨の道を選ばずに済むようにと思って書きました。 この記事は、Windows で Visual Studio を使用したデスクトップアプリケーション開発を想定しています。 VB.NET でも作成可能ですが、サンプルコードでは C# 6.0 を使用しています。どちらでもいいなら C# を使いましょう。 C# または VB.NET でExcel…

サンプルコードは、Microsoft.Office.Interop.Excel を使用しています。
(リンク先では推奨されていないのですが、外部DLLを使わずにすみますので)

サンプルコード

仕様

  • DataTable の内容を新規 Book に出力し、そのまま(保存せずに)表示します。
  • String 型は文字列にしています。DateTime型は、”yyyy/mm/dd”の形式にしています。(Date 型と DateTime 型で書式を変えたい場合は文字列化した方が良いと思います。)
  • 罫線と列幅の自動調整まで行っています。
VB.NET
Option Strict Off
Imports Microsoft.VisualBasic
Imports System.Data
Public Sub ExportExcel(ByVal dt As DataTable)
Dim xlApp As Object = Nothing
Dim xlBooks As Object = Nothing
Dim xlBook As Object = Nothing
Dim xlSheet As Object = Nothing
Dim xlCells As Object = Nothing
Dim xlRange As Object = Nothing
Dim xlCellStart As Object = Nothing
Dim xlCellEnd As Object = Nothing
Try
xlApp = CreateObject("Excel.Application")
xlBooks = xlApp.Workbooks
xlBook = xlApp.Workbooks.Add
xlSheet = xlBook.WorkSheets(1)
xlCells = xlSheet.Cells
Dim dc As DataColumn
Dim columnData(dt.Rows.Count, 1) As Object
Dim row As Integer = 1
Dim col As Integer = 1
For col = 1 To dt.Columns.Count
row = 1
dc = dt.Columns(col - 1)
'ヘッダー行の出力
xlCells(row, col).value = dc.ColumnName
row = row + 1
' 列データを配列に格納
For i As Integer = 0 To dt.Rows.Count - 1
columnData(i, 0) = String.Format(dt.Rows(i)(col - 1))
Next
xlCellStart = xlCells(row, col)
xlCellEnd = xlCells(row + dt.Rows.Count - 1, col)
xlRange = xlSheet.Range(xlCellStart, xlCellEnd)
' Excel書式設定
Select Case Type.GetTypeCode(dc.DataType)
Case TypeCode.String
xlRange.NumberFormatLocal = "@"
Case TypeCode.DateTime
xlRange.NumberFormatLocal = "yyyy/mm/dd"
'Case TypeCode.Decimal
' xlRange.NumberFormatLocal = "#,###"
End Select
xlRange.value = columnData
Next
xlCells.EntireColumn.AutoFit()
xlRange = xlSheet.UsedRange
xlRange.Borders.LineStyle = 1 'xlContinuous
xlApp.Visible = True
Catch
xlApp.DisplayAlerts = False
xlApp.Quit()
Throw
Finally
If xlCellStart IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCellStart)
If xlCellEnd IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCellEnd)
If xlRange IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlRange)
If xlCells IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCells)
If xlSheet IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheet)
If xlBooks IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBooks)
If xlBook IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook)
If xlApp IsNot Nothing Then System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp)
GC.Collect()
End Try
End Sub
C# (4.0以上)
using System.Data;
public void ExportExcel(DataTable dt)
{
dynamic xlApp = null;
dynamic xlBooks = null;
dynamic xlBook = null;
dynamic xlSheet = null;
dynamic xlCells = null;
dynamic xlRange = null;
dynamic xlCellStart = null;
dynamic xlCellEnd = null;
try
{
xlApp = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
xlBooks = xlApp.Workbooks;
xlBook = xlBooks.Add;
xlSheet = xlBook.WorkSheets(1);
xlCells = xlSheet.Cells;
DataColumn dc;
object[,] columnData = new object[dt.Rows.Count, 1];
int row = 1;
int col = 1;
for (col = 1; (col <= dt.Columns.Count); col++)
{
row = 1;
dc = dt.Columns[(col - 1)];
// ヘッダー行の出力
xlCells[row, col].value2 = dc.ColumnName;
row++;
// 列データを配列に格納
for (int i = 0; (i <= (dt.Rows.Count - 1)); i++)
{
columnData[i, 0] = string.Format("{0}",dt.Rows[i][(col - 1)]);
}
xlCellStart = xlCells[row, col];
xlCellEnd = xlCells[(row + (dt.Rows.Count - 1)), col];
xlRange = xlSheet.Range(xlCellStart, xlCellEnd);
// Excel書式設定
switch (Type.GetTypeCode(dc.DataType))
{
case TypeCode.String:
xlRange.NumberFormatLocal = "@";
break;
case TypeCode.DateTime:
xlRange.NumberFormatLocal = "yyyy/mm/dd";
break;
//case TypeCode.Decimal:
// xlRange.NumberFormatLocal = "#,###";
// break;
}
xlRange.value2 = columnData;
}
xlCells.EntireColumn.AutoFit();
xlRange = xlSheet.UsedRange;
xlRange.Borders.LineStyle = 1; // xlContinuous
xlApp.Visible = true;
}
catch
{
xlApp.DisplayAlerts = false;
xlApp.Quit();
throw;
}
finally
{
if (xlCellStart != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCellStart);
if (xlCellEnd != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCellEnd);
if (xlRange != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlRange);
if (xlCells != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlCells);
if (xlSheet != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheet);
if (xlBook != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook);
if (xlBooks != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBooks);
if (xlApp != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);
GC.Collect();
}
}

C# での注意点

  • 遅延バインディングにするため dynamic 型を使用しているので 4.0 以上が必要です。
  • Microsoft.Csharp の参照設定をしてください。

ネット上のコードで気になったこと

Workbooks の変数格納

Workbooks オブジェクトを変数に格納せず xlSheet = xlBooks.Add.WorkSheets(1); としているものも見受けられました。
c# - How do I properly clean up Excel interop objects? - Stack Overflow
試したところ、VB.NET では変数に格納しなくても EXCEL.EXE は終了しましたが、C# では終了しませんでした。サンプルコードでは VB.NET でも変数に格納するようにしています。

get_Range メソッドとRange プロパティ

Range の 取得に get_Range メソッドを使っている例がありましたが、 get_Range メソッドは MSDN

セルまたはセルの範囲を表す Microsoft.Office.Interop.Excel.Range オブジェクトを取得します。 このメソッドの代わりに Range プロパティを使用してください。

との記載があるのと、c# - Worksheet get_Range throws exception - Stack Overflow
によると、.NET4.0 からエラーになるようです。サンプルコードでは、Range プロパティを使用するようにしました。

Range.ValueRange.Value2

Excel のセルへの書き込みにValue プロパティと Value2 プロパティを使っているものがあります。両者の違いは

Value プロパティとの相違点は、Value2 プロパティでは、通貨型 (Currency) および日付型 (Date) のデータ型を使用しない点だけです。

のようです。
なぜ、二つのコードが出回っているかと言うと、VB.NET ではインテリセンスで Value が出て、C# では Value2 が出るため(Value プロパティにはパラメータがあるが、C# はパラメータ付きプロパティをサポートしていないため)で実質的な違いはないようです。
Parameterized Properties in C# and the mystery of Value2 in Excel – .NET4Office

ReleaseComObjectFinalReleaseComObject

COM の開放に FinalReleaseComObject を使っているものもありましたが、
How to properly release Excel COM objects: C# code examples によると、 FinalReleaseComObject を使うのは冗長のようですので、サンプルコードではReleaseComObject を使っています。

2 comments:

  1. とても素晴らしいサンプルで、大変助かっています。

    DataTableのカラム型がTimeSpanだった場合、Excel書式設定のxlRange.value=columnDataの所で「System.ArgumentException: 'このメソッドの型署名は相互運用機能と互換がありません。'」となってしまうのですが、回避方法をご存知でしょうか。

    ReplyDelete
    Replies
    1. お返事が大変遅くなり申し訳ございません。こちらでは再現できずそのままになっておりました。
      再現できないのは環境かデータの違いによるものと思います。
      TimeSpan型の場合、
      配列格納時の columnData[i, 0] = string.Format("{0}",dt.Rows[i][(col - 1)]) で書式指定する。
      https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/custom-timespan-format-strings
      Excel側のセル書式を文字列にしておく(xlRange.NumberFormatLocal = "@")
      といった対応をとれば回避できないかと思うのですが。

      Delete