今回はセル範囲を結合するプログラムを作ります。
編集対象のワークブック「openxmlsdk18.xlsx」のSheet1シートは、あらかじめ以下のような入力がされているものとします。
今回のプログラムは上記のシートのセル範囲「B2:C4」を結合します。
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_Sample11 {
class Program {
static void Main(string[] args) {
const string FILE = @"C:\Users\Public\Documents\openxmlsdk18.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);
MergeCells(sheet_part, "B2:C4");
}
//ファイルを開く
System.Diagnostics.Process.Start(FILE);
}
public static void MergeCells(WorksheetPart ws_part, string range_address) {
if (!isRangeAddress(range_address)) {
return;
}
string cleaned_address = range_address;
if (range_address.Contains('!')) {
cleaned_address = range_address.Split('!')[1];
}
string topleft_address = cleaned_address.Split(':')[0];
string rightbottom_address = cleaned_address.Split(':')[1];
Cell top_left_cell = GetCell(ws_part, topleft_address);
for (uint colindex = GetColumnIndex(topleft_address);
colindex <= GetColumnIndex(rightbottom_address); colindex++) {
for (uint rowindex = GetRowIndex(topleft_address);
rowindex <= GetRowIndex(rightbottom_address); rowindex++) {
string cell_address = GetColumnName(colindex) + rowindex;
if (cell_address == topleft_address) {
continue;
}
Cell cell = GetCell(ws_part, cell_address);
ClearCellValue(ws_part, cell);
if (top_left_cell.StyleIndex != null) {
cell.StyleIndex = top_left_cell.StyleIndex;
}
}
}
//worksheetにmergecells要素を書き込む
Worksheet sh = ws_part.Worksheet;
MergeCells mergeCells = sh.Elements<mergecells>().FirstOrDefault();
if (mergeCells == null) {
mergeCells = new MergeCells();
sh.InsertAfter(mergeCells, sh.Elements<sheetdata>().First());
}
MergeCell mergeCell = new MergeCell() { Reference = new StringValue(cleaned_address) };
mergeCells.Append(mergeCell); //mergeCellの順番はソートされている必要はない
}
public static void ClearCellValue(WorksheetPart target_ws_part, Cell target_cell) {
if (target_cell == null) {
return;
}
target_cell.RemoveAttribute("t", "");
target_cell.RemoveAllChildren();
}
// 以下省略...
}
}
解説
セル範囲を指定して結合するメソッド「MergeCells」を27行目から58行目に定義しました。Main関数(メソッド)では21行目でこの「MergeCells」を呼び出しています。
MergeCellsの定義では、引数range_addressに渡されたセル範囲を示すアドレスを分解してセル範囲の左上端のセル番地、右下端のセル番地をそれぞれ取得し(36行目、37行目)、
(セル範囲の左上端のセルを除く)セル範囲内のセルを一つずつ訪問します。(40行目~44行目)
訪問したセルに値がセットされている場合はこれを削除し(53行目)、セル範囲の左上端のセルの書式を適用します(55行目)。
最後にworksheetに(もし無ければ)MergeCells要素を挿入し(61行目~66行目)、この処理で新たに追加するMergeCell要素をMergeCells要素に追加します(68行目~69行目)。
このプログラムを実行すると、以下のようにセルが結合されます。
補足説明
「セルの結合」後の罫線
上記のプログラムでは、結合後のセルの書式は結合前のセル範囲の左上端のセルのセル範囲を適用しています。(54行目~56行目)
しかしExcelでは罫線についてはこのような振る舞いにはなりません。以下の図はExcelを操作してセルを結合させた場合の画面写真です。
いろいろと操作してみたところ、Excelでは「結合後の外周の罫線は、結合前のセル選択範囲の外周の罫線が(上下左右それぞれのうちで)同じ場合にそれが適用され、それ以外の場合は罫線なしになる」といったふるまいをするようです。
たとえばセルの結合前のセルB2の上罫線は「二重線」で、セルC2の上罫線はセットされていないので、セル範囲B2:C4を結合した場合の上罫線は「罫線なし」になります。もしもセルC2の上罫線も「二重線」ならばセル結合後の外周の上罫線は「二重線」になります。
セル結合前後の罫線について、厳密にExcelと同じ振る舞いをさせたい場合は54行目~56行目の部分を変更する必要があります。
ClearCellValueメソッド
結合しようとするセル範囲に複数の「値」がセットされている場合、Excelを操作してセル結合をしようとすると『セルを結合をすると、左上の値のみが保持され、他のセルの値は破棄されます』というメッセージが表示され、セル結合後には左上端のセルの値のみが残ります。
この振る舞いをマネするためにClearCellValueメソッドを定義して(72行目~78行目)左上端のセル「以外」のセルの値を削除しています(53行目)。
53行目のClearCellValueメソッドの呼び出しをコメントアウトしてこのプログラムを実行しても、Excel上では正しくセル結合が行われています。セル結合を解除すると結合前のセルの値が再び表示されます(Microsoft Excel for Office 365 / バージョン 1906 で確認しました)。
用途によっては左上端のセル「以外」のセルの値を削除する必要はないのかもしれません。
なお、ClearCellValueメソッドではセルの値が文字列(SharedString)かどうかについて配慮していませんので、セルの値が文字列の場合にはSharedStringsに文字が残ることになります。
これが気持ち悪いなら「文字列の場合には(他のセルから参照されていないことを確認したうえで)SharedStringsから削除する」という処理を実装する必要があります。(この部分については割愛しました。)