这是本文的第四部分也是最后部分,适合新人初步学习泛型、反射,提供了有限的业务场景分析、若干的NPOI接口使用示范,前三部分链接如下:
接第三部分,由于单元格设置语句仅简单的一句话row.CreateCell(i).SetCellValue(value.ToString()),生成的Excel仍然粗陋。
Excel导出通常会遇到若干问题:
- 整数变字符串问题:整型值比如ID列,单元格左上角出现绿色三角符号,单元格变成了字符串类型,不够友好;
- 日期显示问题:有时候需要显示到日期如“2013-5-17”而不要带上“16:50:13”,有时候只希望显示“16:50”,而不是告诉使用人自己拿生成的工作薄设置格式;
- 科学计数法问题:手机号13912345678只是列宽过短时看起来像科学计数法,但超过15位的整数像1234567890123456就真变成1.23457E+15而且最后一位6变成了零。
ICell的实现者HSSFCell有数量不少的属性和方法帮助我们完成单元格格式设置,考虑到应用场景,创建指定类型的单元格并配合格式化选项已经能满足要求,下文使用若干强制转型和控制语句完成功能。
首先扩充Header类,添加一个Format参数。当使用者传入空Format参数时由我们推断单元格类型与值,而当使用者传入非空的Format参数时,我们调用ToString(String format)方法进行格式化。
public class Header { public String Name { get; set; } public String PrintName { get; set; } public String Format { get; set; } public Header(String name) : this(name, name) { } public Header(String name, String printName) : this(name, printName, null) { } public Header(String name, String printName, String format) { Name = name; PrintName = printName; Format = format; } }
我们把权力交给调用者时,便认为他的参数传递是经过判断且有道理的,比如纯数字的条形码显示可能需要调用Decimal.ToString("F0")方法,,但使用者对薪水调用ToString("P2")显示百分号就没办法了;另一部分是设计之初的问题,如双精度Double型精确度,后面再说。
由于ICell.CreateCell(int column, CellType type)创建的单元格类型最终由ICell.SetCellValue()重载版本中参数类型(可以是Boolean、DateTime、Double、String及IRichTextString)决定,所以统一只使用了ICell.CreateCell(int column),必须注意的是用于存放数值的单元格类型枚举只有CellType.NUMERIC,下文还要讲到。新的Export改动点如刚才描述,旧的Person类及Export重载请见前文,实现如下:
public IWorkbook Export<T>(IList<T> records, IList<Header> headers) { if (records == null) throw new ArgumentNullException("records"); if (headers == null) throw new ArgumentNullException("headers"); //这里对headers集合非空、重复性、子元素非空引用等检查视具体需求 PropertyInfo[] props = new PropertyInfo[headers.Count]; for (Int32 i = 0; i < headers.Count; i++) { props[i] = typeof(T).GetProperty(headers[i].Name); //属性数组可能产生空元素 } IWorkbook workbook = new HSSFWorkbook(); ISheet sheet = null; IRow row = null; for (Int32 r = 0; r < records.Count; r++) { if ((r % RowPerSheet) == 0) { Int32 sheetIndex = (Int32)((Double)r / RowPerSheet) + 1; sheet = workbook.CreateSheet("Sheet" + sheetIndex); //工作表名,请按自己需求实现 row = sheet.CreateRow(0); //创建表头 for (Int32 i = 0; i < headers.Count; i++) { row.CreateCell(i).SetCellValue(headers[i].PrintName); } } row = sheet.CreateRow((r % RowPerSheet) + 1); //因为表头存在,行标需要下移1位 for (Int32 i = 0; i < props.Length; i++) { if (props[i] != null) //空引用检查十分必要 { Object value = props[i].GetValue(records[r], null); if (value != null) //空引用检查十分必要 { ICell cell = row.CreateCell(i); if (props[i].PropertyType == typeof(Int32)) { if (String.IsNullOrWhiteSpace(headers[i].Format)) { cell.SetCellValue(((Int32)value)); } else { cell.SetCellValue(((Int32)value).ToString(headers[i].Format)); } } else if (props[i].PropertyType == typeof(Double)) { if (String.IsNullOrWhiteSpace(headers[i].Format)) { cell.SetCellValue(((Double)value)); } else { cell.SetCellValue(((Double)value).ToString(headers[i].Format)); } } else if (props[i].PropertyType == typeof(Decimal)) { if (String.IsNullOrWhiteSpace(headers[i].Format)) { cell.SetCellValue(Convert.ToDouble(value)); } else { cell.SetCellValue(((Decimal)value).ToString(headers[i].Format)); } } else if (props[i].PropertyType == typeof(DateTime)) { if (String.IsNullOrWhiteSpace(headers[i].Format)) { cell.SetCellValue((DateTime)value); } else { cell.SetCellValue(((DateTime)value).ToString(headers[i].Format)); } } else { cell.SetCellValue(value.ToString()); } } } } } //调整每张Sheet列宽自适应 for (Int32 i = 0; i < workbook.NumberOfSheets; i++) { sheet = workbook.GetSheetAt(i); for (Int32 h = 0; h < headers.Count; h++) { sheet.AutoSizeColumn(h); } } return workbook; }
生日类数据显示到年月日即可,我们使用"yyyy-MM-dd"作为格式化参数,薪水未提供格式化参数,客户端调用代码如下:
static void Main(string[] args) { Int32 records = 100; List<Person> persons = new List<Person>(records); for (Int32 i = 0; i < records; i++) { Person person = new Person(); person.ID = i + 1; person.Name = "name" + (i + 1); person.Salary = Math.Abs(Guid.NewGuid().GetHashCode() % 10000) + 5000; person.Birth = DateTime.Now; persons.Add(person); } List<ExcelHelper.Header> headers = new List<ExcelHelper.Header>(); headers.Add(new ExcelHelper.Header("ID")); headers.Add(new ExcelHelper.Header("Name", "名称")); headers.Add(new ExcelHelper.Header("Birth", "生日", "yyyy-MM-dd")); headers.Add(new ExcelHelper.Header("Salary", "薪水")); ExcelHelper excelHelper = new ExcelHelper(); excelHelper.RowPerSheet = 100; String path = @"d:\1.xls"; IWorkbook workbook = excelHelper.Export<Person>(persons, headers); using (FileStream stream = File.Open(path, FileMode.OpenOrCreate)) { workbook.Write(stream); } }
之前提到过双精度的问题,对某个员工的薪酬设置为1234567890123456并输出Excel会怎么样,先做个测试:
Person person = new Person(); person.Salary = 1234567890123456; Console.WriteLine(person.Salary);
输出结果是1.23456789012346E+15也就是说Excel上不仅显示的是科学计数法,而且数值已经变成悲了个剧的1234567890123460。粗暴地讲,这是因为双精度的Double型的有效位数造成的,并不是ToString方法或者Excel写入时产生了变化。
要处理类似问题,仅将Person.Salary声明为Decimal类型还不够,因为单元格类型CellType枚举中用来存数字的只有CellType.NUMERIC,没办法我们需要配合ToString("F0")(或者F2等视具体要求)把值当作字符串写入单元格,改动部分的部分代码如下:
... public Decimal Salary { get; set; } ... person.Salary = 1234567890123456M; .... headers.Add(new ExcelHelper.Header("Salary", "薪水", "F0")); ...
于是能看到Excel上显示的1234567890123456,虽然有绿色小三角,好歹没变成科学计数法和四舍五入。如要求更完备请自行参考NPOI相关API,代码文件已提供,本文完。