ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WinForms中GridControl的Excel导入导出实战:从导出到导入的完整指南

WinForms中GridControl的Excel导入导出实战:从导出到导入的完整指南 1. 为什么绕不开GridControl的Excel导入导出先交代下背景。我这些年做过好几个.NET WinForms项目报表、台账、录入界面全都离不开DevExpress。刚接触这套控件的时候最常用的组件就是GridControl但也是最容易踩坑的组件。数据展示只是基本功真正让新人头疼的是客户永远会提能不能把表格导成Excel能不能把Excel导进来批量更新这类需求。于是GridControl和EXCEL的导入导出就成了必学技能。这篇文章针对的核心场景是WinForms下用DevExpress GridControl展示数据然后把GridView里的数据导出到Excel文件以及反过来把Excel文件内容读入GridView。文章里的代码基于我常用的版本DevExpress 20.2也就是VCL 10.4 Sydney同期的.NET版本但思路和API在其他版本上基本通用。适合刚接触DevExpress的初级开发也适合需要快速在项目里落地导入导出功能的在职工程师。先说明一点DevExpress的GridView自带成熟的导出API不需要你手工拼接Excel更不需要在客户机器上装Office。这个系列从设计思路、代码实现、常见坑三个角度展开争取让你看完就能直接用到项目里。2. 导出Excel之前的方案选型2.1 为什么优先用GridView自带的ExportToXlsx初次接触导入导出时很多人第一反应是找第三方库比如NPOI、EPPlus、MiniExcel。这些库确实强大但在GridControl场景下如果你只用它们就得自己遍历GridView的行列、构建二维数组、再写入Excel工作量大还容易丢格式。我自己做过一次对比把三种常见的导出GridControl数据的方案摆在一起看方案工作量格式保留能力是否要额外引包适用场景GridView.ExportToXlsx小高列宽、样式、自适应都有不需要DevExpress自带绝大多数需求NPOI手动写入大中需要自己设置样式是需要完全自定义Excel内容或合并模板OLEDB/ADODB方式小低很难控制格式是少量数据、临时工具所以正常项目的标准答案就是优先用GridView.ExportToXlsx。它底层是DevExpress的导出引擎能把GridView目前的显示状态包括列头、列宽、数值格式、过滤后的行干净地输出成xlsx文件。你自己只需要控制导出哪个Sheet文件路径是什么要不要带过滤条件。2.2 构建演示环境控件与准备工作在聊代码之前先把环境搭好。我用的是Visual Studio 2019 DevExpress 20.2新建一个WinForms项目从工具箱拖一个GridControl、两个Button到Form上布局大概是这样一个GridControl名叫 gcMain负责展示数据一个按钮叫 btnExport文字写导出Excel一个按钮叫 btnImport文字写导入Excel然后准备一个DataTable作为数据源。这里有个小建议在开发阶段就尽量模拟真实数据量别只用三五条数据测试。我一般会生成几千条因为很多问题比如导出速度、滚动卡顿是在数据量上来后才会暴露的。private DataTable GetDemoData() { DataTable dt new DataTable(); dt.Columns.Add(编号, typeof(int)); dt.Columns.Add(姓名, typeof(string)); dt.Columns.Add(部门, typeof(string)); dt.Columns.Add(工资, typeof(decimal)); dt.Columns.Add(入职日期, typeof(DateTime)); Random rand new Random(); string[] depts { 研发部, 产品部, 市场部, 人事部 }; for (int i 1; i 5000; i) { dt.Rows.Add(i, 员工 i, depts[rand.Next(depts.Length)], 5000 rand.Next(20000), DateTime.Now.AddDays(-rand.Next(1000))); } return dt; }加载到GridControl后记得设置一下列的显示格式比如工资列用{0:C2}日期列用{0:yyyy-MM-dd}。这样做不是为了好看而是导出Excel时格式会一并带过去客户打开Excel看到的是一样的效果。3. 导出Excel的完整实现从基础到进阶3.1 最简导出代码三行搞定核心流程GridControl导出Excel的最小代码量其实非常少。在按钮的Click事件里只需要下面几步private void btnExport_Click(object sender, EventArgs e) { SaveFileDialog dialog new SaveFileDialog(); dialog.Filter Excel文件|*.xlsx; dialog.FileName 导出数据 DateTime.Now.ToString(yyyyMMddHHmmss); if (dialog.ShowDialog() DialogResult.OK) { gridView1.ExportToXlsx(dialog.FileName); } }这段代码执行后当前GridView里的所有可见列、所有行包括滚动条滑到下面才显示的部分都会被导出到指定的xlsx文件。我先说结论这个写法能跑通80%的导出需求特别是内部后台管理的报表导出用户要的就是界面上看到什么Excel里就有什么。但如果你直接拿这段代码上线后续可能会遇到几个尴尬的情况用户界面上有隐藏列比如主键ID导出时却跑出来了用户在筛选框里过滤了数据结果导出的是过滤前的全部行导出的Excel列宽和屏幕显示不一致有的列被截断没有汇总行、没有标题领导看了觉得不够正式。这些都需要在基础版上做增强。3.2 核心增强控制导出列、文件名与导出选项最需要处理的是列的控制。GridControl里我们经常用gridView1.Columns操作列对象Column有一个Visible属性控制界面显示。但导出时应该尊重另一套逻辑有些列界面里要隐藏导出时要带出来比如数据库主键。这种情况下不能只依赖Visible而是要在导出前临时调整。我的习惯做法是维护一个导出的列集合private void btnExport_Click(object sender, EventArgs e) { SaveFileDialog dialog new SaveFileDialog(); dialog.Filter Excel文件|*.xlsx; dialog.FileName 员工工资表 DateTime.Now.ToString(yyyyMMdd); if (dialog.ShowDialog() ! DialogResult.OK) return; // 记录原始可见状态 DictionaryGridColumn, bool originalVisible new DictionaryGridColumn, bool(); foreach (GridColumn col in gridView1.Columns) { originalVisible[col] col.Visible; } // 只导出业务需要的列比如把ID列也带出去 gridView1.Columns[编号].Visible true; gridView1.Columns[姓名].Visible true; gridView1.Columns[部门].Visible true; gridView1.Columns[工资].Visible true; gridView1.Columns[入职日期].Visible true; // 导出选项xlsx格式设置标题 XlsxExportOptions options new XlsxExportOptions(); options.TextExportMode TextExportMode.Value; // 按值导出避免公式/文本混杂 options.ExportType DevExpress.Export.ExportType.DataAware; try { gridView1.ExportToXlsx(dialog.FileName, options); } finally { // 恢复原始可见状态避免影响界面 foreach (var pair in originalVisible) { pair.Key.Visible pair.Value; } } }这段代码里有个容易被忽视的地方finally里恢复Visible状态。如果不恢复导出一次后界面上的列就乱了。再说说TextExportMode这个参数在DevExpress里很关键。默认是Value按单元格的实际值导出比如日期字段导出后就是日期数字就是数字。如果设成Text则按控件里显示的文本导出比如日期可能变成了2025-01-01这种字符串类型信息就丢了。一般推荐Value模式Excel里还能继续做日期计算和筛选。3.3 只导出筛选后的数据如果用户在GridView的自动筛选行里输入了条件比如部门选研发部、工资大于10000此刻界面上显示的就是过滤后的结果。但ExportToXlsx默认导出的是GridView里被过滤后的数据因为GridView内部的DataRowCount等于过滤后的行数。实测下来GridView导出是会自动遵循当前过滤状态的不需要额外处理。真正需要注意的反而是分页视图的情况。如果你的项目用了GridView的PagingBehavior或者套了分页控件那么默认导出只会导出当前页。这种情况下的通用做法是弹窗提示用户当前导出仅包含当前页是否继续或者临时取消分页后再导出。我遇到过客户拿着只导出一页的Excel来问为什么数据不全后来在代码里加了二次确认if (gridView1.RowCount 10000) { if (MessageBox.Show(当前数据量较大是否只导出当前筛选结果, 提示, MessageBoxButtons.YesNo, MessageBoxIcon.Question) DialogResult.Yes) { gridView1.ExportToXlsx(dialog.FileName, options); } else { // 取消分页、导出全部再恢复分页 } }3.4 导出时附带汇总统计信息很多报表型页面GridView底部会用GridSummaryItem显示合计、平均值。默认导出Excel的时候汇总行是不会带出去的。如果客户需要汇总数据进Excel我有两个方案方案一GridView自身带OptionsView.ShowFooter然后把gridView1.OptionsView.ShowFooter置为true再设置footerSummary。但导出时可以将汇总项作为一个新的Sheet导出也可以直接用Excel公式在导出的文件里加一行合计。方案二推荐用GridView.CopyToClipboard配合Excel公式的方式或者干脆在导出代码里操作gridView1.Rows追加汇总行到DataTable后再导出。这种方式更直观但操作繁琐。我自己的项目中用得最多的方案是在导出前临时创建一张全局DataTable第一行写表头中间行写数据最后一行用TEXTJOIN或SUM公式生成合计。但因为Excel公式要在文件中才能计算直接拼字符串到单元格只是公式文本不算合格方案。真要实现得用DevExpress的XlExport做公式支持。这里我不过度展开因为常规业务需求只要GridView的导出能带上格式化后的数据就已经解决了90%的问题。汇总行的需求属于锦上添花需要模板二开时再深入。4. 导入Excel没有现成API怎么优雅地完成4.1 明确痛点为什么不能直接LoadFromExcel很多刚接触的人会问DevExpress有没有类似gridView.ImportFromExcel的方法坦诚地讲官方没有提供与Export对称的一键导入API。GridView的数据源是内存对象Excel只是外部文件二者之间没有天然的映射关系。所以导入的真实路径是读Excel文件 - 把数据塞进DataTable/List - 再绑定到GridControl。那读Excel这一步怎么做我对比过两种主流做法用DevExpress自带的SpreadsheetControl组件它可以打开Excel文件并能访问工作表单元格。但单独引入一个控件只是为导入有点重。用OLEDB方式把Excel当数据库查询。写法简单但引擎老旧而且对xlsx的支持依赖本机Office组件版本经常出现未在本地计算机上注册Microsoft.ACE.OLEDB.12.0这种问题。考虑到读者大多是普通WinForms开发我推荐两条路如果对文件格式要求不高、数据量中等用NPOI读如果希望轻量级而且不引第三方包可以用DevExpress自带的SpreadsheetControl打开后再读。这里我以NPOI为例写完整的导入流程因为它在工业界用得最多也最好排查问题。4.2 用NPOI读取Excel并填充GridControl首先在NuGet里安装NPOI然后写核心方法private DataTable ReadExcelToDataTable(string filePath) { DataTable dt new DataTable(); IWorkbook workbook; using (FileStream fs File.OpenRead(filePath)) { if (filePath.EndsWith(.xlsx)) workbook new XSSFWorkbook(fs); else if (filePath.EndsWith(.xls)) workbook new HSSFWorkbook(fs); else throw new Exception(不支持的文件格式请选择.xlsx或.xls); } ISheet sheet workbook.GetSheetAt(0); // 默认读第一个Sheet if (sheet null) return dt; // 第一行作为列头 IRow headerRow sheet.GetRow(0); if (headerRow null) return dt; for (int i 0; i headerRow.LastCellNum; i) { string colName headerRow.GetCell(i)?.ToString() ?? 列 i; dt.Columns.Add(colName); } // 从第二行开始读数据 for (int rowIdx 1; rowIdx sheet.LastRowNum; rowIdx) { IRow row sheet.GetRow(rowIdx); if (row null) continue; DataRow dr dt.NewRow(); for (int colIdx 0; colIdx dt.Columns.Count; colIdx) { ICell cell row.GetCell(colIdx); dr[colIdx] cell null ? : GetCellValue(cell); } dt.Rows.Add(dr); } return dt; } private object GetCellValue(ICell cell) { switch (cell.CellType) { case CellType.Numeric: if (DateUtil.IsCellDateFormatted(cell)) return cell.DateCellValue; return cell.NumericCellValue; case CellType.Boolean: return cell.BooleanCellValue; case CellType.Formula: return cell.StringCellValue; // 这里要看公式缓存值 default: return cell.StringCellValue; } }方法写好后导入按钮事件就非常简单private void btnImport_Click(object sender, EventArgs e) { OpenFileDialog dialog new OpenFileDialog(); dialog.Filter Excel文件|*.xlsx;*.xls; if (dialog.ShowDialog() ! DialogResult.OK) return; try { DataTable dt ReadExcelToDataTable(dialog.FileName); gcMain.DataSource dt; } catch (Exception ex) { MessageBox.Show(导入失败 ex.Message); } }4.3 数据校验与类型转换的边界问题导入功能不是读出来塞进去就完事最容易出问题的环节是数据类型转换。举例Excel里工资列可能有的单元格是数值有的因为排班格式变成了字符串12000.00还有的带了千分位逗号12,000。NPOI读取时NumericCellValue不会带格式符号但是StringCellValue就可能带着逗号、货币符号甚至前后空格。如果DataTable的列类型定义成了decimal直接塞字符串就会抛异常。我的处理策略分两层第一层导入时统一走字符串。上面ReadExcelToDataTable里直接给DataTable加string类型列读什么都先转成字符串绑定到GridControl后再由GridControl自己去做展示。这样能保证导入流程不中断Excel里的任何脏数据都不会让整个导入崩溃。第二层业务层按需做校验。如果导入的数据要真正写入数据库那么删除GridControl里的临时DataTable、换成强类型的ListEntity读Excel时逐行转实体遇到转换失败就记录错误行号全部处理完后再统一入库。这种方法更稳妥也让用户可以知道第几行有问题。我早期做导入功能时图省事直接绑定DataTable后就让用户编辑结果用户改了之后一保存日期字段和数字字段各种格式问题后来痛定思痛导入与保存必须分两步走中间加一层ListT或真正的DataTable强类型校验。5. 高频问题排查与性能优化记录5.1 导出后Excel中文乱码、公式丢失如何解决先看中文乱码。用ExportToXlsx导出的文件正常情况下中文不会乱码因为内部就是UTF-8编码的xlsx。如果客户反馈乱码一般是由于他们用WPS或者老版Excel打开导致字体无法识别。解决办法是导出之前设置字体options.ExportHyperlinks true; options.Formula DevExpress.XtraExport.Options.ExportFormulaType.None;另外老用户用.xls格式导出时因为xls是老格式编码支持没有xlsx好建议默认统一导出.xlsx把SaveFileDialog的Filter强制成Excel文件|*.xlsx。再谈公式丢失。如果你在GridView里写了未绑定的计算列UnboundColumn导出时默认会导出计算后的值但Excel里不会保留公式关系。这是DevExpress的行为它导出的就是静态值。如果需要Excel里有公式只能在导出后操作Excel文件本身比如用Excel VBA或NPOI再写一次公式这一般不是开发阶段第一优先级。5.2 大数据量导出卡死的优化方案GridView导出Excel在数据量上百条时看不出差别但如果像我演示代码里那样5000行直接调用ExportToXlsx还是会有明显卡顿尤其是主线程上执行时界面直接假死几秒。原因很简单ExportToXlsx内部是同步生成整个文件期间主线程被占用。我的优化方案有三种异步执行导出逻辑放到await Task.Run()里避免阻塞UI。这个方法最简单也最快见效。分批导出如果数据量达到5万、10万行就算异步执行文件生成也要很久。这时需要分批读取源数据写入多个临时文件再合并。但在多数公司内部系统里5万行已经是极端情况。优化导出引擎选项把options.SheetName设置成短字符、options.RightToLeftDocument设为false等可以减少内部处理开销。实测中用await Task.Run(() gridView1.ExportToXlsx(file, options))处理1万行数据从之前的界面卡死5秒变成了后台生成用户体验提升非常明显。private async void btnExport_Click(object sender, EventArgs e) { SaveFileDialog dialog new SaveFileDialog(); dialog.Filter Excel文件|*.xlsx; if (dialog.ShowDialog() ! DialogResult.OK) return; string fileName dialog.FileName; this.Enabled false; // 防止用户重复点击 try { await Task.Run(() { gridView1.ExportToXlsx(fileName, new XlsxExportOptions()); }); MessageBox.Show(导出完成 fileName); } catch (Exception ex) { MessageBox.Show(导出失败 ex.Message); } finally { this.Enabled true; } }5.3 导入时最常见的5个坑整理一个速查表基本覆盖导入功能上线后用户反馈的问题现象原因解决方案打开xlsx报文件格式不正确SaveFileDialog过滤了xls但用户手动改了后缀统一用OpenFileDialog只允许.xlsx/.xls不信任用户手输读到的日期变成数字没有用DateUtil.IsCellDateFormatted判断用上面GetCellValue里的判断逻辑数字列显示成1.234568E17Excel里数字是科学计数法NPOI拿的是double读数字时统一用cell.NumericCellValue显示时格式化导入5000行很慢NPOI逐行创建DataRow确实慢可以把Excel读取部分放到Task.Run里配合进度条导入后GridControl列头乱掉因为DataTable列顺序和Excel不一致维护一张列映射表按UI的列顺序取值这里单独说下列映射。很多导入场景里Excel的表头不是编号、姓名、部门而是工号、姓名、所属部门中文名和字段名完全对不上。如果直接把Excel的第一行当列头绑定到GridControl后面上是一片乱。正规的做法是给GridControl的列设置FieldName为程序内的字段名然后在读Excel时按表头文字映射到对应的FieldName上。这样即使Excel模板变了只要映射表跟着改程序不用动。5.4 导出性能再进阶1万行以上如何保证可用除了异步还有几个我实际用过的技巧第一导出前关闭GridView的重绘。在ExportToXlsx大文件时控件会不断刷新界面状态虽然我看不到滚动条动但内部重绘很耗资源。可以在导出前调gridView1.BeginUpdate()导出完成后调gridView1.EndUpdate()。第二把不需要的列先隐藏。导出的数据列越多生成的XML节点越多耗时越长。在导出临时改动Visible后再恢复就能显著减少生成时间。第三如果允许直接用GridView的SaveToStream。对于非常极端的场景可以自己做一个CustomExport事件手动控制每一行的写入放弃默认样式只保留数据和表头。比如void gridView1_CustomExport(object sender, DevExpress.XtraGrid.Views.Base.CustomExportEventArgs e) { // e.RowHandle、e.Column 可以自定义导出行为 }这个事件在大数据量、需要特定格式时有奇效但学习成本高。初学阶段建议先用前面几个技巧等确实需要高自由度的定制时再深入。6. 记录一次完整的实战导入导出的流程整合讲了这么多原理和代码最后我分享一个完整的小项目流程是我在一套企业内部人事管理工具里实际用过的实现思路。这套工具的需求就是界面上展示员工信息支持一键导出工资表Excel也支持从Excel批量导入工资调整记录。整个流程分成四步第一步界面布局。顶部一排按钮主体是GridControl。GridView开启OptionsView.ShowAutoFilterRow让每列都有筛选框支持用户筛选后再导出。Footer启用汇总行工资列显示平均值和总和。第二步数据加载。页面加载时从数据库读取员工表绑定到GridControl。绑定后动态生成未绑定列比如年龄算是入职日期计算出来的。未绑定列也可以导出到ExcelDevExpress会先计算出值再导出。第三步导出Excel。调用ExportToXlsx保存文件名为员工花名册_当前日期。用户如果在筛选框输入了研发部Excel只导出研发部数据。这个行为用户测试后很认可说和我们看到的一致。第四步导入Excel。格式化好的Excel里有编号、姓名、新工资、生效日期四列。用户用我们的模板填好后点导入。程序先读Excel到DataTable再逐行校验编号是否存在、新工资是否为数字校验通过的行才更新到数据库不通过的行汇总成错误报告显示给用户。这套流程上线后最大的价值是导出让数据走得出导入让数据进得来。客户不用再自己复制粘贴到Excel里加工也不用一条条在页面上手改。我在实际做这个项目时最深的体会是GridControl的导入导出代码本身不复杂真正花时间的永远是边界情况——用户Excel里多了一列怎么办、日期格式混了怎么办、数据量大导出卡死怎么办。这些坑只有做一遍才能记住。如果你正准备做类似功能我的建议是先把最简单的导出流程跑通再逐步加上筛选导出、汇总行、列映射和异步处理。每一步都不难但每一步都会让功能离好用更近一点。对了最后留一个小技巧开发时如果不知道某个GridView的导出选项怎么用直接把XlsxExportOptions对象new出来按下点号看智能提示里每个属性的注释。DevExpress的注释很全很多时候比看文档还直观。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进