我們在項目中常常會涉及到對Excel的讀寫操作,而市面上也有很多的操作Excel方式。經過整理常用的是以下幾種方式:
1. COM組件操作Excel讀寫
2. Ado.net方式操作Excel讀寫
3. 開源的第三方組件NPOI
4. Open XML 方式讀寫Excel
上述3種方式其實各有優缺點~,今天這裡是要講述的有關com組件的讀寫Excel操作。
這段時間準備用這3種方式分別體驗下讀寫操作Excel的樂趣,要使用Com組件的方式操作Excel,我們要做的非同步首先是要引入相應的Office的dll當然也可以預設從Office的安裝目錄下找到
添加之,因為所有的Com組件操作都需要添加該dll才能進行操作。要注意的一點如果你在開發過程中出現了下面的這種異常
此時你必須將點開剛剛引用的DLL屬性,將其中的Embed Interop Types修改為False, 這樣在建立ExcelApplication類的時候就不會編譯不通過了。
具體原因可見http://blogs.msdn.com/b/mshneer/archive/2009/12/07/interop-type-xxx-cannot-be-embedded-use-the-applicable-interface-instead.aspx
進入正題,簡單來說對於我們一般的操作Excel文檔讀寫,我們只需要瞭解4個對象,Application, Workbook, Worksheet, Range
1. Application對象: 就是Excel執行個體,不僅僅是一個Excel檔案,而是整體的Excel程式。
2. WorkBook就是實質意義上的某個Excel檔案,你可以進行儲存操作等等。
3. Worksheet是傳統意義上的某個活頁簿類型。
4. Range是我們使用的最多的,你可以理解成一個地區塊,例如常見的"A2:B5"這種表示方式。
更詳細的的概念可以參考微軟的官方文檔 http://msdn.microsoft.com/zh-cn/library/wss56bz7(v=vs.80).aspx
我們可以封裝一個常用的ExcelUtil類來協助我們通過Com組件的方式操作Excel。
首先,我們就可以動手來從一個Excel的sheet中讀取我們需要的內容,我們可以把控制項的引用名先弄短一點,例如這樣:
using Excel = Microsoft.Office.Interop.Excel;
聲明下常用的私人變數
private Excel.Application excelApplication = null;private Excel.Workbooks excelWorkbooks;private Excel.Workbook excelWorkbook;private Excel.Worksheet excelWorksheet;private Excel.Range excelRange;private int activeSheetIndex;private string saveFilePath = string.Empty;private string openFilePath = string.Empty;
常用的公用屬性
/// <summary>/// 擷取或設定當前有效活動Sheet索引/// </summary>public int ActiveSheetIndex{ get { return activeSheetIndex; } set { activeSheetIndex = value; }}/// <summary>/// 擷取或設定/// </summary>public Excel.Workbook Workbook{ get { return excelWorkbook; } set { excelWorkbook = value; }}/// <summary>/// 擷取設定當前Excel含有的Worksheet數/// </summary>public int WorksheetsCount{ get { if (excelWorkbook == null) return 0; if (excelWorkbook.Worksheets == null) return 0; return excelWorkbook.Worksheets.Count; }}
傳入Excel檔案路徑開啟一個Excel檔案
public bool OpenExcelApplication(string path) { if (excelApplication != null) CloseExcelApplication(); if (string.IsNullOrEmpty(path)) throw new Exception("請選擇一個檔案!"); if (!File.Exists(path)) throw new Exception(path + "檔案不存在!"); else { try { //點擊引用到的第三方組件然後屬性中將Embed Interop Types置為False, ActiveSheet.UsedRange.Rows.Count excelApplication = new Excel.ApplicationClass(); excelWorkbooks = excelApplication.Workbooks; excelWorkbook = excelWorkbooks.Open(path, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value) as Excel.Workbook; excelWorksheet = excelWorkbook.Worksheets[activeSheetIndex] as Excel.Worksheet; excelApplication.Visible = false; return true; } catch (Exception ex) { CloseExcelApplication(); throw new Exception(string.Format("(1)程式中沒有安裝Excel程式。(2)或沒有安裝Excel所需要支援的.NetFramework\n詳細資料:{0}", ex.Message)); } } }
每次操作完Excel都要對Excel中所使用到的對象進項釋放資源的操作如下
/// <summary>/// 關閉Excel程式/// </summary>public void CloseExcelApplication(){ try { Save(); excelWorksheet = null; excelWorkbook = null; excelWorkbooks = null; excelRange = null; if (excelApplication != null) { excelApplication.Workbooks.Close(); excelApplication.Quit(); excelApplication = null; } } catch (Exception ex) { throw new Exception(ex.Message); } finally { GC.Collect(); GC.WaitForPendingFinalizers(); }}
讀取一個Sheet的時候我們一般先要擷取到相應的當前的Worksheet對象,然後從這個Worksheet對象中擷取到相應的Range對象例如從A1:C7等
主要的代碼如下,擷取到當前有效ActiveIndex,對應的worksheet對象
Excel.Worksheet activeWorksheet = excelWorkbook.Worksheets[activeIndex] as Excel.Worksheet;if (activeWorksheet == null) activeWorksheet = new Excel.Worksheet();//每次擷取到最新的活動sheet時候必須啟用activeWorksheet.Activate();
//擷取到當前你需要擷取的資料的Range對象Excel.Range excelRange = activeWorksheet.get_Range("A1", "C3");
這裡可能有人會用到說想要知道哪些行列的地區邊界裡面是有資料的其實這裡它內建提供了2種方式可以獲得當前有資料的區塊Range範圍(預設從A1開始)
/* 假設該表的左上方在儲存格 a1,並且注意該表中間沒有空行和空列,則: sheets['sheet1'].range['a1'].CurrentRegion.Rows.Count sheets['sheet1'].range['a1'].CurrentRegion.Columns.Count 返回該表的行數,Columns返回該表的列數 */public ExcelRange GetCurrentRegion(string startRange, Excel.Worksheet activeWorksheet){ return new ExcelRange() { Row = activeWorksheet.Range[startRange.ToUpper()].CurrentRegion.Rows.Count, Column = activeWorksheet.Range[startRange.ToUpper()].CurrentRegion.Columns.Count };}public class ExcelRange{ #region 構造方法 public ExcelRange() { this.Row = 0; this.Column = 0; } #endregion #region 公用屬性 public int Row { get; set; } public int Column { get; set; } #endregion}
此處的startRange對象就是預設以左上方的哪個Cell座標當成起始位置,獲得知道當前右下角的最後一個Cell的座標例如此處的C3
另外一種方法
/* 可以利用VBA中的SpecialCells函數,返回一個 Range 對象,此對象代表與指定類型及 值相匹配的所有儲存格,下面為其可用的常數: xlCellTypeAllFormatConditions 任意格式的儲存格 xlCellTypeAllValidation 具有有效條件的儲存格 xlCellTypeBlanks 空儲存格 xlCellTypeComments 包含注釋的儲存格 xlCellTypeConstants 包含常量的儲存格 xlCellTypeFormulas 包含公式的儲存格 xlCellTypeLastCell 已用地區的最後一個儲存格 /////××××//這個常數在確定範圍最有用! xlCellTypeSameFormatConditions 具有相同格式的儲存格 xlCellTypeSameValidation 具有相同有效條件的儲存格 xlCellTypeVisible 所有可見儲存格 比如我們可以利用這個函數確定最後一個包含資料的儲存格: ExcelApp.WorkBooks.ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell,EmptyParam).Activate; lastRow := ExcelApp.ActiveCell.Row; lastCol := ExcelApp.ActiveCell.Column; */public ExcelRange GetCurrentRegion(Excel.Worksheet activeSheet){ activeSheet.Cells.SpecialCells(Excel.XlCellType.xlCellTypeLastCell, Missing.Value).Activate(); //此處的xlCellTypeLastCell是重點擷取到最後一個有下角的座標。 Excel.Application excelApp = ((activeSheet.Parent as Excel.Workbook).Parent as Excel.Application).Application; return new ExcelRange() { Row = excelApp.ActiveCell.Row, Column = excelApp.ActiveCell.Row };}
這裡不建議使用他內建的UsedRange.Row和UsedRange.Column屬性是因為它們十分坑爹的把你曾經使用過的元素所在的範圍也包含進去了,也就是說如果你曾經在D3寫過東西後來又擦掉使用空白的情況下UsedRange屬性計算的是原來的範圍,而這往往不是我們所要的
接下來接著上面我們擷取到Range元素後最重要的一步就是對他進行操作了擷取資料的操作了這是擷取一個儲存格的值的操作
//row,column分別是值cell所在的行和列,預設從1開始
string cellValue = (excelRange.Cells[row, col] as Excel.Range) == null ? string.Empty : (excelRange.Cells[row, col] as Excel.Range).Value;
如果要擷取到當前這個Range範圍內所有的元素呢我們就需要迴圈對應的行和咧,如下
string cellValue;for (int row = 1; row <= excelRange.Rows.Count; row++){ for (int col = 1; col <= excelRange.Columns.Count; col++) { cellValue = (excelRange.Cells[row, col] as Excel.Range) == null ? string.Empty : (excelRange.Cells[row, col] as Excel.Range).Value; }}
而這裡有個很大的弊端就是效率,看了網上很多用COM組件操作的方法說使用COM組件操作讀寫的效率很低,其實他可以一點也不低。主要因為我們再迴圈中一次次的操作了Excel的Range對象然後在擷取他的Value對象這樣十分耗損資源,後來在網上找到瞭解決方案一句話就疏通了整個案情啊~在很早的一篇小馬哥的部落格中找到了答案 http://www.cnblogs.com/maweifeng/archive/2005/06/28/182483.html
其實我們的Range對象可以和數組進行很好的互通性,我們其實每次只需要把當前要擷取的Range地區元素賦值給一個二維數組然後對二維數組在進行相應的迴圈讀取或者寫入操作,這速度的提升是飛一般的感覺啊~
我用Stopwatch監視了下,寫入一個5000行10列的資料用Range的方式讀取元素花了94880ms相當於1分30幾秒,而轉用二維數組的方式後只用4秒,這個差距其實是很大的
所以建議我們真的要使用COM來操作Excel的朋友可以用數組的方式來操作讀寫
object[,] dataValueRange = new object[rowCount, columnCount];dataValueRange = excelRange.Value;for (int row = 1; row <= rowCount; row++){ for (int col = 1; col <= columnCount; col++) { cellValue = dataValueRange[row, col] == null ? string.Empty : dataValueRange[row, col].ToString(); }}
對於寫入Excel其實上面的步驟也是相同的
只是你可以將要寫入的元素先寫入二維數組中然後將它整個賦值給對應Range的Value值
這樣就可以避免效率的問題
COM組件操作Excel的方式先總結到這~下回繼續補充~ 希望對需要的朋友有用~
註:本文章中涉及技術共用著作權歸本人所有,如發現未經允許用作非法用作商業用途,將進行法律追究