1. 项目背景与需求分析
在数据处理和报表生成领域,Excel文件导出是最常见的需求之一。特别是在财务、统计、库存管理等业务场景中,我们经常需要将数据库查询结果或内存中的数据集合导出为结构化的Excel表格。其中,一个典型的需求就是"相同内容的相邻单元格自动合并"功能。
想象这样一个场景:某电商平台需要生成月度销售报表,数据按"省份→城市→商品类别"三级结构展示。如果直接导出原始数据,会出现大量重复的省份名称和城市名称,不仅影响美观,更重要的是降低了报表的可读性。这时就需要实现纵横方向的自动合并功能——纵向合并相同省份的单元格,横向合并相同商品类别的表头。
传统做法是手动在Excel中操作合并单元格,但面对成百上千行数据时,这种方法效率极低且容易出错。通过C#编程实现自动化合并,可以显著提升工作效率,减少人为错误。
2. 技术选型与准备工作
2.1 主流Excel操作库对比
在C#生态中,操作Excel主要有以下几种方案:
-
Microsoft.Office.Interop.Excel:
- 优点:功能最全面,直接调用本地安装的Excel程序
- 缺点:依赖Office安装,性能较差,不适合服务器环境
-
EPPlus:
- 优点:纯托管代码,不依赖Office,支持.xlsx格式
- 缺点:不支持旧版.xls格式
-
NPOI:
- 优点:同时支持.xls和.xlsx,不依赖Office
- 缺点:API设计较为复杂
-
ClosedXML:
- 优点:基于Open XML,API设计友好
- 缺点:仅支持.xlsx格式
综合考虑功能需求、性能和易用性,我们选择EPPlus作为实现方案。它足够轻量,API设计合理,且能满足大多数现代Excel操作需求。
2.2 开发环境准备
确保你的开发环境满足以下要求:
- Visual Studio 2019或更高版本
- .NET Framework 4.7.2+ 或 .NET Core 3.1+
- 通过NuGet安装EPPlus包:
bash复制
Install-Package EPPlus -Version 5.8.8
提示:EPPlus 5.x版本开始需要商业许可证才能在非开源项目中使用。如果是个人学习或开源项目,可以使用4.5.3.3版本(MIT许可证)。
3. 核心算法设计与实现
3.1 合并单元格的基本原理
Excel中的单元格合并本质上是设置单元格的Merge属性。在EPPlus中,可以通过worksheet.Cells[startRow, startCol, endRow, endCol].Merge = true来实现。
自动合并的关键在于识别哪些相邻单元格的内容相同。算法思路如下:
- 按列遍历数据,记录当前单元格的值
- 与下一个单元格比较,如果相同则扩展合并范围
- 遇到不同值时执行合并操作,并重置合并范围
3.2 纵向合并实现代码
以下是一个完整的纵向合并实现示例:
csharp复制public void MergeVerticalCells(ExcelWorksheet worksheet, int columnIndex, int startRow = 1)
{
if (worksheet == null || worksheet.Dimension == null)
return;
int rowCount = worksheet.Dimension.Rows;
if (rowCount <= startRow) return;
string prevValue = worksheet.Cells[startRow, columnIndex].Text;
int mergeStartRow = startRow;
for (int row = startRow + 1; row <= rowCount; row++)
{
string currentValue = worksheet.Cells[row, columnIndex].Text;
if (currentValue == prevValue)
{
continue;
}
else
{
if (mergeStartRow < row - 1)
{
worksheet.Cells[mergeStartRow, columnIndex, row - 1, columnIndex].Merge = true;
// 垂直居中显示
worksheet.Cells[mergeStartRow, columnIndex, row - 1, columnIndex].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
}
prevValue = currentValue;
mergeStartRow = row;
}
}
// 处理最后一组相同值
if (mergeStartRow < rowCount)
{
worksheet.Cells[mergeStartRow, columnIndex, rowCount, columnIndex].Merge = true;
worksheet.Cells[mergeStartRow, columnIndex, rowCount, columnIndex].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
}
}
3.3 横向合并实现代码
横向合并的原理与纵向类似,只是遍历方向不同:
csharp复制public void MergeHorizontalCells(ExcelWorksheet worksheet, int rowIndex, int startCol = 1)
{
if (worksheet == null || worksheet.Dimension == null)
return;
int colCount = worksheet.Dimension.Columns;
if (colCount <= startCol) return;
string prevValue = worksheet.Cells[rowIndex, startCol].Text;
int mergeStartCol = startCol;
for (int col = startCol + 1; col <= colCount; col++)
{
string currentValue = worksheet.Cells[rowIndex, col].Text;
if (currentValue == prevValue)
{
continue;
}
else
{
if (mergeStartCol < col - 1)
{
worksheet.Cells[rowIndex, mergeStartCol, rowIndex, col - 1].Merge = true;
// 水平居中显示
worksheet.Cells[rowIndex, mergeStartCol, rowIndex, col - 1].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
}
prevValue = currentValue;
mergeStartCol = col;
}
}
// 处理最后一组相同值
if (mergeStartCol < colCount)
{
worksheet.Cells[rowIndex, mergeStartCol, rowIndex, colCount].Merge = true;
worksheet.Cells[rowIndex, mergeStartCol, rowIndex, colCount].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
}
}
4. 高级功能扩展
4.1 多列联动合并
在实际业务中,经常需要根据多列的组合值来决定是否合并。例如,在省市区三级联动中,可能需要在显示省份时合并,但当省份相同时,还要检查城市是否相同。
csharp复制public void MergeMultiColumnCells(ExcelWorksheet worksheet, int[] columnIndexes, int startRow = 1)
{
if (worksheet == null || worksheet.Dimension == null || columnIndexes == null || columnIndexes.Length == 0)
return;
int rowCount = worksheet.Dimension.Rows;
if (rowCount <= startRow) return;
// 获取第一行的值作为初始比较基准
var prevValues = columnIndexes.Select(col => worksheet.Cells[startRow, col].Text).ToArray();
int mergeStartRow = startRow;
for (int row = startRow + 1; row <= rowCount; row++)
{
bool isSame = true;
for (int i = 0; i < columnIndexes.Length; i++)
{
string currentValue = worksheet.Cells[row, columnIndexes[i]].Text;
if (currentValue != prevValues[i])
{
isSame = false;
break;
}
}
if (isSame)
{
continue;
}
else
{
if (mergeStartRow < row - 1)
{
foreach (int col in columnIndexes)
{
worksheet.Cells[mergeStartRow, col, row - 1, col].Merge = true;
worksheet.Cells[mergeStartRow, col, row - 1, col].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
}
}
// 更新比较基准
prevValues = columnIndexes.Select(col => worksheet.Cells[row, col].Text).ToArray();
mergeStartRow = row;
}
}
// 处理最后一组相同值
if (mergeStartRow < rowCount)
{
foreach (int col in columnIndexes)
{
worksheet.Cells[mergeStartRow, col, rowCount, col].Merge = true;
worksheet.Cells[mergeStartRow, col, rowCount, col].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
}
}
}
4.2 条件合并控制
有时我们可能需要根据业务规则决定是否合并,例如只合并特定值或满足特定条件的行:
csharp复制public void MergeCellsWithCondition(ExcelWorksheet worksheet, int columnIndex, Func<string, bool> condition, int startRow = 1)
{
if (worksheet == null || worksheet.Dimension == null || condition == null)
return;
int rowCount = worksheet.Dimension.Rows;
if (rowCount <= startRow) return;
string prevValue = worksheet.Cells[startRow, columnIndex].Text;
int mergeStartRow = startRow;
for (int row = startRow + 1; row <= rowCount; row++)
{
string currentValue = worksheet.Cells[row, columnIndex].Text;
if (currentValue == prevValue && condition(currentValue))
{
continue;
}
else
{
if (mergeStartRow < row - 1 && condition(prevValue))
{
worksheet.Cells[mergeStartRow, columnIndex, row - 1, columnIndex].Merge = true;
worksheet.Cells[mergeStartRow, columnIndex, row - 1, columnIndex].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
}
prevValue = currentValue;
mergeStartRow = row;
}
}
// 处理最后一组相同值
if (mergeStartRow < rowCount && condition(prevValue))
{
worksheet.Cells[mergeStartRow, columnIndex, rowCount, columnIndex].Merge = true;
worksheet.Cells[mergeStartRow, columnIndex, rowCount, columnIndex].Style.VerticalAlignment = ExcelVerticalAlignment.Center;
}
}
5. 性能优化与注意事项
5.1 大数据量处理优化
当处理数万行数据时,直接操作Excel可能会遇到性能问题。以下是几个优化建议:
-
禁用计算和屏幕更新:
csharp复制
package.Workbook.CalcMode = ExcelCalcMode.Manual; package.Workbook.FormulaParserManager.LoadFunctionModule(WorkbookFormulaParserManager.FunctionModuleType.Full); -
批量设置样式:
避免在循环中单独设置每个单元格的样式,可以先合并单元格,然后一次性设置样式。 -
分块处理:
对于极大数据集,可以考虑分块处理,例如每次处理5000行。
5.2 常见问题与解决方案
-
合并后内容显示问题:
- 现象:合并后只显示第一个单元格的内容
- 原因:Excel合并单元格的默认行为
- 解决:在合并前确保所有待合并单元格内容相同
-
边框样式丢失:
- 现象:合并后边框样式不一致
- 解决:合并后再统一设置边框样式
csharp复制var border = worksheet.Cells[mergeStartRow, columnIndex, row - 1, columnIndex].Style.Border; border.Bottom.Style = border.Top.Style = border.Left.Style = border.Right.Style = ExcelBorderStyle.Thin; -
公式引用失效:
- 现象:合并单元格导致公式引用错误
- 解决:在合并前处理好所有公式,或使用绝对引用
5.3 实际应用中的经验技巧
-
动态确定合并列:
可以通过分析数据特征自动确定哪些列需要合并,例如:csharp复制public List<int> DetectMergeColumns(ExcelWorksheet worksheet, int sampleRows = 100) { var mergeColumns = new List<int>(); if (worksheet.Dimension == null) return mergeColumns; int rowCount = Math.Min(worksheet.Dimension.Rows, sampleRows); int colCount = worksheet.Dimension.Columns; for (int col = 1; col <= colCount; col++) { string firstValue = worksheet.Cells[1, col].Text; bool canMerge = true; for (int row = 2; row <= rowCount; row++) { if (worksheet.Cells[row, col].Text != firstValue) { canMerge = false; break; } } if (canMerge) mergeColumns.Add(col); } return mergeColumns; } -
保留合并前的原始数据:
在合并操作前,建议先将原始数据保存到另一个工作表,方便后续核查。 -
添加合并标记:
可以在合并的单元格添加特殊标记,便于后续识别:csharp复制worksheet.Cells[mergeStartRow, columnIndex].AddComment("Merged cells", "System");
6. 完整示例与应用场景
6.1 销售报表导出示例
以下是一个完整的销售报表导出示例,展示如何应用自动合并功能:
csharp复制public void ExportSalesReport(List<SalesRecord> records, string filePath)
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Sales Report");
// 设置表头
worksheet.Cells[1, 1].Value = "Region";
worksheet.Cells[1, 2].Value = "City";
worksheet.Cells[1, 3].Value = "Product";
worksheet.Cells[1, 4].Value = "Sales";
worksheet.Cells[1, 5].Value = "Date";
// 填充数据
int row = 2;
foreach (var record in records.OrderBy(r => r.Region).ThenBy(r => r.City).ThenBy(r => r.Date))
{
worksheet.Cells[row, 1].Value = record.Region;
worksheet.Cells[row, 2].Value = record.City;
worksheet.Cells[row, 3].Value = record.Product;
worksheet.Cells[row, 4].Value = record.Amount;
worksheet.Cells[row, 5].Value = record.Date.ToString("yyyy-MM-dd");
row++;
}
// 自动合并
MergeVerticalCells(worksheet, 1); // 合并Region列
MergeMultiColumnCells(worksheet, new[] {1, 2}); // 联合合并Region和City列
// 设置样式
using (var range = worksheet.Cells[1, 1, 1, 5])
{
range.Style.Font.Bold = true;
range.Style.Fill.PatternType = ExcelFillStyle.Solid;
range.Style.Fill.BackgroundColor.SetColor(Color.LightGray);
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
}
// 自动调整列宽
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
// 保存文件
package.SaveAs(new FileInfo(filePath));
}
}
6.2 库存管理报表
在库存管理系统中,自动合并功能可以用于生成分类汇总报表:
csharp复制public void ExportInventoryReport(List<InventoryItem> items, string filePath)
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Inventory");
// 设置表头
worksheet.Cells[1, 1].Value = "Category";
worksheet.Cells[1, 2].Value = "SubCategory";
worksheet.Cells[1, 3].Value = "Product Code";
worksheet.Cells[1, 4].Value = "Product Name";
worksheet.Cells[1, 5].Value = "Quantity";
worksheet.Cells[1, 6].Value = "Location";
// 填充数据
int row = 2;
foreach (var item in items.OrderBy(i => i.Category).ThenBy(i => i.SubCategory))
{
worksheet.Cells[row, 1].Value = item.Category;
worksheet.Cells[row, 2].Value = item.SubCategory;
worksheet.Cells[row, 3].Value = item.ProductCode;
worksheet.Cells[row, 4].Value = item.ProductName;
worksheet.Cells[row, 5].Value = item.Quantity;
worksheet.Cells[row, 6].Value = item.Location;
row++;
}
// 自动合并
MergeVerticalCells(worksheet, 1); // 合并Category列
MergeVerticalCells(worksheet, 2); // 合并SubCategory列
MergeVerticalCells(worksheet, 6); // 合并Location列
// 条件格式:高亮低库存
var range = worksheet.Cells[2, 5, row - 1, 5];
var lowStockRule = range.ConditionalFormatting.AddExpression();
lowStockRule.Style.Fill.BackgroundColor.Color = Color.LightPink;
lowStockRule.Formula = "=E2<10";
// 保存文件
package.SaveAs(new FileInfo(filePath));
}
}
7. 测试与验证策略
7.1 单元测试设计
为确保合并功能的正确性,应编写全面的单元测试:
csharp复制[TestClass]
public class ExcelMergeTests
{
[TestMethod]
public void TestVerticalMerge()
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Test");
// 准备测试数据
worksheet.Cells[1, 1].Value = "A";
worksheet.Cells[2, 1].Value = "A";
worksheet.Cells[3, 1].Value = "B";
worksheet.Cells[4, 1].Value = "B";
worksheet.Cells[5, 1].Value = "B";
worksheet.Cells[6, 1].Value = "C";
// 执行合并
var merger = new ExcelMerger();
merger.MergeVerticalCells(worksheet, 1);
// 验证合并结果
Assert.IsTrue(worksheet.Cells["A1:A2"].Merge);
Assert.IsTrue(worksheet.Cells["A3:A5"].Merge);
Assert.IsFalse(worksheet.Cells["A6"].Merge);
}
}
[TestMethod]
public void TestHorizontalMerge()
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Test");
// 准备测试数据
worksheet.Cells[1, 1].Value = "X";
worksheet.Cells[1, 2].Value = "X";
worksheet.Cells[1, 3].Value = "Y";
worksheet.Cells[1, 4].Value = "Y";
worksheet.Cells[1, 5].Value = "Y";
worksheet.Cells[1, 6].Value = "Z";
// 执行合并
var merger = new ExcelMerger();
merger.MergeHorizontalCells(worksheet, 1);
// 验证合并结果
Assert.IsTrue(worksheet.Cells["A1:B1"].Merge);
Assert.IsTrue(worksheet.Cells["C1:E1"].Merge);
Assert.IsFalse(worksheet.Cells["F1"].Merge);
}
}
}
7.2 性能测试
对于大数据量场景,应进行性能测试:
csharp复制[TestMethod]
public void TestMergePerformance()
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Performance");
int rowCount = 100000;
int colCount = 10;
// 生成测试数据 - 每10行重复一次
for (int row = 1; row <= rowCount; row++)
{
for (int col = 1; col <= colCount; col++)
{
worksheet.Cells[row, col].Value = $"Value{(row-1)/10}";
}
}
var merger = new ExcelMerger();
var stopwatch = Stopwatch.StartNew();
// 合并所有列
for (int col = 1; col <= colCount; col++)
{
merger.MergeVerticalCells(worksheet, col);
}
stopwatch.Stop();
Console.WriteLine($"Merged {rowCount} rows x {colCount} columns in {stopwatch.ElapsedMilliseconds} ms");
Assert.IsTrue(stopwatch.ElapsedMilliseconds < 5000, "Merge operation took too long");
}
}
7.3 边界条件测试
确保代码能够处理各种边界情况:
csharp复制[TestMethod]
public void TestEdgeCases()
{
// 空工作表测试
TestMergeWithEmptyWorksheet();
// 单行数据测试
TestMergeWithSingleRow();
// 所有行相同测试
TestMergeWithAllIdenticalRows();
// 交替值测试
TestMergeWithAlternatingValues();
}
private void TestMergeWithEmptyWorksheet()
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Empty");
var merger = new ExcelMerger();
// 应不抛出异常
merger.MergeVerticalCells(worksheet, 1);
merger.MergeHorizontalCells(worksheet, 1);
}
}
private void TestMergeWithSingleRow()
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("SingleRow");
worksheet.Cells[1, 1].Value = "Test";
var merger = new ExcelMerger();
merger.MergeVerticalCells(worksheet, 1);
Assert.IsFalse(worksheet.Cells["A1"].Merge, "Single cell should not be merged");
}
}
8. 替代方案与扩展思路
8.1 使用ClosedXML实现
如果你更喜欢ClosedXML的API风格,以下是等效的实现:
csharp复制public void MergeWithClosedXML(string filePath)
{
using (var workbook = new XLWorkbook())
{
var worksheet = workbook.Worksheets.Add("Sheet1");
// 填充测试数据
worksheet.Cell(1, 1).Value = "A";
worksheet.Cell(2, 1).Value = "A";
worksheet.Cell(3, 1).Value = "B";
worksheet.Cell(4, 1).Value = "B";
// 合并单元格
worksheet.Range("A1:A2").Merge();
worksheet.Range("A3:A4").Merge();
// 设置样式
worksheet.Range("A1:A4").Style.Alignment.SetVertical(XLAlignmentVerticalValues.Center);
workbook.SaveAs(filePath);
}
}
8.2 前端实现方案
如果导出操作发生在Web应用中,也可以考虑在前端实现合并功能:
-
使用SheetJS (xlsx.js):
javascript复制function mergeCellsInJS() { const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.aoa_to_sheet([ ["Region", "Sales"], ["East", 100], ["East", 150], ["West", 200] ]); // 设置合并范围 if (!worksheet["!merges"]) worksheet["!merges"] = []; worksheet["!merges"].push({s: {r:1, c:0}, e: {r:2, c:0}}); XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1"); XLSX.writeFile(workbook, "report.xlsx"); } -
使用ExcelJS:
javascript复制async function mergeWithExcelJS() { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet("Sheet1"); worksheet.addRows([ ["Region", "Sales"], ["East", 100], ["East", 150], ["West", 200] ]); worksheet.mergeCells("A2:A3"); await workbook.xlsx.writeFile("report.xlsx"); }
8.3 与数据库结合的高级方案
对于超大数据集,可以考虑在数据库层面先进行预处理:
-
SQL Server方案:
sql复制-- 使用窗口函数标识合并范围 SELECT Region, City, Product, Sales, CASE WHEN Region = LAG(Region) OVER (ORDER BY Region, City) THEN NULL ELSE Region END AS DisplayRegion, CASE WHEN City = LAG(City) OVER (PARTITION BY Region ORDER BY City) THEN NULL ELSE City END AS DisplayCity FROM SalesData ORDER BY Region, City, Product -
在C#中处理:
csharp复制public void ExportFromDatabase(string connectionString, string filePath) { using (var connection = new SqlConnection(connectionString)) { var command = new SqlCommand(@" SELECT CASE WHEN Region = LAG(Region) OVER (ORDER BY Region, City) THEN '' ELSE Region END AS DisplayRegion, CASE WHEN City = LAG(City) OVER (PARTITION BY Region ORDER BY City) THEN '' ELSE City END AS DisplayCity, Product, Sales FROM SalesData ORDER BY Region, City, Product", connection); connection.Open(); using (var reader = command.ExecuteReader()) using (var package = new ExcelPackage()) { var worksheet = package.Workbook.Worksheets.Add("Sales"); // 设置表头 worksheet.Cells[1, 1].Value = "Region"; worksheet.Cells[1, 2].Value = "City"; worksheet.Cells[1, 3].Value = "Product"; worksheet.Cells[1, 4].Value = "Sales"; // 填充数据 int row = 2; while (reader.Read()) { worksheet.Cells[row, 1].Value = reader["DisplayRegion"]; worksheet.Cells[row, 2].Value = reader["DisplayCity"]; worksheet.Cells[row, 3].Value = reader["Product"]; worksheet.Cells[row, 4].Value = reader["Sales"]; row++; } // 自动合并空白单元格 MergeVerticalCells(worksheet, 1); MergeVerticalCells(worksheet, 2); package.SaveAs(new FileInfo(filePath)); } } }
9. 实际项目中的集成建议
9.1 封装为通用服务
在实际项目中,建议将Excel导出功能封装为独立服务:
csharp复制public interface IExcelExportService
{
byte[] ExportToExcel<T>(IEnumerable<T> data, List<ExcelColumnDefinition<T>> columns);
}
public class ExcelColumnDefinition<T>
{
public string Header { get; set; }
public Func<T, object> ValueSelector { get; set; }
public bool AutoMerge { get; set; } = false;
public string Format { get; set; }
}
public class EPPlusExcelExportService : IExcelExportService
{
public byte[] ExportToExcel<T>(IEnumerable<T> data, List<ExcelColumnDefinition<T>> columns)
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Export");
// 设置表头
for (int i = 0; i < columns.Count; i++)
{
worksheet.Cells[1, i + 1].Value = columns[i].Header;
}
// 填充数据
int row = 2;
foreach (var item in data)
{
for (int col = 0; col < columns.Count; col++)
{
var value = columns[col].ValueSelector(item);
worksheet.Cells[row, col + 1].Value = value;
if (!string.IsNullOrEmpty(columns[col].Format))
{
worksheet.Cells[row, col + 1].Style.Numberformat.Format = columns[col].Format;
}
}
row++;
}
// 自动合并
for (int col = 0; col < columns.Count; col++)
{
if (columns[col].AutoMerge)
{
MergeVerticalCells(worksheet, col + 1);
}
}
// 设置表头样式
using (var range = worksheet.Cells[1, 1, 1, columns.Count])
{
range.Style.Font.Bold = true;
range.Style.Fill.PatternType = ExcelFillStyle.Solid;
range.Style.Fill.BackgroundColor.SetColor(Color.LightGray);
}
// 自动调整列宽
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
return package.GetAsByteArray();
}
}
// 合并方法同上...
}
9.2 与ASP.NET Core集成
在Web应用中,可以通过Controller提供导出功能:
csharp复制[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
private readonly IExcelExportService _excelService;
public ReportsController(IExcelExportService excelService)
{
_excelService = excelService;
}
[HttpGet("sales")]
public IActionResult ExportSalesReport([FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate)
{
var salesData = GetSalesData(startDate, endDate); // 从数据库获取数据
var columns = new List<ExcelColumnDefinition<SalesRecord>>
{
new() { Header = "Region", ValueSelector = r => r.Region, AutoMerge = true },
new() { Header = "City", ValueSelector = r => r.City, AutoMerge = true },
new() { Header = "Product", ValueSelector = r => r.Product },
new() { Header = "Amount", ValueSelector = r => r.Amount, Format = "#,##0.00" },
new() { Header = "Date", ValueSelector = r => r.Date.ToString("yyyy-MM-dd") }
};
var excelBytes = _excelService.ExportToExcel(salesData, columns);
return File(excelBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "SalesReport.xlsx");
}
}
9.3 动态列与合并配置
更高级的方案是允许调用方动态配置哪些列需要合并:
csharp复制public class ExcelExportRequest<T>
{
public IEnumerable<T> Data { get; set; }
public List<ExcelColumnConfig> ColumnConfigs { get; set; }
public string SheetName { get; set; } = "Sheet1";
}
public class ExcelColumnConfig
{
public string PropertyName { get; set; }
public string HeaderText { get; set; }
public bool AutoMerge { get; set; }
public string Format { get; set; }
}
public byte[] DynamicExport<T>(ExcelExportRequest<T> request)
{
var propertyInfos = typeof(T).GetProperties();
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add(request.SheetName);
// 设置表头
for (int i = 0; i < request.ColumnConfigs.Count; i++)
{
worksheet.Cells[1, i + 1].Value = request.ColumnConfigs[i].HeaderText;
}
// 填充数据
int row = 2;
foreach (var item in request.Data)
{
for (int col = 0; col < request.ColumnConfigs.Count; col++)
{
var config = request.ColumnConfigs[col];
var prop = propertyInfos.FirstOrDefault(p => p.Name == config.PropertyName);
if (prop != null)
{
var value = prop.GetValue(item);
worksheet.Cells[row, col + 1].Value = value;
if (!string.IsNullOrEmpty(config.Format))
{
worksheet.Cells[row, col + 1].Style.Numberformat.Format = config.Format;
}
}
}
row++;
}
// 自动合并配置的列
for (int col = 0; col < request.ColumnConfigs.Count; col++)
{
if (request.ColumnConfigs[col].AutoMerge)
{
MergeVerticalCells(worksheet, col + 1);
}
}
return package.GetAsByteArray();
}
}
10. 总结与进阶方向
在实现Excel自动合并功能的过程中,有几个关键点值得特别注意:
-
合并时机的选择:应该在所有数据填充完成后再执行合并操作,避免在数据填充过程中频繁合并影响性能。
-
样式的处理顺序:先合并单元格,再设置样式。如果顺序颠倒,可能会导致样式应用不完整。
-
性能与内存管理:对于大型Excel文件,要注意及时释放资源,避免内存泄漏。使用
using语句确保ExcelPackage等对象被正确释放。 -
异常处理:添加适当的异常处理逻辑,特别是在文件操作和数据库访问环节。
对于想要进一步深入学习的开发者,可以考虑以下几个方向:
-
模板引擎集成:结合Razor引擎或其他模板系统,实现更复杂的Excel模板填充。
-
动态图表生成:在导出数据的同时,自动生成相应的图表和数据分析结果。
-
跨平台支持:研究如何在Linux服务器环境下使用.NET Core处理Excel文件。
-
与报表工具集成:将导出功能与专业报表工具(如Reporting Services)结合,提供更多报表选项。
-
自定义函数支持:在导出的Excel中嵌入自定义函数,实现更复杂的业务逻辑。
在实际项目中,我遇到过的一个典型问题是合并后打印分页不正确。解决方案是在合并后手动设置分页符,确保每个合并区域不会被分割到不同页面:
csharp复制// 在合并后设置分页
worksheet.PrinterSettings.RepeatRows = new ExcelAddress("1:1"); // 重复标题行
worksheet.PrinterSettings.FitToPage = true;
另一个实用技巧是为合并的单元格添加数据验证,防止用户意外修改关键分组字段:
csharp复制// 为合并列添加数据验证
var validation = worksheet.DataValidations.AddListValidation("A2:A100");
validation.ShowErrorMessage = true;
validation.ErrorTitle = "Invalid Input";
validation.Error = "This cell is merged and cannot be modified directly";
