1. 程式人生 > >C# Excel操作記錄 及 程式碼優化考慮

C# Excel操作記錄 及 程式碼優化考慮

最近,因為有需求,找了很多C#操作Excel的資料來學習,完了準備在這裡做個記錄,以備後日不時之需。

我看到的,操作Excel的方式有四種:

  1. com元件
  2. OleDb
  3. npoi
  4. epplus

com元件

首先,com 元件的方式指的是利用 office 的 Excel 元件對Excel檔案進行操作,這種方式需要電腦上安裝有Excel,專案中也要新增相關引用,專案執行時也要啟動Excel程序。

據說,這種方式的功能很強大,說是Excel能實現的功能都能夠通過這種方法在程式碼裡進行設定,反正我是沒有驗證過,因為它實在是太慢了。其他方法1s就可以搞定的,用它穩定在8.7s...

OleDb

使用Microsoft Jet 提供程式用於連線到 Excel 工作簿,將Excel檔案作為資料來源來讀寫,這種方法讀寫都相當快,就是不是很靈活。

它需要使用指定的連線字串初始化 System.Data.OleDb.OleDbConnection 類的新例項,.xls檔案 和 .xlsx檔案的連線字串並不一樣。

case ".xls":
          strConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + pathName + 
";Extended Properties='Excel 8.0;HDR=Yes;IMEX=2;'";
          break;
case ".xlsx":
          strConn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + pathName + 
";Extended Properties='Excel 12.0;HDR=Yes;IMEX=2;'";
          break;

上面的HDR用於判斷是否將第一行作為資料讀取(個人感覺應該只在讀操作有用),IMEX用於判斷是讀取還是寫入,這個需要自己嘗試,我只能說,我在寫入操作的時候將其設為0,讀取的時候設為1,你也可以設為2,錯了再調整...

它能返回所有的sheet的名稱,以及單個sheet的所有列名,這有時是相當有用的。

        #region 獲取Excel檔案的表名 返回list集合
        /// <param name="excelFileName">路徑名</param>
        /// <returns></returns>
        public static List<string> GetExcelTableName(OleDbConnection conn, string pathName)
        {
            List<string> tableName = new List<string>();
            if (File.Exists(pathName))
            {
                //conn.Open();
                System.Data.DataTable dt = conn.GetOleDbSchemaTable(System.Data.OleDb.OleDbSchemaGuid.Tables, null);
                string strSheetTableName = null;
                foreach (System.Data.DataRow row in dt.Rows)
                {
                    strSheetTableName = row["TABLE_NAME"].ToString();
                    //過濾無效SheetName   
                    if (strSheetTableName.Contains("$") && strSheetTableName.Replace("'", "").EndsWith("$"))
                    {
                        strSheetTableName = strSheetTableName.Replace("'", "");   //可能會有 '1X$' 出現
                        strSheetTableName = strSheetTableName.Substring(0, strSheetTableName.Length - 1);
                        tableName.Add(strSheetTableName);
                    }
                }
            }
            return tableName;
        }

        #endregion
        #region 獲取EXCEL工作表的列名 返回list集合
        /// <param name="Path">Excel路徑名</param>
        /// <returns></returns>
        public static List<string> getExcelFileInfo(string pathName)
        {
            string strConn;
            List<string> list_ColumnName = new List<string>();
            FileInfo file = new FileInfo(pathName);
            if (!file.Exists) { throw new Exception("檔案不存在"); }
            string extension = file.Extension;
            switch (extension)
            {
                case ".xls":
                    strConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + pathName + ";Extended Properties='Excel 8.0;HDR=Yes;IMEX=2;'";
                    break;
                case ".xlsx":
                    strConn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + pathName + ";Extended Properties='Excel 12.0;HDR=Yes;IMEX=2;'";
                    break;
                default:
                    strConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + pathName + ";Extended Properties='Excel 8.0;HDR=Yes;IMEX=1;'";
                    break;
            }
            System.Data.OleDb.OleDbConnection conn = new System.Data.OleDb.OleDbConnection(strConn);
            conn.Open();
            System.Data.DataTable table = conn.GetOleDbSchemaTable(System.Data.OleDb.OleDbSchemaGuid.Tables, new object[] { null, null, null, null });

            string TableName = null;
            string ColumnName = null;
            foreach (System.Data.DataRow drow in table.Rows)
            {
                TableName = drow["Table_Name"].ToString();
                if (TableName.Contains("$") && TableName.Replace("'", "").EndsWith("$"))
                {
                    System.Data.DataTable tableColumns = conn.GetOleDbSchemaTable(System.Data.OleDb.OleDbSchemaGuid.Columns, new object[] { null, null, TableName, null });
                    foreach (System.Data.DataRow drowColumns in tableColumns.Rows)
                    {
                        ColumnName = drowColumns["Column_Name"].ToString();
                        list_ColumnName.Add(ColumnName);
                    }
                }
            }
            return list_ColumnName;
        }

        #endregion

讀寫的部分準備用copy來進行說明,雖然不會用到。

        #region 讀取Excel,將第一行作為標題,其他行作為資料,再將其匯出到Excel,對空白行進行處理
        public static bool copy(string copyPath, string pastePath, string copy_sheetName)
        {
            DataSet ds = new DataSet();
            OleDbConnection copy_objConn = null;//Excel的連線
            OleDbConnection paste_objConn = null;//Excel的連線
            try
            {  
                string strExtension = System.IO.Path.GetExtension(copyPath);//獲取副檔名
                string strFileName = System.IO.Path.GetFileName(copyPath);//獲取檔名
                switch (strExtension)
                {
                    case ".xls":
                        copy_objConn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + copyPath
                            + ";" + "Extended Properties=\"Excel 8.0;HDR=YES;IMEX=1;\"");
                        break;
                    case ".xlsx":
                        copy_objConn = new OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + copyPath
                            + ";" + "Extended Properties=\"Excel 12.0;HDR=YES;IMEX=1;\"");
                        break;
                    default:
                        copy_objConn = null;
                        break;
                }
                if (copy_objConn == null)
                {
                    return false;
                }
                copy_objConn.Open();
                string strSql = "select * from [" + copy_sheetName + "] where [系統物料號] is not null";
                OleDbCommand objCmd = new OleDbCommand(strSql, copy_objConn);//獲取Excel指定Sheet表中的資訊
                OleDbDataAdapter myData = new OleDbDataAdapter(strSql, copy_objConn);
                myData.Fill(ds, copy_sheetName);//填充資料

                DataTable dt = ds.Tables[0];
                strExtension = System.IO.Path.GetExtension(pastePath);//獲取副檔名
                strFileName = System.IO.Path.GetFileName(pastePath);//獲取檔名
                switch (strExtension)
                {
                    case ".xls":
                        paste_objConn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + pastePath
                            + ";" + "Extended Properties=\"Excel 8.0;HDR=YES;IMEX=0;\"");
                        break;
                    case ".xlsx":
                        paste_objConn = new OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + pastePath
                            + ";" + "Extended Properties=\"Excel 12.0;HDR=YES;IMEX=0;\"");
                        break;
                    default:
                        paste_objConn = null;
                        break;
                }
                if (paste_objConn == null)
                {
                    return false;
                }
                paste_objConn.Open();
                StringBuilder strSQL = new StringBuilder();
                System.Data.OleDb.OleDbCommand cmd;
                string tableName = dt.TableName.Substring(0, dt.TableName.Length - 1);
                try
                {
                    List<string> list = GetExcelTableName(paste_objConn, pastePath);
                    if (list.Contains(tableName))
                    {
                        //覆蓋檔案時可能會出現Table 'Sheet1' already exists.所以這裡先刪除了一下
                        cmd = new System.Data.OleDb.OleDbCommand(string.Format("drop table {0}", tableName), paste_objConn);
                        cmd.ExecuteNonQuery();
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine("drop table failed!" + e.Message);
                }
                //建立表格欄位
                strSQL.Append("CREATE TABLE ").Append("[" + tableName + "]");
                strSQL.Append("(");
                for (int i = 0,int dt_column_count = dt.Columns.Count; i < dt_column_count ; i++)
                {
                    strSQL.Append("[" + dt.Columns[i].ColumnName + "] text,");
                }
                strSQL = strSQL.Remove(strSQL.Length - 1, 1);
                strSQL.Append(")");

                cmd = new System.Data.OleDb.OleDbCommand(strSQL.ToString(), paste_objConn);
                cmd.ExecuteNonQuery();
                //新增資料
                StringBuilder strvalue = null;
                for (int i = 0,int dt_row_count = dt.Rows.Count; i < dt_row_count; i++)
                {
                    strSQL.Clear();
                    strvalue = new StringBuilder();
                    for (int j = 0; j < dt.Columns.Count; j++)
                    {
                        strvalue.Append("'" + dt.Rows[i][j].ToString() + "'");
                        if (j != dt.Columns.Count - 1)
                        {
                            strvalue.Append(",");
                        }
                    }
                    cmd.CommandText = strSQL.Append(" insert into [" + dt.TableName.Substring(0, dt.TableName.Length - 1) + "] values (").Append(strvalue).Append(")").ToString();
                    cmd.ExecuteNonQuery();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("copy失敗!" + e.Message);
                return false;
            }
            finally
            {
                copy_objConn.Close();
                paste_objConn.Close();
            }
            return true;
        }
        #endregion

有一點需要注意的是,win32 平臺是可以正常用的,any CPU 平臺是不可以的,需要安裝微軟的程式等操作。

因為這個原因,我也沒有找有沒有操作格式的方法,有需要的自己找找看。

NPOI

這是花費時間最久的外掛,用完只能說真的不好用。

想要用這個外掛的需要在NuGet程式包裡搜尋安裝到專案中。

讀寫都不是很慢,但是資料量太大的無法處理,大概30000 * 20 的規模已經不能勝任了。

它對於.xls檔案 和 .xlsx檔案的處理也是不一樣的:

ISheet sheet = null;
IWorkbook workbook = null;
switch (strExtension)
{
      case ".xls":
           workbook = new HSSFWorkbook(fs);
           sheet = workbook.GetSheet(sheetName);
           break;
      case ".xlsx":
           workbook = new XSSFWorkbook(fs);
           sheet = workbook.GetSheet(sheetName);
           break;
      default:
           break;
}

npoi讀取Excel需要對內容進行型別判斷

private static object GetValueType(ICell cell, IWorkbook workbook)
{
    if (cell == null)
        return null;
    switch (cell.CellType)
    {
        case CellType.Blank: //BLANK:
            return null;
        case CellType.Boolean: //BOOLEAN:
            return cell.BooleanCellValue;
        case CellType.Numeric: //NUMERIC:
            return cell.NumericCellValue;
        case CellType.String: //STRING:
            return cell.StringCellValue;
        case CellType.Error: //ERROR:
            return cell.ErrorCellValue;
        case CellType.Formula: //FORMULA:
               
        default:
            return "=" + cell.CellFormula;
    }
}

現在來說遇到的第一個問題,比如說,我希望讀取A1的內容怎麼辦,用下面的方法可以嗎?

IRow row1 = sheet.GetRow(0);
ICell cell1 = row1.GetCell(0);
object value = GetValueType(cell1, workbook);

一般情況,是可以的,但是如果是公式,出來的結果並不是希望的結果,對上面的方法進行修改:

private static object GetValueType(ICell cell, IWorkbook workbook)
{
    if (cell == null)
        return null;
    switch (cell.CellType)
    {
        case CellType.Blank: //BLANK:
            return null;
        case CellType.Boolean: //BOOLEAN:
            return cell.BooleanCellValue;
        case CellType.Numeric: //NUMERIC:
            return cell.NumericCellValue;
        case CellType.String: //STRING:
            return cell.StringCellValue;
        case CellType.Error: //ERROR:
            return cell.ErrorCellValue;
        case CellType.Formula: //FORMULA:
            IFormulaEvaluator evaluator = null;
            if (workbook is HSSFWorkbook)
                evaluator = new HSSFFormulaEvaluator(workbook);
            else
                evaluator = new XSSFFormulaEvaluator(workbook);
            cell = evaluator.EvaluateInCell(cell);
            return cell.NumericCellValue;
                    
        default:
            return "=" + cell.CellFormula;
    }
}

修改過後,會發現,一部分公式成功獲得結果,一部分公式提示捕獲異常,在進行修改:

private static object GetValueType(ICell cell, IWorkbook workbook)
{
    if (cell == null)
        return null;
    switch (cell.CellType)
    {
        case CellType.Blank: //BLANK:
            return null;
        case CellType.Boolean: //BOOLEAN:
            return cell.BooleanCellValue;
        case CellType.Numeric: //NUMERIC:
            return cell.NumericCellValue;
        case CellType.String: //STRING:
            return cell.StringCellValue;
        case CellType.Error: //ERROR:
            return cell.ErrorCellValue;
        case CellType.Formula: //FORMULA:
            IFormulaEvaluator evaluator = null;
            if (workbook is HSSFWorkbook)
                evaluator = new HSSFFormulaEvaluator(workbook);
            else
                evaluator = new XSSFFormulaEvaluator(workbook);
            cell = evaluator.EvaluateInCell(cell);
            try
            {
                return cell.NumericCellValue;
            }
            catch
            {
                return cell.StringCellValue;
            }
                    
        default:
            return "=" + cell.CellFormula;
    }
}

這樣應該能在大多數情況下,獲取我們希望的值了。

第二個問題,同一個sheet中資料區域中間隔著很多空行,不知道你們有沒有遇到過這種情況,npoi讀取Excel檔案的行數時,即使該行沒有資料,但是隻要最初的行高被動過,也或被讀到,即,把3000行的行高改了,但是其實只有100行資料,它計算的行數就會是3000,如果你知道只需要讀取100行,但是如果從200行開始又有100行資料怎麼辦?如果說這個也知道,那當我沒說。事實上,雖然npoi統計出來有那麼多行,但是當你獲取空行時就會報異常,提示你引用物件沒有獲取例項,大概是這麼個意思,這說明sheet裡是沒有存這些空行的資訊的,除了行數。如果你對Excel結構不太清楚,那麼就需要在用異常做流程處理,雖然這樣做實際上效率很低。

        /// <summary>
        /// Excel匯入成Datable
        /// </summary>
        /// <param name="strExcelPath">匯入路徑(包含檔名與副檔名)</param>
        /// <returns></returns>
        private static DataTable ExcelToDataTable(string strExcelPath, string sheetName)
        {
            DataTable dataTable = new DataTable();
            FileStream fs = null;
            try
            {
                //獲取副檔名
                string strExtension = System.IO.Path.GetExtension(strExcelPath);
                
                using (fs = new FileStream(strExcelPath, FileMode.Open, FileAccess.Read))
                {
                    ISheet sheet = null;
                    IWorkbook workbook = null;
                    switch (strExtension)
                    {
                        case ".xls":
                            workbook = new HSSFWorkbook(fs);
                            sheet = workbook.GetSheet(sheetName);
                            break;
                        case ".xlsx":
                            workbook = new XSSFWorkbook(fs);
                            sheet = workbook.GetSheet(sheetName);
                            break;
                        default:
                            break;
                    }
                    if (sheet == null)
                        Console.WriteLine("檔案字尾名錯誤!");
                    //表頭
                    IRow header = sheet.GetRow(sheet.FirstRowNum);
                    List<int> columns = new List<int>();
                    object obj = null;
                    for (int i = 0, int dt_column_count = header.LastCellNum; i < dt_column_count; i++)
                    {
                        obj = GetValueType(header.GetCell(i), workbook); string a = header.GetCell(0).StringCellValue;
                        if (obj == null || obj.ToString() == string.Empty)
                        {
                            dataTable.Columns.Add(new DataColumn("Columns" + i.ToString()));
                            //continue;
                        }
                        else
                            dataTable.Columns.Add(new DataColumn(obj.ToString()));
                        columns.Add(i);
                    }
                    //資料
                    object value = null;
                    DataRow dr = null;
                    for (int i = sheet.FirstRowNum + 1, int dt_row_count = sheet.LastRowNum; i <= dt_row_count; i++)
                    {
                        
                        try
                        {
                            value = GetValueType(sheet.GetRow(i).GetCell(0), workbook);
                        }
                        catch
                        {
                            continue;
                        }
                        if (value != null)
                        {
                            dr = dataTable.NewRow();
                            bool hasValue = false;
                            foreach (int j in columns)
                            {
                                dr[j] = GetValueType(sheet.GetRow(i).GetCell(j), workbook);
                                if (dr[j] != null && dr[j].ToString() != string.Empty)
                                    hasValue = true;
                            }
                            if (hasValue)
                                dataTable.Rows.Add(dr);
                        }
                    }
                    return dataTable;
                }

            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                return dataTable;
            }
            finally
            {
                if(fs != null)
                    fs.Close();
            }
        }

讀取的情況應該沒什麼大問題的,下面貼上Excel轉datatable的方法,也可以轉到集合裡,按需求做些修改就行了。

第三個問題,關於這個寫入的問題,實際上我到最終也沒什麼好方法,如果有人知道,歡迎幫我解惑。

先貼上一段寫入的方法:

        /// <summary>
        /// Datable匯出成Excel
        /// </summary>
        /// <param name="dt">資料來源</param>
        /// <param name="strExcelPath">匯出路徑(包括檔名與副檔名)</param>
        /// <param name="sheetName">生成的sheet的name</param>
        /// <returns></returns>
        private static bool TableToExcel(DataTable dt, string strExcelPath, string sheetName)
        {
            //獲取副檔名
            string strExtension = System.IO.Path.GetExtension(strExcelPath);
            //string strFileName = System.IO.Path.GetFileName(strExcelPath);
            IWorkbook workbook = null;
            switch (strExtension)
            {
                case ".xls":
                    workbook = new HSSFWorkbook();
                    break;
                case ".xlsx":
                    workbook = new XSSFWorkbook();
                    break;
                default:
                    break;
            }
            if (workbook == null)
                return false;
            ISheet sheet = workbook.CreateSheet(sheetName);

            //表頭  
            ICell cell = null;
            IRow row = sheet.CreateRow(0);
            for (int i = 0,int column_count = dt.Columns.Count; i < column_count; i++)
            {
                cell = row.CreateCell(i);
                cell.SetCellValue(dt.Columns[i].ColumnName);
            }

            //資料  
            IRow row1 = null;
            ICell cell = null;
            for (int i = 0, int row_count = dt.Rows.Count; i < row_count; i++)
            {
                row1 = sheet.CreateRow(i + 1);
                for (int j = 0, int column_count = dt.Columns.Count; j < column_count; j++)
                {
                    cell = row1.CreateCell(j);
                    cell.SetCellValue(dt.Rows[i][j].ToString());
                }
            }

            //轉為位元組陣列  
            MemoryStream stream = new MemoryStream();
            workbook.Write(stream);
            var buf = stream.ToArray();

            //儲存為Excel檔案
            FileMode mode = FileMode.OpenOrCreate;
            FileAccess access = FileAccess.ReadWrite;
            FileShare share = FileShare.ReadWrite;
            
            using (FileStream fs = new FileStream(strExcelPath, mode, access, share))
            {
                fs.Write(buf, 0, buf.Length);
                fs.Flush();
                fs.Close();
            }

            return true;
        }

事實上,我一開始在資料庫裡讀了一些資料然後寫入Excel,成功了,我就沒管了。

知道我在一系列讀取後在進行寫入時出現了問題,整個Excel檔案都無法打開了。我經過一系列除錯,發現讀取之後都是沒問題的,寫入的最後出現問題,即FileStream出現後的那幾句程式碼,我懷疑過很多地方,比如之前的fs未close,實際上不需要手動close;再比如new FileStream時的引數,這讓我又發現了一個問題,我進行寫入操作的時候會將原先的刪除再新建,並且更改引數並不起效果,我猜測應該將原有檔案作為引數傳進來,找了一下確實是這個問題,在已存在的Excel檔案新增sheet的完整程式碼:

        /// <summary>
        /// Datable匯出成Excel
        /// </summary>
        /// <param name="dt">資料來源</param>
        /// <param name="strExcelPath">匯出路徑(包括檔名與副檔名)</param>
        /// <param name="sheetName">生成的sheet的name</param>
        /// <param name="flag">是新建or開啟:true 為 create;false 為 open</param>
        /// <returns></returns>
        private static bool TableToExcel(DataTable dt, string strExcelPath, string sheetName, bool flag)
        {
            //獲取副檔名
            string strExtension = System.IO.Path.GetExtension(strExcelPath);
            //string strFileName = System.IO.Path.GetFileName(strExcelPath);
            IWorkbook workbook = null;
            switch (strExtension)
            {
                case ".xls":
                    if(flag)
                        workbook = new HSSFWorkbook();
                    else
                        workbook = new HSSFWorkbook(File.OpenRead(strExcelPath));
                    break;
                case ".xlsx":
                    if(flag)
                        workbook = new XSSFWorkbook();
                    else
                        workbook = new XSSFWorkbook(File.OpenRead(strExcelPath));
                    break;
                default:
                    break;
            }
            if (workbook == null)
                return false;
            ISheet sheet = workbook.CreateSheet(sheetName);

            //表頭  
            IRow row = sheet.CreateRow(0);
            ICell cell = null;
            for (int i = 0, int column_count = dt.Columns.Count; i < column_count; i++)
            {
                cell = row.CreateCell(i);
                cell.SetCellValue(dt.Columns[i].ColumnName);
            }

            //資料  
            IRow row1 = null;
            ICell cell = null;
            for (int i = 0, int row_count = dt.Rows.Count; i < row_count; i++)
            {
                row1 = sheet.CreateRow(i + 1);
                for (int j = 0, int column_count = dt.Columns.Count; j < column_count; j++)
                {
                    cell = row1.CreateCell(j);
                    cell.SetCellValue(dt.Rows[i][j].ToString());
                }
            }

            //轉為位元組陣列  
            MemoryStream stream = new MemoryStream();
            workbook.Write(stream);
            var buf = stream.ToArray();

            //儲存為Excel檔案
            FileMode mode = FileMode.OpenOrCreate;
            FileAccess access = FileAccess.ReadWrite;
            FileShare share = FileShare.ReadWrite;
            
            using (FileStream fs = new FileStream(strExcelPath, mode, access, share))
            {
                fs.Write(buf, 0, buf.Length);
                fs.Flush();
                fs.Close();
            }

            return true;
        }

雖然上面程式碼可以新增sheet,並且原有的sheet依然存在,但是這有個前提條件:原先存在的sheet需要時通過npoi新增的,也就是說通過office新建的sheet,上面的方法是不行的,會導致檔案損壞,無法開啟。

為了解決這個問題,我看了很多資料,據說是因為npoi寫入會在末尾新增一個標誌,那麼可以猜測一下,是不是給自己新建的sheet加一個這樣的標誌就能解決問題?我是沒有進行這個測試,最後是新建了一個Excel檔案。有哪位大神知道這種方法或者其他方法解決這個問題的,歡迎指導。

最後說一下,這個方式是可以設定單元格格式的,下面貼上一部分程式碼,對第一行設定格式:

            //設定列寬
            sheet.SetColumnWidth(0,25 * 256);
            sheet.SetColumnWidth(1, 45 * 256);
            sheet.SetColumnWidth(2, 10 * 256);
            sheet.SetColumnWidth(3, 6 * 256);
            sheet.SetColumnWidth(4, 20 * 256);
            sheet.SetColumnWidth(5, 20 * 256);
            sheet.SetColumnWidth(6, 20 * 256);
            sheet.SetColumnWidth(7, 20 * 256);

            //字型
            IFont font = workbook.CreateFont();
            font.IsBold = true; //字型為粗體
            font.FontName = "等線";

            //建立單元格格式
            IDataFormat dataFormat = workbook.CreateDataFormat();
            //ICellStyle style1 = workbook.CreateCellStyle();
            //style1.DataFormat = dataFormat.GetFormat("0"); //精度到整數
            //ICellStyle style2 = workbook.CreateCellStyle();
            //style2.DataFormat = dataFormat.GetFormat("0.00%"); //百分數
            //ICellStyle style3 = workbook.CreateCellStyle();
            //style3.SetFont(font);
            //ICellStyle style4 = workbook.CreateCellStyle(); 
            //style4.FillBackgroundColor = IndexedColors.White.Index;
            //style4.FillForegroundColor = IndexedColors.Black.Index; 
            //style4.FillPattern = FillPattern.SolidForeground;

            //表頭  style : 字型粗體,背景色為藍,居中對齊
            ICellStyle style_header = workbook.CreateCellStyle();
            style_header.SetFont(font);
            style_header.FillBackgroundColor = IndexedColors.White.Index;
            style_header.Alignment = HorizontalAlignment.Center;
            //style_header.FillForegroundColor = IndexedColors.LightBlue.Index;
            style_header.FillForegroundColor = (short)44;
            style_header.FillPattern = FillPattern.SolidForeground;

            IRow row = sheet.CreateRow(0);
            ICell cell = null;
            for (int i = 0, int count = firstRowName.Count; i < count; i++)
            {
                cell = row.CreateCell(i);
                cell.SetCellValue(firstRowName[i]);
                cell.CellStyle = style_header;
            }

epplus

這種方法挺不錯的,但是由於某些原因並沒有深入瞭解,不過資料量比npoi強,而且沒那麼多問題。

放一個比較全的測試方法吧,裡面進行了很多功能測試,我自己測試沒通過的都有標誌,沒標註的應該都是可以成功的。

        public static Boolean Load_Main()
        {
            FileInfo newFile = new FileInfo(System.AppDomain.CurrentDomain.BaseDirectory + "test1.xlsx");
            
            using (ExcelPackage package = new ExcelPackage(newFile)) //建立Excel
            {
                #region 獲取sheet
                if (package.Workbook.Worksheets["test1"] != null)
                {
                    package.Workbook.Worksheets.Delete("test1");//刪除工作表
                }
                //建立一個新的sheet
                ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("test1");
                //獲取excel中的一個sheet用於後續操作(通過模板寫資料的可用此方法)
                ExcelWorksheet worksheet2 = package.Workbook.Worksheets["Sheet1"];
                if (package.Workbook.Worksheets["hidesheet"] != null)
                {
                    package.Workbook.Worksheets.Delete("hidesheet");//刪除工作表
                }
                ExcelWorksheet hideSheet = package.Workbook.Worksheets.Add("hidesheet");
                #endregion
                #region 新增資料
                /*  Epplus中給單元格賦值非常簡單,兩種方法:(ps:Epplus的所有行列數都是以1開始的)
                 *  1. worksheet.Cells[1, 1].Value = "名稱";//直接指定行列數進行賦值
                 *  2. worksheet.Cells["A1"].Value = "名稱";//直接指定單元格進行賦值
                 */
                worksheet.Cells[1, 1].Value = "名稱";
                worksheet.Cells[1, 2].Value = "單價";
                worksheet.Cells[1, 3].Value = "銷量";

                worksheet.Cells[2, 1].Value = "洗衣機";
                worksheet.Cells[2, 2].Value = 1000;
                worksheet.Cells[2, 3].Value = 1000;

                worksheet.Cells[3, 1].Value = "空調";
                worksheet.Cells[3, 2].Value = 2300;
                worksheet.Cells[3, 3].Value = 1760;

                worksheet.Cells[4, 1].Value = "冰箱";
                worksheet.Cells[4, 2].Value = 3400;
                worksheet.Cells[4, 3].Value = 1530;

                worksheet.Cells[5, 1].Value = "電視機";
                worksheet.Cells[5, 2].Value = 7800;
                worksheet.Cells[5, 3].Value = 2700;
                #endregion
                #region 公式應用
                //乘法公式,第二列乘以第三列的值賦值給第四列
                worksheet.Cells["E2"].Formula = "B2*C2";//單個單元格
                worksheet.Cells["D2:D5"].Formula = "B2*C2";//多個單元格
                //自動求和 第二列中,將2,3,4,5行求和值賦給第六行
                //worksheet2.Cells[6, 2, 6, 4].Formula = string.Format("SUBTOTAL(9,{0})", new ExcelAddress(2, 2, 5, 2).Address);
                worksheet.Cells["B7:D7"].Formula = string.Format("SUBTOTAL(9,{0})", new ExcelAddress(2, 2, 5, 2).Address);
                #endregion
                #region 設定單元格格式
                worksheet.Cells[5, 3].Style.Numberformat.Format = "#,##0.00";//這是保留兩位小數
                #endregion
                #region 設定單元格對齊方式
                using (ExcelRange range = worksheet.Cells[1, 1, 5, 3])
                {
                    range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;//水平居中
                    range.Style.VerticalAlignment = ExcelVerticalAlignment.Center;//垂直居中
                }
                //worksheet.Cells[1, 4, 1, 5].Merge = true;//合併單元格
                //worksheet.Cells.Style.WrapText = true;//自動換行
                #endregion
                #region 設定單元格字型樣式
                using (ExcelRange range = worksheet.Cells[1, 1, 1, 3])
                {
                    range.Style.Font.Bold = true;
                    range.Style.Font.Color.SetColor(Color.White);
                    range.Style.Font.Name = "微軟雅黑";
                    range.Style.Font.Size = 12;
                    range.Style.Fill.PatternType = ExcelFillStyle.Solid;
                    range.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(128, 128, 128));
                }
                #endregion
                #region 設定單元格背景樣式
                worksheet.Cells[1, 1].Style.Fill.PatternType = ExcelFillStyle.Solid;
                worksheet.Cells[1, 1].Style.Fill.BackgroundColor.SetColor(Color.FromArgb(128, 128, 128));//設定單元格背景色
                #endregion
                #region 設定單元格邊框,兩種方法
                //worksheet.Cells[1, 1].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));//設定單元格所有邊框
                //worksheet.Cells[1, 1].Style.Border.Bottom.Style = ExcelBorderStyle.Thin;//單獨設定單元格底部邊框樣式和顏色(上下左右均可分開設定)
                //worksheet.Cells[1, 1].Style.Border.Bottom.Color.SetColor(Color.FromArgb(191, 191, 191));

                worksheet.Cells[1, 1].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[1, 2].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[1, 3].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));

                worksheet.Cells[2, 1].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[2, 2].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[2, 3].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));

                worksheet.Cells[3, 1].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[3, 2].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[3, 3].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));

                worksheet.Cells[4, 1].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[4, 2].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[4, 3].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));

                worksheet.Cells[5, 1].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[5, 2].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                worksheet.Cells[5, 3].Style.Border.BorderAround(ExcelBorderStyle.Thin, Color.FromArgb(191, 191, 191));
                #endregion
                #region 設定單元格的行高和列寬
                worksheet.Cells.Style.ShrinkToFit = true;//單元格自動適應大小
                worksheet.Row(1).Height = 15;//設定行高
                worksheet.Row(1).CustomHeight = true;//自動調整行高
                worksheet.Column(1).Width = 15;//設定列寬
                #endregion
                #region 設定sheet背景
                worksheet.View.ShowGridLines = false;//去掉sheet的網格線
                worksheet.Cells.Style.Fill.PatternType = ExcelFillStyle.Solid;
                worksheet.Cells.Style.Fill.BackgroundColor.SetColor(Color.LightGray);//設定背景色
                //worksheet.BackgroundImage.Image = Image.FromFile(@"C:\Users\cai.j\Pictures\Saved Pictures\8.jpg");//設定背景圖片 沒看出效果
                #endregion
                #region 插入圖片
                ExcelPicture picture = worksheet.Drawings.AddPicture("logo1", Image.FromFile(@"C:\Users\cai.j\Pictures\Saved Pictures\9.jpg"));//插入圖片
                picture.SetPosition(100, 100);//設定圖片的位置
                picture.SetSize(100, 100);//設定圖片的大小
                #endregion
                #region 插入形狀 看不出來
                //ExcelShape shape = worksheet.Drawings.AddShape("shape", eShapeStyle.Rect);//插入形狀
                //shape.Font.Color = Color.Red;//設定形狀的字型顏色
                //shape.Font.Size = 15;//字型大小
                //shape.Font.Bold = true;//字型粗細
                //shape.Fill.Style = eFillStyle.NoFill;//設定形狀的填充樣式
                //shape.Border.Fill.Style = eFillStyle.NoFill;//邊框樣式
                //shape.SetPosition(200, 300);//形狀的位置
                //shape.SetSize(80, 30);//形狀的大小
                //shape.Text = "test";//形狀的內容
                #endregion
                #region 給圖片加超連結  有問題
                //picture = worksheet.Drawings.AddPicture("logo2", Image.FromFile(@"C:\Users\cai.j\Pictures\Saved Pictures\5.jpg"), new ExcelHyperLink("http:\\www.baidu.com", UriKind.Relative));
                //picture.SetPosition(700, 700);//設定圖片的位置
                //picture.SetSize(100, 100);//設定圖片的大小
                #endregion
                #region 給單元格加超連結
                //worksheet.Cells[1, 1].Hyperlink = new ExcelHyperLink("http:\\www.baidu.com", UriKind.Relative);
                #endregion
                #region 隱藏sheet 有問題
                //worksheet.Hidden = eWorkSheetHidden.Hidden;//隱藏sheet
                //worksheet.Column(1).Hidden = true;//隱藏某一列
                //worksheet.Row(1).Hidden = true;//隱藏某一行
                #endregion
                #region 建立柱狀圖
                ExcelChart chart = worksheet.Drawings.AddChart("chart", eChartType.ColumnClustered);//建立圖表

                ExcelChartSerie serie = chart.Series.Add(worksheet.Cells[2, 3, 5, 3], worksheet.Cells[2, 1, 5, 1]);//選擇資料,設定圖表的x軸和y軸
                serie.HeaderAddress = worksheet.Cells[1, 3];//設定圖表的圖例

                chart.SetPosition(150, 10);//設定位置
                chart.SetSize(500, 300);//設定大小
                chart.Title.Text = "銷量走勢";//設定圖表的標題
                chart.Title.Font.Color = Color.FromArgb(89, 89, 89);//設定標題的顏色
                chart.Title.Font.Size = 15;//標題的大小
                chart.Title.Font.Bold = true;//標題的粗體
                chart.Style = eChartStyle.Style15;//設定圖表的樣式
                chart.Legend.Border.LineStyle = eLineStyle.Solid;
                chart.Legend.Border.Fill.Color = Color.FromArgb(217, 217, 217);//設定圖例的樣式
                #endregion
                #region Excel加密和鎖定 沒測過
                //worksheet.Protection.IsProtected = true;//設定是否進行鎖定
                //worksheet.Protection.SetPassword("yk");//設定密碼
                //worksheet.Protection.AllowAutoFilter = false;//下面是一些鎖定時許可權的設定
                //worksheet.Protection.AllowDeleteColumns = false;
                //worksheet.Protection.AllowDeleteRows = false;
                //worksheet.Protection.AllowEditScenarios = false;
                //worksheet.Protection.AllowEditObject = false;
                //worksheet.Protection.AllowFormatCells = false;
                //worksheet.Protection.AllowFormatColumns = false;
                //worksheet.Protection.AllowFormatRows = false;
                //worksheet.Protection.AllowInsertColumns = false;
                //worksheet.Protection.AllowInsertHyperlinks = false;
                //worksheet.Protection.AllowInsertRows = false;
                //worksheet.Protection.AllowPivotTables = false;
                //worksheet.Protection.AllowSelectLockedCells = false;
                //worksheet.Protection.AllowSelectUnlockedCells = false;
                //worksheet.Protection.AllowSort = false;
                #endregion
                #region 下拉框 
                var citys = InitCity();
                for (int idx = 0, int count = citys.Count; idx < count; idx++)
                {
                    hideSheet.Cells[idx + 1, 1].Value = citys[idx].Name;
                    //hideSheet.Cells[idx + 1, 2].Value = citys[idx].ParentId;
                }
                var val = worksheet.DataValidations.AddListValidation("H1");//設定下拉框顯示的資料區域
                val.Formula.ExcelFormula = "=hideSheet!$A:$A";//資料區域的名稱  //資料需要時通過程式碼新增的
                val.Prompt = "選擇省份";//下拉提示
                val.ShowInputMessage = true;//顯示提示內容
                #endregion
                package.Save();//儲存Excel
            }

            return true;
        }


        private static  List<KeyValue> InitCity()
        {
            var list = new List<KeyValue>
            {
                new KeyValue {Id = 1, ParentId = 1, Name = "上海市1"},
                new KeyValue {Id = 2, ParentId = 2, Name = "徐州市"},
                new KeyValue {Id = 3, ParentId = 2, Name = "蘇州市"},
                new KeyValue {Id = 4, ParentId = 2, Name = "崑山市"},
                new KeyValue {Id = 5, ParentId = 2, Name = "南京市"},
                new KeyValue {Id = 6, ParentId = 3, Name = "北京市1"}
            };
            return list;
        }

另外,直接將datatable轉化成Excel的程式碼很簡單,也可以將其他的集合輸出到Excel,像list、dictionary等,需要的自己研究下,我就不貼了。

        #region 匯出到Excel ExportByEPPlus(DataTable sourceTable, string strFileName)
        /// <summary>
        /// 使用EPPlus匯出Excel(xlsx)
        /// </summary>
        /// <param name="sourceTable">資料來源</param>
        /// <param name="strFileName">xlsx檔名(不含字尾名)</param>
        public static string ExportByEPPlus(DataTable sourceTable, string strFileName)
        {
            string FileName = AppDomain.CurrentDomain.BaseDirectory + strFileName + ".xlsx";
            if (System.IO.File.Exists(FileName))                                //存在則刪除
                System.IO.File.Delete(FileName);
            ExcelPackage ep = new ExcelPackage();
            ExcelWorkbook wb = ep.Workbook;
            ExcelWorksheet ws = wb.Worksheets.Add("result");
            ws.Cells["A1"].LoadFromDataTable(sourceTable, false);
            ep.SaveAs(new FileInfo(FileName));
            return FileName;
        }
        #endregion

最後記錄一下,寫程式碼時考慮到的優化tip:

  • 儘可能使用區域性變數

主要是為了記憶體考慮,區域性變數在方法呼叫結束後就over了。另外,就是區域性變數建立的速度更快。

  • 減少重複計算

主要是針對迴圈裡面

for (int idx = 0, int count = citys.Count; idx < count; idx++)
{
}
  • 迴圈內不要不斷建立物件引用,可以更改引用
            IRow row = null;
            ICell cell = null;
            for (int i = 0, int row_count = dt.Rows.Count; i < row_count; i++)
            {
                row = sheet.CreateRow(i + 1);
                for (int j = 0, int column_count = dt.Columns.Count; j < column_count; j++)
                {
                    cell = row.CreateCell(j);
                    cell.SetCellValue(dt.Rows[i][j].ToString());
                }
            }
  • 使用帶緩衝的輸入輸出流進行IO操作
  • 非必要真的別再catch裡進行流程控制