かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

C#でExcel 2007形式のファイルを出力する

以前NPOIに関する記事でExcel(2003)形式のファイルを出力するというのを紹介しました。現段階ではサポートされている環境が多い(どれくらいいるんだろう…)という理由から古き良きExcel 2003のxls形式のファイルを取り扱うことが多いですが、このExcel 2003のサポートがもうすぐ切れます!

Office 製品のプロダクト サポート ライフサイクルから引用
 http://www.microsoft.com/ja-jp/office/previous/2003/business/lifecycle.aspx

Office 2003 Editions
 ・メインストリーム サポート :2009 年 4 月 14 日
 ・延長サポート終了日 :2014 年 4 月 8 日

あと2年無いんですね〜。ということで、もうすぐOffice2007以降での標準フォーマットであるxlsx形式のファイルを出力できてもいいはず!!新しいOffice 2007以降のExcel(以降Excel 2007と書きますね)でもMicrosoft Visual C# .NET を使用して Microsoft Excel を自動化する方法にあるようにオートメーション使ってファイルを吐くこともできますが、サーバーサイドでファイルを吐くことを考えると、この方法は嫌な感じしかしないというかサポートされてないのでいやんな感じです。




OpenXML

ということで、Office Open XMLというフォーマットのファイルは仕様ががっつりと標準化されて公開されているので、自前でゴリゴリ吐き出すというのもありますが、あまりにも現実的ではない!そこで、Open XML SDK 2.0 for Microsoft Officeというものがマイクロソフトから提供されています。こいつを使うと、とてもめんどくさい感じでExcelのファイルを吐いたりできます。どれくらいめんどくさいかって、指定したファイルの指定したセルの値を取得するだけでこんなコードになるとか・・・。コード自体は、Office 2010 Sample: Open XML SDK 2.0 Code Snippets for Visual Studio 2010でインストールされるコードスニペットのものを拝借してます。

//using System;
//using DocumentFormat.OpenXml.Packaging;
//using DocumentFormat.OpenXml.Spreadsheet;
//using System.Linq;
//using DocumentFormat.OpenXml;

// Given a workbook file, a sheet name, and row and column integer values, retrieve the value of the cell.
// Call the function like this to retrieve the value from cell B3:
// String value = XLGetCellValueRowCol("Sample.xlsx", "Sheet1", 2, 3);
public  string XLGetCellValueRowCol(string fileName, string sheetName, int colNumber, int row)
{
  // The challenge: Convert a cell number into a letter name.
  return XLGetCellValueRowCol(fileName, sheetName, GetColumnName(colNumber), row);
}

private  string GetColumnName(int column)
{
  // This algorithm was found here:
  // http://stackoverflow.com/questions/181596/how-to-convert-a-column-number-eg-127-into-an-excel-column-eg-aa

  // Given a column number, retrieve the corresponding
  // column string name:
  int value = 0;
  int remainder = 0;
  string result = string.Empty;
  value = column;

  while (value > 0)
  {
    remainder = (value - 1) % 26;
    result = (char)(65 + remainder) + result;
    value = (int)(Math.Floor((double)((value - remainder) / 26)));
  }
  return result;
}

// Given a workbook file, a sheet name, and row and column names, retrieve the value of the cell.
// Call the function like this:
// String value = XLGetCellValue("Sample.xlsx", "Sheet1", "B", 3);

public  string XLGetCellValueRowCol(string fileName, string sheetName, string colName, int row)
{
  string value = null;

  using (SpreadsheetDocument document = SpreadsheetDocument.Open(fileName, false))
  {

    WorkbookPart wbPart = document.WorkbookPart;

    // Find the sheet with the supplied name, and then use that Sheet object
    // to retrieve a reference to the appropriate worksheet.
    Sheet theSheet = wbPart.Workbook.Descendants<Sheet>().
      Where(s => s.Name == sheetName).FirstOrDefault();

    if (theSheet == null)
    {
      throw new ArgumentException("sheetName");
    }

    // Retrieve a reference to the worksheet part, and then use its Worksheet property to get 
    // a reference to the cell whose address matches the address you've supplied:
    WorksheetPart wsPart = (WorksheetPart)(wbPart.GetPartById(theSheet.Id));
    Cell theCell = wsPart.Worksheet.Descendants<Cell>().
      Where(c => c.CellReference == colName + row.ToString()).FirstOrDefault();

    // If the cell doesn't exist, return an empty string.
    if (theCell != null)
    {
      value = theCell.InnerText;

      // If the cell represents an integer number, you're done. 
      // For dates, this code returns the serialized value that 
      // represents the date. The code handles strings and booleans
      // individually. For shared strings, the code looks up the corresponding
      // value in the shared string table. For booleans, the code converts 
      // the value into the words TRUE or FALSE.
      if (theCell.DataType != null)
      {
        switch (theCell.DataType.Value)
        {
          case CellValues.SharedString:
            // For shared strings, look up the value in the shared strings table.
            var stringTable = wbPart.GetPartsOfType<SharedStringTablePart>().FirstOrDefault();
            // If the shared string table is missing, something's wrong.
            // Just return the index that you found in the cell.
            // Otherwise, look up the correct text in the table.
            if (stringTable != null)
            {
              value = stringTable.SharedStringTable.ElementAt(int.Parse(value)).InnerText;
            }
            break;

          case CellValues.Boolean:
            switch (value)
            {
              case "0":
                value = "FALSE";
                break;
              default:
                value = "TRUE";
                break;
            }
            break;
        }
      }
    }
  }
  return value;
}

ClosedXML

え〜そんなわけで、Open XMLは提供されているのはウェルカムなんですがAPIが低レベルで使うには苦痛が伴いそうです。そこで!!Open XMLをラップしてもっと直観的なAPIに仕立て上げてくれたものがClosedXMLです。Codeplexで公開されています。

ドキュメントにコード例が割とあるのと、直観的なAPIなので迷うことはないと思いますがNPOIの記事で書いたのと同じExcelファイルを吐きだすコードを書いてみようと思います。コンソールアプリケーションを作成し、NugetからClosedXMLをインストールします。

そしてコードを書いていきます。

namespace ClosedXMLSample
{
    using System;
    using System.Diagnostics;
    using System.Linq;
    using ClosedXML.Excel;

    class Program
    {
        static void Main(string[] args)
        {
            // Excelのブックを作成
            var book = new XLWorkbook();

            // シートを作成
            var sheet = book.Worksheets.Add("何とかレポート");

            // ヘッダーにあたる行を作成
            CreateHeaderRow(sheet);

            // とりあえず10行くらいデータ作成
            foreach (var i in Enumerable.Range(2, 10))
            {
                CreateRow(sheet, i);
            }

            // 表のテーマを適用
            // テーマの定数はたくさんあるのであててみて表示して自分のほしいものか
            // どうかを試行錯誤して見つけるといいかも
            sheet.Range("A1:C11").CreateTable()
                .Theme = XLTableTheme.TableStyleMedium6;

            // セルの幅をコンテンツの内容にあわせてリサイズ
            sheet.Columns(1, 3).AdjustToContents();

            // 保存
            book.SaveAs("output.xlsx");
        }

        // ヘッダー行を作成する
        private static void CreateHeaderRow(IXLWorksheet sheet)
        {
            // 行を取得(インデックスは1からはじまる)
            var row = sheet.Row(1);
            // ヘッダー列の値を設定(インデックスは1からはじまる)
            row.Cell(1).Value = "ID";
            row.Cell(2).Value = "名前";
            row.Cell(3).Value = "誕生日";
        }

        private static Random r = new Random();

        // index行目のデータを作る
        private static void CreateRow(IXLWorksheet sheet, int index)
        {
            // 対象の行を取得して
            var row = sheet.Row(index);

            // id列と名前と誕生日を適用に設定する
            row.Cell(1).Value = index;
            row.Cell(2).Value = "田中 太郎" + index;
            row.Cell(3).Value = DateTime.Now.AddYears(r.Next(10));
            
            // 日付列のフォーマットを設定
            var dateTimeCell = row.Cell(3);
            dateTimeCell.Style.DateFormat.Format = "yyyy年mm月dd日";
        }
    }
}

書いてみて思ったのは、こんな機能あるだろうな〜と思う名前でずばりその機能があることが多いです!さくさくっと書けました。実行して生成されるExcelファイルは以下のような感じです。

個人的にはNPOIよりもいい感じに使えます。2年後には、NPOIじゃなくてこっちを使ってもいいかもですね!あと、最終的に行き着く先がOpen XML SDKなのでNPOIみたいに微妙に読めないファイルがあるということが少ないかも?という淡い期待を抱いてたりします。