1. 从零开始:为什么选择 Interop 来操作 Excel?
如果你是一名 C# 开发者,需要处理 Excel 文件,你可能会在 NuGet 上看到一堆眼花缭乱的库:EPPlus、NPOI、ClosedXML,还有我们今天要聊的Microsoft.Office.Interop.Excel。在开始敲代码之前,我们得先搞清楚,为什么在 2024 年,我们还要讨论这个看起来有点“古老”的技术?
简单来说,Microsoft.Office.Interop.Excel(后面我们简称 Interop)是微软官方提供的一套 COM 互操作程序集。它不是一个独立的 DLL,而是一套让你能在 C# 代码里,像使用 VBA 一样,几乎完全控制 Excel 这个桌面应用程序的桥梁。这意味着,你在 VBA 里能做的所有事情——从打开文件、读写单元格、设置格式、创建图表,到运行宏、调用 Excel 内置函数——通过 Interop 都能在 C# 里实现。
那么,它的核心优势是什么?场景的完整性和操作的底层性。当你需要生成一个格式极其复杂、带有大量自定义样式、条件格式、数据验证、甚至内嵌图表和控件的报表模板时,EPPlus 或 ClosedXML 可能会让你抓狂,因为它们对高级格式的支持是有限的,或者语法完全不同。而 Interop 允许你直接调用 Excel 对象模型,你录制的宏代码几乎可以原封不动地翻译成 C#。另一个典型场景是“所见即所得”的模板填充:你的业务部门已经用 Excel 做好了一个精美的、带有复杂公式和格式的报表模板,你只需要用程序往里面填数据,然后保存。用 Interop 打开这个模板文件,填充数据,保存,格式和公式完美保留。这是其他轻量级库难以媲美的。
当然,它的劣势也同样明显,而且非常致命:它严重依赖本地安装的、完整版本的 Microsoft Excel。你的服务器或客户端电脑上必须安装 Excel(通常是专业版或以上),这意味着它无法在 Linux 服务器或无 GUI 的服务器核心版上运行。其次,它是COM(组件对象模型)技术,如果你不妥善管理资源,著名的“Excel 进程残留”问题会让你痛不欲生——即使你的程序退出了,Excel.exe 进程还在后台运行,占用内存,直到耗尽系统资源。此外,它的性能在处理海量数据时也不如那些纯 .NET 的库。
所以,我的建议是:把 Interop 当作一把“手术刀”,而不是“柴刀”。当你面对的是复杂的、格式驱动的、需要与 Excel 深度交互的桌面端或特定服务器任务时,它是无可替代的利器。但对于简单的数据导出导入、在 Web 服务器上批量生成报表,请优先考虑 EPPlus、NPOI 这类纯托管库。
2. 环境搭建与第一个“Hello World”程序
理论说再多,不如动手跑一遍。我们先来把环境搭起来,写一个最简单的程序,感受一下 Interop 的工作流程。
2.1 项目创建与引用添加
首先,创建一个新的 C# 控制台应用程序项目(.NET Framework 或 .NET Core/.NET 5+ 都行,但 Interop 是 COM 组件,在非 Windows 环境下无法使用,所以本质上还是 Windows 应用)。我建议使用 .NET Framework 4.7.2 或以上,或者 .NET 6/8 的 Windows 桌面应用,兼容性最好。
接下来,添加对 Interop 程序集的引用。这里有个关键点:不要直接在 NuGet 里搜Microsoft.Office.Interop.Excel并安装那个“主互操作程序集(PIA)”。那个包版本可能很旧,且在某些新环境下配置复杂。微软更推荐的方式是使用“嵌入互操作类型”。
正确的添加步骤:
- 在 Visual Studio 的“解决方案资源管理器”中,右键点击你的项目 -> “添加” -> “COM 引用”。
- 在弹出的“COM 引用”对话框中,在列表里找到并勾选“Microsoft Excel 16.0 Object Library”(版本号可能因你安装的 Office 版本而异,如 15.0 对应 Office 2013,14.0 对应 Office 2010)。
- 点击“确定”。
Visual Studio 会自动为你生成一个互操作程序集(Interop.Excel.dll)的引用。此时,在引用列表里,找到这个Microsoft.Office.Interop.Excel引用,右键点击它,选择“属性”。在属性窗口中,将“嵌入互操作类型”设置为True。这个操作非常重要,它会把必要的类型信息编译进你的程序集,避免了在目标机器上还需要注册 PIA 的麻烦,简化了部署。
现在,你可以在代码文件顶部添加using Excel = Microsoft.Office.Interop.Excel;来使用一个简短的别名,让代码更清晰。
2.2 核心对象模型初窥
Interop 编程的核心是理解 Excel 的对象模型,它是一个层次化的结构:
- Application: 代表整个 Excel 应用程序。你可以启动它、设置全局属性(如是否显示界面)、退出它。
- Workbook: 代表一个 Excel 工作簿文件(.xlsx, .xls 等)。
- Worksheet: 代表工作簿中的一个工作表。
- Range: 这是最常用、最核心的对象,代表一个或多个单元格。可以是一个单元格(如
A1),一个区域(如A1:B10),一行,一列,甚至整个工作表。
2.3 第一个程序:创建、写入、保存
让我们写一个经典的程序:创建一个新工作簿,在 A1 单元格写入“Hello Excel from C#!”,然后保存到桌面。
using System; using Excel = Microsoft.Office.Interop.Excel; namespace ExcelInteropDemo { class Program { static void Main(string[] args) { // 声明核心对象,初始化为null Excel.Application excelApp = null; Excel.Workbook workbook = null; Excel.Worksheet worksheet = null; try { // 1. 启动 Excel 应用程序 excelApp = new Excel.Application(); // 可选:让Excel在后台运行,不显示界面。对于自动化处理,建议设置为False。 excelApp.Visible = false; // 可选:关闭警告提示(如覆盖文件) excelApp.DisplayAlerts = false; // 2. 创建一个新的工作簿(默认会带一个工作表) workbook = excelApp.Workbooks.Add(); // 获取第一个工作表(索引从1开始) worksheet = (Excel.Worksheet)workbook.Worksheets[1]; // 3. 操作单元格 Excel.Range rangeA1 = worksheet.Range["A1"]; rangeA1.Value2 = "Hello Excel from C#!"; // 也可以直接写:worksheet.Cells[1, 1] = "Hello..."; // 4. 保存工作簿 string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); string filePath = System.IO.Path.Combine(desktopPath, "MyFirstInterop.xlsx"); workbook.SaveAs(filePath); Console.WriteLine($"文件已保存至:{filePath}"); } catch (Exception ex) { Console.WriteLine($"操作失败:{ex.Message}"); } finally { // 5. !!!最重要的部分:妥善释放资源!!! if (workbook != null) { workbook.Close(false); // false表示不保存更改(因为我们已经SaveAs了) System.Runtime.InteropServices.Marshal.ReleaseComObject(workbook); } if (excelApp != null) { excelApp.Quit(); System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp); } // 强制垃圾回收,帮助清理COM引用(非必需,但有时有帮助) GC.Collect(); GC.WaitForPendingFinalizers(); // 对于worksheet和range等对象,如果局部使用,也应在使用后Release。 // 但更佳实践是避免对中间对象(如worksheet.Range)进行多次引用和Release,容易出错。 // 一个简化策略是:只确保Application和Workbook这两个顶级对象被正确关闭和Release。 } Console.ReadKey(); } } }运行这个程序,你应该能在桌面看到一个名为MyFirstInterop.xlsx的文件,打开后 A1 单元格正是我们写入的内容。注意,因为设置了excelApp.Visible = false,整个过程在后台静默完成,你不会看到 Excel 窗口闪一下。
注意:上面代码的
finally块是 Interop 编程的“生命线”。不按规则释放 COM 对象,Excel 进程就会像幽灵一样留在内存中。ReleaseComObject是减少 COM 引用计数的标准方式。调用Quit()和Close()是告诉 Excel 关闭。两者结合使用才稳妥。
3. 核心操作详解:读写、格式与公式
掌握了基本流程后,我们来深入最常用的操作。这部分内容会非常具体,你可以像查字典一样回来翻阅。
3.1 单元格与区域的读写
读写数据是基础中的基础。Range对象的Value或Value2属性是最常用的。
// 假设 worksheet 是一个有效的 Worksheet 对象 Excel.Range targetCell = worksheet.Cells[5, 3]; // 第5行,第3列(即C5) targetCell.Value2 = 123.45; // 写入数字 targetCell.Value2 = DateTime.Now; // 写入日期时间 targetCell.Value2 = "这是一个字符串"; // 写入文本 // 读取数据 object cellValue = targetCell.Value2; // Value2 返回 object,需要根据实际情况转换 if (cellValue != null) { Console.WriteLine($"C5 的值是:{cellValue.ToString()}"); } // 操作一个区域 Excel.Range dataRange = worksheet.Range["A1:D10"]; // 一次性写入一个二维数组(效率远高于循环写入单个单元格) object[,] dataArray = new object[10, 4]; for (int i = 0; i < 10; i++) { for (int j = 0; j < 4; j++) { dataArray[i, j] = $"Row{i+1}-Col{j+1}"; } } dataRange.Value2 = dataArray; // 一次性读取一个区域到二维数组 object[,] readBackArray = (object[,])dataRange.Value2;为什么用Value2而不是Value?Value属性返回的是object类型,但 Excel 会尝试将其包装为一个特定的object类型。Value2则直接返回底层值(对于数字是double,对于字符串是string,等等),不进行任何货币或日期格式的转换,性能稍好,且是微软推荐用于读取和写入值的主要属性。除非你需要获取单元格的格式化文本(Text属性),否则优先使用Value2。
3.2 单元格格式设置
让报表看起来专业,格式设置必不可少。
Excel.Range headerRange = worksheet.Range["A1:D1"]; // 1. 合并单元格 headerRange.Merge(); headerRange.Value2 = "销售数据报表"; headerRange.HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter; headerRange.VerticalAlignment = Excel.XlVAlign.xlVAlignCenter; // 2. 字体 headerRange.Font.Name = "微软雅黑"; headerRange.Font.Size = 14; headerRange.Font.Bold = true; headerRange.Font.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.White); // 3. 填充(背景色) headerRange.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.DarkBlue); headerRange.Interior.Pattern = Excel.XlPattern.xlPatternSolid; // 4. 边框 headerRange.Borders.LineStyle = Excel.XlLineStyle.xlContinuous; headerRange.Borders.Weight = Excel.XlBorderWeight.xlThin; headerRange.Borders.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Black); // 5. 数字格式 Excel.Range numberRange = worksheet.Range["C2:C100"]; numberRange.NumberFormat = "#,##0.00_);[Red](#,##0.00)"; // 会计格式,负数红色 Excel.Range dateRange = worksheet.Range["B2:B100"]; dateRange.NumberFormat = "yyyy-mm-dd"; // 日期格式3.3 公式与函数
Interop 的强大之处在于可以直接操作 Excel 公式。
// 在 D2 单元格写入一个求和公式 worksheet.Range["D2"].Formula = "=SUM(A2:C2)"; // 使用 R1C1 引用样式(相对引用) worksheet.Range["D3"].FormulaR1C1 = "=SUM(RC[-3]:RC[-1])"; // 对当前行左边三列到左边一列求和 // 写入数组公式(旧版本Excel,如.xls) // worksheet.Range["E2:E10"].FormulaArray = "=A2:A10*B2:B10"; // 计算工作簿中的所有公式 workbook.Calculate(); // 或者计算特定工作表 // worksheet.Calculate(); // 读取公式的结果(不是公式字符串本身) object formulaResult = worksheet.Range["D2"].Value2; Console.WriteLine($"D2 公式的结果是:{formulaResult}"); // 如果你想获取公式字符串本身,用 .Formula 属性 string formulaString = worksheet.Range["D2"].Formula;3.4 行、列与工作表操作
// 插入行/列 Excel.Range row5 = worksheet.Rows[5]; row5.Insert(Excel.XlInsertShiftDirection.xlShiftDown); // 在第5行插入一行,原第5行下移 Excel.Range columnC = worksheet.Columns["C"]; columnC.Insert(Excel.XlInsertShiftDirection.xlShiftToRight); // 在C列插入一列 // 删除行/列 worksheet.Rows["10:15"].Delete(); // 删除10到15行 worksheet.Columns["H"].Delete(); // 删除H列 // 调整列宽行高 worksheet.Columns["A:D"].AutoFit(); // A到D列自动调整宽度 worksheet.Rows[1].RowHeight = 30; // 设置第一行行高为30磅 worksheet.Columns["E"].ColumnWidth = 15; // 设置E列列宽为15个字符宽度 // 操作工作表 Excel.Worksheet newSheet = (Excel.Worksheet)workbook.Worksheets.Add(); // 新增工作表 newSheet.Name = "数据分析"; // 重命名 Excel.Worksheet firstSheet = (Excel.Worksheet)workbook.Worksheets[1]; firstSheet.Delete(); // 删除第一个工作表(系统会提示,如果 DisplayAlerts=false 则直接删)4. 高级技巧与实战中的“坑”
掌握了基本操作,可以应付大部分场景。但要写出健壮、高效的 Interop 代码,你必须了解下面这些高级技巧和避坑指南。
4.1 资源管理与进程清理的“正确姿势”
这是 Interop 编程的头号难题。前面示例中的try...finally块是基础,但还不够完善。当你在循环中创建大量Range对象时,问题会更复杂。
最佳实践原则:为每个显式创建的 COM 对象调用Marshal.ReleaseComObject,并确保在 finally 块中执行。但要注意顺序:先关闭子对象(Workbook),再关闭父对象(Application)。
一个更稳健的辅助方法:
public static void ReleaseComObject(object obj) { if (obj != null) { try { System.Runtime.InteropServices.Marshal.ReleaseComObject(obj); } catch { } // 忽略释放异常 finally { obj = null; } } } // 在 finally 块中这样用 finally { if (range != null) ReleaseComObject(range); if (worksheet != null) ReleaseComObject(worksheet); if (workbook != null) { try { workbook.Close(false); } catch { } ReleaseComObject(workbook); } if (excelApp != null) { try { excelApp.Quit(); } catch { } ReleaseComObject(excelApp); } GC.Collect(); GC.WaitForPendingFinalizers(); }如何检查 Excel 进程是否残留?运行你的程序后,打开任务管理器,查看是否有EXCEL.EXE进程。如果有,并且你的程序已经结束,说明资源释放有问题。一个可靠的测试方法是:在短时间内(比如一个循环里)多次运行你的 Interop 代码,观察任务管理器中的 Excel 进程数量是否持续增长。
4.2 性能优化:减少交互,批量操作
与 COM 的每一次跨进程调用都是有开销的。最影响性能的操作是在循环中逐个读写单元格。
反面教材(极慢):
for (int i = 1; i <= 10000; i++) { worksheet.Cells[i, 1].Value2 = i; // 每次循环都是一次COM调用 }正确做法(极快):
object[,] data = new object[10000, 1]; for (int i = 0; i < 10000; i++) { data[i, 0] = i + 1; } Excel.Range bigRange = worksheet.Range[worksheet.Cells[1, 1], worksheet.Cells[10000, 1]]; bigRange.Value2 = data; // 一次COM调用完成全部写入同理,读取大量数据时,也应一次性读入一个数组 (object[,]),然后在内存中处理。
4.3 事件处理与回调
Interop 允许你订阅 Excel 的事件,例如工作表变更、工作簿保存等。这在你需要与用户交互或实现复杂自动化时很有用。
// 声明事件变量 private Excel.Application excelApp; private void SetupEventHandlers() { excelApp = new Excel.Application(); excelApp.Visible = true; // 订阅工作簿打开事件 excelApp.WorkbookOpen += (Excel.Workbook Wb) => { Console.WriteLine($"工作簿 {Wb.Name} 被打开了。"); }; // 订阅单元格选择变更事件(需要获取特定的Worksheet对象) Excel.Workbook wb = excelApp.Workbooks.Add(); Excel.Worksheet ws = (Excel.Worksheet)wb.Worksheets[1]; ws.SelectionChange += (Excel.Range Target) => { Console.WriteLine($"你选中了:{Target.Address}"); }; }注意:事件处理会增加代码复杂度,并且必须注意在程序退出前取消订阅事件,否则可能导致内存泄漏或无法正常关闭 Excel。
4.4 处理已存在的文件与模板
这是 Interop 的强项。假设你有一个设计好的模板ReportTemplate.xlsx,里面已经设置好了所有格式、公式、图表位置。
string templatePath = @"C:\Templates\ReportTemplate.xlsx"; string outputPath = @"C:\Reports\FinalReport_20240515.xlsx"; Excel.Workbook templateWorkbook = excelApp.Workbooks.Open(templatePath); Excel.Worksheet dataSheet = (Excel.Worksheet)templateWorkbook.Worksheets["Data"]; // 找到模板中预设的数据起始位置(例如,从B5开始填充) int startRow = 5; int startCol = 2; // B列 // 假设你有一个数据列表 List<SalesRecord> records = GetSalesData(); for (int i = 0; i < records.Count; i++) { dataSheet.Cells[startRow + i, startCol].Value2 = records[i].ProductName; dataSheet.Cells[startRow + i, startCol + 1].Value2 = records[i].Amount; // ... 填充其他字段 } // 填充后,公式会自动计算(如果模板里有引用这些数据的公式) // 例如,模板的“Summary”工作表里有一个SUM公式引用了Data!C5:C100,现在它就会得到正确结果。 // 另存为新文件,不破坏原模板 templateWorkbook.SaveAs(outputPath); templateWorkbook.Close(false); // 关闭模板工作簿,不保存更改这种方式完美分离了格式设计(由业务人员在 Excel 中完成)和数据处理(由开发人员在代码中完成),非常灵活。
4.5 常见错误与调试技巧
HRESULT: 0x800A03EC错误:这通常是一个通用错误,可能原因很多。最常见的是文件路径无效、文件被占用、或尝试访问不存在的工作表/区域。仔细检查路径字符串,确保文件未被其他进程(包括另一个 Excel 实例)以独占方式打开。- “远程过程调用失败” (RPC_E_SERVERFAULT):这通常是由于未正确释放 COM 对象,导致 Excel 进程状态异常。严格遵循资源释放模式是预防的关键。
- 权限问题:在 IIS 或 Windows 服务中运行 Interop 代码时,默认的应用程序池身份(如
ApplicationPoolIdentity)可能没有权限启动桌面应用程序(Excel)。这需要配置 DCOM 权限,非常复杂且不推荐。强烈不建议在 Web 服务器或无 UI 会话的服务中使用 Interop。这是它最致命的部署限制。 - 使用
dynamic类型:为了简化代码,有些人喜欢用dynamic。虽然写起来方便(不需要类型转换),但你会失去编译时类型检查和 IntelliSense 支持,并且性能略有下降。对于大型项目,建议使用显式类型。 - 调试时查看 Excel 对象:在调试模式下,你可以在“即时窗口”或“监视窗口”中查看 Interop 对象的属性。例如,输入
?worksheet.Name可以查看工作表名。这有助于理解对象当前的状态。
5. 替代方案浅析与选型建议
虽然本文聚焦 Interop,但一个全面的开发者应该知道其他选择。这里简单对比一下:
EPPlus(开源,纯 .NET): 目前 .NET 平台处理 Open XML 格式 (.xlsx) 的事实标准。无需安装 Office,性能好,功能强大,支持图表、数据验证、条件格式等大部分高级特性。对于绝大多数服务器端生成和读取 .xlsx 文件的需求,这是首选。缺点是对 .xls (旧格式) 支持有限,对 Excel 某些极其复杂的特性(如某些类型的控件、宏)不支持。
NPOI(开源,纯 .NET): 源自 Java 的 POI 项目。同时支持读写旧的 .xls (HSSF) 和新的 .xlsx (XSSF) 格式。功能也非常全面,社区活跃。与 EPPlus 定位类似,是另一个优秀的备选。两者在功能上各有千秋,选择哪个更多是个人或团队偏好。
ClosedXML(开源,纯 .NET): 建立在 Open XML SDK 之上,提供了更友好、更类似 LINQ 的 API。它的口号是“让操作 Excel 文件变得简单”。如果你觉得 EPPlus 或 NPOI 的 API 不够直观,可以试试 ClosedXML。
选型决策树:
- 需求:在服务器(尤其是无 GUI 的 Linux/Windows Server Core)上生成/读取 Excel 文件。
- 答案:毫不犹豫,选择EPPlus或NPOI。不要考虑 Interop。
- 需求:客户端 WinForms/WPF 应用,需要打开一个现有的、带有复杂宏和 ActiveX 控件的 .xlsm 模板文件,填充数据,并允许用户交互式编辑后保存。
- 答案:Interop可能是唯一可行的选择。因为你需要 Excel 应用程序本身来运行宏和承载控件。
- 需求:桌面应用,需要将数据导出为格式精美、带有复杂条件格式和图表的数据报表。
- 评估:如果格式复杂度一般,EPPlus可以胜任,且部署更简单。如果格式极其复杂,或者业务方提供了现成的、无法轻易用代码重现的模板,则Interop更合适。
- 需求:批量转换大量 Excel 文件(如 .xls 转 .xlsx),或进行一些简单的数据提取。
- 答案:优先尝试NPOI(因为它支持 .xls)。如果 NPOI 处理不了某些特性,再考虑用Interop作为后备方案,但必须处理好资源释放和可能的环境依赖。
6. 一个完整的实战案例:销售数据报表生成器
让我们把前面所有的知识串联起来,构建一个稍微复杂点的例子。场景是:从一个数据库(这里用模拟数据)读取销售记录,填充到一个设计好的模板中,进行一些计算和格式美化,然后保存并尝试用 Excel 打开它。
using System; using System.Collections.Generic; using System.Drawing; using Excel = Microsoft.Office.Interop.Excel; namespace SalesReportGenerator { class Program { public class SalesRecord { public string Date { get; set; } public string Region { get; set; } public string Product { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal TotalAmount => Quantity * UnitPrice; } static void Main(string[] args) { Excel.Application excelApp = null; Excel.Workbook workbook = null; Excel.Worksheet dataSheet = null; try { // === 1. 初始化 Excel === excelApp = new Excel.Application(); excelApp.Visible = true; // 这次我们让它可见,方便观察 excelApp.DisplayAlerts = false; // === 2. 打开模板文件 === string templatePath = @"C:\Temp\SalesReportTemplate.xlsx"; // 请确保此文件存在 // 如果模板不存在,我们就创建一个新工作簿并构建基本结构 if (!System.IO.File.Exists(templatePath)) { Console.WriteLine("模板未找到,创建新工作簿..."); workbook = excelApp.Workbooks.Add(); dataSheet = (Excel.Worksheet)workbook.Worksheets[1]; dataSheet.Name = "SalesData"; BuildTemplateStructure(dataSheet); } else { workbook = excelApp.Workbooks.Open(templatePath); dataSheet = (Excel.Worksheet)workbook.Worksheets["SalesData"]; if (dataSheet == null) { throw new Exception("模板中未找到名为 'SalesData' 的工作表。"); } } // === 3. 模拟获取数据 === List<SalesRecord> salesData = GenerateMockData(); // === 4. 清空旧数据(从第4行开始,保留表头)=== Excel.Range oldDataRange = dataSheet.Range["A4"].EntireRow.Resize[salesData.Count + 5, 6]; // 多清几行 oldDataRange.ClearContents(); oldDataRange.ClearFormats(); // === 5. 写入新数据 === int startRow = 4; // 数据起始行 for (int i = 0; i < salesData.Count; i++) { SalesRecord record = salesData[i]; int currentRow = startRow + i; dataSheet.Cells[currentRow, 1].Value2 = record.Date; dataSheet.Cells[currentRow, 2].Value2 = record.Region; dataSheet.Cells[currentRow, 3].Value2 = record.Product; dataSheet.Cells[currentRow, 4].Value2 = record.Quantity; dataSheet.Cells[currentRow, 5].Value2 = record.UnitPrice; // 总金额列(F列)我们让Excel公式去计算,体现模板的灵活性 dataSheet.Cells[currentRow, 6].FormulaR1C1 = $"=RC[-2]*RC[-1]"; // =D列 * E列 } // === 6. 应用格式 === int lastDataRow = startRow + salesData.Count - 1; Excel.Range dataBlock = dataSheet.Range[$"A{startRow}:F{lastDataRow}"]; // 设置边框 dataBlock.Borders.LineStyle = Excel.XlLineStyle.xlContinuous; dataBlock.Borders.Weight = Excel.XlBorderWeight.xlThin; // 设置货币格式 dataSheet.Range[$"E{startRow}:E{lastDataRow}"].NumberFormat = "#,##0.00"; dataSheet.Range[$"F{startRow}:F{lastDataRow}"].NumberFormat = "#,##0.00"; // 数量列居中对齐 dataSheet.Range[$"D{startRow}:D{lastDataRow}"].HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter; // === 7. 添加汇总行 === int summaryRow = lastDataRow + 2; dataSheet.Cells[summaryRow, 5].Value2 = "总计:"; dataSheet.Cells[summaryRow, 5].Font.Bold = true; dataSheet.Cells[summaryRow, 6].Formula = $"=SUM(F{startRow}:F{lastDataRow})"; dataSheet.Cells[summaryRow, 6].NumberFormat = "#,##0.00"; dataSheet.Cells[summaryRow, 6].Font.Bold = true; dataSheet.Cells[summaryRow, 6].Interior.Color = ColorTranslator.ToOle(Color.LightYellow); // === 8. 自动调整列宽 === dataSheet.Columns["A:F"].AutoFit(); // === 9. 保存文件 === string outputPath = @"C:\Temp\SalesReport_Generated_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".xlsx"; workbook.SaveAs(outputPath); Console.WriteLine($"报表已生成:{outputPath}"); // 可选:激活工作表,让用户看到 dataSheet.Activate(); } catch (Exception ex) { Console.WriteLine($"生成报表时出错:{ex.Message}"); Console.WriteLine(ex.StackTrace); } finally { // === 10. 资源清理 === // 注意:因为 excelApp.Visible = true,我们不会立即关闭Excel,让用户查看。 // 在实际自动化任务中,通常设置为 false 并在 finally 中关闭。 // 这里我们只释放对象引用,让用户手动关闭Excel窗口。 if (dataSheet != null) System.Runtime.InteropServices.Marshal.ReleaseComObject(dataSheet); // workbook 和 excelApp 由于用户可能还在查看,暂不关闭和Release。 // 在实际无头任务中,必须关闭。 // if (workbook != null) { workbook.Close(false); ReleaseComObject(workbook); } // if (excelApp != null) { excelApp.Quit(); ReleaseComObject(excelApp); } // GC.Collect(); GC.WaitForPendingFinalizers(); } Console.WriteLine("按任意键退出程序..."); Console.ReadKey(); // 程序退出后,需要用户手动关闭Excel窗口。在实际服务中,这是不允许的。 } static void BuildTemplateStructure(Excel.Worksheet ws) { // 创建表头 string[] headers = { "日期", "区域", "产品", "数量", "单价", "总金额" }; Excel.Range headerRange = ws.Range["A3:F3"]; for (int i = 0; i < headers.Length; i++) { ws.Cells[3, i + 1].Value2 = headers[i]; } headerRange.Font.Bold = true; headerRange.Interior.Color = ColorTranslator.ToOle(Color.LightGray); headerRange.HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter; } static List<SalesRecord> GenerateMockData() { var records = new List<SalesRecord>(); var rand = new Random(); string[] regions = { "华东", "华北", "华南", "西部" }; string[] products = { "产品A", "产品B", "产品C", "产品D" }; for (int i = 0; i < 50; i++) { records.Add(new SalesRecord { Date = DateTime.Now.AddDays(-rand.Next(30)).ToString("yyyy-MM-dd"), Region = regions[rand.Next(regions.Length)], Product = products[rand.Next(products.Length)], Quantity = rand.Next(1, 100), UnitPrice = rand.Next(50, 500) }); } return records; } } }这个案例演示了从模拟数据源到生成格式化报表的完整流程,涵盖了打开/创建文件、批量数据操作、公式设置、格式美化等核心环节。你可以看到,通过合理组织代码,Interop 能够完成相当复杂的报表生成任务。
最后,记住 Interop 是一把强大的双刃剑。它赋予你控制 Excel 的终极能力,但也带来了环境依赖和资源管理的复杂性。在启动一个涉及 Interop 的新项目前,务必再次评估:是否真的非它不可?如果答案是否定的,那么 EPPlus 或 NPOI 会是让你睡得更安稳的选择。如果答案是肯定的,那么希望这篇详尽的指南,能帮你避开路上的那些坑,顺利抵达目的地。