在C#开发中,处理Excel文件的需求非常普遍。你可能遇到过需要将WinForms应用中DataGridView控件展示的数据导出为Excel报表的场景。传统方式可能会考虑使用微软的Office Interop组件,但这种方式需要安装Office软件,性能较差且容易出错。而EPPlus作为一款开源库,完全用C#编写,不依赖Office,处理速度更快,功能也更加强大。
我曾在多个项目中尝试过不同的Excel处理方案,最终发现EPPlus在以下场景表现尤为出色:
在Visual Studio中安装EPPlus非常简单。我推荐使用NuGet包管理器,这是最稳妥的方式:
安装完成后,你会在项目引用中看到EPPlus的dll文件。建议在代码文件顶部添加using语句:
csharp复制using OfficeOpenXml;
using OfficeOpenXml.Table;
EPPlus 5.0+版本引入了许可证验证机制。虽然对个人开发者免费,但需要明确声明使用场景。我通常在程序启动时(如Main方法或Form构造函数)添加这行代码:
csharp复制ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
如果是商业项目,需要购买商业许可证。这个设置只需要执行一次,建议放在应用程序初始化阶段。
将DataGridView数据导出到Excel的第一步是将其转换为DataTable。下面是我经过多次优化后的转换代码:
csharp复制private DataTable ConvertDataGridViewToDataTable(DataGridView dgv)
{
var dt = new DataTable();
// 添加列头
foreach (DataGridViewColumn column in dgv.Columns)
{
// 处理可能的空列名
string columnName = string.IsNullOrEmpty(column.HeaderText)
? $"Column{dgv.Columns.IndexOf(column)}"
: column.HeaderText;
dt.Columns.Add(columnName);
}
// 添加行数据
foreach (DataGridViewRow row in dgv.Rows)
{
// 跳过新行(未提交的行)
if (!row.IsNewRow)
{
DataRow dr = dt.NewRow();
for (int j = 0; j < dgv.Columns.Count; j++)
{
// 处理空单元格值
dr[j] = row.Cells[j].Value ?? DBNull.Value;
}
dt.Rows.Add(dr);
}
}
return dt;
}
这段代码有几个关键改进点:
当处理大型DataGridView时(超过1万行),转换性能可能成为瓶颈。我通过以下方式显著提升了转换速度:
dgv.SuspendLayout(),完成后调用dgv.ResumeLayout()优化后的代码如下:
csharp复制private DataTable ConvertLargeDataGridViewToDataTable(DataGridView dgv)
{
dgv.SuspendLayout();
try
{
var dt = new DataTable();
// 预定义列
foreach (DataGridViewColumn column in dgv.Columns)
{
dt.Columns.Add(column.HeaderText);
}
// 预分配行空间
dt.BeginLoadData();
dt.MinimumCapacity = dgv.Rows.Count;
// 并行处理行
var rows = new DataRow[dgv.Rows.Count - 1]; // 减去新行
Parallel.For(0, dgv.Rows.Count - 1, i =>
{
if (!dgv.Rows[i].IsNewRow)
{
DataRow dr = dt.NewRow();
for (int j = 0; j < dgv.Columns.Count; j++)
{
dr[j] = dgv.Rows[i].Cells[j].Value ?? DBNull.Value;
}
rows[i] = dr;
}
});
dt.Rows.AddRange(rows.Where(r => r != null).ToArray());
dt.EndLoadData();
return dt;
}
finally
{
dgv.ResumeLayout();
}
}
有了DataTable后,我们可以使用EPPlus创建Excel文件。下面是一个完整的导出方法:
csharp复制public void ExportToExcel(DataTable data, string filePath)
{
// 验证输入
if (data == null || data.Rows.Count == 0)
throw new ArgumentException("数据表不能为空");
// 初始化Excel包
using (var package = new ExcelPackage())
{
// 添加工作表
var worksheet = package.Workbook.Worksheets.Add("Sheet1");
// 加载数据
worksheet.Cells["A1"].LoadFromDataTable(data, true);
// 自动调整列宽
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
// 设置表头样式
using (var range = worksheet.Cells[1, 1, 1, data.Columns.Count])
{
range.Style.Font.Bold = true;
range.Style.Fill.PatternType = ExcelFillStyle.Solid;
range.Style.Fill.BackgroundColor.SetColor(Color.LightGray);
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
}
// 保存文件
package.SaveAs(new FileInfo(filePath));
}
}
这个方法实现了:
要让导出的Excel更专业,可以添加更多格式设置:
csharp复制// 添加在LoadFromDataTable之后
// 设置交替行颜色
var allCells = worksheet.Cells[worksheet.Dimension.Address];
var table = worksheet.Tables.Add(allCells, "DataTable");
table.TableStyle = TableStyles.Medium9;
// 设置数字格式
for (int i = 0; i < data.Columns.Count; i++)
{
if (data.Columns[i].DataType == typeof(DateTime))
{
worksheet.Column(i + 1).Style.Numberformat.Format = "yyyy-mm-dd hh:mm:ss";
}
else if (data.Columns[i].DataType == typeof(decimal))
{
worksheet.Column(i + 1).Style.Numberformat.Format = "#,##0.00";
}
}
// 冻结首行
worksheet.View.FreezePanes(2, 1);
我推荐使用以下方法生成导出路径,避免硬编码:
csharp复制public string GenerateExportPath(string baseFolder = null, string fileName = null)
{
// 默认基础路径:我的文档下的子文件夹
baseFolder = baseFolder ?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"AppExports");
// 按日期创建子文件夹
var datePath = DateTime.Now.ToString("yyyy\\\\MM\\\\dd");
var fullPath = Path.Combine(baseFolder, datePath);
// 确保目录存在
Directory.CreateDirectory(fullPath);
// 生成文件名
fileName = fileName ?? $"Export_{DateTime.Now:HHmmss}.xlsx";
return Path.Combine(fullPath, fileName);
}
这个方法会自动创建按日期组织的文件夹结构,如:
C:\Users\用户名\Documents\AppExports\2023\06\15\Export_143022.xlsx
在实际应用中,必须考虑各种异常情况:
csharp复制public bool SafeExportToExcel(DataTable data, out string filePath)
{
filePath = null;
try
{
filePath = GenerateExportPath();
// 检查文件是否已存在
if (File.Exists(filePath))
{
// 尝试删除旧文件
File.Delete(filePath);
}
ExportToExcel(data, filePath);
return true;
}
catch (IOException ioEx)
{
// 文件被占用等情况
filePath = GenerateExportPath(fileName: $"Export_{DateTime.Now:HHmmss}_retry.xlsx");
try
{
ExportToExcel(data, filePath);
return true;
}
catch
{
return false;
}
}
catch (Exception ex)
{
// 记录日志等操作
return false;
}
}
当数据量很大时(超过10万行),内存可能成为瓶颈。我采用分块处理策略:
csharp复制public void ExportLargeDataToExcel(DataTable data, string filePath, int chunkSize = 50000)
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Data");
// 写入表头
for (int i = 0; i < data.Columns.Count; i++)
{
worksheet.Cells[1, i + 1].Value = data.Columns[i].ColumnName;
}
// 分块写入数据
int totalRows = data.Rows.Count;
for (int chunkStart = 0; chunkStart < totalRows; chunkStart += chunkSize)
{
int chunkEnd = Math.Min(chunkStart + chunkSize, totalRows);
int chunkLength = chunkEnd - chunkStart;
// 写入一个数据块
for (int i = 0; i < chunkLength; i++)
{
DataRow row = data.Rows[chunkStart + i];
for (int j = 0; j < data.Columns.Count; j++)
{
worksheet.Cells[i + 2 + chunkStart, j + 1].Value = row[j];
}
}
// 定期刷新内存
if (chunkStart % (chunkSize * 5) == 0)
{
package.Save();
}
}
package.SaveAs(new FileInfo(filePath));
}
}
EPPlus支持创建专业图表和数据透视表:
csharp复制public void ExportWithChart(DataTable data, string filePath)
{
using (var package = new ExcelPackage())
{
// 添加数据工作表
var dataSheet = package.Workbook.Worksheets.Add("Data");
dataSheet.Cells["A1"].LoadFromDataTable(data, true);
// 添加图表工作表
var chartSheet = package.Workbook.Worksheets.Add("Charts");
// 创建柱状图
var chart = chartSheet.Drawings.AddChart("SalesChart", eChartType.ColumnClustered);
chart.Title.Text = "销售数据统计";
// 设置数据范围
var series = chart.Series.Add(
dataSheet.Cells[2, 2, data.Rows.Count + 1, 2], // Y轴数据
dataSheet.Cells[2, 1, data.Rows.Count + 1, 1]); // X轴数据
// 设置图表位置和大小
chart.SetPosition(1, 0, 3, 0);
chart.SetSize(800, 400);
package.SaveAs(new FileInfo(filePath));
}
}
在处理大量数据时,我发现这些技巧特别有用:
csharp复制worksheet.View.ShowFormulas = false;
worksheet.Calculate();
csharp复制// 差的做法
for(int i=1; i<=100; i++)
{
worksheet.Cells[i, 1].Value = i;
}
// 好的做法
var range = worksheet.Cells[1, 1, 100, 1];
range.LoadFromEnumerable(Enumerable.Range(1, 100));
对于特别大的导出任务,可以使用后台线程避免UI冻结:
csharp复制private async void btnExport_Click(object sender, EventArgs e)
{
btnExport.Enabled = false;
try
{
var data = ConvertDataGridViewToDataTable(dataGridView1);
await Task.Run(() =>
{
string filePath;
if (SafeExportToExcel(data, out filePath))
{
// 需要在UI线程显示消息
this.Invoke((MethodInvoker)delegate {
MessageBox.Show($"导出成功!文件已保存到:\n{filePath}");
});
}
});
}
finally
{
btnExport.Enabled = true;
}
}
在实际项目中,我遇到过各种导出问题,以下是几个典型场景的解决方法:
问题1:导出速度慢
问题2:内存不足异常
问题3:格式丢失
csharp复制worksheet.Column(1).Style.Numberformat.Format = "@"; // 文本格式
问题4:特殊字符导致文件损坏
csharp复制string safeValue = Regex.Replace(input, @"[\u0000-\u001F]", string.Empty);
问题5:导出后文件无法打开