將.NET數據導出為Excel文件,有許多種方法,我這里介紹采用COM組件來操作Excel文件,並且還會涉及異步、同步、進程管理、文件定位等內容,使用WPF做到一個盡量可用的導出界面。
一、WPF前台
這個就不用多說了,堆上幾個按鈕,做一個數據錄入的東西,一個狀態條:
我這里的數據錄入,就是用了幾個Textbox,實際上大家可以用任何東西(DataGrid、ListView等),因為在最后都會轉成List<MyData>的形式進行導出的,MyData是表示數據記錄的對象:
1 // 自定義數據類 2 public struct MyData 3 { 4 public string Col1, Col2, Col3; 5 public MyData(string col1, string col2, string col3) 6 { 7 Col1 = col1; Col2 = col2; Col3 = col3; 8 } 9 }
二、后台
1、錄入組織數據就不說了,先來說下選擇默認導出路徑:
1 using Forms = System.Windows.Forms; 2 // 選擇導出目錄 3 private void SelectPath() 4 { 5 var dialog = new Forms.FolderBrowserDialog(); 6 dialog.ShowDialog(); 7 string path = dialog.SelectedPath; 8 if (path != "") 9 { 10 _path = path; 11 if (path[path.Length - 1] != '\\') 12 _path += '\\'; 13 } 14 }
代碼使用System.Windows.Forms命名空間下的FloderBrowserDialog來選擇目錄,並把選擇的path保存到全局變量中,另外還有一個判斷,如果路徑結尾不是'\\'的話,就加上這個字符,以便於后面合成文件全路徑。效果圖:
2、如果是導出到非默認的路徑,並命名文件,則:
1 // 保存文件到指定目錄 2 private void SaveFile() 3 { 4 // .... 5 var dialog = new Forms.SaveFileDialog(); 6 dialog.FileOk += new CancelEventHandler((o, e) => 7 { 8 BTN_Export.Content = "取消"; 9 var fullName = dialog.FileName; 10 int i = fullName.LastIndexOf('\\') + 1; 11 int j = fullName.LastIndexOf('.'); 12 _bgWorker.RunWorkerAsync(new ExportInput<MyData>(_sources, 13 fullName.Substring(0, i), 14 fullName.Substring(i, j - i), 15 fullName.Substring(j, fullName.Length - j), _heads)); 16 }); 17 dialog.InitialDirectory = ServerPath; 18 dialog.DefaultExt = ".xlsx"; 19 dialog.FileName = "MyData"; 20 dialog.Filter = "Excel 2010文檔|*.xlsx|Excel 2003文檔|*.xls"; 21 dialog.ShowDialog(); 22 // ... 23 }
這里使用了System.Windows.Froms的SaveFileDialog方法,彈出一個文件保存對話框,我們輸入、選擇路徑、文件名、后綴后,點擊“保存”,就能通過dialog.FileName得到全路徑,然后分別截取目錄、文件名、后綴,構成參數類ExportInput<MyData>,以啟動后台線程進行導出。

1 /// <summary> 2 /// 導出成Excel文件時需要傳入的參數類 3 /// </summary> 4 public class ExportInput<T> 5 { 6 /// <summary> 7 /// 數據源 8 /// </summary> 9 public IEnumerable<T> Sources { get; set; } 10 /// <summary> 11 /// 列的表頭 12 /// </summary> 13 public IEnumerable<string> Headers { get; set; } 14 /// <summary> 15 /// 文件的名稱 16 /// </summary> 17 public string FileName { get; set; } 18 /// <summary> 19 /// 文件的絕對路徑 20 /// </summary> 21 public string Path { get; set; } 22 /// <summary> 23 /// 文件后綴 24 /// </summary> 25 public string Ext { get; set; } 26 27 /// <summary> 28 /// 構造傳入參數 29 /// </summary> 30 /// <param name="sources">數據源</param> 31 /// <param name="filename">文件名</param> 32 /// <param name="path">文件的絕對路徑</param> 33 /// <param name="headers">列的表頭</param> 34 public ExportInput(IEnumerable<T> sources, string path, string filename, string ext, IEnumerable<string> headers = null) 35 { 36 Sources = sources; 37 FileName = filename; 38 Path = path; 39 Ext = ext; 40 Headers = headers; 41 } 42 }
3、導出時使用的后台線程來自System.ComponentModel.BackgroundWorker,使用它可以非常方便地完成線程運行、取消、通知的功能:
1 private BackgroundWorker _bgWorker = new BackgroundWorker(); 2 // 初始化 3 private void Window_Loaded(object sender, RoutedEventArgs e) 4 { 5 // ... 6 _bgWorker.WorkerReportsProgress = true; 7 _bgWorker.WorkerSupportsCancellation = true; 8 _bgWorker.DoWork += new DoWorkEventHandler(ExcelHelper.ExportMyData); 9 _bgWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(OnWorkCompleted); 10 _bgWorker.ProgressChanged += new ProgressChangedEventHandler(OnProgressChanged); 11 } 12 // 報告進度 13 private void OnProgressChanged(object sender, ProgressChangedEventArgs e) 14 { 15 PB_State.Value = e.ProgressPercentage; 16 } 17 // 導出完成 18 private void OnWorkCompleted(object sender, RunWorkerCompletedEventArgs e) 19 { 20 if (e.Error != null) 21 MessageBox.Show("導出失敗:" + e.Error.Message); 22 else if (e.Cancelled) 23 MessageBox.Show("已取消導出!"); 24 else 25 MessageBox.Show("導出成功!"); 26 }
WorkerReportsProgress、WorkerSupportsCancellation這兩個布爾值分別是是否支持報告后台線程進度、是否支持取消后台線程的功能,DoWork是后台工作線程的委托,在上面代碼中,用的是ExcelHelper.ExportMyData這個靜態事件處理函數來完成導出功能。RunWorkerCompleted、ProgressChanged 分別是工作完成、進度改變時回調給前台的委托。
4、終於到了導出的部分了,代碼如下:

1 using Excel = Microsoft.Office.Interop.Excel; 2 3 /// <summary> 4 /// 導出成Excel文件 5 /// </summary> 6 public class ExcelHelper 7 { 8 /// <summary> 9 /// 導出Excel時使用的同步 10 /// </summary> 11 private static object syncRoot = new object(); 12 13 /// <summary> 14 /// 將數據集導出為Excel文件 15 /// </summary> 16 public static void ExportMyData(object sender, DoWorkEventArgs e) 17 { 18 // 創建Excel 19 Monitor.Enter(syncRoot); 20 var proListStart = Process.GetProcessesByName("EXCEL"); 21 Excel.Application excelApp = new Excel.Application(); 22 var proList = Process.GetProcessesByName("EXCEL").Except(proListStart, new ProcessComparer()); 23 Monitor.Exit(syncRoot); 24 try 25 { 26 // 檢查參數 27 var input = (ExportInput<MyData>)e.Argument; 28 var bgWorker = (BackgroundWorker)sender; 29 // 創建工作簿 30 Excel.Workbook excelDoc = excelApp.Workbooks.Add(); 31 // 創建工作表 32 Excel.Worksheet excelSheet = (Excel.Worksheet)excelDoc.Worksheets[1]; 33 // 數字類型以文本格式顯示 34 excelSheet.Cells.NumberFormat = "@"; 35 // 單元格索引從1開始 36 int i = 1, j = 1, count = input.Sources.Count(); 37 // 導入標題 38 if (input.Headers != null) 39 { 40 foreach (string head in input.Headers) 41 excelSheet.Cells[1, j++] = head; 42 ++i; 43 } 44 //將數據導入到工作表的單元格 45 foreach (MyData data in input.Sources) 46 { 47 if (bgWorker.CancellationPending) 48 { 49 e.Cancel = true; 50 return; 51 } 52 j = 1; 53 excelSheet.Cells[i, j++] = data.Col1; 54 excelSheet.Cells[i, j++] = data.Col2; 55 excelSheet.Cells[i, j++] = data.Col3; 56 ++i; 57 bgWorker.ReportProgress((95 * i - 190) / count); 58 } 59 //將其進行保存到指定的路徑 60 excelDoc.SaveAs(input.Path + input.FileName + input.Ext, 61 input.Ext == ".xls" ? Excel.XlFileFormat.xlExcel7 : Excel.XlFileFormat.xlOpenXMLWorkbook); 62 excelDoc.Close(); 63 // 返回路徑 64 e.Result = input.Path; 65 bgWorker.ReportProgress(100); 66 } 67 catch (System.Exception ex) 68 { 69 throw ex; 70 } 71 finally 72 { 73 excelApp.Quit(); 74 // 釋放COM組件,其實就是將其引用計數減1 75 System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp); 76 excelApp = null; 77 //釋放可能還沒釋放的進程 78 KillProcess(proList); 79 } 80 } 81 }
首先,引用Microsoft.Office.Interop.Excel命名空間,如果機器上安裝了office,那么它的位置是在
C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Excel\14.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Excel.dll
的位置,office版本不同“14.0.0.0__71e9bce111e9429c目錄”可能名稱會有一點差別。
接下來,啟動Excel進程:
Excel.Application excelApp = new Excel.Application();
創建工作簿:
Excel.Workbook excelDoc = excelApp.Workbooks.Add();
創建工作表:
Excel.Worksheet excelSheet = (Excel.Worksheet)excelDoc.Worksheets[1];
填入數據(注意到行和列都是從1開始的):
excelSheet.Cells[行, 列] = 數據;
在填入數據時,每趕往記錄前,都判斷一次是否取消導出,每填入一條記錄后,就使用bgWorker.ReportProgress()匯報工作進度。
1 if (bgWorker.CancellationPending) 2 { 3 e.Cancel = true; 4 return; 5 }
將工作簿保存到指定的路徑,關閉:
1 excelDoc.SaveAs(input.Path + input.FileName + input.Ext, 2 input.Ext == ".xls" ? Excel.XlFileFormat.xlExcel7 : Excel.XlFileFormat.xlOpenXMLWorkbook); 3 excelDoc.Close();
網上很多地方說保存成office2003用的枚舉是Excel.XlFileFormat.xlExcel8,經過我實際測試,這個枚舉是從office 2007才開始出現的,如果機器上安裝了2007及更高版本的office的話是可以正常使用的,如果機器上只安裝了office 2003,則只有用xlExcel7這個枚舉才能正常保存為excel2003文檔。
5、優化
上面雖然功能完成了,但是還不夠,打開任務管理器,每導出一次會發現Excel.exe進程多一個,也就是說Excel.exe進程沒有被關閉,需要手動釋放資源。首先,釋放Com資源非常簡單:
1 excelApp.Quit(); 2 // 釋放COM組件,其實就是將其引用計數減1 3 System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp); 4 excelApp = null;
但是從系統中刪除線程就比較麻煩,有一種方式是把所有Excel.exe進程關閉,但是這會影響事先打開的Excel文件。所以我這里創建了一個列表保存用來導出Excel的進程,並在導出結束后關閉這些進程:
1 // 獲取已打開的Excel程序 Interaction.GetObject(null, "Excel.Application") as Excel.Application; 2 Monitor.Enter(syncRoot); 3 var proListStart = Process.GetProcessesByName("EXCEL"); 4 Excel.Application excelApp = new Excel.Application(); 5 var proList = Process.GetProcessesByName("EXCEL").Except(proListStart, new ProcessComparer()); 6 Monitor.Exit(syncRoot);
在創建Excel應用前進入鎖定,並記錄當前Excel.exe進程列表,然后創建,對比判斷新增的進程,結束鎖定。對比判斷ProcessComparer類,實現了IEqualityComparer<Process>接口,通過進程的Id來標識唯一性。
在導出結束之后,我再調用KillProcess函數,把proList列表中的進程全部關閉,以釋放資源:
1 foreach (Process theProc in list) 2 if (theProc.CloseMainWindow() == false) 3 theProc.Kill();
三、總結
這個東西本來就做好很久了,一直沒時間寫博文,現在感覺寫博文有種很想偷懶的感覺,唉,不行了,對文字工作不感冒。這個東西實際上難度不大,關鍵是各種配合起來,達到諧調的目的,然后資源釋放那塊也琢磨了不少方法才采用的死辦法的,看有沒有園友能找到更好的釋放進程的方法。
有一個問題,現在我是使用List<實體對象>這樣的數據源的,這就是說每一個實體對象都是會要一個導出處理函數的,希望大家注意,如果是想使用通用性的處理函數,數據源可以更改為一個本身就有行、列概念的對象,然后可以修改一下傳入參數應該能完成想要的功能了。
疲勞中~~~
轉載請注明原址:http://www.cnblogs.com/lekko/archive/2012/10/19/2696121.html