這是本文的第四部分也是最后部分,適合新人初步學習泛型、反射,提供了有限的業務場景分析、若干的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,代碼文件已提供,本文完。