ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GridControl与Excel导入导出全攻略:绑定、导出、导入与避坑

GridControl与Excel导入导出全攻略:绑定、导出、导入与避坑 1. 为什么我最终选了GridControl做Excel导出而不是手动拼数据先说个背景。我之前维护过一套WinForms老项目表格用的是原生DataGridView导出Excel的方式是NPOI一个个单元格填值。功能是能跑但维护成本很高客户每个季度提一次改列需求我就要在导出代码里调一次列顺序、改一次列宽、处理一次合并单元格。某天我接手一个基于DevExpress的新模块发现表格组件换成了GridControl让我眼前一亮的不只是界面好看而是它在导入导出这条链路上已经内置了非常成熟的数据通道。GridControl是DevExpress全家桶里最常用的数据表格控件之一底层基于GridView或LayoutView、CardView等视图。它的核心价值在于你只需要把DataSource绑定好剩下的展示、排序、筛选、分组、汇总、分页全部交给控件自己处理。而导出Excel这件事更是DevExpress做得相当顺手的一环。这篇博文我就围绕GridControl和Excel之间的导入导出把整个流程从环境准备、代码实现到踩坑经验完完整整过一遍。内容主要针对WinForms平台语言是C#。如果你是刚接触DevExpress的开发者或者是已经在用但一直靠“另存为”手工导Excel的人这篇文章正好能帮你把这块能力补上。我先把话放前面很多人在这个功能上第一反应是去循环遍历行、列再用第三方Excel库写文件。这个思路不是不行但属于在错误的层次上解决问题。GridControl本身已经把数据封装成了结构化的DataSource直接用官方API走一遍导入导出代码量能少一半而且能天然保持一致格式。2. 开发前的准备工作控件版本、工具箱和数据源2.1 DevExpress版本选择与工程引用我用的是DevExpress 21.2的WinForms版本NuGet上直接搜DevExpress.WinForms.Grid就能拿到。需要说明的是DevExpress的授权机制是按开发者收费的如果你在公司项目中使用请确保项目组已经购买了相应的授权或者你使用的是有授权的试用版本。版本不同API命名空间有细微差异但核心的导出方法从15.x到23.x几乎没变过也就是说本文里的代码在大多数版本上都能直接编译通过。新建一个WinForms项目之后工具栏里会出现DevExpress相关的组件。如果没有你需要在GAC里找到DevExpress.XtraGrid.v21.2.dll和DevExpress.XtraExport.v21.2.dll手动拖到工具箱。这两个程序集非常重要前者是GridControl本体后者是导出Excel的实现依赖。2.2 一个最小可运行的数据源绑定示例我先给出一段最基础的数据绑定代码后面所有的演示都建立在这个基础上using System.Data; using DevExpress.XtraGrid; // 创建DataTable作为数据源 DataTable dt new DataTable(Orders); dt.Columns.Add(OrderId, typeof(int)); dt.Columns.Add(CustomerName, typeof(string)); dt.Columns.Add(Amount, typeof(decimal)); dt.Columns.Add(OrderDate, typeof(DateTime)); // 塞入几条测试数据 dt.Rows.Add(1001, 张三, 299.5m, DateTime.Now.AddDays(-3)); dt.Rows.Add(1002, 李四, 1299.0m, DateTime.Now.AddDays(-2)); dt.Rows.Add(1003, 王五, 568.0m, DateTime.Now.AddDays(-1)); // 绑定到GridControl gridControl1.DataSource dt;这里有个很多人第一次用会犯的错误绑定DataTable之后Columns的顺序不一定是你在DataTable里定义的顺序。GridView默认按列名排序如果你要控制显示顺序需要在GridView的Columns集合里手动指定VisibleIndexgridView1.Columns[CustomerName].VisibleIndex 0; gridView1.Columns[Amount].VisibleIndex 1; gridView1.Columns[OrderDate].VisibleIndex 2; gridView1.Columns[OrderId].VisibleIndex 3;实测下来这一步在导出Excel时特别关键。因为导出功能是以GridView的列设置为准而不是以DataTable的列顺序为准。如果这里不调整导出的Excel列顺序就会和界面不一致。另外还有一点如果你不想在界面上显示某列但又希望它出现在导出的Excel里可以用gridView1.Columns[xxx].Visible false来隐藏界面显示。导出时控件仍会按照GridView的列集合去处理只是你需要额外设置导出选项让隐藏列也参与导出这一点我后面会细说。2.3 为什么建议用DataTable而不是List作为DataSource很多人习惯把GridControl的DataSource绑定成一个ListT代码写起来是方便比如gridControl1.DataSource orderList; // ListOrder但一旦你后面的逻辑需要动态增删列、修改列类型、或者直接把Excel数据反推回DataTableList 就显得很僵硬。Excel本身就是一种二维表结构用DataTable来承接它是最自然的映射。我这几个项目下来结论很明确只要是涉及GridControl和Excel交互的场景数据源一律用DataTable这样在导入Excel时也可以直接用dt.Load(reader)来填充两边格式天然对齐。3. 导出到Excel的三种玩法与我的实测心得3.1 官方原生导出最简单的一种方式DevExpress为GridControl预置了多种导出方式可以直接调用gridControl1.ExportToXlsx(path)完成导出。这是一行代码版本非常适合快速给客户演示using DevExpress.XtraExport; using DevExpress.XtraSpreadsheet; string filePath C:\Exports\订单数据.xlsx; gridControl1.ExportToXlsx(filePath);执行完之后打开Excel文件你能看到界面上的数据、列宽、标题都已经同步过去了。但这只是最浅层的用法。如果GridView里做了分组、汇总、筛选默认导出只会导出筛选后的数据分组状态不会保留。如果客户要求导出的Excel里也要有分组合计你需要额外处理。3.2 更精细的导出通过GridView操作在实际项目中我更喜欢用gridView.ExportToXlsx系列方法因为它可以配合XlsxExportOptions做更细粒度的控制。比如设置Sheet名称、是否导出隐藏列、是否保留视图样式等using DevExpress.XtraExport; using DevExpress.XtraSpreadsheet; string filePath C:\Exports\订单数据.xlsx; XlsxExportOptions options new XlsxExportOptions(); options.SheetName 订单明细; options.ExportMode ExportMode.SingleFile; // 所有数据导出到一个Sheet options.ShowGridLines true; options.TextExportMode TextExportMode.Text; gridView1.ExportToXlsx(filePath, options);这里有个非常关键的参数ExportMode。它有两个值SingleFile所有Row合并导出一个Sheet适合常规数据表。SingleFilePageByPage每页一个Sheet适合按分页拆分的报表。如果你的GridView开启了分页而客户希望导出的Excel也是每页一张表用SingleFilePageByPage最合适。否则用默认的单Sheet模式。当然还有两种常见导出格式gridView1.ExportToXls(filePath); // 旧版xls格式兼容老Excel gridView1.ExportToPdf(filePath); // PDF格式这个不是Excel但也很常用通常来说如果客户没有强制的旧格式要求统一用xlsx就好。xls格式对行数和样式支持都有限制碰到大表或复杂样式容易出现数据截断。另外还有一个我经常用来快速交付的API——gridView1.ExportToExcelOld。这个方法是旧版专有的导出实现会调用内置的Excel导出引擎速度快但样式控制粒度较粗。如果只是导一张没有特殊格式的表用它也完全没问题。3.3 所见即所得导出需要处理隐藏列和栏位宽度这是我在实际业务里碰到的一个高频问题界面上明明设置了一个隐藏列Visible false导出Excel后不显示客户说丢了数据。要解决这个问题需要一个开关gridView1.OptionsPrint.ExpandAllGroups true; ExportOptions.ExportRawData true; // 导出原始数据而不是视图数据ExportRawData这个参数会让导出操作跳过GridView的显示逻辑直接将DataSource里的完整数据写进Excel。但要注意一旦开启这个选项GridView上做的排序、筛选就不会影响导出结果。如果你希望“导出内容等于用户当前看到的内容”就不要开启它。列宽的问题也需要专门设置。默认导出时Excel的列宽是根据GridView里的宽度来定的但如果你用代码动态调整过GridView列宽或者GridView里的列是自适应宽度的导出的Excel列宽可能很窄。解决方法是在导出前手动设置GridView的列宽gridView1.Columns[Amount].Width 120; gridView1.Columns[CustomerName].Width 180;如果需要更省事的做法可以在导出前调用gridView1.BestFitColumns();这个函数会根据内容自动调整GridView各列的最优宽度然后你再导出Excel列宽就会自然适应。不过要注意BestFitColumns本身是个界面操作在数据量特别大比如超过5万行的时候会有一瞬间卡顿建议配合BeginUpdate和EndUpdate使用。3.4 导出到剪贴板我屡试不爽的隐藏技能除了直接保存为文件DevExpress还支持把GridView内容复制到剪贴板这个功能在实际交付中意外的好用。我接过一个需求客户不想生成文件只希望点击按钮后能直接粘贴到微信聊天窗口或者邮件里。当时我直接用gridView1.SelectAll()gridView1.CopyToClipboard()实现了。gridView1.SelectAll(); gridView1.CopyToClipboard();这里有一个小坑如果你之前用gridControl1.DataSource dt绑定数据执行SelectAll之前GridView必须已经完成数据加载。如果有异步加载的情况要先用gridView1.BeginDataUpdate()和gridView1.EndDataUpdate()包住数据填充逻辑。更精细的剪贴板操作可以控制是否复制列头、是否带上格式标记DevExpress.Utils.ClipboardHelper.CopyToClipboard(gridView1, true, true);两个布尔参数分别代表“是否包含列头”和“是否包含格式信息”。实测下来如果只是贴到纯文本框或聊天窗口里格式信息可以置为false不然会带上一堆HTML标签。4. 导入Excel到GridControl从文件流到网格显示的完整链路4.1 用OpenFileDialog选文件别忽略Filter和CheckFileExists导入Excel的第一步是让用户选择一个Excel文件。常规写法是using System.Windows.Forms; OpenFileDialog dlg new OpenFileDialog(); dlg.Filter Excel文件|*.xlsx;*.xls; dlg.CheckFileExists true; dlg.Multiselect false; if (dlg.ShowDialog() ! DialogResult.OK) return; string fileName dlg.FileName;这里有两个容易忽略的细节。第一个是CheckFileExists true。如果不设置用户输入一个不存在的路径时后续的读取操作会抛出FileNotFoundException而且是在你处理完一堆前置逻辑之后才炸影响体验。第二个是Filter里同时包含.xlsx和.xls。旧版DevExpress的导入库对.xls支持良好但如果你的DevExpress版本较老打开高版本Excel保存的.xlsx文件可能会识别失败。建议在Filter里同时给这两个选项。4.2 使用官方库把Excel读成DataSet我的标准方案DevExpress要导入Excel到GridControl标准的做法是先把Excel内容读入DataSet再把DataSet里的Table作为GridControl的DataSource。这样做的原因是一个Excel文件可能包含多个Sheet一个Sheet自然对应一个DataTable用DataSet可以自然映射这种结构。我用的是DevExpress.XtraSpreadsheet中的SpreadsheetControl来读取因为它在读取格式兼容性上表现更好而且不依赖Office是否安装using DevExpress.XtraSpreadsheet; using DevExpress.XtraSpreadsheet.Import; DataSet ds new DataSet(); using (FileStream stream new FileStream(fileName, FileMode.Open, FileAccess.Read)) { SpreadsheetControl spreadsheet new SpreadsheetControl(); spreadsheet.LoadDocument(stream, DevExpress.XtraSpreadsheet.DocumentFormat.Xlsx); for (int i 0; i spreadsheet.Document.Sheets.Count; i) { Worksheet sheet spreadsheet.Document.Sheets[i]; DataTable dt sheet.CreateDataTable(0, 0, sheet.GetUsedRange().LastRowIndex, sheet.GetUsedRange().LastColumnIndex, true); dt.TableName sheet.Name; ds.Tables.Add(dt); } }这段代码读入整个工作簿并遍历每个Sheet逻辑很清晰。其中CreateDataTable的五个参数分别是起始行索引、起始列索引、结束行索引、结束列索引、是否将第一行作为列名。我在多个项目中测试过这种方式对常规的数据表格比如财务台账、订单明细读取十分稳定。但有一个限制它默认把第一行当作列头如果你的Excel第一行是文件标题第二行才是真正的列头那就需要把起始行索引改为1然后再手动删掉DataTable里多余的空行。或者你也可以用一个更轻量的做法直接用OleDB或者NPOI来读取Excel再转入DataTable但这个做法需要额外引入第三方依赖库我当时为了减少项目引用数量就采用了DevExpress自带方案。4.3 把DataSet绑定到GridControl几个关键操作读取完Excel之后绑定这一步其实简单但有一个细节要注意如果DataSet里有多张Table直接绑定整个DataSet会让用户面对表名列表。绝大多数业务场景只需要第一个Sheet所以我会默认取第一张表if (ds.Tables.Count 0) { gridControl1.DataSource ds.Tables[0]; }如果你确实要在界面上提供一张下拉框让用户选择Sheet可以做一个ComboBox绑定ds.Tables的TableName。这种情况下要注意切换Sheet时的数据状态清理最干净的方式是先把gridControl1.DataSource置为null再绑定新的TablegridControl1.DataSource null; gridControl1.DataSource ds.Tables[comboBoxSheet.SelectedIndex];直接把gridControl1.DataSource newTable也不是不行但某些旧版本会残留之前的View状态偶尔出现列重复或样式错乱。稳妥起见先置空再赋值。4.4 导入时最容易踩的坑类型推断和空值Excel导入最让人头疼的问题不是读不出来而是读出来之后类型不对。Excel本身是弱类型存储一列里哪怕只混入一个文本型数字整个列在CreateDataTable时就会推断成string导致后期排序、合计全部出问题。我遇到过一个真实情况导入员工工资表“工资金额”这一列有3000多行数字偏偏Excel里某一行的数值格式是文本导入后整列变成字符串界面汇总时Sum函数直接失效。解决办法有两个路线路线一读取时设置单元格格式。在Excel里全选该列设置单元格格式为数值保存后再导入。路线二在代码里做类型修正。读取之后遍历该列的DataRow用decimal.TryParse转换如果全部能转就手动把该列类型改为decimal。路线二更通用我一般这样处理DataColumn col dt.Columns[Amount]; if (col.DataType ! typeof(decimal)) { bool canConvert dt.AsEnumerable() .All(row row[col] DBNull.Value || decimal.TryParse(row[col].ToString(), out _)); if (canConvert) { DataTable newDt dt.Clone(); newDt.Columns[Amount].DataType typeof(decimal); foreach (DataRow row in dt.Rows) { decimal val 0; decimal.TryParse(row[Amount].ToString(), out val); newDt.Rows.Add(row.ItemArray.Select((item, i) i col.Ordinal ? (object)val : item).ToArray()); } dt newDt; } }还有一个空值问题Excel里的空单元格在DataTable里可能是DBNull也可能是空字符串取决于读取引擎。如果你后面的逻辑会对空值做运算比如合计某列建议提前统一处理foreach (DataRow row in dt.Rows) { foreach (DataColumn col in dt.Columns) { if (row[col] DBNull.Value) row[col] 0; } }这个操作在数据量大时会有点慢如果数据行数超过10万建议用dt.BeginLoadData()和dt.EndLoadData()包住。当然如果你不想用DevExpress自带的SpreadsheetControl做读取也可以走老路子用OleDbstring connStr ProviderMicrosoft.ACE.OLEDB.12.0;Data Source fileName ;Extended PropertiesExcel 12.0;HDRYES;; using (OleDbConnection conn new OleDbConnection(connStr)) { conn.Open(); DataTable sheetTable conn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null); string firstSheet sheetTable.Rows[0][TABLE_NAME].ToString(); OleDbDataAdapter adapter new OleDbDataAdapter(SELECT * FROM [ firstSheet ], conn); adapter.Fill(dt); }这个方案代码量更少但它有一个致命伤它依赖本机安装了Microsoft ACE OLE DB驱动而且64位/32位匹配容易出问题。如果客户的机器是精简版Office这条路子就跑不通。所以我的建议是能用DevExpress自带方案就用自带方案尽量少引入外部依赖。5. 倒腾几个小时的坑导入后更新数据时我学到的事5.1 明明导入了界面不刷新怎么办有时候你完成了导入代码也没有报错但GridControl界面就是没变化。这时候先检查DataSource有没有真的更新。我遇到过一次是因为我没有把gridControl1.DataSource重新赋值而是用((DataTable)gridControl1.DataSource).ImportRows(newRows)去追加数据。ImportRows确实修改了DataTable的内容但GridView没感知到这个变更界面自然不会更新。这时候需要手动通知一下gridView1.RefreshData();如果是完全替换数据源不一定需要RefreshData但如果你是在原有Table上追加或修改行就必须调用这个方法。5.2 从Excel导入后拖拽列宽排序全乱了这是另一个让我印象深刻的案例。导入大表后我gridControl1.DataSource dt界面上所有列自动生成列宽乱七八糟排序状态也没有。原因是GridView之前已经存在它的缓存列设置和新数据源的字段没有一一对应。解决办法是在切换DataSource之前先重置ViewgridView1.ClearColumnsFilter(); gridControl1.DataSource null; gridControl1.DataSource dt;如果你的场景是“DataTable的列结构每次可能不同”更彻底的做法是清空所有列后重建gridView1.Columns.Clear(); gridControl1.DataSource dt;这样GridView会重新根据DataSource的结构生成列。5.3 大数据量导入时的卡顿处理Excel里三四万行数据在界面上直接显示GridControl本身是虚拟化渲染不会卡死但如果你在导入后执行了gridView1.BestFitColumns()那就会有一段时间的CPU飙升因为控件要遍历所有行来计算最佳宽度。我的解决策略是大数据量导入后不做自动列宽适配而是采用固定列宽。只有在小数据量下才做BestFit。另外导入大数据时建议用挂起更新的方式gridView1.BeginUpdate(); try { gridControl1.DataSource dt; } finally { gridView1.EndUpdate(); }BeginUpdate/EndUpdate这个组合能避免在设置DataSource的过程中触发多次重复重绘体感差别非常明显。5.4 导入的日期变成数字一个最多人问的问题Excel里的日期在内部是用序列号存储的如果读取时没有做类型转换GridView里可能显示一串类似44927的数字这其实是距离1900年1月1日的天数。解决这个问题的关键字在于CustomColumnDisplayText事件gridView1.CustomColumnDisplayText (s, e) { if (e.Column.FieldName OrderDate e.Value ! null e.Value is double) { DateTime date DateTime.FromOADate((double)e.Value); e.DisplayText date.ToString(yyyy-MM-dd); } };这只是一个显示层的修正。如果要让数据本身变成DateTime类型还得回到DataTable里做列类型转换显示层修正只是治标。如果你只是给客户查看用显示层修正就够用了。6. 我的建议这些代码到底该怎么组织6.1 封装成通用工具类每次都要在按钮事件里写一堆导入导出代码很快你就会厌烦。更合理的做法是把这些逻辑抽到一个公共类里我命名它为ExcelHelperpublic static class ExcelHelper { public static void ExportGridViewToXlsx(GridView gridView, string filePath, string sheetName Sheet1, bool exportHiddenColumns false) { XlsxExportOptions options new XlsxExportOptions(); options.SheetName sheetName; options.ExportMode ExportMode.SingleFile; if (exportHiddenColumns) { gridView.OptionsPrint.ExpandAllGroups true; options.ExportRawData true; } gridView.ExportToXlsx(filePath, options); } public static DataTable ImportExcelToDataTable(string fileName) { DataTable result null; using (FileStream stream new FileStream(fileName, FileMode.Open, FileAccess.Read)) { SpreadsheetControl spreadsheet new SpreadsheetControl(); spreadsheet.LoadDocument(stream, DocumentFormat.Xlsx); Worksheet sheet spreadsheet.Document.Sheets[0]; result sheet.CreateDataTable(0, 0, sheet.GetUsedRange().LastRowIndex, sheet.GetUsedRange().LastColumnIndex, true); } return result; } }这样写的好处是页面代码非常干净按钮事件里只有两三行。多个窗体同时需要这个功能时改一个地方就行。6.2 选取导出路径时别忘记用SaveFileDialog很多人做导出时直接写死路径或让用户输入路径这样非常不友好。标准做法是SaveFileDialog saveDlg new SaveFileDialog(); saveDlg.Filter Excel 工作簿|*.xlsx; saveDlg.FileName $订单数据_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx; if (saveDlg.ShowDialog() ! DialogResult.OK) return; ExcelHelper.ExportGridViewToXlsx(gridView1, saveDlg.FileName, 订单明细, true);文件名里带时间戳这个习惯看似不起眼实际帮我挡掉了无数次“文件覆盖”引发的客户投诉。同一个文件反复导出时时间戳能帮你快速区分哪个是最新版本。6.3 批量导出多个GridView到同一个Excel用绑定方式更稳有一次客户提了个需求界面上有3个GridControl点击一个按钮要把3张表导出到同一个Excel文件的3个Sheet里。我最初的做法是连续调用3次ExportToXlsx第二次、第三次传入的路径相同。结果不行——新导出会直接覆盖之前的文件而不是追加Sheet。正确做法是使用ExportToWorkbook方法或者先导出到多个临时文件再合并。前者是DevExpress的原生支持但使用方式略复杂using DevExpress.XtraExport; using DevExpress.XtraSpreadsheet; Workbook workbook new Workbook(); // 先把第一个GridView导出为xlsx字节流 using (MemoryStream ms new MemoryStream()) { gridView1.ExportToXlsx(ms); workbook.LoadDocument(ms, DocumentFormat.Xlsx); } // 把第二个GridView导出为另一个xlsx字节流 using (MemoryStream ms2 new MemoryStream()) { gridView2.ExportToXlsx(ms2); Worksheet sheet workbook.LoadDocument(ms2, DocumentFormat.Xlsx); sheet.Name 第二个表; } workbook.SaveDocument(outputPath, DocumentFormat.Xlsx);这个方法我第一次写的时候试了很久才跑通但跑通之后非常稳定。核心思想就是先把每个GridView导出成独立的Stream再用Workbook把它们合并进同一个文档。6.4 关于导出的文件被占用不要慌导出的Excel文件如果被Excel程序打开着再次调用ExportToXlsx时会抛IOException。处理方式至少有两种一是在导出前判断文件是否存在且被占用。二是用try-catch捕获IOException后提示用户关闭文件。第二种更实用因为文件占用状态的判断本身也有延迟。我一般在所有导出按钮事件里统一加一个异常捕获try { ExcelHelper.ExportGridViewToXlsx(gridView1, filePath, Sheet1, true); MessageBox.Show(导出成功, 提示, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (IOException) { MessageBox.Show(文件被占用请先关闭同名Excel文件后再试。, 导出失败, MessageBoxButtons.OK, MessageBoxIcon.Warning); } catch (Exception ex) { MessageBox.Show(导出失败 ex.Message, 错误, MessageBoxButtons.OK, MessageBoxIcon.Error); }这个处理虽然简单但在交付给客户使用的时候能省去大量“导出没反应”的困惑反馈。6.5 导出前把列名改成中文这才是真正的交付级做法我见过很多开发者的导出效果数据是对的但Excel的第一行列名是OrderId、Amount这种英文。客户看到的第一反应就是这表不是给我们做的吧解决方式是在导出前临时修改GridView的列标题文本gridView1.Columns[OrderId].Caption 订单编号; gridView1.Columns[CustomerName].Caption 客户名称; gridView1.Columns[Amount].Caption 订单金额; gridView1.Columns[OrderDate].Caption 下单日期;设置Caption之后导出的Excel列头会优先使用Caption而不是FieldName。如果项目本身就在界面上用中文显示这个步骤通常已经做好了。如果没做导出一份“字段名表”给客户确实是交付事故级别的体验。7. 其他值得收藏的GridView-Excel联动技巧7.1 导出带样式表头加粗、冻结首行DevExpress导出的Excel样式默认比较素有时候客户会要求“表头加粗”“首行冻结”。这些需求不一定要在Excel里二次调整代码里就能实现。使用XlsxExportOptions的ExcelExportOptions或CustomizeCell回调可以实现简单样式。比如表头加粗XlsxExportOptions options new XlsxExportOptions(); options.SheetName Sheet1; options.ShowGridLines true; options.CustomizeCell (s, e) { if (e.GridRow 0) { e.Formatting.Font.Bold true; e.Formatting.BackColor Color.LightGray; } }; gridView1.ExportToXlsx(filePath, options);这里e.GridRow 0表示表头行。e.Formatting直接作用于Excel单元格样式比导出后去改样式要可靠得多。至于冻结首行可以用SpreadsheetControl在导出后打开文件设置FrozenRowCount再另存using (SpreadsheetControl sc new SpreadsheetControl()) { sc.LoadDocument(filePath); sc.Document.Sheets[0].FrozenRowCount 1; sc.SaveDocument(filePath, DocumentFormat.Xlsx); }这个方案稍微有点绕但确实有效。如果你的客户不强制要求冻结我建议不必做这一步因为会增加约1~2秒的处理时间。7.2 导入时强制首列作为唯一标识防止重复导入导入Excel到GridControl的功能常常配套一个“保存到数据库”的动作。这就会引入一个重复数据问题同一份Excel导入两次数据库里就出现两批相同记录。我在一个仓储管理项目里的处理方式导入成功后不直接保存而是先按业务主键比如商品编码和数据库现有数据做比对把重复行高亮出来让用户确认后再保存。高亮行用GridView的RowStyle事件if (row.Cells[IsDuplicate].Value ! null (bool)row.Cells[IsDuplicate].Value) { e.Appearance.BackColor Color.LightYellow; e.Appearance.ForeColor Color.DarkRed; }这种“导入-校验-高亮-确认落库”的交互流程比“导入即保存”要稳得多客户满意度也高很多。我强烈建议你在做导入功能时不要跳过后面的校验环节哪怕只是简单的重复项检查。7.3 一个冷门但有用的APIExportToHtml和ExportToImageExcel导出大家都会用但有些客户会提一些奇怪的需求“我要把这张表发到邮件里”“我要把它贴到大屏上”。这时候用ExportToHtml或ExportToImage会非常方便gridView1.ExportToHtml(C:\Exports\表.html); gridView1.ExportToImage(C:\Exports\表.png);导出的HTML文件可以放到网页里展示图片可以插入到Word报告或PPT里。虽然标题是“导入导出至Excel”但这两样东西在项目交付时帮我救过不少场一并分享出来。前阵子有个客户的主管每周要把订单明细贴到公司周报里之前是手动截图每次都要调列宽。后来我用ExportToImage给做了个按钮一键导出表格图片客户直呼“这才是真正的效率提升”。在导出图片之前建议先对GridView做一次BestFitColumns()不然图片里的列宽可能显示不全。7.4 别忽略GridView.OptionsBehavior里的可编辑状态还有一个小坑默认情况下GridView是可编辑的。如果你导入Excel后用户直接在GridControl里改了数据但不小心点了别的地方数据就悄悄变了。对于纯展示报表来说最好在加载数据前把GridView置为只读gridView1.OptionsBehavior.Editable false; gridView1.OptionsBehavior.ReadOnly true; gridView1.OptionsSelection.EnableAppearanceFocusedCell false;这样用户只能看、不能改数据安全性高很多。如果要让用户改再单独开一个编辑模式通过按钮切换。这个设计在“导入数据供审核”的场景里尤其重要——审核人不需要改数据只需要看全貌。8. 几个实战场景的直接参考代码8.1 场景一一键导出当前筛选结果到Excel有时界面上已经做了筛选客户希望“导出的Excel应该和当前看到的一样”。private void btnExportFiltered_Click(object sender, EventArgs e) { SaveFileDialog dlg new SaveFileDialog(); dlg.Filter Excel 工作簿|*.xlsx; if (dlg.ShowDialog() ! DialogResult.OK) return; XlsxExportOptions options new XlsxExportOptions { SheetName 筛选结果, ExportMode ExportMode.SingleFile }; gridView1.ExportToXlsx(dlg.FileName, options); }注意这里不要设置ExportRawData true否则筛选状态会被忽略。8.2 场景二一键导入Excel并刷新Gridprivate void btnImport_Click(object sender, EventArgs e) { OpenFileDialog dlg new OpenFileDialog(); dlg.Filter Excel文件|*.xlsx;*.xls; dlg.CheckFileExists true; if (dlg.ShowDialog() ! DialogResult.OK) return; try { DataTable dt ExcelHelper.ImportExcelToDataTable(dlg.FileName); gridControl1.DataSource dt; gridView1.PopulateColumns(); } catch (Exception ex) { MessageBox.Show(导入失败 ex.Message); } }PopulateColumns()会根据DataTable的列结构重建GridView列。这比依赖GridView里的缓存列更稳妥尤其是在不同Excel文件列结构不一样的情况下。8.3 场景三动态生成“导出模板”如果你的业务是要求用户先下载模板填好数据后再上传导入这个场景也可以直接用GridControl生成模板。做法是建一个只有列头没有数据的DataTable绑定到GridControl再导出成一个空Excel文件DataTable template new DataTable(Template); template.Columns.Add(商品编码, typeof(string)); template.Columns.Add(商品名称, typeof(string)); template.Columns.Add(数量, typeof(int)); template.Columns.Add(单价, typeof(decimal)); gridControl1.DataSource template; gridView1.ExportToXlsx(template.xlsx);这个方式生成的模板文件列头是一行标准的中文客户填写之后你再导入列名正好能对应上。我经常用这套组合拳来做数据上报类型的需求省掉了手工制作Excel模板的时间。9. 把这些内容串起来一个完整按钮的代码结合上面所有内容我给出一个同时包含“导出”和“导入”的完整示例窗体代码你可以直接复制到自己的测试项目中运行。public partial class MainForm : Form { DataTable orderDataTable; public MainForm() { InitializeComponent(); LoadTestData(); } private void LoadTestData() { orderDataTable new DataTable(Orders); orderDataTable.Columns.Add(OrderId, typeof(int)); orderDataTable.Columns.Add(CustomerName, typeof(string)); orderDataTable.Columns.Add(Amount, typeof(decimal)); orderDataTable.Columns.Add(OrderDate, typeof(DateTime)); orderDataTable.Rows.Add(1001, 张三, 299.5m, DateTime.Now); orderDataTable.Rows.Add(1002, 李四, 1299.0m, DateTime.Now.AddDays(-1)); orderDataTable.Rows.Add(1003, 王五, 568.0m, DateTime.Now.AddDays(-2)); gridControl1.DataSource orderDataTable; gridView1.Columns[OrderId].Caption 订单编号; gridView1.Columns[CustomerName].Caption 客户名称; gridView1.Columns[Amount].Caption 订单金额; gridView1.Columns[OrderDate].Caption 下单日期; gridView1.BestFitColumns(); } private void btnExport_Click(object sender, EventArgs e) { SaveFileDialog dlg new SaveFileDialog(); dlg.Filter Excel 工作簿|*.xlsx; dlg.FileName $订单数据_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx; if (dlg.ShowDialog() ! DialogResult.OK) return; try { XlsxExportOptions options new XlsxExportOptions(); options.SheetName 订单明细; options.ExportMode ExportMode.SingleFile; options.ShowGridLines true; gridView1.ExportToXlsx(dlg.FileName, options); MessageBox.Show(导出成功, 提示, MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (IOException) { MessageBox.Show(文件被占用请先关闭同名Excel文件。, 导出失败, MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void btnImport_Click(object sender, EventArgs e) { OpenFileDialog dlg new OpenFileDialog(); dlg.Filter Excel文件|*.xlsx;*.xls; dlg.CheckFileExists true; if (dlg.ShowDialog() ! DialogResult.OK) return; try { orderDataTable ExcelHelper.ImportExcelToDataTable(dlg.FileName); gridControl1.DataSource null; gridControl1.DataSource orderDataTable; gridView1.PopulateColumns(); gridView1.BestFitColumns(); } catch (Exception ex) { MessageBox.Show(导入失败 ex.Message); } } }这段代码如果你跑一遍就会对GridControlExcel的完整链路有一个直观感受。很多问题列顺序乱、数据不刷新、文件被占用都要在实际运行中才能真正理解。我在实际使用中还发现一个细节在比较新的DevExpress版本中gridView1.ExportToXlsx方法在导出时默认会把GridView底部的汇总行也一起导出。如果你的GridView打开了ShowFooter且添加了求和项導出的Excel在表底部会出现Total或类似字样。如果客户不想要这个汇总行请在导出前临时设置bool showFooter gridView1.OptionsView.ShowFooter; gridView1.OptionsView.ShowFooter false; gridView1.ExportToXlsx(filePath, options); gridView1.OptionsView.ShowFooter showFooter;否则客户很容易问“这个Total是哪来的怎么删掉”这类问题看似小但在实际沟通中解释成本不低。还有一点关于Office版本的兼容性DevExpress的导出引擎不依赖本机安装的Office导出的是标准xlsx文件Excel 2007以上版本都能打开。这比很多依赖Word/Excel COM组件的方案稳得多。我接手过一个老项目原开发用的是Microsoft.Office.Interop.Excel客户机器上没装Office就跑不起来后来我全改成DevExpress导出彻底摆脱了这个环境依赖。如果你是新手不知道选哪条路直接认准DevExpress自带API是没错的。如果你正在用DevExpress的其他控件比如PivotGridControl或TreeList它们的导出思路也是类似的都是先绑定DataSource再调用ExportToXlsx或者ExportToXls。学会了GridControl这一套其他控件可以无缝迁移这也是DevExpress设计上比较统一的好处。
返回列表