NPOI读取Excel时合并单元格不自动展开,需通过sheet.MergedRegions显式判断行列是否在合并区内,仅首单元格(FirstRow/FirstColumn)有值,其余位置可能为空;应封装安全读值函数先检查合并区域再获取实际显示值。
合并单元格在NPOI中不是“自动展开”的
读取excel时,isheet.getrow() 和 irow.getcell() 返回的始终是物理行列位置上的单元格,不会因为上方/左侧有合并区域就自动填充值或调整索引。合并信息是独立存储的,必须显式查询 sheet.mergedregions 才能知道哪些区域被合并了。
常见错误现象:cell.StringCellValue 为空,但肉眼可见该位置有内容——大概率是它位于合并区域的非首单元格(即非 FirstRow, FirstColumn 位置)。
- 用
sheet.NumMergedRegions判断是否存在合并 - 遍历
sheet.MergedRegions获取每个CellRangeAddress -
CellRangeAddress提供FirstRow/LastRow/FirstColumn/LastColumn,可用来判断某物理单元格是否属于某个合并区 - 真正有值的只可能是合并区左上角单元格(
sheet.GetRow(FirstRow)?.GetCell(FirstColumn)),其余位置GetCell()可能返回null或空值
如何根据行列坐标查到“实际显示值”
不能直接 sheet.GetRow(row).GetCell(col) 就认为拿到了有效内容,尤其在含合并单元格的报表中。需要封装一个安全读值函数,内部先检查当前行列是否落在任一合并区内。
示例逻辑:
public static string GetMergedCellValue(ISheet sheet, int row, int col)
{
var cell = sheet.GetRow(row)?.GetCell(col);
if (cell != null && !string.IsNullOrEmpty(cell.ToString().Trim()))
return cell.ToString().Trim();
// 检查是否为合并区内的非首单元格
for (int i = 0; i < sheet.NumMergedRegions; i++)
{
var region = sheet.GetMergedRegion(i);
if (region.IsInRange(row, col))
{
var topCell = sheet.GetRow(region.FirstRow)?.GetCell(region.FirstColumn);
return topCell?.ToString().Trim() ?? "";
}
}
return "";
}
注意:CellRangeAddress.IsInRange() 是 NPOI 2.5+ 提供的方法;旧版本需手动比较 row >= FirstRow && row = FirstColumn && col 。
NPOI读取合并单元格的性能与边界坑
遍历 MergedRegions 查值在小表中没问题,但若单表有数百个合并区、又频繁按行列查值(比如渲染表格控件),会明显变慢。此时应预构建索引映射,例如 Dictionary,首次加载时扫一遍所有合并区填充该字典。
容易踩的坑:
-
sheet.GetMergedRegion(i)返回的是CellRangeAddress副本,修改它不影响原始合并结构 - 写入时用
sheet.AddMergedRegion()后,必须确保目标区域内的非首单元格不写入数据,否则 Excel 打开时可能报错或显示异常 - HSSF(.xls)和 XSSF(.xlsx)对合并区域数量有限制:HSSF 最多 4096 个,XSSF 理论无上限但过多会影响内存和解析速度
- 某些导出工具生成的 Excel 可能存在“重叠合并区域”,NPOI 不校验,
IsInRange()可能命中多个,建议取第一个或按业务规则过滤
为什么不用 EPPlus?NPOI 的兼容性代价在哪
EPPlus 对合并单元格的抽象更友好(如 worksheet.Cells[row, col].Merge 属性),但它不支持 .xls(OLE 格式),且从 v6 起要求 .NET 5+ 和商业授权(免费版限制功能)。NPOI 仍是处理老旧系统导出的 .xls 文件或需 .NET Framework 支持场景的唯一可行选择。
代价在于:NPOI 的 API 更贴近 Excel 文件结构,意味着你得自己维护“逻辑单元格”和“物理单元格”的映射关系。没有银弹——要兼容老格式,就得接受多一层判断逻辑。
最常被忽略的一点:CellRangeAddress 的行列索引是从 0 开始的,但 Excel 界面显示的是 1 基(第1行、A列),调试时拿 Excel 里看到的“A1”去对应代码里的 FirstRow=0, FirstColumn=0 容易看反,建议打日志时统一转成“A1”样式输出。


















