Open XML SDK 備忘録

おもにExcelファイル (.xlsx) を扱います。

P-SPACE
就学奨励費システム

マイナンバー対応 特別支援教育 就学奨励費 支給業務支援システム

詳細...

P-SPACE
就学奨励費ソフトウェア

特別支援教育 就学奨励費 支給業務支援ソフトウェア

詳細...

開発室
(備忘録そのほか)

システム開発にまつわる(かもしれない)調査・実験結果など

詳細...

まずはセルデータから - その3

前置きが長くなってしまいました。前回前々回の内容を踏まえて、Open XML SDKを使ってセルにデータをセットするプログラムを紹介します。

using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using System;
using System.Linq;
using System.Text.RegularExpressions;

namespace OpenXMLSDK_Sample1 {
    class Program {
        static void Main(string[] args) {
            const string FILE = @"C:\Users\Public\Documents\Book1.xlsx";
            const string SHEET_NAME = "Sheet1";
            const string CELL_ADDRESS = "A6";
            const int VALUE = 123456;

            using (SpreadsheetDocument excelDoc = SpreadsheetDocument.Open(FILE, true)) {
                WorkbookPart bookPart = excelDoc.WorkbookPart;

                int sheet_index = GetWorksheetIndex(bookPart, SHEET_NAME);
                WorksheetPart sheet_part = GetWorksheetPart(bookPart, sheet_index);

                //行の特定
                uint rowIndex = GetRowIndex(CELL_ADDRESS);

                Row row = sheet_part.Worksheet.Descendants<Row>().FirstOrDefault(r => r.RowIndex == rowIndex);
                if (row == null) {
                    //行がない場合は行を新規作成する
                    row = new Row() { RowIndex = new UInt32Value(rowIndex) };

                    SheetData sh_data = sheet_part.Worksheet.Descendants<SheetData>().First();
                    //場所を特定して行を挿入(行は行番号で昇順に並ばないとだめ)
                    Row rRow = sh_data.Elements<Row>().LastOrDefault(r => r.RowIndex < rowIndex);

                    if (rRow == null) {
                        sh_data.InsertAt(row, 0);
                    } else {
                        sh_data.InsertAfter(row, rRow);
                    }
                }

                uint colIndex = GetColumnIndex(CELL_ADDRESS);
                //セルを特定
                Cell cell = row.Elements<Cell>().FirstOrDefault(c => c.CellReference.Value == CELL_ADDRESS);
                if (cell == null) {
                    cell = new Cell() { CellReference = new StringValue(CELL_ADDRESS) };

                    //cellは昇順に書き込む必要がある
                    Cell rCell = row.Elements<Cell>().LastOrDefault(c => GetColumnIndex(c.CellReference) < colIndex);
                    if (rCell == null) {
                        row.InsertAt(cell, 0);
                    } else {
                        row.InsertAfter(cell, rCell);
                    }
                }

                //値をセット(セットする値が文字列ではない場合)
                cell.CellValue = new CellValue(VALUE.ToString());

                //保存
                sheet_part.Worksheet.Save();
            }

            //ファイルを開く
            System.Diagnostics.Process.Start(FILE);
        }

        static int GetWorksheetIndex(WorkbookPart book_part, string sheet_name) {
            Sheets sheets = book_part.Workbook.GetFirstChild<Sheets>();

            //シートのエレメントの順番(0スタート)
            int sheet_index = 0;
            foreach (Sheet sh in sheets) {
                if (sh.Name == sheet_name)
                    break;
                sheet_index++;
            }
            return sheet_index;
        }

        static WorksheetPart GetWorksheetPart(WorkbookPart book_part, int index) {
            Sheets sheets = book_part.Workbook.GetFirstChild<Sheets>();
            if (index >= sheets.Count()) {
                return null;
            }

            string target_relationshipID = sheets.Descendants<Sheet>().ElementAt(index).Id;
            return (WorksheetPart)book_part.GetPartById(target_relationshipID);
        }

        public static uint GetColumnIndex(string cellName) {
            string column_name = GetColumnName(cellName).ToUpper();
            const string colChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
            uint column_index = 0;
            for (int i = column_name.Length; i > 0; i--) {
                column_index += (uint)((colChars.IndexOf(column_name.Substring(column_name.Length - i, 1)) + 1) * Math.Pow(26, i - 1));
            }
            return column_index;
        }

        //https://msdn.microsoft.com/ja-jp/library/office/Cc822064.aspx
        // Given a cell name, parses the specified cell to get the row index.
        public static uint GetRowIndex(string cellName) {
            // Create a regular expression to match the row index portion the cell name.
            Regex regex = new Regex(@"\d+");
            Match match = regex.Match(cellName);
            return uint.Parse(match.Value);
        }
        
        //https://msdn.microsoft.com/ja-jp/library/office/Cc822064.aspx
        public static string GetColumnName(string cellName) {
            Match match = Regex.Match(cellName, "[A-Za-z]+");
            return match.Value;
        }
    }
}

準備

当然といえば当然ですが、OpenXML SDKを使えるようにします。(Nugetで検索して、インストールするのが簡単です。)

それから、参照設定に「WindowsBase」を追加します。

コードの説明

上記のコードにおいて、GetRowIndex、GetColumnName の二つのメソッドはMSDN (https://msdn.microsoft.com/ja-jp/library/office/Cc822064.aspx) から拝借してきました。

上記のコードは「C:\Users\Public\Documents\Book1.xlsx のSheet1のセルA6に123456をセットする」コードです。

16行目で、C:\Users\Public\Documents\Book1.xlsx を開き、17行目でそのWorkbookパーツ(/xl/workbook.xml)を特定します。
つづいて、20行目でWorksheetパーツ(/xl/worksheets/sheet1.xml)を特定します。

            using (SpreadsheetDocument excelDoc = SpreadsheetDocument.Open(FILE, true)) {
                WorkbookPart bookPart = excelDoc.WorkbookPart;

                int sheet_index = GetWorksheetIndex(bookPart, SHEET_NAME);
                WorksheetPart sheet_part = GetWorksheetPart(bookPart, sheet_index);

ここまでが前回の内容(シート名を手がかりに、/xl/workbook.xml ファイルからリレーションシップIDを取得する。取得したリレーションシップIDをキーに/xl/_res/workbook.xml.rels ファイルからその定義が記述されているWorksheetパーツファイルを特定する。)に相当します。

つづいて、23行目から25行目までで、セルアドレスの行番号を手がかりに行(r要素)を特定します。
26行目から39行目は行が存在しない場合の処理です。行が存在しない場合(注目する行にデータを持つセルがかつて存在したことがない場合は、その行は存在しません)、新たに行を作成し、SheetDataの子要素としてこれを挿入します。

                //行の特定
                uint rowIndex = GetRowIndex(CELL_ADDRESS);

                Row row = sheet_part.Worksheet.Descendants<Row>().FirstOrDefault(r => r.RowIndex == rowIndex);
                if (row == null) {
                    //行がない場合は行を新規作成する
                    row = new Row() { RowIndex = new UInt32Value(rowIndex) };

                    SheetData sh_data = sheet_part.Worksheet.Descendants<SheetData>().First();
                    //場所を特定して行を挿入(行は行番号で昇順に並ばないとだめ)
                    Row rRow = sh_data.Elements<Row>().LastOrDefault(r => r.RowIndex < rowIndex);

                    if (rRow == null) {
                        sh_data.InsertAt(row, 0);
                    } else {
                        sh_data.InsertAfter(row, rRow);
                    }
                }
                

行を特定できたら、今度はセル(c要素)を特定します(41行目から54行目まで)。行の場合と同じく、セルが存在しない場合にはセルを作成し、行に挿入します。

                uint colIndex = GetColumnIndex(CELL_ADDRESS);
                //セルを特定
                Cell cell = row.Elements<Cell>().FirstOrDefault(c => c.CellReference.Value == CELL_ADDRESS);
                if (cell == null) {
                    cell = new Cell() { CellReference = new StringValue(CELL_ADDRESS) };

                    //cellは昇順に書き込む必要がある
                    Cell rCell = row.Elements<Cell>().LastOrDefault(c => GetColumnIndex(c.CellReference) < colIndex);
                    if (rCell == null) {
                        row.InsertAt(cell, 0);
                    } else {
                        row.InsertAfter(cell, rCell);
                    }
                }

ここまででセルを特定できました。あとは57行目でセルのv要素をセットし、60行目でシートへの編集内容を保存します。

                //値をセット(セットする値が文字列ではない場合)
                cell.CellValue = new CellValue(VALUE.ToString());

                //保存
                sheet_part.Worksheet.Save();

このコードを実行すると、64行目でC:\Users\Public\Documents\Book1.xlsxを開きます。セルA6に正しく「123456」がセットされていることを確認してください。

GetColumnIndexの実装がちょっとわかりづらいかもしれませんが、Excelの列名は26進数とみなせるので、列名の末尾から一文字ずつ切り出し、桁の重みをかけながら加算して10進数に変換し、列番号(0スタートです)を求めています。

で、文字列をセットするには...

上記のコードはセルの値に数値をセットする例だったので、セルを特定したらあとは57行目「cell.CellValue = new CellValue(VALUE.ToString());」だけでセルデータの編集が終わります。

文字列をセットする場合には、(前々回で確認したように)SharedStringも同時に編集する必要があるので、57行目に相当する部分がもうちょっと複雑になります。

文字列をセットする場合のコードは次回、紹介予定です。