一个Excel导出类的实现过程(四):格式化与若干坑 已补图和代码zip

这是本文的第四部分也是最后部分,适合新人初步学习泛型、反射,提供了有限的业务场景分析、若干的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,代码文件已提供,本文完。

posted @ 2013-05-17 15:07  Jusfr  阅读(1909)  评论(5编辑  收藏  举报