C#中使用NPOI读取和写入Excel数据

本文介绍在C#代码中如何使用NPOI读取和写入Excel数据。

安装NPOI组件

首先,通过Visual Studio菜单“工具”>>“NuGet包管理”>>“管理解决方案的NuGet程序包”,打开NuGet管理窗口;然后,在“浏览”页中搜索“npoi”,选中“NPOI”,并安装到当前项目中。如下图所示。

安装NPOI组件

点击“安装”后接受一些协议,然后等待安装完成即可。

将数据写入Excel

下面的代码(cfx/office/tExcelWriter.cs)是tExcelWriter类的一部分,可复制到Visual Studio中阅读和测试。

C#
using System;
using System.IO;
using System.Data;
using NPOI.SS.UserModel;
using NPOI.HSSF.UserModel;
using NPOI.XSSF.UserModel;

namespace cfx.office
{
    public class tExcelWriter
    {
        private string myFileName;
        private DataTable myData;
        private string mySheetName;
        private int myBeginRow;
        private int myBeginColumn;
        private bool myWriteColumnName;
        //
        private tExcelWriter() { }
        //
        public static tExcelWriter Create(string filename,
            DataTable data, string sheetName = null,
            int beginRow = 1, int beginCol = 1,
            bool writeColumnName = true)
        {
            return new tExcelWriter()
            {
                myFileName = filename,
                myData = data,
                mySheetName = sheetName,
                myBeginRow = beginRow - 1,
                myBeginColumn = beginCol - 1,
                myWriteColumnName = writeColumnName
            };
        }
        //
        public bool Write()
        {
            string ext = Path.GetExtension(myFileName).ToLower();
            if (ext == ".xls") return WriteXls();
            else return WriteXlsx();
        }

        // 将DataTable对象数据写入.xls文件指定工作表的指定位置
        private bool WriteXls()
        {
            try
            {
                if (myData == null || myData.Columns.Count < 1)
                    return false;
                //
                using (HSSFWorkbook wb = new HSSFWorkbook())
                {
                    if (mySheetName == null || mySheetName.Trim().Length == 0)
                        mySheetName = "数据";
                    ISheet sheet = wb.CreateSheet(mySheetName);
                    // 写入工作表
                    int curRow = myBeginRow;
                    // 写入列名
                    if (myWriteColumnName)
                    {
                        IRow sheetRow = sheet.CreateRow(curRow);
                        for (int col = 0; col < myData.Columns.Count; col++)
                        {
                            ICell cell = sheetRow.CreateCell(col + myBeginColumn);
                            cell.SetCellType(CellType.String);
                            cell.SetCellValue(myData.Columns[col].ColumnName);
                        }
                        curRow++;
                    }
                    // 日期和时间格式
                    IDataFormat dataFormat = wb.CreateDataFormat();
                    ICellStyle datetimeStyle = wb.CreateCellStyle();
                    datetimeStyle.DataFormat =
                        dataFormat.GetFormat("yyyy/MM/dd HH:mm:ss");
                    // 写入数据行
                    for (int row = 0; row < myData.Rows.Count; row++)
                    {
                        IRow sheetRow = sheet.CreateRow(curRow++);
                        for (int col = 0; col < myData.Columns.Count; col++)
                        {
                            ICell cell = sheetRow.CreateCell(col + myBeginColumn);
                            if (myData.Rows[row][col] == DBNull.Value)
                            {
                                cell.SetCellValue("");
                            }
                            else
                            {
                                Type dataType = myData.Columns[col].DataType;
                                if (Comm.IsNumeric(dataType))
                                {
                                    cell.SetCellType(CellType.Numeric);
                                    double result;
                                    if (Obj.TryToDbl(myData.Rows[row][col], out result))
                                        cell.SetCellValue(result);
                                }
                                else if (dataType.Name == "DateTime")
                                {
                                    cell.CellStyle = datetimeStyle;
                                    DateTime result;
                                    if (Obj.TryToDate(myData.Rows[row][col], out result))
                                        cell.SetCellValue(result);
                                }
                                else if (dataType.Name == "Boolean")
                                {
                                    cell.SetCellType(CellType.Boolean);
                                    cell.SetCellValue(Obj.ToBool(myData.Rows[row][col]));
                                }
                                else
                                {
                                    cell.SetCellType(CellType.String);
                                    cell.SetCellValue(Obj.ToStr(myData.Rows[row][col]));
                                }
                            }
                        }
                    }
                    //
                    using (FileStream fs = new FileStream(myFileName,
                        FileMode.Create, FileAccess.Write))
                    {
                        wb.Write(fs);
                    }
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        // 将DataTable对象数据写入.xlsx文件指定工作表的指定位置
        private bool WriteXlsx()
        {
            try
            {
                if (myData == null || myData.Columns.Count < 1)
                    return false;
                //
                using (XSSFWorkbook wb = new XSSFWorkbook())
                {
                    if (mySheetName == null || mySheetName.Trim().Length == 0)
                        mySheetName = "数据";
                    ISheet sheet = wb.CreateSheet(mySheetName);
                    // 写入工作表
                    int curRow = myBeginRow;
                    // 写入列名
                    if (myWriteColumnName)
                    {
                        IRow sheetRow = sheet.CreateRow(curRow);
                        for (int col = 0; col < myData.Columns.Count; col++)
                        {
                            ICell cell = sheetRow.CreateCell(col + myBeginColumn);
                            cell.SetCellType(CellType.String);
                            cell.SetCellValue(myData.Columns[col].ColumnName);
                        }
                        curRow++;
                    }
                    // 日期和时间格式
                    IDataFormat dataFormat = wb.CreateDataFormat();
                    ICellStyle datetimeStyle = wb.CreateCellStyle();
                    datetimeStyle.DataFormat =
                        dataFormat.GetFormat("yyyy/MM/dd HH:mm:ss");
                    // 写入数据行
                    for (int row = 0; row < myData.Rows.Count; row++)
                    {
                        IRow sheetRow = sheet.CreateRow(curRow++);
                        for (int col = 0; col < myData.Columns.Count; col++)
                        {
                            ICell cell = sheetRow.CreateCell(col + myBeginColumn);
                            if (myData.Rows[row][col] == DBNull.Value)
                            {
                                cell.SetCellValue("");
                            }
                            else
                            {
                                Type dataType = myData.Columns[col].DataType;
                                if (Comm.IsNumeric(dataType))
                                {
                                    cell.SetCellType(CellType.Numeric);
                                    double result;
                                    if (Obj.TryToDbl(myData.Rows[row][col], out result))
                                        cell.SetCellValue(result);
                                }
                                else if (dataType.Name == "DateTime")
                                {
                                    cell.CellStyle = datetimeStyle;
                                    DateTime result;
                                    if (Obj.TryToDate(myData.Rows[row][col], out result))
                                        cell.SetCellValue(result);
                                }
                                else if (dataType.Name == "Boolean")
                                {
                                    cell.SetCellType(CellType.Boolean);
                                    cell.SetCellValue(Obj.ToBool(myData.Rows[row][col]));
                                }
                                else
                                {
                                    cell.SetCellType(CellType.String);
                                    cell.SetCellValue(Obj.ToStr(myData.Rows[row][col]));
                                }
                            }
                        }
                    }
                    //
                    using (FileStream fs = new FileStream(myFileName,
                        FileMode.Create, FileAccess.Write))
                    {
                        wb.Write(fs);
                    }
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }
        // 其它代码
    }
}

tExcelWriter类中,首先定义了6个内部字段,分别保存写入Excel时需要的参数,包括:

  • myFileName,string类型,写入的Excel文件路径。
  • myData,DataTable类型,包含写入数据的对象。
  • mySheetName,string类型,写入的工作表(WorkSheet)名称。
  • myBeginRow,int类型,在工作表中开始写入数据的行号。
  • myBeginColumn,int类型,在工作表中开始写入数据的列号。
  • myWriteColumnName,bool类型,是否写入列名。

接下来是私有的构造函数,意味着不能在tExcelWriter类的外部使用new关键字创建实例,而创建实例的工作由Create()静态方法完成,方法中包含6个参数,对应了6个内部字段,分别是:

  • filename,指定写入的Excel路径。
  • data,指定写入数据的DataTable对象。
  • sheetName,写入的工作表名称,默认值为null。在不同的写入方法中处理会有所区别,稍后讨论。
  • beginRow和beginCol,指定开始写入数据的位置行号和列号,为更加直观设置数据,参数中使用从1开始的索引;但在Create()方法中会将指定的值减1,因为NPOI中的行号和列号是从0开始的。
  • writeColumnName,是否写入列名,默认为true。

接下来定义了三个方法,分别是:

  • Write()方法,公共方法,其中会根据文件的扩展名分别调用WriteXls()或WriteXlsx()方法。
  • WriteXls()方法,私有方法,将数据写入.xls文件。
  • WriteXlsx()方法,私有方法,将数据写入.xlsx文件。

WriteXls()方法和WriteXlsx()方法的代码几乎一样,只是在创建工作簿(Workbook)的时候使用了不同的类型,在处理.xls文件时使用了HSSFWorkbook类型,处理.xlsx文件时使用了XSSFWorkbook类型。这里,并没有将重复代码定义为单独的方法,在处理不同类型Excel文件时,如果对代码有特殊要求,调整起来会更加方便。

下面以WriteXls()方法为例进行说明。创建工作簿后,首先指定写入工作表的名称,如果指定的mySheetName为null或空白字符串,则使用“数据”;然后,使用工作簿对象的CreateSheet()方法创建工作表(Worksheet)对象。

创建行对象(IRow类型)时,使用工作表对象的CreateRow()方法,其参数为从0开始的行索引(行号),即第一行索引为0、第二行索引1,以此类推。

创建单元格对象(ICell类型)时,使用行对象的CreateCell()方法,其参数为从0开始的列索引(列号)。

单元格对象的SetCellType()方法用于设置单元格类型,其参数使用CellType枚举,成员包括Numeric(0)、String(1)、Formula(2)、Blank(3)、Boolean(4)、Error(5)。

单元格对象的SetCellValue()方法可以设置单元格的值,方法有不同的重载版本,参数分别是string、double、bool、DateTime和IRichTextString类型。

设置单元格格式时使用了IDataFormat和ICellStyle对象,方法中设置显示日期和时间数据格式为“年/月/日 时:分:秒”。

接下来会逐行写入数据,需要注意,单元格数据会根据DataTable对象中列的数据类型分别进行操作。

DBNull值时写入空白字符串。

数值类型时会转换为double类型写入。这里使用Obj.TryToDbl()方法转换,相关定义如下(cfx/Obj.cs):

C#
using System;

namespace cfx
{
    public static class Obj
    {
        // 其它代码
        //
        public static string ToStr(object obj, string defVal = "")
        {
            if (obj != null)
                return obj.ToString();
            else
                return defVal;
        }
        //
        public static bool ToBool(object obj)
        {
            try { return Convert.ToBoolean(obj); }
            catch { return false; }
        }
        // 尝试转换类型
        public static bool TryToDbl(object obj,out double result)
        {
            result = 0D;
            return obj != null && double.TryParse(obj.ToString(), out result);
        }
        public static bool TryToDate(object obj, out DateTime result)
        {
            result = DateTime.MinValue;
            return obj != null && DateTime.TryParse(obj.ToString(), out result);
        }
    }
}

DateTime类型数据会通过Obj.TryToDate()方法转换后写入,转换失败不写入单元格。

Boolean类型时通过Obj.ToBool()方法转换后写入。

其它类型则写入string类型数据。

保存工作簿时使用了Write()方法,其参数使用了FileStream对象,注意FileStream构造函数的使用,参数一指定了文件的路径,这里是需要保存Excel的文件路径;参数二指定打开模式,这里设置为创建(FileMode.Create),此操作会覆盖已存在的同名文件;参数二指定访问模式,这里指定FileAccess.Write,表示写入。

操作成功时,WriteXls()方法返回true,否则返回false。

下面的代码,在Program.cs文件中测试tExcelWriter类的使用。

C#
using System;
using System.Data;
using cfx.office;

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            DataTable data = tApp.DbJet.GetTable(@"select * from t1");
            bool result = tExcelWriter.Create(@"d:\t1.xls", data).Write();
            Console.WriteLine(result);
        }
    }
}

代码会读取t1表中的所有数据,然后写入d:\t1.xls文件,操作成功会显示True。写入内容如下图所示。

将数据导出到Excel文件

下面的代码(cfx/office/tExcelWriter.cs)定义了WriteExisted()和相关方法,用于将数据写入已存在的Excel文件。

C#
using System;
using System.IO;
using System.Data;
using NPOI.SS.UserModel;
using NPOI.HSSF.UserModel;
using NPOI.XSSF.UserModel;

namespace cfx.office
{
    public class tExcelWriter
    {
        // 其它代码
        // 写入已存在的文件
        public bool WriteExisted()
        {
            string ext = Path.GetExtension(myFileName).ToLower();
            if (ext == ".xls") return WriteXlsExisted();
            else return WriteXlsxExisted();
        }

        // 写入已存在的xls文件
        private bool WriteXlsExisted()
        {
            try
            {
                if (myData == null || myData.Columns.Count < 1 ||
                    File.Exists(myFileName) == false)
                    return false;
                //
                using (FileStream fs = new FileStream(myFileName,
                        FileMode.Open, FileAccess.Read))
                {
                    using (HSSFWorkbook wb = new HSSFWorkbook(fs))
                    {
                        int sheetIndex = 0;
                        if (mySheetName != null && mySheetName.Trim().Length > 0)
                            sheetIndex = wb.GetSheetIndex(mySheetName);
                        if (sheetIndex == -1) sheetIndex = 0;
                        ISheet sheet = wb.GetSheetAt(sheetIndex);
                        //
                        int curRow = myBeginRow;
                        // 写入列名
                        if (myWriteColumnName)
                        {
                            IRow sheetRow = sheet.GetRow(curRow);
                            if (sheetRow == null) 
                                sheetRow = sheet.CreateRow(curRow);
                            for (int col = 0; col < myData.Columns.Count; col++)
                            {
                                int colIndex = col + myBeginColumn;
                                ICell cell = sheetRow.GetCell(colIndex);
                                if (cell == null) 
                                    cell = sheetRow.CreateCell(colIndex);
                                cell.SetCellType(CellType.String);
                                cell.SetCellValue(myData.Columns[col].ColumnName);
                            }
                            curRow++;
                        }
                        // 日期和时间格式
                        IDataFormat dataFormat = wb.CreateDataFormat();
                        ICellStyle datetimeStyle = wb.CreateCellStyle();
                        datetimeStyle.DataFormat =
                            dataFormat.GetFormat("yyyy/MM/dd HH:mm:ss");
                        // 写入数据行
                        for (int row = 0; row < myData.Rows.Count; row++)
                        {
                            IRow sheetRow = sheet.GetRow(curRow);
                            if (sheetRow == null)
                                sheetRow = sheet.CreateRow(curRow);
                            for (int col = 0; col < myData.Columns.Count; col++)
                            {
                                int colIndex = col + myBeginColumn;
                                ICell cell = sheetRow.GetCell(colIndex);
                                if (cell == null) 
                                    cell = sheetRow.CreateCell(colIndex);
                                if (myData.Rows[row][col] == DBNull.Value)
                                {
                                    cell.SetCellValue("");
                                }
                                else
                                {
                                    Type dataType = myData.Columns[col].DataType;
                                    if (Comm.IsNumeric(dataType))
                                    {
                                        cell.SetCellType(CellType.Numeric);
                                        double result;
                                        if (Obj.TryToDbl(myData.Rows[row][col], out result))
                                            cell.SetCellValue(result);
                                    }
                                    else if (dataType.Name == "DateTime")
                                    {
                                        cell.CellStyle = datetimeStyle;
                                        DateTime result;
                                        if (Obj.TryToDate(myData.Rows[row][col], out result))
                                            cell.SetCellValue(result);
                                    }
                                    else if (dataType.Name == "Boolean")
                                    {
                                        cell.SetCellType(CellType.Boolean);
                                        cell.SetCellValue(Obj.ToBool(myData.Rows[row][col]));
                                    }
                                    else
                                    {
                                        cell.SetCellType(CellType.String);
                                        cell.SetCellValue(Obj.ToStr(myData.Rows[row][col]));
                                    }
                                }
                            }
                            curRow++;
                        }
                        //
                        using (FileStream fs1 =
                            new FileStream(myFileName, FileMode.Create, FileAccess.Write))
                        {
                            wb.Write(fs1);
                        }
                    }
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        // 写入已存在的xls文件
        private bool WriteXlsxExisted()
        {
            try
            {
                if (myData == null || myData.Columns.Count < 1 ||
                    File.Exists(myFileName) == false)
                    return false;
                //
                using (FileStream fs = new FileStream(myFileName,
                        FileMode.Open, FileAccess.Read))
                {
                    using (XSSFWorkbook wb = new XSSFWorkbook(fs))
                    {
                        int sheetIndex = 0;
                        if (mySheetName != null && mySheetName.Trim().Length > 0)
                            sheetIndex = wb.GetSheetIndex(mySheetName);
                        if (sheetIndex == -1) sheetIndex = 0;
                        ISheet sheet = wb.GetSheetAt(sheetIndex);
                        //
                        int curRow = myBeginRow;
                        // 写入列名
                        if (myWriteColumnName)
                        {
                            IRow sheetRow = sheet.GetRow(curRow);
                            if (sheetRow == null)
                                sheetRow = sheet.CreateRow(curRow);
                            for (int col = 0; col < myData.Columns.Count; col++)
                            {
                                int colIndex = col + myBeginColumn;
                                ICell cell = sheetRow.GetCell(colIndex);
                                if (cell == null)
                                    cell = sheetRow.CreateCell(colIndex);
                                cell.SetCellType(CellType.String);
                                cell.SetCellValue(myData.Columns[col].ColumnName);
                            }
                            curRow++;
                        }
                        // 日期和时间格式
                        IDataFormat dataFormat = wb.CreateDataFormat();
                        ICellStyle datetimeStyle = wb.CreateCellStyle();
                        datetimeStyle.DataFormat =
                            dataFormat.GetFormat("yyyy/MM/dd HH:mm:ss");
                        // 写入数据行
                        for (int row = 0; row < myData.Rows.Count; row++)
                        {
                            IRow sheetRow = sheet.GetRow(curRow);
                            if (sheetRow == null)
                                sheetRow = sheet.CreateRow(curRow);
                            for (int col = 0; col < myData.Columns.Count; col++)
                            {
                                int colIndex = col + myBeginColumn;
                                ICell cell = sheetRow.GetCell(colIndex);
                                if (cell == null)
                                    cell = sheetRow.CreateCell(colIndex);
                                if (myData.Rows[row][col] == DBNull.Value)
                                {
                                    cell.SetCellValue("");
                                }
                                else
                                {
                                    Type dataType = myData.Columns[col].DataType;
                                    if (Comm.IsNumeric(dataType))
                                    {
                                        cell.SetCellType(CellType.Numeric);
                                        double result;
                                        if (Obj.TryToDbl(myData.Rows[row][col], out result))
                                            cell.SetCellValue(result);
                                    }
                                    else if (dataType.Name == "DateTime")
                                    {
                                        cell.CellStyle = datetimeStyle;
                                        DateTime result;
                                        if (Obj.TryToDate(myData.Rows[row][col], out result))
                                            cell.SetCellValue(result);
                                    }
                                    else if (dataType.Name == "Boolean")
                                    {
                                        cell.SetCellType(CellType.Boolean);
                                        cell.SetCellValue(Obj.ToBool(myData.Rows[row][col]));
                                    }
                                    else
                                    {
                                        cell.SetCellType(CellType.String);
                                        cell.SetCellValue(Obj.ToStr(myData.Rows[row][col]));
                                    }
                                }
                            }
                            curRow++;
                        }
                        //
                        using (FileStream fs1 =
                            new FileStream(myFileName, FileMode.Create, FileAccess.Write))
                        {
                            wb.Write(fs1);
                        }
                    }
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }
    }
}

需要将数据写入已存在的Excel文件时,创建工作簿对象的方式有所不同,在创建HSSFWorkbook或XSSFWorkbook对象时,需要使用一个包含写入文件路径的FileStream对象作为构造方法的参数,设置模式为打开(FileMode.Open),访问方式为读取(FileAccess.Read)即可。

获取工作表对象时,如果指定的工作表名称(mySheetName)为null、空白字符串,或者名称不存在,则默认使用第一个工作表(索引0)。其中,工作簿对象的GetSheetIndex()方法会根据工作表名返回索引值,工作表不存在时返回-1。

获取行对象(IRow类型)时,为避免覆盖工作表中已存在的内容,首先会使用工作表对象的GetRow()方法获取,如果行不存在返回null,此时再使用CreateRow()方法创建行。单元格对象(ICell类型)的处理也是这样,首先会使用行对象的GetCell()方法获取单元格对象,如果单元格对象不存在会返回null,此时使用CreateCell()方法创建新的单元格对象。

最后,在保存Excel文件时要注意,这里会创建一个新的FileStream对象,设置打开模式为创建(FileMode.Create),访问方式为写入(FileAccess.Write);然后调用工作簿对象的Write()方法覆盖写入目标文件即可。

接下来,可以在d:\t1.xls文件中创建一个新的工作表,命名为“数据1”,然后在Program.cs文件中修改代码如下。

C#
using System;
using System.Data;
using cfx.office;

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            DataTable data = tApp.DbJet.GetTable(@"select * from t1");
            bool result = 
                tExcelWriter.Create(@"d:\t1.xls", data,"数据1").WriteExisted();
            Console.WriteLine(result);
        }
    }
}

导出数据如下图所示。

将数据导出到已存在的Excel文件

实际工作中,可能还会先定义Excel模板,每次导出数据会复制模板文件到一个新的文件,然后写入数据。下面的代码(cfx/office/tExcelWriter.cs),在tExcelWriter类中添加WriteByTemplate()方法,用于将数据导出到Excel模板文件。

C#
using System;
using System.IO;
using System.Data;
using NPOI.SS.UserModel;
using NPOI.HSSF.UserModel;
using NPOI.XSSF.UserModel;

namespace cfx.office
{
    public class tExcelWriter
    {
        // 其它代码
        // 按模板写入,参数为模板文件路径
        public bool WriteByTemplate(string template)
        {
            try
            {
                if (File.Exists(template) == false) 
                    return false;
                File.Copy(template, myFileName, true);
                return WriteExisted();
            }
            catch { return false; }
        }
        //
    }
}

WriteByTemplate()方法的参数需要指定模板文件的路径,方法中会根据文件扩展名分别调用WriteXlsExisted()和WriteXlsxExisted()方法。

接下来,在d:盘下创建template.xlsx文件,并修改第一个工作表内容如下图所示。

定义Excel模板

下面的代码,在Program.cs文件中测试按模板导出数据。

C#
using System;
using System.Data;
using cfx.office;

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            DataTable data = tApp.DbJet.GetTable(@"select * from t1");
            bool result =
                tExcelWriter.Create(@"d:\t1.xlsx", data, null, 5, 3, false)
                .WriteByTemplate(@"d:\template.xlsx");
            Console.WriteLine(result);
        }
    }
}

执行代码,会复制d:\template.xlsx文件到d:\t1.xlsx,然后从第5行第3列开始写入数据,完成效果如下图所示。

按模板写入Excel

读取Excel数据

接下来会实现读取Excel数据的功能,并将读取的数据保存到DataTable对象。下面的代码(cfx/office/tExcelReader.cs)创建tExcelReader类实现Excel数据读取功能。

C#
using System;
using System.Data;
using NPOI.SS.UserModel;

namespace cfx.office
{
    public class tExcelReader
    {
        // Excel文件名
        private string myFileName;
        // 读取式作表索引
        private int mySheetIndex;
        // 如果设置工作表名称,则优先使用
        private string mySheetName;
        // 开始读取数据的行和列
        private int myBeginColumn;
        private int myBeginRow;
        // 第一行作为列名
        private bool myFirstRowAsColumnName;
        // 读取的行数,大于0时有效,否则读取全部
        private int myReadRowCount;
        //
        private tExcelReader() { }
        //
        public static tExcelReader Create(string filename,
            int sheetIndex = 0, string sheetName = null,
            int beginRow = 1, int beginCol = 1,
            bool firstRowAsColName = true, int readRowCount = -1)
        {
            return new tExcelReader()
            {
                myFileName = filename,
                mySheetIndex = sheetIndex,
                mySheetName = sheetName,
                myBeginRow = beginRow - 1,
                myBeginColumn = beginCol - 1,
                myFirstRowAsColumnName = firstRowAsColName,
                myReadRowCount = readRowCount
            };
        }

        // 执行读取操作
        public DataTable Read()
        {
            try
            {
                IWorkbook wb = WorkbookFactory.Create(myFileName);
                // 优先使用名称获取工作表
                if (mySheetName != null)
                    mySheetIndex = wb.GetSheetIndex(mySheetName);
                if (mySheetIndex < 0 || mySheetIndex > wb.NumberOfSheets - 1)
                    mySheetIndex = 0;
                ISheet sheet = wb.GetSheetAt(mySheetIndex);
                // 读取第一行列数为全部行读取列数
                int colCount = sheet.GetRow(myBeginRow).LastCellNum;
                int rowCount = sheet.LastRowNum + 1;
                // 读取数据并添加到DataTable对象
                DataTable tbl = new DataTable();
                // 添加DataTable对象的列
                for (int col = myBeginColumn; col < colCount; col++)
                    tbl.Columns.Add();
                // 
                IRow readRow;
                // 读取行数据计数
                int readRowCounter = 0;
                //
                for (int row = myBeginRow; row < rowCount; row++)
                {
                    readRow = sheet.GetRow(row);
                    DataRow dataRow = tbl.NewRow();
                    for (int col = myBeginColumn; col < colCount; col++)
                    {
                        int tblColIndex = col - myBeginColumn;
                        try
                        {
                            ICell cell = readRow.GetCell(col);
                            if (cell == null)
                            {
                                dataRow[tblColIndex] = DBNull.Value;
                            }
                            else if (cell.CellType == CellType.Numeric)
                            {
                                // Excel中的日期时间值是Double类型
                                if (DateUtil.IsCellDateFormatted(cell))
                                    dataRow[tblColIndex] = cell.DateCellValue;
                                else
                                    dataRow[tblColIndex] = cell.NumericCellValue;
                            }
                            else if (cell.CellType == CellType.Boolean)
                            {
                                dataRow[tblColIndex] =
                                    cell.BooleanCellValue ? 1 : 0;
                            }
                            else
                            {
                                // 其它格式以文本形式显示,纯空白字符设置为空值
                                string val = cell.StringCellValue.Trim();
                                if (val.Length == 0)
                                    dataRow[tblColIndex] = DBNull.Value;
                                else
                                    dataRow[tblColIndex] = val;
                            }
                        }
                        catch
                        {
                            dataRow[tblColIndex] = DBNull.Value;
                        }
                    }
                    tbl.Rows.Add(dataRow);
                    // 读取行
                    readRowCounter++;
                    if (myReadRowCount > 0 && myReadRowCount == readRowCounter)
                        break;
                }
                // 数据第一行提升为标题,然后从数据行删除
                if (myFirstRowAsColumnName)
                {
                    string sName;
                    for (int col = 0; col < tbl.Columns.Count; col++)
                    {
                        sName = tbl.Rows[0][col].ToString().Trim();
                        if (sName != "")
                            tbl.Columns[col].ColumnName = sName;
                    }
                    tbl.Rows.RemoveAt(0);
                }
                //
                return tbl;
            }
            catch
            {
                return null;
            }
        }
        //
    }
}

tExcelReader类中,首先定义了一些内部字段,用于保存读取Excel数据的参数;私有构造函数确定不能在tExcelReader类的外部使用new关键字创建实例;Create()静态方法用于创建tExcelReader实例,其参数与这些字段一一对应,方法的参数包括:

  • filename,string类型,设置读取数据的Excel文件路径。
  • sheetIndex,int类型,指定读取工作表的索引,默认为0,即读取第一个工作表。
  • sheetName,string类型,默认为null。设置读取的工作表名称,如果设置了名称,则优先使用。
  • beginRow和beginCol,int类型,设置开始读取位置的行号和列号,使用从1开始的序号。方法中会减1,因为NPOI中的行和列索引是从0开始的。
  • firstRowAsColName,bool类型,默认为true。设置是否将第一行提升为表的列标题。
  • readRowCount,int类型,设置从起始行开始读取的最大行数,默认为-1,表示读取全部行。

数据读取操作使用Read()方法完成。方法中,首先使用WorkbookFactory.Create()方法创建工作簿对象,参数为Excel文件路径;此方法会自动识别文件类型。

接下来,确定读取的工作表时会优先使用工作表名称,如果没有指定工作表名称则使用索引值,如果索引值小于0或大于等于工作表的数量则默认使用0,即读取第一个工作表。

方法中,使用读取的第一行的列数为全部行的读取列数,这里使用了行对象的LastCellNum属性,此属性为只读属性,返回行中的单元格数量(最大索引加1)。

获取工作表的行数时,使用工作表对象的LastRowNum属性加1,其中,LastRowNum属性会返回最大的行索引值。

读取单元格数据时会分别处理以下情况:

  • 当单元格对象为null时,会在DataTable中写入DBNull值。
  • 当单元格数据为数值时,首先使用DateUtil.IsCellDateFormatted(cell)方法判断是不是日期格式,当数据为日期格式时,使用DateCellValue属性获取日期数据;如果只是单纯的数值数据,则使用NumericCellValue属性获取数值数据。
  • 当单元格为布尔(Boolean)类型时需要注意,这里会将TRUE值转换为1,FALSE值转换为0,然后写入DataTable对象。这么做的原因是,在很多应用场景下,读取的Excel数据可能需要导入数据库,而关系型数据库没有布尔类型,所以使用1和0分别表示true和false值。
  • 当单元格为其它类型时,会使用StringCellValue属性获取文本数据,这里,如果数据为空字符串或纯空白字符,会在DataTable对象中写入DBNull值,否则写入删除开始和结束位置空白字符后的文本内容。
  • 最后,当单元格数据处理异常时,同样会在DataTable对象中写入DBNull值。

下面了解一些DataTable对象的动态操作。

首先,添加列时可以使用DataTable对象Columns集合中的Add()方法,方法可以使用指定的列名作为参数,不指定列名时,列名会自动使用Column1、Column2、……。获取列对象(DataColumn)时,可以在Columns集合中使用从0开始的数值索引或列名索引,读取列名时可以使用DataColumn对象的ColumnName属性。

创建新行时,使用DataTable对象的NewRow()方法,方法会返回一个新的DataRow对象。此时,行对象中的数据单元会自动匹配表中列的设置。

行对象中,可以使用0 开始的数值索引或列名索引获取列对应的数据单元。

将行对象添加到DataTable对象时,使用Rows集合中的Add()方法。

在Read()方法的最后,如果需要将第一行提升为列名,会将第一行数据设置为对应的列名,然后将第一行数据删除。删除行时使用了Rows集合中的RemoveAt()方法,参数为删除行的索引(从0开始)。这里应注意,如对应的数据为空字符串或纯空白字符,则不作为列名。

假设d:\t1.xls文件中有如下图所示的数据。

准备读取的数据

下面的代码,在Program.cs文件中测试tExcelReader类的使用。

C#
using System;
using System.Data;
using cfx.office;

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            DataTable data = tExcelReader.Create(@"d:\t1.xls").Read();
            Console.WriteLine(data.Rows.Count);
            Console.WriteLine(data.Columns.Count);
            for(int col = 0; col < data.Columns.Count; col++)
            {
                Console.WriteLine(data.Columns[col].ColumnName);
            }
        }
    }
}

代码会读取d:\t1.xls文件中第一个工作表的数据,读取位置默认为第一行第一列。执行代码会显示读取完成后的数据行数、列数和列名,结果如下图所示。

读取Excel数据