メインコンテンツへスキップ
  1. 開発室/
  2. OpenXML SDK/

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

openxmlsdk - この記事は連載の一部です
パート 5: この記事

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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://learn.microsoft.com/ja-jp/office/open-xml/spreadsheet/how-to-get-a-column-heading-in-a-spreadsheet
        // 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://learn.microsoft.com/ja-jp/office/open-xml/spreadsheet/how-to-get-a-column-heading-in-a-spreadsheet
        public static string GetColumnName(string cellName) {
            Match match = Regex.Match(cellName, "[A-Za-z]+");
            return match.Value;
        }
    }
}

準備
#

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

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

コードの説明
#

上記のコードにおいて、GetRowIndex、GetColumnName の二つのメソッドはMicrosoft Learn から拝借してきました。

上記のコードは「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)を特定します。

16
17
18
19
20
 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の子要素としてこれを挿入します。

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    //行の特定
    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行目まで)。行の場合と同じく、セルが存在しない場合にはセルを作成し、行に挿入します。

41
42
43
44
45
46
47
48
49
50
51
52
53
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行目でシートへの編集内容を保存します。

56
57
58
59
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行目に相当する部分がもうちょっと複雑になります。

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

openxmlsdk - この記事は連載の一部です
パート 5: この記事