海量、多页数据导出到Excel(C#版本)
最近做了一个项目,其中需要将多个DataGridView中的数据导出到一个Excel文件中,如:整体情况统计表、审核情况明细表及不合格原因统计表。开始觉得难度不大,可是深入进去后发现问题多多。
1、 数据量大:一个明细表中包含的数据基本上超过10w,不能采用常规的一行行导入,必须批量导入;
2、 即使是批量导入,也需要接近1分钟,必须使用多线程;
3、 在多线程进行时,必须在页面通知用户导出正在进行中,需要使用事件通知;
4、 Excel的版本不同,每个sheet页能够显示的数据量不一样,2003是65536,而2007可以达到100多万行。如果客户使用的是Excel2003,单独对明细表数据来说就需要多个sheet页来保存。另外每个DataGridView中的数据也需要单独的sheet页来保存。在这里我将后者称之为逻辑页,前者为物理页;
5、 需要方便扩展,即使是其他类型的数据也能够重用代码,不能仅限于上述的三个DataGridView中显示的数据。
运行效果示意图
经过百度,在网上发现一种将数据以object[,]二维数组的形式直接导出到Excel的range区域是最省时的方式,于是整个方法的设计思路就出来了,代码如下:
1、设计一个特性类,对要显示的数据类中的属性进行说明,在这里我主要使用特性类来保存中文列标题以及指明某个属性是否需要导出到Excel中。
public class ChineseNameAttribute : Attribute
{
public ChineseNameAttribute(string cname, bool show)
{
ChName = cname; this.ShowAsTitle = show;
}
public ChineseNameAttribute(string cname) : this(cname, false) { }
public bool ShowAsTitle { get; set; }
public string ChName { get; set; }
}
2、为要导出到Excel中数据类创建一个基类ExcelDataBase,指定两个方法,一个是获取标题,另一个是获取对象的数组形式,因为上面说了,我需要的数据格式为object[,]二维数组,那么我的每一个对象就应该是object[]一维数组。
using System;
using System.Collections.Generic;
using System. Reflection;
public class ExcelDataBase
{
public virtual string[] GetTitle()
{
List<string> titles = new List<string>();
//获取当前对象的类型描述,这里的对象实际上是子类对象
Type type = this.GetType();
//获取当前对象的所以属性信息
PropertyInfo[] pis = type.GetProperties();
foreach (PropertyInfo pi in pis)
{
//获取我们定义在属性上的特性描述,描述方式在下面
ChineseNameAttribute cnAtt = (ChineseNameAttribute)Attribute
.GetCustomAttribute(pi, typeof(ChineseNameAttribute));
//根据属性上的特性描述,决定该属性是否显示及显示的标题名称
if (cnAtt.ShowAsTitle == true)
titles.Add(cnAtt.ChName);
}
return titles.ToArray();
}
//将当前对象的属性转换成object[]数组
public virtual object[] GetValue()
{
List<object> values = new List<object>();
Type type = this.GetType();
PropertyInfo[] pis = type.GetProperties();
foreach (PropertyInfo pi in pis)
{
ChineseNameAttribute cnAtt = (ChineseNameAttribute)Attribute
.GetCustomAttribute(pi, typeof(ChineseNameAttribute));
if (cnAtt.ShowAsTitle == true)
values.Add(pi.GetValue(this, null).ToString());
}
return values.ToArray();
}
}
3、设计我们显示在DataGridView中的数据类型(处于个人喜好,我绑定到DataGridView都是泛型集合List<SomeClass>)
public class TotalStatistics : ExcelDataBase
{
[ChineseNameAttribute("营业部名称", true)]
public string KHDCode { get; set; }
[ChineseNameAttribute("凭证总数", true)]
public int ImageCounts { get; set; }
[ChineseNameAttribute("已审核数量", true)]
public int AuditedCounts { get; set; }
[ChineseNameAttribute("合格数量", true)]
public int PassedCounts { get; set; }
[ChineseNameAttribute("不合格数量", true)]
public int UnPassedCounts { get; set; }
[ChineseNameAttribute("合格率", true)]
public string PassPercent //只读属性
{
get {
return Math. Round((((decimal)PassedCounts * 100) / AuditedCounts), 2)
.ToString() + "%";
}
}
//多个DataGridView的数据源要导入到同一个Excel文件,因此数据类型必须统一为
//List<ExcelDataBase>。但是如同我们不能将一个List<String>隐式转换为List<object>
//我们也不能将一个List<TotalStatistics>隐式转换为List<ExcelDataBase>
public static List<ExcelDataBase> Convert(List<TotalStatistics> tsList)
{
List<ExcelDataBase> edbList = new List<ExcelDataBase>();
foreach (TotalStatistics ts in tsList)
edbList.Add(ts as ExcelDataBase);
return edbList;
}
}
另外两个类ErrorReason.cs和AuditingDetail.cs设计和TotalStatistics类似,代码就不贴出来了。
4、对要显示在sheet逻辑页中的数据进行封装
public class ExcelSheetInfo
{
public string SheetName { get; set; }
public List<ExcelDataBase> SheetData { get; set; }
public int ColumnCount
{
get {
if (SheetData.Count == 0)
return 0;
ExcelDataBase edb = SheetData[0];
return edb.GetTitle().Length;
}
}
/// 将数据按照每页数据上限分解成多个物理页
/// <param name="maxPageLength">每页数据上限</param>
public List<object[,]> GetSheetDataInPages(int maxPageLength)
{
List<object[,]> sheetPageDatas = new List<object[,]>();
if (this.SheetData.Count == 0)
return sheetPageDatas;
object[,] pageDatas;
int totalCount = SheetData.Count; / /需要导出的数据总数量
//实际需要的sheet页数量
int pageCount = (totalCount + maxPageLength - 1) / maxPageLength;
int columnCount = ColumnCount;
for (int i = 0; i < pageCount; i++)
{
//每一页导出的数据上限
int upLine = Math.Min((i + 1) * maxPageLength, totalCount);
pageDatas = new object[maxPageLength, columnCount];
for (int j = i * maxPageLength; j < (i + 1) * maxPageLength; j++)
{
for (int k = 0; k < columnCount; k++)
{
if(j < upLine)
//加上单引号,防止数字被Excel以科学计数法显示
pageDatas[j % maxPageLength, k] = "'" +
SheetData[j].GetValue()[k];
else
//一定要补足空行,否则如果你的逻辑sheet中数据有10w行,而
//实际sheet中最大容纳6w行,则实际需要2页来显示数据(6w+
//4w)。如果不补空行,则每一页都只会显示4w。第二页会使第
//一页的数据行数减少
pageDatas[j % maxPageLength, k] = "";
}
}
sheetPageDatas.Add(pageDatas);
}
return sheetPageDatas;
}
}
5、对导出要Excel的完整数据进行封装
public class ExcelInfo
{
public List<ExcelSheetInfo> ExcelDatas { get; set; }
public string ExcelFilePath { get; set; }
public int MaxSheetLines
{
get
{
if (ExcelDatas.Count == 0)
return 0;
int max = 0;
foreach (ExcelSheetInfo sheetInfo in ExcelDatas)
{
max = Math.Max(sheetInfo.SheetData.Count, max);
}
return max;
}
}
}
6、Excel操作类
using Microsoft.Win32;
using System.Windows.Forms;
using Excel = Microsoft.Office.Interop.Excel;
public class ExcelUtility
{
//定义两个事件,对应导出开始和结束
public event EventHandler OnExportStart;
public event EventHandler OnExportEnd;
private Excel.Application xlApp = new Excel.Application();
private Excel.Workbooks workbooks;
private Excel.Workbook workbook;
private Excel.Worksheet worksheet;
private Excel.Range range = null;
private Range myrange = null;
private System.Reflection.Missing miss = System.Reflection.Missing.Value;//空数据变量
public static string GetExcelFilterString()
{
List<string> keys = new List<string>();
keys.Add(@"SOFTWARE\\Microsoft\\Office\\11.0\\Word\\InstallRoot\\");
keys.Add(@"SOFTWARE\\Microsoft\\Office\\12.0\\Word\\InstallRoot\\");
string filter = "";
RegistryKey regk = Registry.LocalMachine;
RegistryKey akey = regk.OpenSubKey(keys[0]);
if (akey != null)
filter += "Excel2003(*.xls)|*.xls";
akey = regk.OpenSubKey(keys[0]);
if (akey != null)
{
if (!string.IsNullOrEmpty(filter))
{
filter += "|";
}
filter += "Excel2007(*.xlsx)|*.xlsx";
}
return filter;
}
/// 线程版导出Excel(用于被多线程调用导出Excel,参数必须是Object类型)
public void ExcelExportInThread(object datas)
{
ExcelInfo ei = datas as ExcelInfo;
ExportToExcel(ei);
}
public void ExportToExcel(ExcelInfo excelInfo)
{
#region 1、获取全部要导出的数据总数,并触发导出开始事件
int allExcelCount = 0;
foreach (ExcelSheetInfo sheetInfo in excelInfo. ExcelDatas)
allExcelCount += sheetInfo. SheetData.Count;
if (null != this.OnExportStart) //判断事件是否被订阅
OnExportStart(this, null);
#endregion
#region 2、创建一个workbook,根据Excel版本号获取每个sheet所能显示最大行数
workbooks = xlApp.Workbooks;
workbook = workbooks.Add(Excel.XlWBATemplate.xlWBATWorksheet);
int maxPageLines = 65535;
if (excelInfo.ExcelFilePath.IndexOf("xlsx") != -1)
maxPageLines = 1048575;
maxPageLines = Math.Min(maxPageLines, excelInfo.MaxSheetLines);
#endregion
//循环处理每一个逻辑sheet中要显示的数据
foreach (ExcelSheetInfo sheetInfo in excelInfo. ExcelDatas)
{
//将一个逻辑sheet中的数据按实际sheet能够容纳的大小分解成多个sheet页
List<object[,]> sheetPages = sheetInfo. GetSheetDataInPages(maxPageLines);
#region 循环创建worksheet,并添加到workbook中
//倒序输出,因为新加入的sheet会默认放置在第一页
for (int i = sheetPages.Count - 1; i >= 0; i--)
{
//新建一个工作表,指定工作表名称
worksheet = (Excel.Worksheet)workbook.Worksheets.Add(miss, miss, miss, miss);
worksheet. Name = sheetInfo. SheetName + (i + 1).ToString();
#region 写入标题
string[] sheetTitle = sheetInfo. SheetData[0].GetTitle();
for (int j = 0; j < sheetTitle.Length; j++)
{
worksheet. Cells[1, j + 1] = sheetTitle[j];
range = (Excel.Range)worksheet. Cells[1, j + 1];
range.Interior.ColorIndex = 20;//背景颜色
range.Font.Bold = true;//粗体
range.Font.Size = 10;
range.Font.Name = "Arial";
range.Font.Underline = true; //设置字体是否有下划线
range.HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter;//居中
//加边框
range.BorderAround(Excel.XlLineStyle.xlContinuous,
Excel.XlBorderWeight.xlThin,
Excel.XlColorIndex.xlColorIndexAutomatic, null);
range.EntireColumn.AutoFit();//自动调整列宽
range.EntireRow.AutoFit();//自动调整行高
}
#endregion
#region 批量导入数据
myrange = worksheet.get_Range("A2", System.Reflection.Missing.Value);
myrange = myrange.get_Resize(maxPageLines, sheetTitle.Length);
myrange.set_Value(System.Reflection.Missing.Value, sheetPages[i]);
myrange.Font.Size = 9;
myrange.Font.Name = "Arial";
myrange.BordersExcel.XlBordersIndex.xlInsideHorizontal].Weight =
Excel.XlBorderWeight.xlThin;
myrange.EntireColumn.AutoFit();//自动调整列宽
myrange.EntireRow.AutoFit();//自动调整行高
#endregion
}
#endregion
}
try
{
workbook.Saved = true;
workbook.SaveCopyAs(excelInfo. ExcelFilePath);
}
catch (Exception ex)
{
MessageBox.Show("导出文件时出错,文件可能正被打开\n" + ex. Message);
return;
}
workbooks. Close();
if (xlApp != null)
{
xlApp.Workbooks.Close();
xlApp.Quit();
int generation = System.GC.GetGeneration(xlApp);
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);
xlApp = null;
SYSTEM. GC.Collect(generation);
}
GC.Collect();//强行销毁
#region 强行杀死最近打开的Excel进程
System.Diagnostics.Process[] excelProc =
System.Diagnostics.Process.GetProcessesByName("EXCEL");
System.DateTime startTime = new DateTime();
int m, killId = 0;
for (m = 0; m < excelProc.Length; m++)
{
if (startTime < excelProc[m].StartTime)
{
startTime = excelProc[m].StartTime;
killId = m;
}
}
if (excelProc[killId].HasExited == false)
excelProc[killId].Kill();
#endregion
if (null != this.OnExportEnd)
OnExportEnd(this, null);
}
}
7、页面调用
public partial class FrmImgStatistic : Form
{
//利用一个timer控制实现走马灯效果
int tickCount = 0;
private void timerExcelExport_Tick(object sender, EventArgs e)
{
StringBuilder text = new StringBuilder("正在导出验证结果");
string symbol = "。。。。。。。。。。";
text.Append(symbol.Substring(0, tickCount));
this.lblDirExport.Text = text.ToString();
tickCount = (tickCount + 1) % 11;
}
//导出按钮点击事件
private void btnStatisticExcel_Click(object sender, EventArgs e)
{
string filter = ExcelUtility.GetExcelFilterString();
if (String.IsNullOrEmpty(filter))
{
MessageBox.Show(this, "您的机器上没有安装Microsoft Office Excel\n软件,无法生成报表文件。", "警告",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
List<TotalStatistics> tsList = dgvTotal.DataSource as List<TotalStatistics>;
List<ErrorReason> erList = dgvErrorReason.DataSource as List<ErrorReason>;
List<AuditingDetail> adList = dgvTotal.DataSource as List<AuditingDetail>;
if (tsList.Count + erList.Count + adList.Count == 0)
return;
SaveFileDialog sfd = new SaveFileDialog();
sfd.FileName = "凭证质量控制系统报表";
sfd.Filter = filter ;
DialogResult result = sfd.ShowDialog();
if (result != System.Windows.Forms.DialogResult.OK)
return;
ExcelUtility eu = new ExcelUtility();
//订阅事件
eu.OnExportStart += new EventHandler(eu_OnExportStart);
eu.OnExportEnd += new EventHandler(eu_OnExportEnd);
ExcelInfo ei = new ExcelInfo() { ExcelFilePath = sfd.FileName };
ei.ExcelDatas = new List<ExcelSheetInfo>();
ExcelSheetInfo sheetInfo = new ExcelSheetInfo() { SheetName = "不合格原因统计表" };
sheetInfo. SheetData = ErrorReason.Convert(erList);
ei.ExcelDatas.Add(sheetInfo);
sheetInfo = new ExcelSheetInfo() { SheetName = "审核情况明细表" };
sheetInfo.SheetData = AuditingDetail.Convert(adList);
ei.ExcelDatas.Add(sheetInfo);
sheetInfo = new ExcelSheetInfo() { SheetName = "整体情况统计表" };
sheetInfo.SheetData = TotalStatistics.Convert(tsList);
ei.ExcelDatas.Add(sheetInfo);
//利用多线程实现导出
Thread excelThread = new Thread(eu. ExcelExportInThread);
excelThread.Start(ei);
}
//由于执行事件处理函数的线程不是创建控件的线程,所以需要invoke
public delegate void ExcelEventHandler(bool isShowLabel);
private void SetControlStatus(bool isShowLabel)
{
this.lblDirExport.Visible = isShowLabel;
this.btnStatisticExcel.Enabled = !isShowLabel;
if (isShowLabel)
this.timerExcelExport.Start();
else
{
this.timerExcelExport.Stop();
MessageBox.Show(this, "报表导出完毕。", "提示信息", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
}
void eu_OnExportEnd(object sender, EventArgs e)
{
ExcelEventHandler handler = new ExcelEventHandler(SetControlStatus);
this.Invoke(handler, false);
}
void eu_OnExportStart(object sender, EventArgs e)
{
ExcelEventHandler handler = new ExcelEventHandler(SetControlStatus);
this.Invoke(handler, true);
}
}