OpenXMLでExcelファイルを操作しよう (4) - 全てのセルの値を取得したい

## はじめに

『[C#コードレシピ集](https://www.amazon.co.jp/dp/4297122650)』では掲載できなかったOpen XMLを使ってExcelを操作するレシピを5回に分けて紹介しています。

1. [OpenXMLでExcelファイルを操作しよう (1) - Excelファイルを新規作成したい](https://qiita.com/gushwell/items/5c689e86014105313017)
1. [OpenXMLでExcelファイルを操作しよう (2) - セルに値を設定したい](https://qiita.com/gushwell/items/66ef40e9fedf8e0b6237)  
1. [OpenXMLでExcelファイルを操作しよう (3) - セルの値を読み込みたい](https://qiita.com/gushwell/items/d719fd287be1b4815f2f)
1. [OpenXMLでExcelファイルを操作しよう (4) - 全てのセルの値を取得したい](https://qiita.com/gushwell/items/456e9da791f1a8d5bb59)  ← 今ここ
1. [OpenXMLでExcelファイルを操作しよう (5) - セルに書式を設定したい](https://qiita.com/gushwell/items/51c90012e724088d318c)

コードレシピの環境は、C#9.0 + .NET 5 のコンソールアプリケーションです。

なお、『[C#コードレシピ集](https://www.amazon.co.jp/dp/4297122650)』では、NPOIを使ってエクセルファイルを操作するレシピを掲載しています。

## 定義するメソッド

前回は指定したセルの値を読み取るコードを示しましたが、今回はシート内の全てのセルの値を読み取るコードを示します。

前回同様、MyExcelBookクラスにこれらのメソッドを追加定義します。定義するのは以下の2つのメソッドです。

メソッド | 機能
------------|----------------------------
GetAllCellValues | シート内の全てのセルを値を取得する
ToCellInfo | GetAllCellValuesの下請けメソッド

## 追加で定義するクラス

OpenXmlのCellクラスをそのまま利用しても良いのですが、ここでは必要最小限の情報を持ったCellInfoクラス(ここではrecordを利用)を定義します。

```cs
public record CellInfo
{
    public int Row { get; set; }

    public string Col { get; set; }

    public string Value { get; set; }
}
```

# MyExcelBook　クラス

では、MyExcelBookクラスを示します。コードが長くなるので、今回利用しないコードは省略します。

```cs
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Spreadsheet;
using DocumentFormat.OpenXml.Packaging;

sealed class MyExcelBook : IDisposable 
{
    private MyExcelBook() { }

    public void Dispose() => _document?.Dispose();

    private SpreadsheetDocument _document;
    private WorkbookPart _workbookpart;
    private WorksheetPart _worksheetPart;
    private Sheets _sheets;

    public void SelectSheet(string sheetName) 
    {
        var theSheet = _workbookpart.Workbook.Descendants<Sheet>().
            Where(s => s.Name == sheetName).FirstOrDefault();
        if (theSheet == null)
            throw new ArgumentException("sheetName");
        _worksheetPart = (WorksheetPart)(_workbookpart.GetPartById(theSheet.Id));
    }

    public static MyExcelBook Open(string filePath) 
    {
        var obj = new MyExcelBook();
        obj._document = SpreadsheetDocument.Open(filePath, false);
        obj._workbookpart = obj._document.WorkbookPart;
        return obj;
    }

    public string GetCellValue(Cell cell)
    {
        if (cell == null)
            return null;
        if (cell.CellFormula != null)
            return cell.CellValue.Text;
        var value = cell.InnerText;
        if (cell.DataType == null)
            return value;
        switch (cell.DataType.Value)
        {
            case CellValues.SharedString:  // 文字列
                // 単一の SharedStringTablePart への参照を取得する必要がある
                var stringTable = _workbookpart
                    .GetPartsOfType<SharedStringTablePart>()
                    .FirstOrDefault();
                if (stringTable != null)
                {
                    // valueはindexを表している。SharedStringTableから要素を取得
                    var element = stringTable.SharedStringTable
                        .ElementAt(int.Parse(value));
                    // そのInnterTextがセルの値
                    value = element.InnerText;
                }
                break;
            case CellValues.Boolean:    // ブール値
                value = (value != "0").ToString().ToLower();
                break;
        }
        return value;
    }


    // 以降が追加したメンバー
    public IEnumerable<CellInfo[]> GetAllCellValues()
    {
        if (_worksheetPart == null)
        {
            Console.WriteLine("WorksheetPart Not Found !!");
            yield break;
        }
        var ws = _worksheetPart.Worksheet;
        foreach (var row in ws.Descendants<Row>())
        {
            var list = new List<CellInfo>();
            // 注意:値が空のセルがあると列挙から除外される。
            // これに対応するには、cell.CellReferenceの値を見て
            // セルの位置を確認する必要がある。
            foreach (Cell cell in row)
            {
                list.Add(ToCellInfo(cell));
            }
            yield return list.ToArray();
        }
    }

    private CellInfo ToCellInfo(Cell cell)
    {
        return new CellInfo {
            Col = Regex.Match(cell.CellReference.Value, @"^[A-Z]+").Value,
            Row = int.Parse(Regex.Match(cell.CellReference.Value, @"[0-9]+$").Value),
            Value = GetCellValue(cell)
        };
    }

    // 以下省略 （前回までに定義したメソッドの一部は省略）
}    
```

## MyExcelBookクラスを利用し、全てのセルの値を取り出す

前述のMyExcelBookクラスを利用し、全てのセルの値を取り出し、コンソールに出力するコードを示します。


```cs
foreach (var row in xls.GetAllCellValues())
{
    foreach (var cell in row)
    {
        Console.Write($"{cell.Col}{cell.Row}: {cell.Value}\t");
    }
    Console.WriteLine();
}
```

## 実行例

下記のエクセルファイルを入力ファイルとします。

![スクリーンショット 2021-10-18 20.20.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/7264ecb8-bff9-89bf-caf4-decbd63c5e33.png)


C4は、`=SUM(C1:C3)`, D4は、`=AVERAGE(D1:D3)`の関数が設定されています。

以下が実行結果です。


```
A1: A1  B1: B1  C1: 123 D1: 10.1
A2: A2  B2: B2  C2: 234 D2: 20.2
A3: A3  B3: B3  C3: 345 D3: 30.3
A4: Totle       C4: 702 D4: 20.2
```