[檔案輸出]寫個Helper讓NPOI變得更好用 (使用NPOI產出Excel檔案)

NPOI是很老牌的可以讓你產出xls與xlsx的元件

但可能就是因為他太老牌了 許多操作應用上並不便利

通常程式碼中只要用了NPOI 都會看起來落落長很醜

很像.net 以前 大家寫 webform 用 datatable + gridview 在塞資料的那種味道

程式碼中 會有超多創建格子、給格式、給值.......幾百行、千行無限循環

跟現在大家喜歡寫程式習慣並不太符合

所以今天我們試著來包裝NPOI讓他變的更易用些

完整程式碼詳見 https://gitlab.com/jesperlai/NpoiHelper

 

當然先看看最後我們希望怎麼叫用

假設我要輸出的資料長這樣

class Sample
{
    public string Id { get; set; }   //故意在這裡定義為string 但輸出到Excel中需轉為數字
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}

static void Main(string[] args)
{
    var data = new List<Sample>
    {
        new Sample { Id = "1", Name = "A", Birthday = DateTime.Parse("2001/1/1 12:00") },
        new Sample { Id = "2", Name = "B", Birthday = DateTime.Parse("2001/1/2 12:00") },
        new Sample { Id = "3", Name = "C", Birthday = DateTime.Parse("2001/1/3 12:00") },
    };
}

 

而希望Excel的結果長這樣

1. 原本流水號這個欄位是文字,但在輸出時我希望他變成數字且格式化為0.00

2. 原本生日是有時分秒的,但在輸出時我希望只有年-月-日

 

叫用的程式碼如下

var param = GetParam(data);
NpoiHelper.ExportExcel(param);
private static NpoiParam<Sample> GetParam(List<Sample> data)
{
    var param = new NpoiParam<Sample>
    {
        Workbook = new XSSFWorkbook(),            //要用一個新的或是用你自己的範本
        Data = data,                              //資料
        FileFullName = @"D:\result.xlsx",         //Excel檔要存在哪
        SheetName = "Data",                       //Sheet要叫什麼名子
        ColumnMapping = new List<ColumnMapping>   //欄位對應 (處理Excel欄名、格式轉換)
        {
            new ColumnMapping { ModelFieldName = "Id", ExcelColumnName = "流水號", DataType = NpoiDataType.Number, Format = "0.00"},
            new ColumnMapping { ModelFieldName = "Name", ExcelColumnName = "名子", DataType = NpoiDataType.String},
            new ColumnMapping { ModelFieldName = "Birthday", ExcelColumnName = "生日", DataType = NpoiDataType.Date, Format="yyyy-MM-dd"},
        },
        FontStyle = new FontStyle                //是否需自定Excel字型大小
        {
            FontName = "Calibri",
            FontHeightInPoints = 11,
        },
        ShowHeader = true,                      //是否畫表頭
        IsAutoFit = true,                       //是否啟用自動欄寬
    };

    return param;
}

如果你有試著自己使用過NPOI 你看到這裡應該會理解

這與傳統作法 每支程式各自在繞迴圈 [0] [1] [2]... 給值給格式的程式碼看起來乾淨許多

 

當然我同事的需求不僅於此 他們還有許多更特別的需求

有天有人問我 可不可以我自訂好一些表頭或側欄 你只幫我在中間塞資料進去就好

例如我自己做好一個範本

中間框起來的地方才是我資料要放的地方

 

也就是結果要像這樣

 

如此一來他的參數就會變得像

private static NpoiParam<Sample> GetParam(List<Sample> data)
{
    var param = new NpoiParam<Sample>
    {
        Workbook = new XSSFWorkbook(@"D:\template.xlsx"),   //要用一個新的或是用你自己的範本
        Data = data,                                        //資料
        FileFullName = @"D:\result.xlsx",                   //Excel檔要存在哪
        SheetName = "Data",                                 //Sheet要叫什麼名子
        ColumnMapping = new List<ColumnMapping>             //欄位對應 (處理Excel欄名、格式轉換)
        {
            new ColumnMapping { ModelFieldName = "Id", ExcelColumnName = "流水號", DataType = NpoiDataType.Number, Format = "0.00"},
            new ColumnMapping { ModelFieldName = "Name", ExcelColumnName = "名子", DataType = NpoiDataType.String},
            new ColumnMapping { ModelFieldName = "Birthday", ExcelColumnName = "生日", DataType = NpoiDataType.Date, Format="yyyy-MM-dd"},
        },
        ShowHeader = false,                                 //是否畫表頭
        ColumnStartFrom = 2,
        RowStartFrom = 1,
    };

    return param;
}

 

 

那最後就來看一下程式碼

首先當然別忘了到Nuget安裝NPOI

 

Model相關

using NPOI.SS.UserModel;
using System.Collections.Generic;

namespace MySample.Npoi
{
    public enum NpoiDataType
    {
        String,
        Number,
        Date
    }

    public class FontStyle
    {
        public string FontName { get; set; }
        public short? FontHeightInPoints { get; set; }

        public FontStyle()
        {
            FontName = null;
            FontHeightInPoints = null;
        }
    }

    public class ColumnMapping
    {
        public string ModelFieldName { get; set; }
        /// <summary>
        /// 若以範本初始化 Excel 則此欄可不填
        /// </summary>
        public string ExcelColumnName { get; set; }
        public NpoiDataType DataType { get; set; }
        /// <summary>
        /// 如果是 String 則這個欄位不生效
        /// </summary>
        public string Format { get; set; }
    }

    public class NpoiParam<T>
    {
        private int? _RowStartFrom;
        private int? _ColumnStartFrom;
        private bool? _ShowHeader;
        private bool? _IsAutoFit;
        private FontStyle _fontStyle;

        /// <summary>
        /// 請用 HSSFWorkbook 或 XSSFWorkbook 實體化 IWorkbook
        /// </summary>
        public IWorkbook Workbook { get; set; }
        /// <summary>
        /// 最後excel檔要被寫出到哪裡
        /// </summary>
        public string FileFullName { get; set; }
        /// <summary>
        /// 資料
        /// </summary>
        public List<T> Data { get; set; }
        /// <summary>
        /// 欲新增(或已存在)的 Sheet Name
        /// </summary>
        public string SheetName { get; set; }
        /// <summary>
        /// 與 Excel 檔間的欄位對應
        /// </summary>
        public List<ColumnMapping> ColumnMapping { get; set; }
        /// <summary>
        /// 預設從第 1 行開始塞資料 ( 第 0 行為標題欄位 )
        /// </summary>
        public int RowStartFrom
        {
            get { return _RowStartFrom ?? 1; }
            set { _RowStartFrom = value; }
        }
        /// <summary>
        /// 預設從第 0 欄開始塞資料
        /// </summary>
        public int ColumnStartFrom
        {
            get { return _ColumnStartFrom ?? 0; }
            set { _ColumnStartFrom = value; }
        }
        /// <summary>
        /// 是否excel要畫表頭 (預設畫表頭 = true)
        /// </summary>
        public bool ShowHeader
        {
            get { return _ShowHeader ?? true; }
            set { _ShowHeader = value; }
        }
        /// <summary>
        /// 是否自動調整欄寬 (預設不自動調整欄寬 = false)
        /// </summary>
        public bool IsAutoFit
        {
            get { return _IsAutoFit ?? false; }
            set { _IsAutoFit = value; }
        }

        /// <summary>
        /// 自己決定文字預設格式
        /// </summary>
        public FontStyle FontStyle
        {
            get { return _fontStyle ?? new FontStyle(); }
            set { _fontStyle = value; }
        }
    }
}

 

NpoiHelper

using NPOI.SS.UserModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace MySample.Npoi
{
    public static class NpoiHelper
    {
        /// <summary>
        /// 創造 Excel 檔
        /// </summary>
        public static void ExportExcel<T>(NpoiParam<T> p)
        {
            //依情況決定要建新的 Sheet 或是用舊的 (即來自範本)
            ISheet sht = GetSheet(p);

            if (p.Data.Any())
            {
                //有資料塞格子
                SetSheetValue(ref p, ref sht);
            }
            else
            {
                //若沒資料在起點寫入No Data !
                CreateNewRowOrNot(ref sht, p.RowStartFrom, p.ColumnStartFrom);
                sht.GetRow(p.RowStartFrom).CreateCell(p.ColumnStartFrom).SetCellValue("No Data !");
            }

            Flush(p.Workbook, p.FileFullName);
        }

        private static ISheet GetSheet<T>(NpoiParam<T> param)
        {
            //在 workbook 中以 sheet name 尋找 是否找得到sheet
            if (param.Workbook.GetSheet(param.SheetName) == null)
            {
                //找不到建一張新的sheet
                ISheet sht = param.Workbook.CreateSheet(param.SheetName);
                sht = CreateColumn(param);

                return sht;
            }
            else
            {
                //找得到即為要塞值的目標
                return param.Workbook.GetSheet(param.SheetName);
            }
        }

        private static ISheet CreateColumn<T>(NpoiParam<T> p)
        {
            var sht = p.Workbook.GetSheet(p.SheetName);
            sht.CreateRow(0);
            ICellStyle columnStyle = GetBaseCellStyle(p.Workbook, p.FontStyle);

            if (p.ShowHeader)
            {
                for (int i = 0; i < p.ColumnMapping.Count; i++)
                {
                    var offset = i + p.ColumnStartFrom;

                    sht.GetRow(0).CreateCell(offset);                                                //先創建格子
                    sht.GetRow(0).GetCell(offset).CellStyle = columnStyle;                           //綁定基本格式
                    sht.GetRow(0).GetCell(offset).SetCellValue(p.ColumnMapping[i].ExcelColumnName);  //給值
                }
            }

            return sht;
        }

        private static void SetSheetValue<T>(ref NpoiParam<T> p, ref ISheet sht)
        {
            //要從哪一行開始塞資料 (有可能自定範本 可能你原本範本內就有好幾行表頭 2行 3行...)
            int line = p.RowStartFrom;

            //有可能前面幾欄是自訂好的 得跳過幾個欄位再開始塞
            int columnOffset = p.ColumnStartFrom;

            //根據標題列先處理所有Style (對Npoi來說 '創建'Style在workbook中是很慢的操作 作越少次越好 絕對不要foreach在塞每行列實際資料時重覆作 只通通在標題列做一次就好)
            ICellStyle[] cellStyleArr = InitialColumnStyle(p.Workbook, p.ColumnMapping, p.FontStyle);

            foreach (var item in p.Data)
            {
                //如果 x 軸有偏移值 則表示這行他已經自己建了某幾欄的資料 我們只負責塞後面幾欄 所以並非每次都create new row
                CreateNewRowOrNot(ref sht, line, columnOffset);

                for (int i = 0; i < p.ColumnMapping.Count; i++)
                {
                    //建立格子 (需考量 x 軸有偏移值)
                    var cell = sht.GetRow(line).CreateCell(i + columnOffset);

                    //綁定style (記得 綁定是不慢的 但建新style是慢的 不要在迴圈裡無意義的反覆建style 只在標題處理一次即可)
                    cell.CellStyle = cellStyleArr[i];

                    //給值
                    string value = GetValue(item, p.ColumnMapping, i);               //reflection取值
                    SetCellValue(value, ref cell, p.ColumnMapping[i].DataType);      //幫cell填值
                }

                line++;
            }

            //處理AutoFit (必定是在最後做的 因為你得把所有格子都塞完以後才知道每欄多寬是你需要的)
            if (p.IsAutoFit)
            {
                for (int i = 0; i < p.ColumnMapping.Count; i++)
                {
                    sht.AutoSizeColumn(i);
                }
            }
        }

        private static ICellStyle[] InitialColumnStyle(IWorkbook wb, List<ColumnMapping> columnMapping, FontStyle fontStyle)
        {
            ICellStyle[] styleArr = new ICellStyle[columnMapping.Count];

            for (int i = 0; i < columnMapping.Count; i++)
            {
                //取通用格式
                ICellStyle cellStyle = GetBaseCellStyle(wb, fontStyle);

                //處理格式輸出
                if (!String.IsNullOrWhiteSpace(columnMapping[i].Format))
                {
                    cellStyle.DataFormat = GetCellFormat(wb, columnMapping[i].Format);
                }

                styleArr[i] = cellStyle;
            }

            return styleArr;
        }

        private static void CreateNewRowOrNot(ref ISheet sht, int line, int columnOffset)
        {
            //如果是從自定範本來則不能重畫格子 例如他給我範本 只要我畫後面三格 前兩格他自己做好了 如果我整行重畫 他自己畫的兩格也會消失
            if (columnOffset == 0 || line > sht.LastRowNum)
            {
                sht.CreateRow(line);
            }
        }

        private static void SetCellValue(string value, ref ICell cell, NpoiDataType type)
        {
            switch (type)
            {
                //字串沒有格式
                case NpoiDataType.String:
                    if (!String.IsNullOrWhiteSpace(value)) cell.SetCellValue(value);
                    break;

                //轉日期
                case NpoiDataType.Date:
                    if (!String.IsNullOrWhiteSpace(value)) cell.SetCellValue(Convert.ToDateTime(value));
                    break;

                //轉數字
                case NpoiDataType.Number:
                    if (!String.IsNullOrWhiteSpace(value)) cell.SetCellValue(Convert.ToDouble(value));
                    break;

                //不會發生;
                default:
                    break;
            }
        }

        private static ICellStyle GetBaseCellStyle(IWorkbook wb, FontStyle fontStyle)
        {
            //畫線
            ICellStyle cellStyle = wb.CreateCellStyle();
            cellStyle.BorderLeft = BorderStyle.Thin;
            cellStyle.BorderBottom = BorderStyle.Thin;
            cellStyle.BorderTop = BorderStyle.Thin;
            cellStyle.BorderRight = BorderStyle.Thin;

            //預設字型大小
            IFont font1 = wb.CreateFont();
            font1.FontName = (fontStyle.FontName == null) ? "Arial" : fontStyle.FontName;
            font1.FontHeightInPoints = (fontStyle.FontHeightInPoints == null) ? (short)10 : fontStyle.FontHeightInPoints.Value;
            cellStyle.SetFont(font1);

            return cellStyle;
        }

        private static short GetCellFormat(IWorkbook wb, string formatStr)
        {
            IDataFormat dataFormat = wb.CreateDataFormat();
            return dataFormat.GetFormat(formatStr);
        }

        private static string GetValue<T>(T obj, List<ColumnMapping> columnMapping, int order)
        {
            var fieldName = columnMapping[order].ModelFieldName;
            var prop = typeof(T).GetProperties().Where(q => q.Name == fieldName).First();
            var value = prop.GetValue(obj, null);

            return (value == null) ? "" : value.ToString();
        }

        private static void Flush(IWorkbook wb, string fullName)
        {
            //若檔案已存在先刪除
            if (File.Exists(fullName)) File.Delete(fullName);

            using (FileStream targetFs = File.Create(fullName))
            {
                wb.Write(targetFs);
                wb = null;
            }
        }
    }
}

 

使用範例

using MySample.Console.Models;
using NPOI.XSSF.UserModel;
using System.Collections.Generic;
using System;

namespace MySample.Console
{
    class Program
    {
        class Sample
        {
            public string Id { get; set; }   //故意在這裡定義為string 但輸出到Excel中需轉為數字
            public string Name { get; set; }
            public DateTime Birthday { get; set; }
        }

        static void Main(string[] args)
        {
            var data = new List<Sample>
            {
                new Sample { Id = "1", Name = "A", Birthday = DateTime.Parse("2001/1/1 12:00") },
                new Sample { Id = "2", Name = "B", Birthday = DateTime.Parse("2001/1/2 12:00") },
                new Sample { Id = "3", Name = "C", Birthday = DateTime.Parse("2001/1/3 12:00") },
            };

            var param = GetParam(data);
            NpoiHelper.ExportExcel(param);
        }

        private static NpoiParam<Sample> GetParam(List<Sample> data)
        {
            var param = new NpoiParam<Sample>
            {
                Workbook = new XSSFWorkbook(),            //要用一個新的或是用你自己的範本
                Data = data,                              //資料
                FileFullName = @"D:\result.xlsx",         //Excel檔要存在哪
                SheetName = "Data",                       //Sheet要叫什麼名子
                ColumnMapping = new List<ColumnMapping>   //欄位對應 (處理Excel欄名、格式轉換)
                {
                    new ColumnMapping { ModelFieldName = "Id", ExcelColumnName = "流水號", DataType = NpoiDataType.Number, Format = "0.00"},
                    new ColumnMapping { ModelFieldName = "Name", ExcelColumnName = "名子", DataType = NpoiDataType.String},
                    new ColumnMapping { ModelFieldName = "Birthday", ExcelColumnName = "生日", DataType = NpoiDataType.Date, Format="yyyy-MM-dd"},
                },
                FontStyle = new FontStyle                //是否需自定Excel字型大小
                {
                    FontName = "Calibri",
                    FontHeightInPoints = 11,
                },
                ShowHeader = true,                      //是否畫表頭
                IsAutoFit = true,                       //是否啟用自動欄寬
            };

            return param;
        }
    }
}