在我們設計軟體的很多地方,都看到需要對錶格數據進行導入和導出的操作,主要是方便客戶進行快速的數據處理和分享的功能,本篇隨筆介紹基於WPF實現DataGrid數據的導入和導出操作。 ...
在我們設計軟體的很多地方,都看到需要對錶格數據進行導入和導出的操作,主要是方便客戶進行快速的數據處理和分享的功能,本篇隨筆介紹基於WPF實現DataGrid數據的導入和導出操作。
1、系統界面設計
在我們實現數據的導入導出功能之前,我們在主界面需要提供給客戶相關的操作按鈕,如下界面所示,在列表的頂端提供導入Excel、導出PDF、導出Excel。
由於這些操作功能基本上在各個頁面模塊,可能都會用到,因此儘可能的抽象到基類,以及提供通用的處理操作,實在有差異的,也可以通過一些屬性或者事件方法的覆蓋方式來實現即可。
因此我們在Xaml裡面定義按鈕的時候,基本上是調用視圖模型的方法來通用化的處理,如下代碼所示。
<Button Margin="5" hc:IconElement.Geometry="{StaticResource t_import}" Command="{Binding ImportExcelCommand}" Content="導入Excel" Style="{StaticResource ButtonWarning}" /> <Button Margin="5" hc:IconElement.Geometry="{StaticResource SaveGeometry}" Command="{Binding ViewModel.ExportPdfCommand}" CommandParameter="用戶信息列表" Content="導出PDF" Style="{StaticResource ButtonSuccess}" /> <Button Margin="5" hc:IconElement.Geometry="{StaticResource SaveGeometry}" Command="{Binding ViewModel.ExportExcelCommand}" CommandParameter="用戶信息列表" Content="導出Excel" Style="{StaticResource ButtonSuccess}" />
而導入的處理操作函數ImportExcelComand的定義如下所示(註意這裡聲明瞭RelayCommand)代碼會自動生成Command的尾碼Command方法的。
/// <summary> /// 導出內容到Excel /// </summary> [RelayCommand] private void ImportExcel() { var page = App.GetService<ImportExcelData>(); page!.ViewModel.Items?.Clear(); page!.ViewModel.TemplateFile = $"系統用戶信息-模板.xls"; page!.OnDataSave -= ExcelData_OnDataSave; page!.OnDataSave += ExcelData_OnDataSave; //導航到指定頁面 ViewModel.Navigate(typeof(ImportExcelData)); }
而其中 ImportExcelData 是我們定義的通用導入頁面窗體類,這裡只需要實現一些屬性的設置(根據子類的不同而調整,後期可以用代碼生成工具生成),以及一些事件用於子類延後實現,從而可以實現自定義的數據處理的功能。
我們在下麵再細說批量導入的處理細節。
2、數據導出到Excel
數據導出到Excel,在我們的Winform端中很常見,而WPF這裡也是一樣的處理方式,通用利用Excel的操作組件的封裝類來實現,可以基於NPOI,也可以基於Aspose.Cell實現,根據自己的需要實現簡單的封裝調用即可。
導出到Excel,首先需要彈出選擇目錄的對話框進行選取目錄,然後用於生成Excel的文件,如下界面所示。
這個處理,由於WPF可以調用.net裡面的System.Windows.Forms,因此我們直接調用裡面的對話框處理封裝即可,這個類來自於我們的Winform的UI公用類庫部分。
在前面隨筆,我們介紹過為了WPF開發的方便,我們設計了幾個視圖基類,用於減少代碼的處理。
對於不同的業務類,我們也只需要根據實際情況,生成對應的業務視圖模型類即可。
我們把通用的導出操作放到了這個視圖基類BaseListViewModel 裡面即可,如下代碼所示。
/// <summary> /// 觸發導出Excel處理命令 /// </summary> [RelayCommand] protected virtual async Task ExportExcel(string title = "列表數據") { var table = await this.ConvertItems(this.Items); BaseExportExcel(table, title); }
而其中對於DataTable的處理Excel,提供一個通用的方法。
/// <summary> /// 可供重寫的基類函數,導出Excel /// </summary> public virtual void BaseExportExcel(DataTable table, string title = "列表數據") { string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", title)); if (!string.IsNullOrEmpty(file)) { try { string error = ""; AsposeExcelTools.DataTableToExcel2(table, file, out error); if (!string.IsNullOrEmpty(error)) { MessageDxUtil.ShowError(string.Format("導出Excel出現錯誤:{0}", error)); } else { if (MessageDxUtil.ShowYesNoAndTips("導出成功,是否打開文件?") == System.Windows.MessageBoxResult.Yes) { Process.Start("explorer.exe", file); } } } catch (Exception ex) { LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.Message); } } }
其中FileDialogHelper.SaveExcel 的代碼如下所示。
/// <summary> /// 保存Excel對話框,並返回保存全路徑 /// </summary> /// <returns></returns> public static string SaveExcel(string filename, string initialDirectory) { return Save("保存Excel", ExcelFilter, filename, initialDirectory); } /// <summary> /// 以指定的標題彈出保存文件對話框 /// </summary> /// <param name="title">對話框標題</param> /// <param name="filter">尾碼名過濾</param> /// <param name="filename">預設文件名</param> /// <param name="initialDirectory">初始化目錄</param> /// <returns></returns> public static string Save(string title, string filter, string filename, string initialDirectory) { //多語言支持 title = JsonLanguage.Default.GetString(title); var dialog = new SaveFileDialog(); dialog.Filter = filter; dialog.Title = title; dialog.FileName = filename; dialog.RestoreDirectory = true; if (!string.IsNullOrEmpty(initialDirectory)) { dialog.InitialDirectory = initialDirectory; } if (dialog.ShowDialog() == DialogResult.OK) { return dialog.FileName; } return string.Empty; }
而其中SaveFileDialog是屬於.net 中System.Windows.Forms裡面的內容,WPF可以直接調用。
而DataTableToExcel2 方法,這是我們封裝的一個使用Aspose.Cell的調用,主要用於快速處理DataTable到Excel的操作封裝,我們也可可以利用其它操作Excel的封裝,如NPOI等都可以實現。
代碼如下所示。
/// <summary> /// 把DataTabel轉換成Excel文件 /// </summary> /// <param name="datatable">DataTable對象</param> /// <param name="filepath">目標文件路徑,Excel文件的全路徑</param> /// <param name="error">錯誤信息:返回錯誤信息,沒有錯誤返回""</param> /// <returns></returns> public static bool DataTableToExcel2(DataTable datatable, string filepath, out string error) { error = ""; var wb = new Aspose.Cells.Workbook(); try { if (datatable == null) { error = "DataTableToExcel:datatable 為空"; return false; } //為單元格添加樣式 var style = wb.CreateStyle(); //設置居中 style.HorizontalAlignment = Aspose.Cells.TextAlignmentType.Center; //設置背景顏色 style.ForegroundColor = System.Drawing.Color.FromArgb(153, 204, 0); style.Pattern = BackgroundType.Solid; style.Font.IsBold = true; int rowIndex = 0; for (int i = 0; i < datatable.Columns.Count; i++) { DataColumn col = datatable.Columns[i]; string columnName = col.Caption ?? col.ColumnName; wb.Worksheets[0].Cells[rowIndex, i].PutValue(columnName); wb.Worksheets[0].Cells[rowIndex, i].SetStyle(style); } rowIndex++; foreach (DataRow row in datatable.Rows) { for (int i = 0; i < datatable.Columns.Count; i++) { wb.Worksheets[0].Cells[rowIndex, i].PutValue(row[i].ToString()); } rowIndex++; } for (int k = 0; k < datatable.Columns.Count; k++) { wb.Worksheets[0].AutoFitColumn(k, 0, 150); } wb.Worksheets[0].FreezePanes(1, 0, 1, datatable.Columns.Count); wb.Save(filepath); return true; } catch (Exception e) { error = error + " DataTableToExcel: " + e.Message; return false; } }
導出Excel的內容如下界面所示。另外導出文檔的內容,我們可以用於導入的數據模板的。
我們可以根據需要設置要導出的列即可。
3、數據導出到PDF
同樣,數據導出到PDF的處理操作類似,也是通過視圖基類的封裝方法,實現快速的導出到PDF處理,如下是視圖基類裡面的實現方法。
/// <summary> /// 觸發導出PDF處理命令 /// </summary> [RelayCommand] protected virtual async Task ExportPdf(string title = "列表數據") { var table = await this.ConvertItems(this.Items); BaseExportPdf(table, title); } /// <summary> /// 可供重寫的基類函數,導出PDF /// </summary> public virtual void BaseExportPdf(DataTable table, string title = "列表數據") { var pdfFile = FileDialogHelper.SavePdf(); if (!pdfFile.IsNullOrEmpty()) { bool isLandscape = true;//是否為橫向列印,預設為true bool includeHeader = true;//是否每頁包含表頭信息 var headerAlignment = iText.Layout.Properties.HorizontalAlignment.CENTER;//頭部的對其方式,預設為居中 float headerFontSize = 9f;//頭部字體大小 float rowFontSize = 9f;//行記錄字體大小 float? headerFixHeight = null;//頭部的固定高度,否則為自適應 var success = TextSharpHelper.ExportTableToPdf(title, table, pdfFile, isLandscape, includeHeader, headerAlignment, headerFontSize, rowFontSize, headerFixHeight); //提示信息 var message = success ? "導出操作成功" : "導出操作失敗"; if (success) { Growl.SuccessGlobal(message); Process.Start("explorer.exe", pdfFile); } else { Growl.ErrorGlobal(message); } } }
通過把List<T>的列表轉換為常規的DataTable來處理,我們就可以利用之前我們隨筆《在Winform分頁控制項中集成導出PDF文檔的功能》介紹到的PDF導出函數來實現WPF數據導出到PDF的處理。
上面的 TextSharpHelper 就是對於itext7進行的封裝,實現PDF的導出處理。
引入相關的Nugget類後,封裝它的輔助類代碼如下所示。
/// <summary> /// 基於iText7對PDF的導出處理 /// </summary> public static class TextSharpHelper { /// <summary> /// datatable轉PDF方法 /// </summary> /// <param name="title">標題內容</param> /// <param name="data">dataTable數據</param> /// <param name="pdfFile">PDF文件保存的路徑</param> /// <param name="isLandscape">是否為橫向列印,預設為true</param> /// <param name="includeHeader">是否每頁包含表頭信息</param> /// <param name="headerAlignment">頭部的對其方式,預設為居中對其</param> /// <param name="headerFontSize">頭部字體大小</param> /// <param name="rowFontSize">行記錄字體大小</param> /// <param name="headerFixHeight">頭部的固定高度,否則為自適應</param> /// <returns></returns> public static bool ExportTableToPdf(string title, DataTable data, string pdfFile, bool isLandscape = true, bool includeHeader = true, iText.Layout.Properties.HorizontalAlignment headerAlignment = iText.Layout.Properties.HorizontalAlignment.CENTER, float headerFontSize = 9f, float rowFontSize = 9f, float? headerFixHeight = null) {var writer = new PdfWriter(pdfFile); PdfDocument pdf = new PdfDocument(writer); pdf.SetDefaultPageSize(isLandscape ? PageSize.A4.Rotate() : PageSize.A4); //A4橫向 var doc = new Document(pdf);//設置標題 if (!string.IsNullOrEmpty(title)) { var param = new Paragraph(title) .SetFontColor(iText.Kernel.Colors.ColorConstants.BLACK) .SetBold() //粗體 .SetFontSize(headerFontSize + 5) .SetTextAlignment(TextAlignment.CENTER); //居中 doc.Add(param); } var table = new Table(data.Columns.Count) .SetTextAlignment(TextAlignment.CENTER) .SetVerticalAlignment(VerticalAlignment.MIDDLE) .SetWidth(new UnitValue(UnitValue.PERCENT, 100));//縮放比例 table.UseAllAvailableWidth(); //添加表頭 foreach (DataColumn dc in data.Columns) { var caption = !string.IsNullOrEmpty(dc.Caption) ? dc.Caption : dc.ColumnName; var cell = new Cell().Add(new Paragraph(caption)) .SetBold() .SetVerticalAlignment(VerticalAlignment.MIDDLE) .SetHorizontalAlignment(headerAlignment) .SetPadding(1) .SetFontSize(headerFontSize); if (headerFixHeight.HasValue) { cell.SetHeight(new UnitValue(UnitValue.POINT, headerFixHeight.Value)); } table.AddHeaderCell(cell); } //插入數據 var colorWhite = Color.ConvertRgbToCmyk(iText.Kernel.Colors.WebColors.GetRGBColor("White"));// System.Drawing.Color.White; var colorEvent = iText.Kernel.Colors.WebColors.GetRGBColor("LightCyan");// System.Drawing.Color.LightCyan; var EventRowBackColor = Color.ConvertRgbToCmyk(colorEvent); for (int i = 0; i < data.Rows.Count; i++) { table.StartNewRow();//第一列開啟新行 var backgroudColor = ((i % 2 == 0) ? colorWhite : EventRowBackColor); for (int j = 0; j < data.Columns.Count; j++) { var text = data.Rows[i][j].ToString(); var cell = new Cell() .SetBackgroundColor(backgroudColor) .SetFontSize(rowFontSize) .SetVerticalAlignment(VerticalAlignment.MIDDLE) .Add(new Paragraph(text)); table.AddCell(cell); } } doc.Add(table); pdf.Close(); writer.Close(); return true; } }
導出PDF的文檔效果如下所示。
4、導入Excel數據
Excel數據的導入,可以降低批量處理數據的難度和繁瑣的界面一個個錄入,這種是一種常見的操作方式,我們主要提供固定的模板給客戶下載錄入數據,然後提交進行批量的導入即可。
導入的界面處理,我們這裡涉及一個通用的導入界面(和WInform端的界面類似),這樣我們每個不同的業務導入處理都可以重用,只需要設置一些不同的屬性,以及一些事件的處理即可,如下是通用的界面效果。
我們這裡主要針對性的介紹它的設計方式,前面我們介紹,在業務界面裡面調用它的時候,如下代碼所示。
/// <summary> /// 導出內容到Excel /// </summary> [RelayCommand] private void ImportExcel() { var page = App.GetService<ImportExcelData>(); page!.ViewModel.Items?.Clear(); page!.ViewModel.TemplateFile = $"系統用戶信息-模板.xls"; page!.OnDataSave -= ExcelData_OnDataSave; page!.OnDataSave += ExcelData_OnDataSave; //導航到指定頁面 ViewModel.Navigate(typeof(ImportExcelData)); }
這個通用的窗體裡面的視圖模型,定義了一個模板的文件名稱,以及一個通用的數據DataTable的集合,以及一個事件用於子類的導入轉換的實現,它的視圖模型類代碼如下所示。
/// <summary> /// 批量導入Excel數據的視圖模型基類 /// </summary> public partial class ImportExcelDataViewModel : BaseViewModel { [ObservableProperty] private string templateFile; [ObservableProperty] private string importFilePath; [ObservableProperty] private DataTable items;
我們為了給客戶打開模板文件,方便用於錄入Excel數據,因此我們在本地打開模板文件即可。
/// <summary> /// 打開模板文件 /// </summary> [RelayCommand] private void OpenFile() { if (!this.TemplateFile.IsNullOrEmpty()) { var realFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, this.TemplateFile); if (File.Exists(realFilePath)) { Process.Start("explorer.exe", realFilePath); } else { MessageDxUtil.ShowError($"沒有找到該模板文件:{realFilePath}"); } } }
在通用的導入頁面的後臺代碼裡面,我們需要實現一些如選擇Excel後,顯示數據到DataGrid的操作,以及批量保存數據的處理。
/// <summary> /// 選擇Excel文件後,顯示Excel裡面的表格數據 /// </summary> [RelayCommand] private void BrowseExcel() { string file = FileDialogHelper.OpenExcel(); if (!string.IsNullOrEmpty(file)) { this.ViewModel.ImportFilePath = file; ViewData(); } }
/// <summary> /// 查看Excel文件並顯示在界面上操作 /// </summary> private void ViewData() { if (this.txtFilePath.Text == "") { MessageDxUtil.ShowTips("請選擇指定的Excel文件"); return; } try { var myDs = new DataSet(); string error = ""; AsposeExcelTools.ExcelFileToDataSet(this.txtFilePath.Text, out myDs, out error); this.ViewModel.Items = myDs.Tables[0]; } catch (Exception ex) { LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.Message); } }
導入處理的操作代碼如下所示。
/// <summary> /// 批量保存數據到資料庫 /// </summary> /// <returns></returns> [RelayCommand] private async Task<CommonResult> SaveData() { if (ViewModel.Items == null || ViewModel.Items?.Rows?.Count == 0) return new CommonResult(false); if (MessageDxUtil.ShowYesNoAndWarning("該操作將把數據導入到系統資料庫中,您確定是否繼續?") == System.Windows.MessageBoxResult.Yes) { var dt = this.ViewModel.Items; foreach (DataRow dr in dt.Rows) { try { await OnDataSave(dr); } catch (Exception ex) { LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.Message); } } return new CommonResult(true, "操作成功"); } return new CommonResult(false); }
註意,我們這裡使用了事件的處理,把數據的轉換邏輯留給子類去實現的。
/// <summary> /// 數據保存的事件 /// </summary> public event SaveDataHandler OnDataSave;
這樣我們在用戶信息的導入頁面UserListPage.xaml.cs裡面的代碼就可以根據實際的情況進行實現事件了。
/// <summary> /// 導出內容到Excel /// </summary> [RelayCommand] private void ImportExcel() { var page = App.GetService<ImportExcelData>(); page!.ViewModel.Items?.Clear(); page!.ViewModel.TemplateFile = $"系統用戶信息-模板.xls"; page!.OnDataSave -= ExcelData_OnDataSave; page!.OnDataSave += ExcelData_OnDataSave; //導航到指定頁面 ViewModel.Navigate(typeof(ImportExcelData)); }
這個事件的實現,主要就是把個性化的用戶信息(用戶信息模板裡面定義的欄位),轉換為DataTable的行信息即可,如下代碼所示。具體根據模板設計的情況進行修改即可。
具體就不再一一贅述,主要就是基類邏輯和具體實現分離,實現不同的業務功能處理即可。
以上即是我們一個列表通用頁面裡面,往往需要用到的通用性的導入、導出操作的介紹,希望對讀者在開發WPF應用功能上有所啟發,有所參考,善莫大焉。
專註於代碼生成工具、.Net/.NetCore 框架架構及軟體開發,以及各種Vue.js的前端技術應用。著有Winform開發框架/混合式開發框架、微信開發框架、Bootstrap開發框架、ABP開發框架、SqlSugar開發框架等框架產品。轉載請註明出處:撰寫人:伍華聰 http://www.iqidi.com