NPOI导出Excel及使用问题

因为最近公司质管部门提出了一个统计报表的需求:要求导出一个2016及2017年度深圳区域的所有供应商的费用成本计算——一个22列的Excel表,其中还包括多列的合并单元格;说实话,统计报表功能其实我还是很少涉及的,以前都是直接用DataTable+输出流导出Excel,因为涉及到合并单元格,明显用输出流就不合适了,此时NPOI开源框架就很合适了;当然还有其他组件可以选择,比如EPPlush,微软自带组件,以及收费的Aspose.Cells;因为NPOI资料比较多且公司用的组件也是这个,所以就选择它了;

一步一步按需导出

由于这个功能因为查询出来的数据量很大需要单独抽离出来(不能放到公司的系统上),所以我就新建了一个控制台应用程序,简单的做了一个导出Excel的功能,因为刚开始需求没要求需要合并单元格,所以我这边就很快的做出来了:核心代码如下:

  1. ExportToExcelByNOPI(dt, GetOriginColumns(dt), $"2016年度成本费用统计.xlsx"));
  2. private void DataTableToExcel(DataTable dt, string[] titles, string file)
  3. {
  4. using (FileStream fs = new FileStream(file, FileMode.OpenOrCreate))
  5. using (StreamWriter sw = new StreamWriter(new BufferedStream(fs), Encoding.Default))
  6. {
  7. string title = "";
  8. //拼接表头
  9. for (int i = 0; i < dt.Columns.Count; i++)
  10. {
  11. title += titles[i] + "\t";//自动跳到下一单元格
  12. }
  13. title = title.Substring(0, title.Length - 1) + "\n";
  14. sw.Write(title);
  15. foreach (DataRow row in dt.Rows)
  16. {
  17. string line = "";
  18. for (int i = 0; i < dt.Columns.Count; i++)
  19. {
  20. //line += row[i].ToString().Trim() + "\t"; //内容:自动跳到下一单元格
  21. line += row[i].ToString().Trim() + "\t";//自动跳到下一单元格
  22. }
  23. line = line.Substring(0, line.Length - 1) + "\n";
  24. sw.Write(line);
  25. }
  26. }
  27. }

把导出的excel发过去发现根本不符合他们的要求,说要对哪些行合并单元格,这样有利于他们数据分析,这样的话就得NPOI上场了;刚开始想法很简单,只要他们的值相等,我就把他合并单元格,毕竟像订单号是唯一的么,那么订单号所附带的如订单重量,数量等都是相同的(其实还是想的太当然了,导致了后面的一系列的问题)

  1. private void ExportToExcelByNOPI(DataTable dt, string title, string strFilename)
  2. {
  3. if ((dt == null) || string.IsNullOrEmpty(strFilename))
  4. {
  5. return;
  6. }
  7. if (File.Exists(strFilename))
  8. {
  9. File.Delete(strFilename);
  10. }
  11. //添加表头
  12. for (int i = 0; i < dt.Columns.Count; i++)
  13. {
  14. ICell cell = headerrow.CreateCell(i);
  15. cell.CellStyle = style;
  16. cell.SetCellValue(dt.Columns[i].ColumnName);
  17. }
  18. //添加第一行数据
  19. IRow row = sheet.CreateRow(1);
  20. for (int j = 0; j < dt.Columns.Count; j++)
  21. {
  22. string cellText = dt.Rows[0][j].ToString();
  23. row.CreateCell(j).SetCellValue(cellText);
  24. }
  25. //从第二行开始循环,和上一行进行判断,如果相同,则合并
  26. for (int i = 1; i < dt.Rows.Count; i++)
  27. {
  28. row = sheet.CreateRow(i + 1);
  29. for (int j = 0; j < dt.Columns.Count; j++)
  30. {
  31. string cellText = dt.Rows[i][j].ToString();
  32. row.CreateCell(j).SetCellValue(cellText);
  33. string temp = dt.Rows[i - 1][j].ToString();
  34. //这里是合并单元格条件判断,如值是否相等,是否在合并列要求之内
  35. if (!string.IsNullOrEmpty(temp) && cellText== temp && ColumnsName.Contains(dt.Columns[j].ColumnName))
  36. {
  37. CellRangeAddress region = new CellRangeAddress(i, i+1, j, j);
  38. sheet.AddMergedRegion(region);
  39. }
  40. }
  41. }
  42. style.Alignment = HorizontalAlignment.Center;
  43. style.VerticalAlignment = VerticalAlignment.Center;
  44. style.Alignment = HorizontalAlignment.Center;//居中显示
  45. using (FileStream fs = new FileStream(strFilename, FileMode.Open, FileAccess.ReadWrite))
  46. using (MemoryStream ms = new MemoryStream())
  47. {
  48. workbook.Write(ms);
  49. var buf = ms.ToArray();
  50. fs.Write(buf, 0, buf.Length);
  51. fs.Flush();
  52. }
  53. }

导出来发现Excel里面合并之后的内容不对了,有的合并单元格不对(比如订单号是01,有两个产品P1,P2,这就有两行,如果这两行订单号是相同的,则按需求是要合并的,但是其他列的值有的合并多了,稍微一细想就知道原因了,我只是单纯的比较上一行与下一行的列值,那么下一行的其它订单的产品信息如规格,值相同的话也会被合并,这就不符合我们的要求了,所以还得在这个基础之上在加限制条件;

因为这个表是已订单为维护的,那么我们就以这列为参照合并规则来记录这列被合并的行数,然后我们标记这个行数记为sameCount,那么每列的值我们都会比较,如果在sameCount行列值相同,则合并;那么就得写个辅助类CellCalculateHelper来计算出要求被合并的列在sameCount行值是否都相同:

  1. internal class CellCalculateHelper
  2. {
  3. /// <summary>
  4. /// 从startRow行开始比较相同订单号的行数
  5. /// </summary>
  6. internal static (int startRow, int sameCount) GetRepeaterCount(int startRow, DataTable dt)
  7. {
  8. var i = startRow;
  9. var sameCount = 0;
  10. while (dt.Rows[startRow][0].ToString() == dt.Rows[i][0].ToString())
  11. {
  12. if ((i + 1) == dt.Rows.Count) break;
  13. sameCount++;
  14. i++;
  15. }
  16. return (startRow, sameCount);
  17. }
  18. internal static bool IsMergeRegionMaxRepeatCount(DataTable dt, string columnName, int startRow, int sameCount)
  19. {
  20. var start = startRow + sameCount - 1;
  21. while (sameCount - 1 != 0)
  22. {
  23. if (dt.Rows[start][columnName].ToString() == dt.Rows[start - 1][columnName].ToString()){
  24. start = start - 1;
  25. sameCount--;
  26. }else{
  27. return false;
  28. }
  29. }
  30. return true;
  31. }
  32. }

有了这个帮助类,就好办了,改造上面的ExportToExcelByNOPI方法如下:

  1. private void ExportToExcelByNOPI(DataTable dt, string title, string strFilename)
  2. {
  3. ...
  4. //添加第一行数据这样上面代码相同
  5. //记住第一行相同的最大行数
  6. var tuple = CellCalculateHelper.GetRepeaterCount(0, dt);
  7. //第一行数据遍历
  8. for (int i = 0; i < dt.Rows.Count; i++){
  9. IRow row = sheet.CreateRow(i + 1);
  10. //如果当前行数等于最大相同的函数(相当于合并之后的下一行数据,必定与上一行数据不同)
  11. if (i == tuple.startRow + tuple.sameCount){
  12. tuple = CellCalculateHelper.GetRepeaterCount(i, dt);
  13. }
  14. //遍历列
  15. for (int j = 0; j < dt.Columns.Count; j++){
  16. string cellText = dt.Rows[i][j].ToString();
  17. row.CreateCell(j).SetCellValue(cellText);
  18. if (tuple.sameCount > 1){
  19. //需要合并
  20. string tempValue = dt.Rows[i][j].ToString();
  21. //指定列合并单元格
  22. if (!string.IsNullOrWhiteSpace(tempValue) &&
  23. cellText == tempValue &&
  24. ColumnsName.Contains(dt.Columns[j].ColumnName)){
  25. //判断是否是参照行DDNO
  26. if (i >= tuple.startRow + tuple.sameCount - 1) continue;
  27. if ((ColumnsName[0] == dt.Columns[j].ColumnName)){
  28. //下一行与上一行合并
  29. CellRangeAddress region = new CellRangeAddress(i + 1, i + 2, j, j);
  30. sheet.AddMergedRegion(region);
  31. }else{
  32. //判断该列的最大sameCount行值是否相同,如果不同,不合并;相同则合并
  33. if (CellCalculateHelper.IsMergeRegionMaxRepeatCount(dt, dt.Columns[j].ColumnName, i, tuple.sameCount)){
  34. CellRangeAddress region = new CellRangeAddress(i + 1, i + 2, j, j);
  35. sheet.AddMergedRegion(region);
  36. }
  37. }
  38. }
  39. }
  40. }
  41. }
  42. ...
  43. ...
  44. }

这样导出来的数据就是正确符合业务同事的要求了!

后记

写到这里以为这些都是一帆风顺的吗?

NO!

我被坑在一个奇怪的地方,至今我也没想到原因:期初,我是用控制台应用程序想简单的导出excel的,也测试了从数据库查出一个供应商的所有订单信息导出excel是没问题的,于是当我查询出所有的供应商的时候,bug出现了,程序运行一段时间后毫无反应了(并不是死机,也没有报内存溢出的错误),因为数据量很大,所以当时我还跟个煞笔似的在那里等结束,等我吃完中饭回来发现还是没有成功导出,我就意识到不对了,但是不报任何异常,我根本查不到问题现在那,我接着尝试换种写法导出excel——分页,以及分批次导出不同的excel;这种是可以的,到这我心里就知道估计是内存问题了,最后我把整个控制台项目换成类库,然后新建web应用程序能一次运行成功,更加让我坚信是内存问题,但是为什么控制台应用程序不会报内存溢出的错呢?这个我真的无从查起啊,有朋友知道,希望能告诉我

2017年12月29日修补:

前面修改之后还是不对,由于数据量太大,我在看了前部分的数据没问题因为就OK,实际上问题还是比较明显的,就是当有2个以上相同的数据列时,合并单元格就会有问题,原来我想的是H1,H2合并成为H21,然后继续循环H3,接着合并,我以为H21与H3合并会成为一个在Excel中三行一列组成的合并单元格,但是结果发现是H3与H4合并之后在与H21拼接的两个2行1列的单元格,这就有大问题,后来我就把合并单元格条件部分修正如下,便完美了。代码如下:

  1. //判断是否是参照行OrdNO
  2. if (i > tuple.startRow + tuple.sameCount - 1) continue;
  3. //新增的i == tuple.startRow 是为了防止多次合并
  4. if ((ColumnsName[0] == sourceTable.Columns[j].ColumnName) && i == tuple.startRow)
  5. {
  6. //startRow与startRow+sameCount行合并(也就是一次性合并相同行数单元格)
  7. CellRangeAddress region = new CellRangeAddress(tuple.startRow + 1, tuple.startRow + tuple.sameCount, j, j);
  8. sheet.AddMergedRegion(region);
  9. }else
  10. {
  11. if (CellCalculateHelper.IsMergeRegionMaxRepeatCount(sourceTable, sourceTable.Columns[j].ColumnName, tuple.startRow, tuple.sameCount) && i == tuple.startRow)
  12. {
  13. CellRangeAddress region = new CellRangeAddress(tuple.startRow + 1, tuple.startRow + tuple.sameCount, j, j);
  14. sheet.AddMergedRegion(region);
  15. }
  16. }

NPOI导出Excel及使用问题的更多相关文章

  1. NPOI导出Excel (C#) 踩坑 之--The maximum column width for an individual cell is 255 charaters

    /******************************************************************* * 版权所有: * 类 名 称:ExcelHelper * 作 ...

  2. Asp.Net 使用Npoi导出Excel

    引言 使用Npoi导出Excel 服务器可以不装任何office组件,昨天在做一个导出时用到Npoi导出Excel,而且所导Excel也符合规范,打开时不会有任何文件损坏之类的提示.但是在做导入时还是 ...

  3. NPOI导出EXCEL 打印设置分页及打印标题

    在用NPOI导出EXCEL的时候设置分页,在网上有查到用sheet1.SetRowBreak(i)方法,但一直都没有起到作用.经过研究是要设置  sheet1.FitToPage = false; 而 ...

  4. .NET NPOI导出Excel详解

    NPOI,顾名思义,就是POI的.NET版本.那POI又是什么呢?POI是一套用Java写成的库,能够帮助开发者在没有安装微软Office的情况下读写Office的文件. 支持的文件格式包括xls, ...

  5. NPOI导出Excel(含有超过65335的处理情况)

    NPOI导出Excel的网上有很多,正好自己遇到就学习并总结了一下: 首先说明几点: 1.Excel2003及一下:后缀xls,单个sheet最大行数为65335 Excel2007 单个sheet ...

  6. [转]NPOI导出EXCEL 打印设置分页及打印标题

    本文转自:http://www.cnblogs.com/Gyoung/p/4483475.html 在用NPOI导出EXCEL的时候设置分页,在网上有查到用sheet1.SetRowBreak(i)方 ...

  7. 分享使用NPOI导出Excel树状结构的数据,如部门用户菜单权限

    大家都知道使用NPOI导出Excel格式数据 很简单,网上一搜,到处都有示例代码. 因为工作的关系,经常会有处理各种数据库数据的场景,其中处理Excel 数据导出,以备客户人员确认数据,场景很常见. ...

  8. 用NPOI导出Excel

    用NPOI导出Excel public void ProcessRequest(HttpContext context) { context.Response.ContentType = " ...

  9. NPOI导出Excel示例

    摘要:使用开源程序NPOI导出Excel示例.NPOI首页地址:http://npoi.codeplex.com/,NPOI示例博客:http://tonyqus.sinaapp.com/. 示例编写 ...

  10. NPOI导出excel(带图片)

    近期项目中用到Excel导出功能,之前都是用普通的office组件导出的方法,今天尝试用下NPOI,故作此文以备日后查阅. 1.NPOI官网http://npoi.codeplex.com/,下载最新 ...

随机推荐

  1. 改变PowerDesigner数据模型字体大小

    一 改变左侧菜单字体大小Tools----->General------->Fonts-------->根据item选项的不同改变字体的大小 二 改变数据模型Table的字体大小To ...

  2. 25_re模块

    一.re模块的核心功能       1.findall —— 查找所有,返回list lst = re.findall("m", "mai le fo len, mai ...

  3. GoLang学习控制语句之for

    for结构简介 Go语言只有for循环这一种循环结构,Go语言中的for循环语句的三个部分不需要用括号括起来,但循环体必须用 { } 括起来.基本的for循环包含三个由分号分开的组成部分: 初始化语句 ...

  4. Swift5 语言指南(二十五) 自动引用计数(ARC)

    Swift使用自动引用计数(ARC)来跟踪和管理应用程序的内存使用情况.在大多数情况下,这意味着内存管理在Swift中“正常工作”,您不需要自己考虑内存管理.当不再需要这些实例时,ARC会自动释放类实 ...

  5. webApp开发中的总结

    meta标签:  H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 <meta name="viewport" content="width=device-wid ...

  6. flask框架--cookie,session

    今天我又给大家分享一下怎么用flask框架来实现像淘宝购物车一样存储数据,并且把存储的数据删除,这个方法可以用两个方法都可以做成,一个是cookie,另一个是session. session是依赖于c ...

  7. iOS-IAP内购的那些事(iOS内购漏单的问题)

    前言 说起内购,其实挺令开发者厌烦的,原因呢,先不说漏单的问题,首先苹果要扣除30%的销售额哦,可恨不?(我觉得可恨),有些想办法先隐藏掉第三方支付(支付宝.微信等),等项目上线了,再跳过内购使用第三 ...

  8. Centos7安装python3并与python2共存

    1.查看是否已经安装Python CentOS 7.2 默认安装了python2.7.5 因为一些命令要用它比如yum 它使用的是python2.7.5. 使用 python -V 命令查看一下是否安 ...

  9. POJ 2453

    #include <iostream> #include <algorithm> #include <cmath> #define MAXN 1000 #defin ...

  10. 安装MySQL-python时报错

    (py27) [root@test SimpletourDevops]# pip install MySQL-python DEPRECATION: Python 2.7 will reach the ...