Open XML SDK 2.0 を使用して Excel 2010 ブックを生成する

**概要:**Open XML SDK 2.0 を使用して Microsoft Excel 2010 ブックを操作する方法を説明します。

適用対象: Excel 2010 | Office 2010 | Open XML | SharePoint Server 2010 | VBA

公開: 2011 年 4 月

提供元:  Steve Hansen、Grid Logic

目次

  • Open XML ファイル形式の概要

  • Excel ファイルの詳細

  • Open XML ファイルをプログラムで操作する

  • Open XML SDK 2.0 を使用してブックを操作する

  • まとめ

  • その他の技術情報

  • 著者について

サンプル コードのダウンロード (英語)

Open XML ファイル形式の概要

Open XML は、コア ドキュメント指向の Office アプリケーション用オープン ファイル形式です。この Open XML は、バイナリ形式でエンコードされた Microsoft Office アプリケーションで定義済みの既存のワープロ ドキュメント、プレゼンテーション、およびスプレッドシートの代わりに使用できるように設計されています。Open XML ファイル形式には複数の利点があります。たとえば、Open XML ファイル形式を使用すると、そのファイル形式を認識するどのプログラムからでも、ドキュメントのデータにアクセスできます。これは、組織が現在作成しているドキュメントを、今後も使用できるようにするのに役立ちます。また、このファイル形式により、サーバー環境や Office クライアント アプリケーションをインストールできない環境でも、ドキュメントを容易に作成したり操作したりできます。

その名前のとおり、Open XML ファイルは XML を使用して表されます。ただし、1 つの大きな XML ファイルでドキュメントが表されるのではありません。Open XML ドキュメントは関連ファイル (パーツと呼ばれます) のコレクションを使用して表されます。これらのファイルは、パッケージに保存され ZIP アーカイブに圧縮されています。Open XML ドキュメント パッケージは、Open Packaging Conventions (OPC) 仕様でコンパイルされます。OPC はコンテナー ファイル テクノロジで、XML ファイルと XML 以外のファイルを組み合わせて保存し、1 つのエンティティを形成します。

Excel ファイルの詳細

各アイテムがどのように連携しているかは、ブック ファイルを開いて、それぞれのアイテムを見るとよくわかります。Microsoft Excel 2010 ブック パッケージのパーツを確認するために、ファイル名拡張子を .xlsx から .zip に変更します。図 1 と図 2 のブックの例を参照してください。

図 1. 単純なブック

単純なブック

このブックには 2 つのワークシートが含まれます。図 1 のワークシートには、毎年の売り上げが示されています。図 2 は、簡単なグラフを示しています。

図 2. ブック内の基本のグラフ

ブック内の基本グラフ

このブックの名前を Simple Sales Example.xlsx から Simple Sales Example.zip に変更すると、エクスプローラーを使用して、ファイル コンテナーまたはパッケージ内のパーツの構造を確認できます。

図 3. 単純なブックのパーツ構造

単純なブックのパーツ構造

図 3 は、パッケージ内のプライマリ フォルダー、およびワークシート フォルダーに保存されているパーツを示しています。少し深く掘り下げて、図 4 には、sheet1.xml という名前のパーツの XML を示します。

図 4. ワークシート パーツ内の XML の例

ワークシート パーツ内の XML の例

図 4 の XML は、図 1 のワークシートを表すために Excel が必要とする情報を提供します。たとえば、sheetData ノード内には行ノードがあります。空以外のセルが少なくとも 1 つ存在するすべての行に対して行ノードが存在するので、各行には、空以外の各セルに対してノードが存在することになります。

図 1 のセル C3 には値 2008 が含まれており、太字が設定されています。一方、セル C4 には値 182 が含まれます。この値には既定の書式が設定されており、太字にはなっていません。図 4 は各セルの XML 表現を示しています。セル C3 の XML の例を次に示します。

      <c r="C3" s="1">
        <v>2008</v>
      </c>

Open XML ファイルのサイズをできるだけ小さくするために、XML ノードと属性の名前は、多くの場合、非常に短くなっています。上記のフラグメントでは、c はセルを表します。この特定のセルは、r (参照) と s (スタイル インデックス) の 2 つの属性を指定します。参照属性は、セルの場所参照を指定します。

スタイル インデックスは、セルの書式設定に使用されるスタイルへの参照です。スタイルは、xl フォルダー (図 3 の xl フォルダーを参照) にあるスタイル パーツ (styles.xml) で定義されます。セル C3 の XML を、次の例のセル C4 の XML と比較してください。

      <c r="C4">
        <v>182</v>
      </c>

セル C4 では既定の書式設定が使用されるので、スタイル インデックス属性の値を指定する必要はありません。Open XML ドキュメントでスタイル インデックスを使用する方法については、この記事で後ほど少し詳しく説明します。

Open XML ファイル形式の微妙な違いを理解しておくと非常に便利ですが、この記事の本当の目的は、Open XML SDK 2.0 for Microsoft Office を使用して、Open XML ドキュメント、特に Excel ブックをプログラムで操作する方法を説明することです。

Open XML ファイルをプログラムで操作する

Open XML ドキュメントをプログラムで作成または操作する 1 つの方法として、次の概要レベルのパターンを使用します。

  1. Open XML パッケージを開くか作成します

  2. パッケージ パーツを開くか作成します

  3. 操作する必要があるパーツで XML を解析します

  4. 必要に応じて XML を操作します

  5. パーツを保存します

  6. ドキュメントを再パッケージします

System.IO.Packaging 名前空間のクラスを使用すると、手順 3. と 4. を除き、すべてをかなり簡単に実行できます。これらのクラスは、概要レベルのパーツ操作に関連付けられている Open XML パッケージとタスクの作業を簡単に処理できるように設計されています。

このプロセスで最も困難なのは手順 4. の XML の操作です。Open XML ファイル形式に数多く存在する微妙な違いに適切に対応するには、退屈で膨大な詳細情報を把握しておかなければなりません。手順 4. の操作を実行する開発者は、この情報について深く理解しておかなければならないのです。たとえば、セルの書式設定情報は前述のとおりそのセルには保存されません。書式設定の詳細は、別のドキュメント パーツでスタイルとして定義され、Excel のセルには、そのスタイルに関連付けられているスタイル インデックスが保存されます。

Open XML 仕様についてかなりの知識を持っている開発者でも、膨大な数の未加工 XML をプログラムで操作しようとは思わないことがほとんどです。そこで、Open XML SDK 2.0 を使用します。

Open XML SDK 2.0 は、Open XML パッケージおよびパッケージ内の基になる Open XML スキーマ要素の操作を容易にするために開発されました。Open XML SDK 2.0 では、開発者が Open XML パッケージで実行する多数の一般的なタスクがカプセル化されるので、未加工の XML を操作する必要はありません。代わりに, .NET クラスを使用して、IntelliSense サポート、タイプ セーフな開発など、デザイン時に多くのメリットを得ることができます。

注意

Open XML SDK 2.0 は Microsoft ダウンロード センターからダウンロードしてください。

Open XML SDK 2.0 を使用してブックを操作する

この記事では、レポート生成プログラムを構築しながら、Open XML SDK 2.0 を使用して Excel ブックを操作するプロセスについて説明します。ここでは、あなたは Contoso 証券会社に勤務しています。Contoso の ASP.NET Web サイトを使用すると、クライアントがログオンし、さまざまなポートフォリオ レポートをオンラインで表示できますが、多くのユーザーは、Excel のレポートを表示またはダウンロードし、状況に応じてポートフォリオの追加分析を行いたいと考えています。

注意

このコードを独自に簡素化するために、次の例では、コンソール ベースのアプリケーションを構築します。ただし、この例で使用されるテクニックは ASP.NET Web サイトと完全に互換性があります。この例では、Microsoft Excel の要件はまったくありません。

クライアントの場合、結果として望ましいのは、Excel ポートフォリオ レポートを生成するプロセスです。通常、この種類のプロセスに対するアプローチは 2 つあります。1 つ目は、すべてのドキュメントを最初から生成するアプローチです。書式がほとんど、あるいはまったく設定されていない簡単なブックについては、このアプローチが適しています。2 つ目のアプローチは、テンプレートを使用してドキュメントを作成する方法で、通常はこちらのアプローチをお勧めします。Word テンプレートを使用すると言いましたが、実際には Excel テンプレート (*.xltx) のことではありません。ここでは、最終的なブックに必要なすべての書式設定、グラフなどが含まれるブック (*.xlsx) のことを指しています。この場合、参照されるのは、最終的なブックに必要なすべての書式設定、グラフなどが含まれるブック (*.xlsx) の使用です。テンプレートを使用するには、プロセスの最初の手順で、テンプレート ファイルをコピーします。次に、レポートの作成対象のクライアントに関連付けられているデータを追加します。

図 5. ポートフォリオ レポートのサンプル

ポートフォリオ レポートのサンプル

プロジェクトの設定

ポートフォリオ レポート生成プログラムを作成するには、Microsoft Visual Studio 2010 を起動し、PortfolioReportGenerator という名前の新しいコンソール アプリケーションを作成します。

注意

サンプル C# および Visual Basic .NET プロジェクトをダウンロードするには、こちら (英語)をクリックして、コード例をダウンロードしてください。

図 6. ポートフォリオ レポート生成プログラム ソリューションを作成する

ポートフォリオ レポート生成プログラム ソリューションを作成する

次に、2 つのクラス、PortfolioReport と Portfolio をプロジェクトに追加します。PortfolioReport クラスは、Open XML SDK 2.0 を使用してすべてのドキュメント操作を行う主要クラスです。Portfolio クラスは、基本的には、クライアント ポートフォリオを表す必要なプロパティが含まれるデータ構造です。

注意

Portfolio クラスについては、この変更で詳述されています。これは、データ コンテナーとテスト データで、Open XML または Open XML SDK 2.0 に関連するコードは含まれません。

コードを記述する前に、最初の手順として、Open XML と Open XML SDK 2.0 が含まれる任意のプロジェクトに必要な参照を追加します。ここでは、DocumentFormat.OpenXml と WindowsBase の 2 つの特定の参照が必要です。

DocumentFormat.OpenXml には、Open XML SDK 2.0 と一緒にインストールされるクラスが含まれます。Open XML SDK 2.0 のインストール後、この参照が見つからない場合は探すことができます。既定では、この参照は C:\Program Files (x86)\Open XML SDK\V2.0\lib\ にあり、Open XML SDK 2.0 を使用する場合にのみ必要です。未加工の XML を調整して Open XML ドキュメントを操作する場合、この参照は必要ありません。

WindowsBase では、System.IO.Packaging 名前空間にクラスが含まれます。この参照は、Open XML SDK 2.0 を使用しているかどうかに関係なく、すべての Open XML プロジェクトに必要です。System.IO.Packaging 名前空間のクラスは、Open XML パッケージを開く機能を提供します。さらに、Open XML パッケージ内のパーツを操作 (追加、削除、編集) できるようにするクラスもあります。

この時点でのプロジェクトは図 7 のようになります。

図 7. プロジェクトの初期セットアップ後のポートフォリオ レポート生成プログラム プロジェクト

セットアップ後のポートフォリオ レポート生成プログラム プロジェクト

ポートフォリオ レポートを初期化する

前に説明したように、レポート生成プロセスを機能させるには、レポート テンプレートのコピーを作成し、データをそのレポートに追加します。レポート テンプレートは事前に書式設定された Excel ブックで、PortfolioReport.xlsx という名前が付いています。コンストラクターを、このプロセスを実行する PortfolioReport クラスに追加してください。ファイルをコピーするには、System.IO 名前空間をインポートする必要もあります。また、System.IO 名前空間を追加する一方で、Open XML SDK 2.0 に関連する名前空間も追加します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using DocumentFormat.OpenXml;

namespace PortfolioReportGenerator
{
    class PortfolioReport
    {
        string path = "c:\\example\\";
        string templateName = "PortfolioReport.xlsx";

        public PortfolioReport(string client)
        {
            string newFileName = path + client + ".xlsx";
            CopyFile(path + templateName, newFileName);
        }

        private string CopyFile(string source, string dest)
        {
            string result = "Copied file";
            try
            {
                // Overwrites existing files
                File.Copy(source, dest, true);
            }
            catch (Exception ex)
            {
                result = ex.Message;
            }
            return result;
        }
    }
}

PortfolioReport コンストラクターに、レポートを生成する対象のクライアントを表す 1 つのパラメーターが必要であることに注意してください。

パラメーターをメソッドに渡したり、ドキュメントを何度も開いてブック パーツを抽出したりせずにすむように、クラスを対象範囲とする 2 つのプライベート変数を PortfolioReport クラスに追加します。同様に、クラスを対象範囲とする 1 つのプライベート変数を追加して、レポートの生成に使用するデータが含まれる現在の Portfolio オブジェクトへの参照を維持するようにします。これらの変数を適切に使用すると、次の例に示すように、その変数を PortfolioReport コンストラクター内で初期化できます。

        string path = "c:\\example\\";
        string templateName = "PortfolioReport.xlsx";

        WorkbookPart wbPart = null;
        SpreadsheetDocument document = null;
        Portfolio portfolio = null;

        public PortfolioReport(string client)
        {
            string newFileName = path + client + ".xlsx";
            CopyFile(path + templateName, newFileName);
            document = SpreadsheetDocument.Open(newFileName, true);
            wbPart = document.WorkbookPart;
            portfolio = new Portfolio(client);
        }

このコード セグメントは、Open XML SDK 2.0 を使用して簡単にドキュメントを開き、パーツを抽出できることを示しています。PortfolioReport コンストラクターでは、SpreadsheetDocument クラスの Open メソッドを使用してブック ファイルが開きます。SpreadsheetDocument は、DocumentFormat.OpenXml.Packaging 名前空間の一部です。SpreadsheetDocument は、WorkbookPart という名前のプロパティによって、ドキュメント パッケージ内のブック パーツに容易にアクセスできるようにします。プロセスのこの時点で、レポート生成プログラムでは次の操作が完了しています。

  1. PortfolioReport.xlsx ファイルのコピーを作成する

  2. クライアント名にちなんだ名前をそのコピーに付ける

  3. クライアント レポートを編集用に開く

  4. ブック パーツを抽出する

Open XML SDK を使用してワークシートのセルの値を変更する

レポート生成プログラムを完成させるために解決する必要がある主な課題は、Open XML SDK 2.0 を使用して Excel ブック内の値を変更する方法を見つけだすことです。Microsoft Visual Basic for Applications (VBA) または .NET で Excel のオブジェクト モデルを使用すると、セルの値は簡単に変更できます。セルの値 (Excel のオブジェクト モデルの Range オブジェクト) を変更するには、Value プロパティの値を変更します。たとえば、Sales という名前のワークシートのセル B4 の値を 250 に変更するには、次のステートメントを使用します。

    ThisWorkbook.Worksheets("Sales").Range("B4").Value = 250

Open XML SDK 2.0 の動作は少し異なっています。大きな違いは、Excel オブジェクト モデルを使用すると、ワークシートのセルにデータが含まれているかどうかにかかわらず、すべてのセルを操作できるという点です。つまり、オブジェクト モデルについて言えば、ワークシート内にすべてのセルが存在しています。Open XML を操作するときは、オブジェクトが存在しません。これは既定で設定されています。セルに値が含まれない場合、そのセルは存在しません。ファイル形式を指定するという観点から考えると、これは完璧に理にかなっています。ファイルのサイズをできるだけ小さく抑えるために、保存されるのは関連情報のみです。たとえば、図 4 に戻って、sheetData の下にある最初の行ノードを確認してみてください。先頭行は行 3 で、行 1 と 2 はとばされています。これは、最初の 2 行ではすべてのセルが空のためです。同様に、最初の行ノード (行 3) では、最初のセルのアドレスが C3 であることにも注目してください。これは、A3 と B3 が空のためです。

Open XML ドキュメントでは、セルが存在するものと考えることはできません。したがって、最初にセルが存在するかどうかを確認し、存在しない場合は、セルをファイルに追加する必要があります。次の例は、この機能を実行する InsertCellInWorksheet メソッドと、リスト内の他のメソッドを示しています。これらのメソッドを PortfolioReport クラスに追加します。

注意

Microsoft では、数多くの一般的な Open XML SDK 2.0 タスクに対してコード例を用意しています。さらに良いのは、これらの例は、Visual Studio 2010 内で使用できるコード例として入手できるという点です。この記事のコードの一部は、これらのコード例に基づいています。サンプル コードは、こちら (英語)からダウンロードできます。

        // Given a Worksheet and an address (like "AZ254"), either return a 
        // cell reference, or create the cell reference and return it.
        private Cell InsertCellInWorksheet(Worksheet ws, string addressName)
        {
            SheetData sheetData = ws.GetFirstChild<SheetData>();
            Cell cell = null;

            UInt32 rowNumber = GetRowIndex(addressName);
            Row row = GetRow(sheetData, rowNumber);

            // If the cell you need already exists, return it.
            // If there is not a cell with the specified column name, insert one.  
            Cell refCell = row.Elements<Cell>().
                Where(c => c.CellReference.Value == addressName).FirstOrDefault();
            if (refCell != null)
            {
                cell = refCell;
            }
            else
            {
                cell = CreateCell(row, addressName);
            }
            return cell;
        }
        
        // Add a cell with the specified address to a row.
        private Cell CreateCell(Row row, String address)
        {
            Cell cellResult;
            Cell refCell = null;

            // Cells must be in sequential order according to CellReference. 
            // Determine where to insert the new cell.
            foreach (Cell cell in row.Elements<Cell>())
            {
                if (string.Compare(cell.CellReference.Value, address, true) > 0)
                {
                    refCell = cell;
                    break;
                }
            }

            cellResult = new Cell();
            cellResult.CellReference = address;

            row.InsertBefore(cellResult, refCell);
            return cellResult;
        }

        // Return the row at the specified rowIndex located within
        // the sheet data passed in via wsData. If the row does not
        // exist, create it.
        private Row GetRow(SheetData wsData, UInt32 rowIndex)
        {
            var row = wsData.Elements<Row>().
            Where(r => r.RowIndex.Value == rowIndex).FirstOrDefault();
            if (row == null)
            {
                row = new Row();
                row.RowIndex = rowIndex;
                wsData.Append(row);
            }
            return row;
        }

        // Given an Excel address such as E5 or AB128, GetRowIndex
        // parses the address and returns the row index.
        private UInt32 GetRowIndex(string address)
        {
            string rowPart;
            UInt32 l;
            UInt32 result = 0;

            for (int i = 0; i < address.Length; i++)
            {
                if (UInt32.TryParse(address.Substring(i, 1), out l))
                {
                    rowPart = address.Substring(i, address.Length - i);
                    if (UInt32.TryParse(rowPart, out l))
                    {
                        result = l;
                        break;
                    }
                }
            }
            return result;
        }

また、Excel のオブジェクト モデルを使用することが、Open XML ドキュメントの操作と異なる点はまだあります。Excel オブジェクト モデルを使用する場合、セルに入力する値のデータの種類または範囲はそれほど重要ではないのです。Open XML を使用してセルの値を変更する場合、そのプロセスは、値のデータの種類によって異なります。数値の場合、プロセスは Excel のオブジェクト モデルとある程度似ています。Open XML SDK 2.0 には、Cell オブジェクトに関連付けられている CellValue という名前のプロパティがあり、このプロパティを使用すると、数値をセルに割り当てることができます。

文字列 (テキスト) をセルに保存する動作は違います。テキストは、Excel のセルに直接保存されるのではなく、共有文字列テーブルと呼ばれるテーブルに保存されます。共有文字列テーブルは、ブック内の一意の文字列の一覧にすぎず、この一覧では一意の文字列それぞれがインデックスに関連付けられています。セルを文字列に関連付けるために、セルには文字列インデックスへの参照が保持されています。文字列自体は含まれていません。セルの値を文字列に変更する場合は、まず、文字列が共有文字列テーブルに含まれているかどうかを確認する必要があります。文字列がテーブルに含まれる場合は、共有文字列インデックスを検索し、そのインデックスをセルに保存します。文字列が共有文字列テーブルに含まれない場合は、その文字列を追加し、文字列インデックスを取得して、それをセルに保存する必要があります。次の例は、セルの値を変更するときに使用する UpdateValue メソッドと、共有文字列テーブルを更新するときに使用する InsertSharedStringItem を示しています。

        public bool UpdateValue(string sheetName, string addressName, string value, 
                                UInt32Value styleIndex, bool isString)
        {
            // Assume failure.
            bool updated = false;

            Sheet sheet = wbPart.Workbook.Descendants<Sheet>().Where(
                (s) => s.Name == sheetName).FirstOrDefault();

            if (sheet != null)
            {
                Worksheet ws = ((WorksheetPart)(wbPart.GetPartById(sheet.Id))).Worksheet;
                Cell cell = InsertCellInWorksheet(ws, addressName);

                if (isString)
                {
                    // Either retrieve the index of an existing string,
                    // or insert the string into the shared string table
                    // and get the index of the new item.
                    int stringIndex = InsertSharedStringItem(wbPart, value);

                    cell.CellValue = new CellValue(stringIndex.ToString());
                    cell.DataType = new EnumValue<CellValues>(CellValues.SharedString);
                }
                else
                {
                    cell.CellValue = new CellValue(value);
                    cell.DataType = new EnumValue<CellValues>(CellValues.Number);
                }

                if (styleIndex > 0)
                    cell.StyleIndex = styleIndex;
                
                // Save the worksheet.
                ws.Save();
                updated = true;
            }

            return updated;
        }

        // Given the main workbook part, and a text value, insert the text into 
        // the shared string table. Create the table if necessary. If the value 
        // already exists, return its index. If it doesn't exist, insert it and 
        // return its new index.
        private int InsertSharedStringItem(WorkbookPart wbPart, string value)
        {
            int index = 0;
            bool found = false;
            var stringTablePart = 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 (stringTablePart == null)
            {
                // Create it.
                stringTablePart = wbPart.AddNewPart<SharedStringTablePart>();
            }

            var stringTable = stringTablePart.SharedStringTable;
            if (stringTable == null)
            {
                stringTable = new SharedStringTable();
            }

            // Iterate through all the items in the SharedStringTable. 
            // If the text already exists, return its index.
            foreach (SharedStringItem item in stringTable.Elements<SharedStringItem>())
            {
                if (item.InnerText == value)
                {
                    found = true;
                    break;
                }
                index += 1;
            }

            if (!found)
            {
                stringTable.AppendChild(new SharedStringItem(new Text(value)));
                stringTable.Save();
            }

            return index;
        }

前のコード例で重要なのは、セルの書式設定を扱っている部分です。この記事で前述したように、セル ノード内にはセルの書式が保存されません。代わりに、セルにはスタイル インデックスが保存され、このインデックスが、他のパーツ (style.xml) で定義されたスタイルを指定します。このドキュメントで示したテンプレート パターン、および VBA または .NET で Excel のオブジェクト モデルを使用するときに、通常、必要な書式を 1 つ以上のセルの範囲に適用します。データをブックにプログラムで追加すると、範囲に適用したすべての書式が忠実に適用されます。

Open XML ファイルには、データが含まれるセルに関連する情報しか含まれません。このため、新しいセルをファイルに追加した場合、そのセルに書式を設定する必要があるときは、スタイル インデックスを更新する必要があります。したがって、UpdateValue メソッドは、セルに適用するスタイル インデックスを示す styleIndex パラメーターを受け付けます。ゼロ値を渡すと、スタイル インデックスは設定されず、セルには Excel の既定の書式設定が適用されます。

各セルの適切なスタイル インデックスを決定する方法として簡単なのは、必要に応じてブック テンプレート ファイルの書式を設定し、適切なブック パーツを XML モードで開き (図 4 を参照)、書式設定したセルのスタイル インデックスを確認するというものです。

前のコード リストのメソッドを適切に使用して、レポートの生成プロセスは、ポートフォリオ データを取得し、UpdateValue を繰り返し呼び出して、レポートを作成しています。実際、必要なコードを追加してこれを行うと、適切に動作しているように見えます。ただし、1 つだけ問題があります。任意のセルに含まれる数式が、Open XML 操作によって変更された値が含まれるセルを参照していると、その数式が含まれるセルに正しい結果が表示されないのです。これは、Excel では、セル内の数式の結果がキャッシュされるからです。Excel では、セルに含まれるキャッシュされた値が正しいと見なされるので、そのセルは再計算されません。自動計算がオンになっていても、また、F9 キーを押して手動で再計算を実行しても、そのセルの再計算は行われません。

これを解決するには、キャッシュされた値をセルから削除して、Excel でファイルが開いたらすぐに値が Excel で再計算されるようにします。この機能を提供するには、次の例に示す RemoveCellValue メソッドを PortfolioReport クラスに追加します。

        // This method is used to force a recalculation of cells containing formulas. The
        // CellValue has a cached value of the evaluated formula. This
        // prevents Excel from recalculating the cell even if 
        // calculation is set to automatic.
        private bool RemoveCellValue(string sheetName, string addressName)
        {
            bool returnValue = false;

            Sheet sheet = wbPart.Workbook.Descendants<Sheet>().
                Where(s => s.Name == sheetName).FirstOrDefault();
            if (sheet != null)
            {
                Worksheet ws = ((WorksheetPart)(wbPart.GetPartById(sheet.Id))).Worksheet;
                Cell cell = InsertCellInWorksheet(ws, addressName);

                // If there is a cell value, remove it to force a recalculation
                // on this cell.
                if (cell.CellValue != null)
                {
                    cell.CellValue.Remove();
                }
                
                // Save the worksheet.
                ws.Save();
                returnValue = true;
            }

            return returnValue;
        }

PortfolioReport クラスを完成するには、次の例に示す CreateReport メソッドを PortfolioReport クラスに追加します。このクラスは、CreateReport メソッドの UpdateValue を使用して、ポートフォリオ情報を必要なセルに入力します。そして、必要なセルすべてを更新したら、再計算が必要な各セルで RemoveCellValue を呼び出します。最後に、CreateReport は、SpreadsheetDocument で Close メソッドを呼び出して、すべての変更を保存し、ファイルを閉じます。

        // Create a new Portfolio report
        public void CreateReport()
        {
            string wsName = "Portfolio Summary";

            UpdateValue(wsName, "J2", "Prepared for " + portfolio.Name, 0, true);
            UpdateValue(wsName, "J3", "Account # " + 
                        portfolio.AccountNumber.ToString(), 0, true);
            UpdateValue(wsName, "D9", portfolio.BeginningValueQTR.ToString(), 0, false);
            UpdateValue(wsName, "E9", portfolio.BeginningValueYTD.ToString(), 0, false);
            UpdateValue(wsName, "D11", portfolio.ContributionsQTR.ToString(), 0, false);
            UpdateValue(wsName, "E11", portfolio.ContributionsYTD.ToString(), 0, false);
            UpdateValue(wsName, "D12", portfolio.WithdrawalsQTR.ToString(), 0, false);
            UpdateValue(wsName, "E12", portfolio.WithdrawalsYTD.ToString(), 0, false);
            UpdateValue(wsName, "D13", portfolio.DistributionsQTR.ToString(), 0, false);
            UpdateValue(wsName, "E13", portfolio.DistributionsYTD.ToString(), 0, false);
            UpdateValue(wsName, "D14", portfolio.FeesQTR.ToString(), 0, false);
            UpdateValue(wsName, "E14", portfolio.FeesYTD.ToString(), 0, false);
            UpdateValue(wsName, "D15", portfolio.GainLossQTR.ToString(), 0, false);
            UpdateValue(wsName, "E15", portfolio.GainLossYTD.ToString(), 0, false);

            int row = 7;
            wsName = "Portfolio Holdings";

            UpdateValue(wsName, "J2", "Prepared for " + portfolio.Name, 0, true);
            UpdateValue(wsName, "J3", "Account # " + 
                        portfolio.AccountNumber.ToString(), 0, true);
            foreach (PortfolioItem item in portfolio.Holdings)
            {
                UpdateValue(wsName, "B" + row.ToString(), item.Description, 3, true);
                UpdateValue(wsName, "D" + row.ToString(), 
                            item.CurrentPrice.ToString(), 24, false);
                UpdateValue(wsName, "E" + row.ToString(), 
                            item.SharesHeld.ToString(), 27, false);
                UpdateValue(wsName, "F" + row.ToString(), 
                            item.MarketValue.ToString(), 24, false);
                UpdateValue(wsName, "G" + row.ToString(), 
                            item.Cost.ToString(), 24, false);
                UpdateValue(wsName, "H" + row.ToString(), 
                            item.High52Week.ToString(), 28, false);
                UpdateValue(wsName, "I" + row.ToString(), 
                            item.Low52Week.ToString(), 28, false);
                UpdateValue(wsName, "J" + row.ToString(), item.Ticker, 11, true);
                row++;
            }

            // Force re-calc when the workbook is opened
            this.RemoveCellValue("Portfolio Summary", "D17");
            this.RemoveCellValue("Portfolio Summary", "E17");

            // All done! Close and save the document.
            document.Close();
        }

PortfolioReport クラスを使用する

最後にコードをいくつか Program クラスの Main メソッドに追加します (Portfolio クラスのソースをコピーしていることを前提としています)。Main メソッドを変更し、次の例に示すコードを挿入します。Portfolio クラスのソースには、2 つのクライアント、SteveKelly のサンプル データが含まれていることに注意してください。

        static void Main(string[] args)
        {
            PortfolioReport report = new PortfolioReport("Steve");
            report.CreateReport();
            report = new PortfolioReport("Kelly");
            report.CreateReport();
            Console.WriteLine("Reports created!");
            Console.WriteLine("Press ENTER to quit.");
            Console.ReadLine();
        }

これを実行することで、ファイルが非常に迅速に生成されることがわかります。これは、大容量サーバーのシナリオには理想的です。Excel オブジェクト モデルを使用して同じ結果を達成する同様のコードと比べると、そのパフォーマンスの差は歴然としています。Open XML の方がはるかに高速なのです。

まとめ

2007 Microsoft Office system 以降、コア ドキュメント指向の Microsoft Office アプリケーションは、専用のバイナリ ファイル形式から Open XML ファイル形式に切り替わってきました。Open XML ファイル形式は、XML に基づいたオープンな標準ベースのファイル形式です。Open XML ファイルに切り替えることで、開発者の開発機会は新たに広がりますが、こうした機会を利用するには、多大な時間と労力を費やして Open XML 仕様と、退屈で膨大な未加工 XML 操作について理解する必要があります。

Open XML SDK 2.0 は、開発技術の習得時間の短縮に役立ちます。これは、Open XML ドキュメントを操作するために、Open XML 仕様の詳細の多くを、使いやすいクラス ライブラリにカプセル化しているからです。また、Open XML SDK 2.0 には、IntelliSense サポート、タイプ セーフな開発など、デザイン時の機能が用意されているので、開発者の生産性を向上させることもできます。

この記事では、Open XML SDK 2.0 を使用してポートフォリオ レポート生成プログラムを構築する方法のほか、ブックの表示、ワークシートの参照、セルの取得、セルの値の更新など、Excel 関連の一般的なタスクに対する、共通のソリューション パターンおよびアプローチについて説明しました。

その他の技術情報

ここで説明したテーマの詳細については、以下の技術情報を参照してください。

著者について

Steve Hansen は、Grid Logic の創設者です。Grid Logic は、ミネソタ州を拠点とする、ビジネス インテリジェンスおよびインフォメーション ワーカー ソリューションを専門に扱うコンサルティング会社です。開発者であり、技術会議では頻繁に執筆と講演を行う Steve は、Office 用 Visual Studio ツールでの業績により Microsoft の MVP になっています。プログラマでありながらファイナンスおたくでもある Steve は、ミネソタ大学からファイナンスの MBA も取得しています。