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

新しい行の挿入

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

OpenXMLSDKを使って、Excelのシートに新しい行を挿入します。

プログラムの動作確認に、以下のようなワークブック「openxmlsdk19.xlsx」を準備します。

編集対象のワークブック: C:\Users\Public\Documents\openxmlsdk19.xlsx

H2:H6, C7:G7にはそれぞれSUM関数をセットしています。またセルH7には絶対参照を含む数式をセットしています。

解説
#

コードをお見せする前に解説します。

大まかな流れとしては、

  1. 挿入する場所に存在する行とその行以降の行・セルについて、その番地をずらす
  2. 新しい行を挿入する場所に追加する
  3. ブック内のセル番地への参照について、つじつまが合うようにそのアドレスをずらす

という処理を作りこんでいくことになります。

「3.ブック内のセル番地への参照について、つじつまが合うようにそのアドレスをずらす」という処理ですが、これはブック内で行の挿入の影響を受けるすべてのセル参照について行う必要があります。

「行の挿入の影響を受けるすべてのセル参照」といっても、一般的な(高度な機能を使っていない)シートではさほど多くないのでは?と思います。

さてその「つじつまが合うようにその参照をずらす」操作は、新しい行を挿入するという一点から考えれば、『セル範囲(またはセル)のアドレスの行番号に注目し、行の挿入の影響を受ける場合(すなわちセルのアドレスの行番号、あるいはセル範囲の行番号が新しい行を挿入する場所よりも後ろである場合)にセルの行番号を挿入する行数だけ増やし、あるいはセル範囲を拡大する』という処理を行うことになります。

この処理をShiftedRangeOnInsertRowメソッドとして定義しています(132行目~143行目)。

131
132
133
134
135
136
137
138
139
140
141
142
    public static string ShiftedRangeOnInsertRow(string range, uint row_number_at, uint new_row_count) {
        Regex regex = new Regex(@"\$?[A-Za-z]+\$?[1-9][0-9]*", RegexOptions.IgnoreCase);//セル番地にマッチ
        string range_shifted = range;
        MatchCollection mc = regex.Matches(range);
        for (int i = mc.Count - 1; i >= 0; i--) {
            string range_element_address = mc[i].ToString();
            if (GetRowIndex(range_element_address) >= row_number_at) {
                range_shifted = range_shifted.Replace(range_element_address, getOffsetAddress(range_element_address, (int)new_row_count, 0));
            }
        }
        return range_shifted;
    }
仮引数rangeには行番号をずらすことになるかもしれないセル番地またはセル範囲を渡します。row_number_atには行を挿入する場所(行番号)を渡します。new_row_countにはその場所に挿入する行数を渡します。

134行目~140行目ではMatchCollectionの後ろから置換(Replace)をするようにしています。これは置換した番地が、続く繰り返しで再び置換されてしまわないようにするための配慮です。 (たとえば仮引数rangeに"H3:H6"が、new_row_countが3がそれぞれ渡された場合、昇順で処理するとH3 -> H6, H6 -> H9 の順でreplaceすることになり、得られる結果(H9:H9)は期待したものになりません。セル範囲(range)は一般に「左上:右下」なので逆順で処理すればH9 -> H12, H3 -> H6 の順でreplaceすることになり、期待した通りのH6:H12を得ることになります。)

コード
#

  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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
namespace OpenXMLSDK_Sample12{
    class Program {
        static void Main(string[] args) {
            const string FILE = @"C:\Users\Public\Documents\openxmlsdk19.xlsx";
            const string SHEET_NAME = "Sheet1";

            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);

                InsertRows(sheet_part, 3, 1);
                InsertRows(sheet_part, 5, 2);
                InsertRows(sheet_part, 8, 3);

            }
            //ファイルを開く
            System.Diagnostics.Process.Start(FILE);
        }
        
        public static void InsertRows(WorksheetPart ws_part, uint row_number_at, uint new_row_count = 1) {
            if (row_number_at == 0) {
                throw new Exception("row_number_at must be positive");
            }
            if (new_row_count <= 0) {
                throw new Exception("new_row_count must be positive");
            }

            SheetData sheet_data_part = ws_part.Worksheet.GetFirstChild<SheetData>();
            Regex regex = new Regex(@"\$?[A-Za-z]+\$?[1-9][0-9]*", RegexOptions.IgnoreCase);//セル番地にマッチ

            /*** dimension  ***/
            SheetDimension dimension_modified = ws_part.Worksheet.GetFirstChild<SheetDimension>();
            if (isRangeAddress(dimension_modified.Reference)) {
                string last_cell_address = dimension_modified.Reference.Value.Split(':')[1];
                if (row_number_at <= GetRowIndex(last_cell_address)) {
                    dimension_modified.Reference = new StringValue(ShiftedRangeOnInsertRow(dimension_modified.Reference, row_number_at, new_row_count));
                } else {
                    return;
                }
            } else {
                return;
            }
            
            /*** Cell Merge ***/
            MergeCells mergeCells = ws_part.Worksheet.GetFirstChild<MergeCells>();
            if (mergeCells != null) {
                foreach (MergeCell merge_cell in mergeCells) {
                    merge_cell.Reference = ShiftedRangeOnInsertRow(merge_cell.Reference, row_number_at, new_row_count);
                }
            }

            /*** Shift Row and Cell Reference ***/
            foreach (var row in sheet_data_part.Elements<Row>().Where(r => r.RowIndex >= row_number_at)) {
                row.RowIndex+= new_row_count;
                foreach (var cell in row.Elements<Cell>()) {
                    cell.CellReference = $"{GetColumnName(cell.CellReference)}{row.RowIndex}";
                }
            }

            /*** Shift Formula Reference ***/
            foreach (var cell in sheet_data_part.Descendants<Cell>().Where(c => c.CellFormula != null)) {
                /*** Shift Formula ***/
                string formula = cell.CellFormula.Text;
                if (!string.IsNullOrEmpty(formula)) {
                    cell.CellFormula.Text = ShiftedRangeOnInsertRow(formula, row_number_at, new_row_count);
                }

                /*** Shift Formula Reference Attribute ***/
                string reference = cell.CellFormula.Reference;
                if (!string.IsNullOrEmpty(reference)) {
                    cell.CellFormula.Reference = ShiftedRangeOnInsertRow(cell.CellFormula.Reference, row_number_at, new_row_count);
                }
            }

            /*** CalcChain ***/
            WorkbookPart wb_part = ws_part.GetParentParts().ElementAt(0) as WorkbookPart;
            Sheets sheets = wb_part.RootElement.Elements<Sheets>().First();
            string sheet_part_id = wb_part.GetIdOfPart(ws_part);
            string sheet_id = (sheets.First(sh => (sh as Sheet).Id == sheet_part_id) as Sheet).SheetId;
            CalculationChainPart calc_chain_part = wb_part.GetPartsOfType<CalculationChainPart>().FirstOrDefault();
            if (calc_chain_part != null) {
                foreach (CalculationCell cell in calc_chain_part.CalculationChain.Elements<CalculationCell>().Where(c => c.SheetId == sheet_id)) {
                    cell.CellReference = ShiftedRangeOnInsertRow(cell.CellReference, row_number_at, new_row_count);
                }
            }

            /*** name definition ***/
            /*** graph ***/
            /*** pivot table & graph ***/

            /*** Add new Cell and Copy cell style from the previous row ***/
            var row_previous = sheet_data_part.Elements<Row>().FirstOrDefault(r => r.RowIndex == row_number_at - 1);
            SheetDimension dimension = ws_part.Worksheet.GetFirstChild<SheetDimension>();
            uint[] col_range;
            if (row_previous == null) {
                col_range = dimension.Reference.Value.Split(':').Select(s => GetColumnIndex(s)).ToArray();
            } else {
                col_range = row_previous.Spans.InnerText.Split(':').Select(s => Convert.ToUInt32(s)).ToArray();
            }

            for (uint i = 0; i < new_row_count; i++) {
                var row_new = new Row();
                if (row_previous != null) {
                    foreach (var attr in row_previous.GetAttributes()) {
                        row_new.SetAttribute(new OpenXmlAttribute() { LocalName = attr.LocalName, NamespaceUri = attr.NamespaceUri, Prefix = attr.Prefix, Value = attr.Value });
                    }
                }
                row_new.RowIndex = row_number_at + i;

                for (uint j = col_range[0]; j <= col_range[1]; j++) {
                    Cell new_cell = new Cell() { CellReference = $"{GetColumnName(j)}{row_new.RowIndex}" };
                    if (row_previous != null) {
                        Cell previous_cell = GetCell(ws_part, $"{GetColumnName(j)}{row_number_at - 1}");
                        new_cell.StyleIndex = previous_cell.StyleIndex;
                    }
                    row_new.Append(new_cell);
                }
                sheet_data_part.InsertAt<Row>(row_new, (int)row_new.RowIndex.Value - 1);
            }
        }
        
        public static string ShiftedRangeOnInsertRow(string range, uint row_number_at, uint new_row_count) {
            Regex regex = new Regex(@"\$?[A-Za-z]+\$?[1-9][0-9]*", RegexOptions.IgnoreCase);//セル番地にマッチ
            string range_shifted = range;
            MatchCollection mc = regex.Matches(range);
            for (int i = mc.Count - 1; i >= 0; i--) {
                string range_element_address = mc[i].ToString();
                if (GetRowIndex(range_element_address) >= row_number_at) {
                    range_shifted = range_shifted.Replace(range_element_address, getOffsetAddress(range_element_address, (int)new_row_count, 0));
                }
            }
            return range_shifted;
        }
        
        public static string getOffsetAddress(string source_cell_address, int biasRow, int biasColumn) {
            if (string.IsNullOrEmpty(source_cell_address) || !isOneCellAddress(source_cell_address)) {
                return string.Empty;
            }

            long offset_address_row = GetRowIndex(source_cell_address) + biasRow;
            if (offset_address_row < 1) {
                offset_address_row = 1;
            }
            if (offset_address_row > MAX_ROW_INDEX) {
                offset_address_row = MAX_ROW_INDEX;
            }

            long offset_address_col = GetColumnIndex(source_cell_address) + biasColumn;
            if (offset_address_col < 1) {
                offset_address_col = 1;
            }
            if (offset_address_col > MAX_COL_INDEX) {
                offset_address_col = MAX_COL_INDEX;
            }

            //absolute address
            string col_name = GetColumnName((uint)offset_address_col);
            if (source_cell_address.StartsWith("$")) {
                col_name = $"${col_name}";
            }

            string row_number = offset_address_row.ToString();
            if (source_cell_address.LastIndexOf('$') > 0) {
                row_number = $"${row_number}";
            }

            //return GetColumnName((uint)offset_address_col) + offset_address_row;
            return $"{col_name}{row_number}";
        }
        //以下省略
    }
}
「以下省略」とした部分に定義されているメソッドについては 「セルの書式設定 - 表示形式」など、以前の記事をご覧ください。

InsertRowsの定義において、61行目から67行目の部分が、先の「1. 挿入する場所に存在する行とその行以降の行・セルについて、その番地をずらす」処理に該当します。

61
62
63
64
65
66
67
/*** Shift Row and Cell Reference ***/
foreach (var row in sheet_data_part.Elements>Row<().Where(r => r.RowIndex >= row_number_at)) {
    row.RowIndex+= new_row_count;
    foreach (var cell in row.Elements>Cell<()) {
        cell.CellReference = $"{GetColumnName(cell.CellReference)}{row.RowIndex}";
    }
}

同様に100行目から128行目の部分が、先の「2. 新しい行を挿入する場所に追加する」処理に該当します。

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/*** Add new Cell and Copy cell style from the previous row ***/
var row_previous = sheet_data_part.Elements>Row<().FirstOrDefault(r => r.RowIndex == row_number_at - 1);
SheetDimension dimension = ws_part.Worksheet.GetFirstChild>SheetDimension<();
uint[] col_range;
if (row_previous == null) {
    col_range = dimension.Reference.Value.Split(':').Select(s => GetColumnIndex(s)).ToArray();
} else {
    col_range = row_previous.Spans.InnerText.Split(':').Select(s => Convert.ToUInt32(s)).ToArray();
}
 
for (uint i = 0; i < new_row_count; i++) {
    var row_new = new Row();
    if (row_previous != null) {
        foreach (var attr in row_previous.GetAttributes()) {
            row_new.SetAttribute(new OpenXmlAttribute() { LocalName = attr.LocalName, NamespaceUri = attr.NamespaceUri, Prefix = attr.Prefix, Value = attr.Value });
        }
    }
    row_new.RowIndex = row_number_at + i;
 
    for (uint j = col_range[0]; j <= col_range[1]; j++) {
        Cell new_cell = new Cell() { CellReference = $"{GetColumnName(j)}{row_new.RowIndex}" };
        if (row_previous != null) {
            Cell previous_cell = GetCell(ws_part, $"{GetColumnName(j)}{row_number_at - 1}");
            new_cell.StyleIndex = previous_cell.StyleIndex;
        }
        row_new.Append(new_cell);
    }
    sheet_data_part.InsertAt<Row>(row_new, (int)row_new.RowIndex.Value - 1);
}

上記では、挿入する場所(行)の一つ前(上)の行が存在する場合には、その一つ前の行(row_previous)とその行のセルの書式を新しい行にコピーして、行の挿入を行います。

その他、40行目~51行目では行の挿入に伴って、xl/worksheets/sheet1.xmlのdimensionの定義(以下参照)を変更します。

<x:dimension ref="B1:H7" />

例えば新しい行を1行目から7行目までのどこかに2行挿入する場合は「ref=“B1:H9”」となるように変更します。

同様に、54行目~59行目ではセルの結合に関する部分について、セルの結合についての定義を変更します。(例えば新しい行を2行目から6行目までのどこかに2行挿入する場合はref=“B2:H8"となるように変更します。)

他にもセルにハイパーリンクが張られていたり、名前の定義がされていたり、グラフがあったり、…などがあれば、これらについても行の挿入に伴ってセルの参照部分を適切に編集する必要があります。 が、今回のコードでは実装を省略しています。

数式の参照
#

数式に含まれるセル参照も必要に応じてずらす必要があります(70行目~82行目)。

Productive Toolで xl/worksheets/sheet1.xml を確認すると、数式がオートフィルによって入力されている場合は、 コピー元となるセルのref属性にそのコピー先セルの範囲がセットされていることが分かります。

      <x:c r="H4">
        <x:f t="shared" ref="H4:H12" si="0">SUM(C4:G4)</x:f>
        <x:v>10</x:v>
      </x:c>

そこで78行目~81行目のコードでコピー先のセル範囲をずらします。

同様に、CalcChainパートについてもセル参照をずらします。(85行目~94行目)

CalcChainパートは数式の計算の順番を記しているパートです。Excelファイルを扱うアプリケーションによってはCalcChainパートを含んでいる場合にこれをヒントに計算処理の効率化を図ったりするそうです。

Microsoft Excelで作成した「数式を含むExcelファイル」にはCalcChainパートが作られるので、これについてもつじつまが合うようにセル参照をずらす必要があります。

<x:calcChain xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <x:c r="H13" i="1" l="1" />
  <x:c r="H4" i="1" />
  <x:c r="H7" i="1" />
  <x:c r="H11" i="1" />
  <x:c r="H12" i="1" />
  <x:c r="H2" i="1" />
  <x:c r="C13" i="1" />
  <x:c r="D13" i="1" />
  <x:c r="E13" i="1" />
  <x:c r="F13" i="1" />
  <x:c r="G13" i="1" />
</x:calcChain>

CalcChainパートは必須ではなく、無くってもかまいません。それゆえ面倒くさければCalcChainパートを削除してしまうのも一つの手です。
その場合は85行目~94行目を以下のように書き換えます。

85
86
87
88
89
WorkbookPart wb_part = ws_part.GetParentParts().ElementAt(0) as WorkbookPart;
CalculationChainPart calc_chain_part = wb_part.GetPartsOfType<CalculationChainPart>().FirstOrDefault();
if (calc_chain_part != null) {
    wb_part.DeletePart(calc_chain_part);
}

実行結果
#

コードを実行すると、以下のようなファイルになります。

実行結果

次回は列の挿入に取り組みます。

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