using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using NPOI.SS.Util; using NPOI.XSSF.UserModel; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; namespace WebMvcEF.Extensions { public static class NpoiExtension { /// /// 建出物件及名稱並傳回路徑 /// /// /// /// /// public static string Report(string templateName, Action report) { string sNewTemplateName = null; if (report != null) { string template = string.Format(CultureInfo.CurrentCulture, "App_Data\\{0}.xlsx", templateName); string sPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, template); IWorkbook workbook = LoadWorkbook(sPath); sNewTemplateName = string.Format(CultureInfo.CurrentCulture, "temp\\uploads\\{0}{1}.xlsx", templateName, DateTime.Now.ToString("yyyyMMddhhmmss", CultureInfo.CurrentCulture)); report.Invoke(workbook.GetSheetAt(0)); using FileStream fs = new FileStream(AppDomain.CurrentDomain.BaseDirectory + sNewTemplateName, FileMode.Create, FileAccess.Write); workbook.Write(fs); } return sNewTemplateName; } #region 產生Excel /// /// 根據型別產生試算表 /// /// /// public static IWorkbook CreateWorkbook(string type) { if (type != null) { type = type.TrimStart('.').ToLower(CultureInfo.CurrentCulture); } return type switch { "xls" => new HSSFWorkbook(), "xlsx" => new XSSFWorkbook(), _ => throw new ArgumentNullException("Undefined type: " + type), }; } /// /// 根據傳進來的路徑載入試算表 /// /// /// public static IWorkbook LoadWorkbook(string path) { var type = Path.GetExtension(path); using var fs = new FileStream(path, FileMode.Open); if (type != null) { type = type.TrimStart('.').ToLower(CultureInfo.CurrentCulture); } return type switch { "xls" => new HSSFWorkbook(fs), "xlsx" => new XSSFWorkbook(fs), _ => throw new ArgumentNullException("Undefined type: " + type), }; } /// /// 判斷型態是否為xlsx /// /// /// public static bool IsXlsx(string type) { return "xlsx".Equals(type, StringComparison.OrdinalIgnoreCase); } #endregion /// /// 取得目標儲存格 /// /// 工作表 /// 行號 /// 欄號 /// public static ICell Cell(this ISheet sheet, int rowNumber, int cellNumber) { return sheet?.GetRow(rowNumber).GetCell(cellNumber); } /// /// 取得目標儲存格 (特別注意: 本方法行號等同顯示行號,例如第1行請輸入1而非0) /// /// 工作表 /// 行號 (第1行請輸入0) /// 用字元指定欄位 (第一欄請輸入'A') /// public static ICell Cell(this ISheet sheet, int rowNumber, char column) { var cellNumber = char.ToUpper(column, CultureInfo.CurrentCulture) - 'A'; return sheet?.GetRow(rowNumber - 1).GetCell(cellNumber); } #region 擴充儲存格輸入格式 /// /// 設定儲存格數字 (decimal) /// /// 儲存格 /// 填入值 public static void SetCellValue(this ICell cell, decimal value) { if (cell != null) { cell.SetCellValue(Convert.ToDouble(value)); } } /// /// 設定儲存格數字 (decimal?) /// 如果傳入null,不寫值進去 /// /// 儲存格 /// 填入值 public static void SetCellValue(this ICell cell, decimal? value) { if (cell != null && value.HasValue) { cell.SetCellValue(Convert.ToDouble(value, CultureInfo.CurrentCulture)); } } /// /// 設定儲存格數字 (int?) /// 如果傳入null,不寫值進去 /// /// 儲存格 /// 填入值 public static void SetCellValue(this ICell cell, int? value) { if (cell != null && value.HasValue) { cell.SetCellValue(Convert.ToDouble(value, CultureInfo.CurrentCulture)); } } /// /// 設定儲存格數字 (double?) /// 如果傳入null,不寫值進去 /// /// 儲存格 /// 填入值 public static void SetCellValue(this ICell cell, double? value) { if (cell != null && value.HasValue) { cell.SetCellValue(Convert.ToDouble(value, CultureInfo.CurrentCulture)); } } /// /// 設定儲存格日期 (DateTime?) /// 如果傳入null,不寫值進去 /// /// 儲存格 /// 填入值 public static void SetCellValue(this ICell cell, DateTime? value) { if (cell != null && value.HasValue) { cell.SetCellValue(value.Value); } } /// /// 設定文字,簡寫string.Format方法 /// /// 儲存格 /// 文字格式 /// 參數 public static void SetCellValue(this ICell cell, string format, params object[] args) { if (cell != null) { cell.SetCellValue(string.Format(CultureInfo.CurrentCulture, format, args)); } } /// /// 將該格原始文字當作format參數,填入指定項目 /// /// /// format參數 public static void SetFormatValue(this ICell cell, params object[] args) { if (cell != null) { cell.SetCellValue(string.Format(CultureInfo.CurrentCulture, cell.StringCellValue, args)); } } #endregion #region 產生Excel公式 /// /// 取得Excel column的名稱 /// /// /// private static string GetColumnName(int columnNumber) { var dividend = columnNumber; var columnName = string.Empty; int modulo; while (dividend > 0) { modulo = (dividend - 1) % 26; columnName = Convert.ToChar(65 + modulo).ToString() + columnName; dividend = (dividend - modulo) / 26; } return columnName; } /// /// 取得Excel欄位位置 /// /// /// /// public static string GetColumnAddress(int rowNumber, int columnNumber) { return GetColumnName(columnNumber + 1) + (rowNumber + 1).ToString(); } /// /// 產生Sum公式,請用SetCellFormula填入 /// /// 起始列 /// 起始儲存格 /// 結束列 /// 結束儲存格 /// public static string GenerateSumFormula(int startRow, int startColumn, int endRow, int endColumn) { return string.Format("SUM({0}:{1})", GetColumnAddress(startRow, startColumn), GetColumnAddress(endRow, endColumn)); } /// /// 產生計算公式,請用SetCellFormula填入 /// /// /// /// public static string GenerateCalcFormula(string formula, ExcelAddress[] addresses) { var list = new List(); foreach (var address in addresses) { list.Add(GetColumnAddress(address.Row, address.Column)); } return string.Format(formula, list.ToArray()); } #endregion /// /// 插入資料欄(複製被插入的那欄Style),可以插入一個範圍的欄,目前不包含合併儲存格 /// /// 工作表 /// 行號 /// 欄號 /// 行範圍 /// 欄範圍 /// 次數,丟幾次產幾次 /// 指定插入位置 public static void InsertColumn(this ISheet sheet, int rowNumber, int columnNumber, int rowRange = 1, int columnRange = 1, int columnCount = 1, int targetColumnNumber = 0) { // 範圍和次數大於0才會繼續執行 if (rowRange > 0 && columnCount > 0 && columnRange > 0) { // 起始位置 var startNumber = targetColumnNumber > 0 ? targetColumnNumber : columnNumber + columnRange; var addColumnCount = columnCount * columnRange; // 紀錄已經修改過style的column var columnList = new List(); // 每個資料列去處理 for (var r = 0; r < rowRange; r++) { // 取得原始資料列 var sourceRow = sheet.GetRow(rowNumber + r); // 將需要往後推的Cell向後推 for (var c = sourceRow.LastCellNum; c >= startNumber; c--) { // 目標Cell var targetCellNum = c + addColumnCount; // 如果Cell不等於空就移過去 var sourceCell = sourceRow.GetCell(c); if (sourceCell != null) { // 確保目標的Cell可以使用 if (sourceRow.GetCell(targetCellNum) == null) { sourceRow.CreateCell(targetCellNum); } // 複製Cell sourceRow.CopyCell(c, targetCellNum); // 移除原本的Cell sourceRow.RemoveCell(sourceCell); // 複製Column的width if (!columnList.Contains(targetCellNum)) { columnList.Add(targetCellNum); sheet.SetColumnWidth(targetCellNum, sheet.GetColumnWidth(c)); sheet.SetColumnWidth(c, sheet.DefaultColumnWidth); } } } // 從目標位置開始複製 for (var c = columnNumber; c < columnNumber + columnRange; c++) { // 如果Cell不等於空就移過去 var sourceCell = sourceRow.GetCell(c); if (sourceCell != null) { for (var x = 1; x <= columnCount; x++) { // 目標Cell var targetCellNum = c + (x * columnRange); // 確保目標的Cell可以使用 if (sourceRow.GetCell(targetCellNum) == null) { sourceRow.CreateCell(targetCellNum); } // 複製Cell sourceRow.CopyCell(c, targetCellNum); // 複製Column的width if (!columnList.Contains(targetCellNum)) { columnList.Add(targetCellNum); sheet.SetColumnWidth(targetCellNum, sheet.GetColumnWidth(c)); } } } } } } } /// /// 插入資料列(複製被插入的那列Style),可以插入一個範圍的列,公式請自行輸入 /// /// 工作表 /// 行號 /// 範圍 /// 次數,丟幾次產幾次 /// 指定插入位置 public static void InsertRow(this ISheet sheet, int rowNumber, int range = 1, int count = 1, int targetRowNumber = 0) { // 範圍和次數大於0才會繼續執行 if (range > 0 && count > 0) { // 從選取範圍後往下移動幾行 var addRowCount = range * count; var startNumber = targetRowNumber > 0 ? targetRowNumber : rowNumber + range; if (startNumber <= sheet.LastRowNum) { sheet.ShiftRows(startNumber, sheet.LastRowNum, addRowCount, true, false); } // 產生資料列 for (var r = 0; r < range; r++) { // 取得原始資料列 var sourceRow = sheet.GetRow(rowNumber + r); for (var t = 0; t < count; t++) { // 產生新資料列 var newRow = sheet.CreateRow(startNumber + r + t * range); // 沒有來源則跳過 if (sourceRow == null) continue; // 產生新儲存格並填入資料 for (var c = 0; c < sourceRow.LastCellNum; c++) { var newCell = newRow.CreateCell(c); var oldCell = sourceRow.Cells[c]; // 如果該儲存格為Null則跳過這格 if (oldCell == null) continue; // 複製內容 switch (oldCell.CellType) { case CellType.Blank: newCell.SetCellValue(oldCell.StringCellValue); break; case CellType.Boolean: newCell.SetCellValue(oldCell.BooleanCellValue); break; case CellType.Error: newCell.SetCellErrorValue(oldCell.ErrorCellValue); break; case CellType.Numeric: newCell.SetCellValue(oldCell.NumericCellValue); break; case CellType.String: newCell.SetCellValue(oldCell.RichStringCellValue); break; case CellType.Unknown: newCell.SetCellValue(oldCell.StringCellValue); break; case CellType.Formula: newCell.CellFormula = oldCell.CellFormula; break; default: break; } // 複製Cell Style newCell.CellStyle = oldCell.CellStyle; } // 複製Row Style if (sourceRow.RowStyle != null) { newRow.RowStyle = sourceRow.RowStyle; } // 複製Row 高度 newRow.HeightInPoints = sourceRow.HeightInPoints; } } #region 分頁符號處理 if (sheet.RowBreaks.Length > 0) { startNumber = targetRowNumber > 0 ? targetRowNumber : rowNumber + range; var cloneBreaks = sheet.RowBreaks.Where(r => startNumber > r && r >= rowNumber); var resetBreaks = sheet.RowBreaks.Where(r => r >= startNumber); var newRowBreaks = new List(); // 產生新的分頁符號 foreach (var cloneBreak in cloneBreaks) { for (var t = 1; t <= count; t++) { var newRowBreak = cloneBreak + (range * t); newRowBreaks.Add(newRowBreak); } } // 重新設定舊的分頁符號 foreach (var resetBreak in resetBreaks) { // 產生新的分頁符號 var newRowBreak = resetBreak + (range * count); newRowBreaks.Add(newRowBreak); // 移除舊的分頁符號 sheet.RemoveRowBreak(resetBreak); } // 不按照順序插入會無法正常顯示分頁符號 newRowBreaks = newRowBreaks.OrderBy(r => r).ToList(); // 將產生的分頁符號新增進資料表 foreach (var newRowBreak in newRowBreaks) { sheet.SetRowBreak(newRowBreak); } } #endregion 分頁符號處理 #region 合併儲存格處理 if (sheet.NumMergedRegions > 0) { startNumber = targetRowNumber > 0 ? targetRowNumber : rowNumber + range; for (var m = sheet.NumMergedRegions - 1; 0 <= m; m--) { var address = sheet.GetMergedRegion(m); // 清除沒用處的合併儲存格資訊 if (address == null) { sheet.RemoveMergedRegion(m); continue; } // 產生新增範圍內應有的合併儲存格 var rowRange = address.LastRow - address.FirstRow; if (address.FirstRow >= rowNumber && rowNumber + range > address.FirstRow) { for (var t = 0; t < count; t++) { var startRow = startNumber + range * t + address.FirstRow - rowNumber; var endRow = startRow + rowRange; sheet.AddMergedRegion(new CellRangeAddress(startRow, endRow, address.FirstColumn, address.LastColumn)); } } } } #endregion 合併儲存格處理 } } /// /// 清除資料列,可選範圍 /// /// 工作表 /// 行號 /// /// 是否將向下所有資料往上移 public static void ClearRow(this ISheet sheet, int rowNumber, int range = 1, bool shift = false) { if (sheet != null) { for (var i = 0; i < range; i++) { var removeRow = sheet.GetRow(rowNumber + i); if (removeRow != null) { sheet.RemoveRow(removeRow); } } // 如果資料要往上移則 if (shift) { #region 分頁符號處理 // 如果有分頁符號則重新設定分頁符號 if (sheet.RowBreaks.Length > 0) { var rowBreaks = sheet.RowBreaks; var newRowBreaks = new List(); // 儲存需要的分頁符號並刪除資料表中不需要的分頁符號 foreach (var rowBreak in rowBreaks) { if (rowBreak >= rowNumber + range) { var newRowBreak = rowBreak - range; newRowBreaks.Add(newRowBreak); } if (rowBreak > rowNumber) { sheet.RemoveRowBreak(rowBreak); } } // 不按照順序插入會無法正常顯示分頁符號 newRowBreaks = newRowBreaks.OrderBy(r => r).ToList(); // 將需要的分頁符號新增進資料表 foreach (var newRowBreak in newRowBreaks) { sheet.SetRowBreak(newRowBreak); } } #endregion 分頁符號處理 #region 合併儲存格處理 // 因為shiftRow會將合併儲存格往上往下移但不會判斷是否要移除多的合併儲存格,故自行重新設定合併儲存格 var newMergedRegions = new List(); var rangeLastRow = rowNumber + range - 1; for (var i = sheet.NumMergedRegions - 1; 0 <= i; i--) { var address = sheet.GetMergedRegion(i); // 清空因shiftRows而產出的垃圾 if (address == null) { sheet.RemoveMergedRegion(i); continue; } // 若該合併儲存格的row在應被刪除的row底下則重新設定該合併儲存格 if (rowNumber <= address.LastRow) { CellRangeAddress newAddress = null; // 根據情形重新設定合併儲存格(拆開比較容易理解) if (rowNumber < address.FirstRow && rangeLastRow < address.FirstRow) { // 若該刪除的row沒有覆蓋到合併儲存格的row時,則單純將該合併儲存格往上拉回 newAddress = new CellRangeAddress(address.FirstRow - range, address.LastRow - range, address.FirstColumn, address.LastColumn); } else { // 算應該刪除的row數量 var removeStartRow = rowNumber < address.FirstRow ? address.FirstRow : rowNumber; var removeEndRow = rangeLastRow > address.LastRow ? address.LastRow : rangeLastRow; var removeRowCount = removeEndRow - removeStartRow + 1; // 若該合併儲存格的row總數比應該刪除的row數量還多才進行新增 var totalRow = address.LastRow - address.FirstRow + 1; if (totalRow > removeRowCount) { var addressFirstRow = address.FirstRow; var addressLastRow = address.LastRow - removeRowCount; // 若起始row比合併儲存格的row還小的時候則往上拉回 if (rowNumber < address.FirstRow) { var shiftRowCount = address.FirstRow - rowNumber; addressFirstRow -= shiftRowCount; addressLastRow -= shiftRowCount; } newAddress = new CellRangeAddress(addressFirstRow, addressLastRow, address.FirstColumn, address.LastColumn); } } // 刪除原本的合併儲存格 sheet.RemoveMergedRegion(i); // 新增新的合併儲存格 if (newAddress != null) { newMergedRegions.Add(newAddress); } } } #endregion 合併儲存格處理 // 將向下所有資料往上移 if (rowNumber < sheet.LastRowNum) { sheet.ShiftRows(rowNumber + range, sheet.LastRowNum, -range, true, false); } // 因為Shift往上移時候位置會偏移所以在這邊才將合併儲存格新增回去 foreach (var newMergedRegion in newMergedRegions) { sheet.AddMergedRegion(newMergedRegion); } } } } /// /// 重新設定列高功能 /// /// 工作表 /// 行號 public static void AutoHeight(this ISheet sheet, int rowNumber) { short height = -1; if (sheet != null) { var row = sheet.GetRow(rowNumber); var lastCellNum = row.LastCellNum; for (var i = 0; i < lastCellNum; i++) { var columnWidth = sheet.GetColumnWidth(i); // 儲存格可以容納的最大長度 // 如果有被合併的儲存格則會重新設定最大長度 for (var j = 0; j < sheet.NumMergedRegions; j++) { var address = sheet.GetMergedRegion(j); // 過濾空值 if (address == null) { continue; } if (address.FirstRow == rowNumber) { for (var k = i + 1; k <= address.LastColumn; k++) { columnWidth += sheet.GetColumnWidth(k); } } } // 計算新的高度,目前缺少LineBreak的判斷 var texts = row.GetCell(i).ToString().Split(new string[] { "\r\n" }, StringSplitOptions.None); var fontHeight = row.Cells[i].CellStyle.GetFont(sheet.Workbook).FontHeight; var byteCount = 0; foreach (var text in texts) { byteCount += Encoding.Default.GetByteCount(text); } var textWidth = byteCount * fontHeight / 2; var scalingRatio = textWidth > columnWidth ? textWidth / columnWidth * fontHeight : fontHeight; var newHeight = row.Height - fontHeight + ((texts.Length - 1) * fontHeight) + scalingRatio * 3; if (newHeight > height) { height = (short)newHeight; } } row.Height = height; } } /// /// 平均設定列高功能 /// /// 工作表 /// 行號 /// 範圍 public static void AverageHeight(this ISheet sheet, int rowNumber, int range) { var totalHeight = 0f; if (sheet != null) { // 第一次迴圈先計算總高度 for (var i = 0; i < range; i++) { totalHeight += sheet.GetRow(rowNumber + i).HeightInPoints; } // 第二次迴圈將回傳平均高度回去 var averageHeight = totalHeight / range; for (var i = 0; i < range; i++) { sheet.GetRow(rowNumber + i).HeightInPoints = averageHeight; } } } /// /// 將資料填到Excel資料表 /// /// /// /// 要塞入的資料 /// 要當作範本的行數(從0開始算) /// 行高設定 /// public static void Fill(this ISheet sheet, IEnumerable data, int sourceRowNum, RowHeight rowHeight, Action action) { if (sheet != null && data != null) { var sourceRow = sheet.GetRow(sourceRowNum); var cellStyles = sourceRow.Cells.Select(x => x.CellStyle).ToArray(); var cellNum = sourceRow.LastCellNum; var endRowNum = sourceRowNum + data.Count(); var sourceRowHeight = sourceRow.HeightInPoints; var dataCount = data.Count(); var rowNum = sourceRowNum; if (sourceRowNum < sheet.LastRowNum && dataCount >= 2) { sheet.ShiftRows(sourceRowNum + 1, sheet.LastRowNum, dataCount - 1); // 把範本後的內容往下推,避免被蓋過 } foreach (var item in data) { IRow row = null; if (rowNum < endRowNum) { row = sheet.CreateRow(rowNum); for (var c = 0; c < cellNum; c++) { var cell = row.CreateCell(c); cell.CellStyle = cellStyles[c]; } } else { row = sheet.GetRow(rowNum); } action.Invoke(row, item); if (rowHeight == RowHeight.Auto) { sheet.AutoHeight(rowNum); } else if (rowHeight == RowHeight.Fixed) { row.HeightInPoints = sourceRowHeight; } rowNum++; } } } public static void AutoWidth(this ISheet sheet) { if (sheet != null) { for (var cell = 0; cell < sheet.GetRow(0).LastCellNum; cell++) { sheet.AutoSizeColumn(cell); } } } /// /// 設定自訂屬性 /// /// /// /// public static void SetCustomProperty(this IWorkbook workbook, string name, bool value) { if (workbook is HSSFWorkbook) { (workbook as HSSFWorkbook).DocumentSummaryInformation.CustomProperties.Put(name, value); } else if (workbook is XSSFWorkbook) { (workbook as XSSFWorkbook).GetProperties().CustomProperties.AddProperty(name, value); } } /// /// 取得自訂屬性 /// /// /// /// public static bool GetCustomProperty(this IWorkbook workbook, string name) { if (workbook is HSSFWorkbook) { var props = (workbook as HSSFWorkbook).DocumentSummaryInformation.CustomProperties; if (props.ContainsKey(name)) { return (bool)props[name]; } } else if (workbook is XSSFWorkbook) { var props = (workbook as XSSFWorkbook).GetProperties().CustomProperties; if (props.Contains(name)) { return (bool)props.GetProperty(name).Item; } } return false; } } /// /// 每行高度要如何調整 (LibreOffice無法自動換行,但Excel可) /// public enum RowHeight { /// /// 不處理 /// (文字過長時轉成PDF有可能超出格子) /// None, /// /// 使用自製方法判斷高度 /// (有誤差) /// Auto, /// /// 使用範本高度固定設定每一行 /// (文字過長時轉成PDF有可能超出格子,可設高點) /// Fixed } /// /// Excel位置 /// public class ExcelAddress { public ExcelAddress(int row, int column) { Row = row; Column = column; } public int Row { get; set; } public int Column { get; set; } } }