將.NET數(shù)據(jù)導(dǎo)出為Excel文件,有許多種方法,我這里介紹采用COM組件來(lái)操作Excel文件,并且還會(huì)涉及異步、同步、進(jìn)程管理、文件定位等內(nèi)容,使用WPF做到一個(gè)盡量可用的導(dǎo)出界面。
一、WPF前臺(tái)
這個(gè)就不用多說(shuō)了,堆上幾個(gè)按鈕,做一個(gè)數(shù)據(jù)錄入的東西,一個(gè)狀態(tài)條:
我這里的數(shù)據(jù)錄入,就是用了幾個(gè)Textbox,實(shí)際上大家可以用任何東西(DataGrid、ListView等),因?yàn)樵谧詈蠖紩?huì)轉(zhuǎn)成List<MyData>的形式進(jìn)行導(dǎo)出的,MyData是表示數(shù)據(jù)記錄的對(duì)象:
1 // 自定義數(shù)據(jù)類
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 }
二、后臺(tái)
1、錄入組織數(shù)據(jù)就不說(shuō)了,先來(lái)說(shuō)下選擇默認(rèn)導(dǎo)出路徑:
1 using Forms = System.Windows.Forms;
2 // 選擇導(dǎo)出目錄
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來(lái)選擇目錄,并把選擇的path保存到全局變量中,另外還有一個(gè)判斷,如果路徑結(jié)尾不是'\\'的話,就加上這個(gè)字符,以便于后面合成文件全路徑。效果圖:
2、如果是導(dǎo)出到非默認(rèn)的路徑,并命名文件,則:
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方法,彈出一個(gè)文件保存對(duì)話框,我們輸入、選擇路徑、文件名、后綴后,點(diǎn)擊“保存”,就能通過(guò)dialog.FileName得到全路徑,然后分別截取目錄、文件名、后綴,構(gòu)成參數(shù)類ExportInput<MyData>,以啟動(dòng)后臺(tái)線程進(jìn)行導(dǎo)出。
ExportInput參數(shù)類
1 /// <summary>
2 /// 導(dǎo)出成Excel文件時(shí)需要傳入的參數(shù)類
3 /// </summary>
4 public class ExportInput<T>
5 {
6 /// <summary>
7 /// 數(shù)據(jù)源
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 /// 文件的絕對(duì)路徑
20 /// </summary>
21 public string Path { get; set; }
22 /// <summary>
23 /// 文件后綴
24 /// </summary>
25 public string Ext { get; set; }
26
27 /// <summary>
28 /// 構(gòu)造傳入?yún)?shù)
29 /// </summary>
30 /// <param name="sources">數(shù)據(jù)源</param>
31 /// <param name="filename">文件名</param>
32 /// <param name="path">文件的絕對(duì)路徑</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、導(dǎo)出時(shí)使用的后臺(tái)線程來(lái)自System.ComponentModel.BackgroundWorker,使用它可以非常方便地完成線程運(yùn)行、取消、通知的功能:
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 // 報(bào)告進(jìn)度
13 private void OnProgressChanged(object sender, ProgressChangedEventArgs e)
14 {
15 PB_State.Value = e.ProgressPercentage;
16 }
17 // 導(dǎo)出完成
18 private void OnWorkCompleted(object sender, RunWorkerCompletedEventArgs e)
19 {
20 if (e.Error != null)
21 MessageBox.Show("導(dǎo)出失。" + e.Error.Message);
22 else if (e.Cancelled)
23 MessageBox.Show("已取消導(dǎo)出!");
24 else
25 MessageBox.Show("導(dǎo)出成功!");
26 }
WorkerReportsProgress、WorkerSupportsCancellation這兩個(gè)布爾值分別是是否支持報(bào)告后臺(tái)線程進(jìn)度、是否支持取消后臺(tái)線程的功能,DoWork是后臺(tái)工作線程的委托,在上面代碼中,用的是ExcelHelper.ExportMyData這個(gè)靜態(tài)事件處理函數(shù)來(lái)完成導(dǎo)出功能。RunWorkerCompleted、ProgressChanged 分別是工作完成、進(jìn)度改變時(shí)回調(diào)給前臺(tái)的委托。
4、終于到了導(dǎo)出的部分了,代碼如下:
導(dǎo)出Excel
1 using Excel = Microsoft.Office.Interop.Excel;
2
3 /// <summary>
4 /// 導(dǎo)出成Excel文件
5 /// </summary>
6 public class ExcelHelper
7 {
8 /// <summary>
9 /// 導(dǎo)出Excel時(shí)使用的同步
10 /// </summary>
11 private static object syncRoot = new object();
12
13 /// <summary>
14 /// 將數(shù)據(jù)集導(dǎo)出為Excel文件
15 /// </summary>
16 public static void ExportMyData(object sender, DoWorkEventArgs e)
17 {
18 // 創(chuàng)建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 // 檢查參數(shù)
27 var input = (ExportInput<MyData>)e.Argument;
28 var bgWorker = (BackgroundWorker)sender;
29 // 創(chuàng)建工作簿
30 Excel.Workbook excelDoc = excelApp.Workbooks.Add();
31 // 創(chuàng)建工作表
32 Excel.Worksheet excelSheet = (Excel.Worksheet)excelDoc.Worksheets[1];
33 // 數(shù)字類型以文本格式顯示
34 excelSheet.Cells.NumberFormat = "@";
35 // 單元格索引從1開始
36 int i = 1, j = 1, count = input.Sources.Count();
37 // 導(dǎo)入標(biāo)題
38 if (input.Headers != null)
39 {
40 foreach (string head in input.Headers)
41 excelSheet.Cells[1, j++] = head;
42 ++i;
43 }
44 //將數(shù)據(jù)導(dǎo)入到工作表的單元格
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 //將其進(jìn)行保存到指定的路徑
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組件,其實(shí)就是將其引用計(jì)數(shù)減1
75 System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp);
76 excelApp = null;
77 //釋放可能還沒(méi)釋放的進(jìn)程
78 KillProcess(proList);
79 }
80 }
81 }
首先,引用Microsoft.Office.Interop.Excel命名空間,如果機(jī)器上安裝了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目錄”可能名稱會(huì)有一點(diǎn)差別。
接下來(lái),啟動(dòng)Excel進(jìn)程:
Excel.Application excelApp = new Excel.Application();
創(chuàng)建工作簿:
Excel.Workbook excelDoc = excelApp.Workbooks.Add();
創(chuàng)建工作表:
Excel.Worksheet excelSheet = (Excel.Worksheet)excelDoc.Worksheets[1];
填入數(shù)據(jù)(注意到行和列都是從1開始的):
excelSheet.Cells[行, 列] = 數(shù)據(jù);
在填入數(shù)據(jù)時(shí),每趕往記錄前,都判斷一次是否取消導(dǎo)出,每填入一條記錄后,就使用bgWorker.ReportProgress()匯報(bào)工作進(jìn)度。
1 if (bgWorker.CancellationPending) 2 { 3 e.Cancel = true; 4 return; 5 }
將工作簿保存到指定的路徑,關(guān)閉:
1 excelDoc.SaveAs(input.Path + input.FileName + input.Ext, 2 input.Ext == ".xls" ? Excel.XlFileFormat.xlExcel7 : Excel.XlFileFormat.xlOpenXMLWorkbook); 3 excelDoc.Close();
網(wǎng)上很多地方說(shuō)保存成office2003用的枚舉是Excel.XlFileFormat.xlExcel8,經(jīng)過(guò)我實(shí)際測(cè)試,這個(gè)枚舉是從office 2007才開始出現(xiàn)的,如果機(jī)器上安裝了2007及更高版本的office的話是可以正常使用的,如果機(jī)器上只安裝了office 2003,則只有用xlExcel7這個(gè)枚舉才能正常保存為excel2003文檔。
5、優(yōu)化
上面雖然功能完成了,但是還不夠,打開任務(wù)管理器,每導(dǎo)出一次會(huì)發(fā)現(xiàn)Excel.exe進(jìn)程多一個(gè),也就是說(shuō)Excel.exe進(jìn)程沒(méi)有被關(guān)閉,需要手動(dòng)釋放資源。首先,釋放Com資源非常簡(jiǎn)單:
1 excelApp.Quit(); 2 // 釋放COM組件,其實(shí)就是將其引用計(jì)數(shù)減1 3 System.Runtime.InteropServices.Marshal.ReleaseComObject(excelApp); 4 excelApp = null;
但是從系統(tǒng)中刪除線程就比較麻煩,有一種方式是把所有Excel.exe進(jìn)程關(guān)閉,但是這會(huì)影響事先打開的Excel文件。所以我這里創(chuàng)建了一個(gè)列表保存用來(lái)導(dǎo)出Excel的進(jìn)程,并在導(dǎo)出結(jié)束后關(guān)閉這些進(jìn)程:
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);
在創(chuàng)建Excel應(yīng)用前進(jìn)入鎖定,并記錄當(dāng)前Excel.exe進(jìn)程列表,然后創(chuàng)建,對(duì)比判斷新增的進(jìn)程,結(jié)束鎖定。對(duì)比判斷ProcessComparer類,實(shí)現(xiàn)了IEqualityComparer<Process>接口,通過(guò)進(jìn)程的Id來(lái)標(biāo)識(shí)唯一性。
在導(dǎo)出結(jié)束之后,我再調(diào)用KillProcess函數(shù),把proList列表中的進(jìn)程全部關(guān)閉,以釋放資源:
1 foreach (Process theProc in list) 2 if (theProc.CloseMainWindow() == false) 3 theProc.Kill();
三、總結(jié)
這個(gè)東西本來(lái)就做好很久了,一直沒(méi)時(shí)間寫博文,現(xiàn)在感覺(jué)寫博文有種很想偷懶的感覺(jué),唉,不行了,對(duì)文字工作不感冒。這個(gè)東西實(shí)際上難度不大,關(guān)鍵是各種配合起來(lái),達(dá)到諧調(diào)的目的,然后資源釋放那塊也琢磨了不少方法才采用的死辦法的,看有沒(méi)有園友能找到更好的釋放進(jìn)程的方法。
有一個(gè)問(wèn)題,現(xiàn)在我是使用List<實(shí)體對(duì)象>這樣的數(shù)據(jù)源的,這就是說(shuō)每一個(gè)實(shí)體對(duì)象都是會(huì)要一個(gè)導(dǎo)出處理函數(shù)的,希望大家注意,如果是想使用通用性的處理函數(shù),數(shù)據(jù)源可以更改為一個(gè)本身就有行、列概念的對(duì)象,然后可以修改一下傳入?yún)?shù)應(yīng)該能完成想要的功能了。