我們在項目中常常會涉及到對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的方式先總結到這~下回繼續補充~ 希望對需要的朋友有用~
注:本文章中涉及技術共享版權歸本人所有,如發現未經允許用作非法用作商業用途,將進行法律追究