分类: C/C++
2012-12-26 09:33:33
如果你耐心仔细看完本文,相信以后再遇到导出EXCLE操作的时候你会很顺手觉得SO EASY,主要给新手朋友们看的,老鸟可以直接飘过了,花了一晚上的时间写的很辛苦,如果觉得对你有帮助烦请留言支持一下,我会写更多基础的原创内容来回报大家。
C#导出数据到EXCEL表格是个老生常谈的问题了,写这篇文章主要是给和我一样的新手朋友提供两种导出EXCEL的方法并探讨一下导出的效率问题,本文中的代码直接就可用,其中部分代码参考其他的代码并做了修改,抛砖引玉,希望大家一起探讨,如有不对的地方还请大家多多包涵并指出来,我也是个新手,出错也是难免的。
首先先总结下自己知道的导出EXCEL表格的方法,大致有以下几种,有疏漏的请大家补充。
1.数据逐条逐条的写入EXCEL
2.通过OLEDB把EXCEL做为数据源来写
3.通过RANGE范围写入多行多列内存数据到EXCEL
4.利用系统剪贴板写入EXCEL
好了,我想这些方法已经足够完成我们要实现的功能了,方法不在多,在精,不是么?以上4中方法都可以实现导出EXCEL,方法1为最基础的方法,意思就是效率可能不是太高,当遇到数据量过大时所要付出的时间也是巨大的,后面3种方法都是第一种的衍生,在第一种方法效率低下的基础上改进的,这里主要就是一个效率问题了,当然如果你数据量都很小,我想4种方法就代码量和复杂程度来说第1种基本方法就可以了,或当你的硬件非常牛逼了,那再差的方法也可以高效的完成也没有探讨的实际意义了,呵呵说远了,本文主要是在不考虑硬件或同等硬件条件下单从软件角度出发探讨较好的解决方案。
此项目代码已打包在附件中,几乎注释的无微不至,在配合本文中我唐僧般的娓娓道来(靠,谁丢臭鸡蛋砸俺了),相信即使刚入门C#的朋友也能看得懂了吧,大家可以自行下载,代码中演示了两种方法,上述的方法1和方法3,我想足够了,方法3的效率应该是四种中最高的了,其他两种有兴趣的朋友自己实现下哈(方法2在我的程序中也有用到一些,看完你就知道了),程序如下图所示,先加载一个EXCEL表格进DATAGRIDVIEW作为数据源,然后两种方法导出EXCEL,配上一个计数功能给大家直观的看到导出的耗时,大家可以准备个几万行和十多行的两个EXCEL作为数据源来测试,也可以用我打包里的XLS来测试下。
首先使用前都需要加载COM组件Microsoft.Office.Interop.Excel.dll(已打包)
关键代码如下:
方法1,最基本方法,用FOR循环逐条写入EXCEL的CELL中,其他的看代码就行了,关键代码如下
public void ToExcel1(DataGridView gridView, SaveFileDialog saveFileDialog)
{
...........................
for (int i = 0; i < gridView.RowCount; i++)
{
for (int j = 0; j < gridView.ColumnCount; j++)
{
if (gridView[j, i].Value == typeof(string))
{
excel.Cells[i + 2, j + 1] = "" + gridView[i, j].Value.ToString();
}
else
{
excel.Cells[i + 2, j + 1] = gridView[j, i].Value.ToString();
}
}
//进度条加1
progressBar1.Value++;
/*
* 注意此Application.DoEvents(),如果无此句,当切换窗口后回到本程序无法重绘窗体会出现假死状态
* 此处我试过用委托和线程异步调用的方法,但效果没有这句效果好
*/ System.Windows.Forms.Application.DoEvents();
...................
}
方法2,快速保存内存中大量数据到Excel的WorkSheet。关键之处是使用Range一次存储多行多列数据。
public void ToExcel2(DataGridView gridView, SaveFileDialog saveFileDialog)
{
....................
System.Reflection.Missing miss = System.Reflection.Missing.Value;
//创建EXCEL对象appExcel,Workbook对象,Worksheet对象,Range对象
Microsoft.Office.Interop.Excel.Application appExcel;
appExcel = new Microsoft.Office.Interop.Excel.Application();
Microsoft.Office.Interop.Excel.Workbook workbookData;
Microsoft.Office.Interop.Excel.Worksheet worksheetData;
Microsoft.Office.Interop.Excel.Range rangedata;
//设置对象不可见
appExcel.Visible = false;
/* 在调用Excel应用程序,或创建Excel工作簿之前,记着加上下面的两行代码
* 这是因为Excel有一个Bug,如果你的操作系统的环境不是英文的,而Excel就会在执行下面的代码时,报异常。
*/
System.Globalization.CultureInfo CurrentCI = System.Threading.Thread.CurrentThread.CurrentCulture; System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
workbookData = appExcel.Workbooks.Add(miss);
worksheetData =(Microsoft.Office.Interop.Excel.Worksheet)workbookData.Worksheets.Add(miss, miss, miss, miss);
//给工作表赋名称
worksheetData.Name = "saved";
//清零计数并开始计数
TimeP = new System.DateTime(0);
timer1.Start();
label1.Text = TimeP.ToString("HH:mm:ss");
// 保存到WorkSheet的表头,你应该看到,是一个Cell一个Cell的存储,这样效率特别低,解决的办法是,使用Rang,一块一块地存储到Excel
for (int i = 0; i < gridView.ColumnCount; i++)
{
worksheetData.Cells[1, i + 1] = gridView.Columns[i].HeaderText.ToString();
}
//先给Range对象一个范围为A2开始,Range对象可以给一个CELL的范围,也可以给例如A1到H10这样的范围
//因为第一行已经写了表头,所以所有数据都应该从A2开始
rangedata = worksheetData.get_Range("A2", miss);
Microsoft.Office.Interop.Excel.Range xlRang = null;
//iRowCount为实际行数,最大行
int iRowCount = gridView.RowCount;
int iParstedRow = 0, iCurrSize = 0;
//iEachSize为每次写行的数值,可以自己设置,每次写1000行和每次写2000行大家可以自己测试下效率
int iEachSize = 1000;
//iColumnAccount为实际列数,最大列数
int iColumnAccount = gridView.ColumnCount;
//在内存中声明一个iEachSize×iColumnAccount的数组,iEachSize是每次最大存储的行数,iColumnAccount就是存储的实际列数
object[,] objVal = new object[iEachSize, iColumnAccount];
try
{
//给进度条赋最大值为实际行数最大值
progressBar1.Maximum = gridView.RowCount;
iCurrSize = iEachSize;
while (iParstedRow < iRowCount)
{
if ((iRowCount - iParstedRow) < iEachSize)
iCurrSize = iRowCount - iParstedRow;
//用FOR循环给数组赋值
for (int i = 0; i < iCurrSize; i++)
{
for (int j = 0; j < iColumnAccount; j++)
objVal[i, j] = gridView[j, i + iParstedRow].Value.ToString();
progressBar1.Value++; System.Windows.Forms.Application.DoEvents();
}
/*
* 建议使用设置断点研究下哈
* 例如A1到H10的意思是从A到H,第一行到第十行
* 下句很关键,要保证获取Sheet中对应的Range范围
* 下句实际上是得到这样的一个代码语句xlRang = worksheetData.get_Range("A2","H100");
* 注意看实现的过程
* 'A' + iColumnAccount - 1这儿是获取你的最后列,A的数字码为65,大家可以仔细看下是不是得到最后列的字母
* iParstedRow + iCurrSize + 1获取最后行
* 若WHILE第一次循环的话这应该是A2,最后列字母+最后行数字
* iParstedRow + 2要注意,每次循环这个值不一样,他取决于你每次循环RANGE取了多大,循环了几次,也就是iEachSize设置值的大小哦
*/
xlRang = worksheetData.get_Range("A" + ((int)(iParstedRow + 2)).ToString(), ((char)('A' + iColumnAccount - 1)).ToString() + ((int)(iParstedRow + iCurrSize + 1)).ToString());
// 调用Range的Value2属性,把内存中的值赋给Excel
xlRang.Value2 = objVal;
iParstedRow = iParstedRow + iCurrSize;
}
//保存工作表
worksheetData.SaveAs(strName, miss, miss, miss, miss, miss, Microsoft.Office.Interop.Excel.XlSaveAsAccessMode.xlNoChange, miss, miss, miss); System.Runtime.InteropServices.Marshal.ReleaseComObject(xlRang);
xlRang = null;
progressBar1.Value = 0;
//调用方法关闭EXCEL进程,大家可以试下不用的话如果程序不关闭在进程里一直会有EXCEL.EXE这个进程并锁定你的EXCEL表格
this.KillSpecialExcel(appExcel);
timer1.Stop();
MessageBox.Show("数据已经成功导出到:" + saveFileDialog.FileName.ToString(), "导出完成", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
timer1.Stop();
return;
}
// 别忘了在结束程序之前恢复你的环境! System.Threading.Thread.CurrentThread.CurrentCulture = CurrentCI;
最后再顺便说下本程序中的一些部分:
1.做过EXCEL导出的朋友应该遇到过一个情况,当导出完毕后进程中那个讨厌的EXCEL.EXE老是无法关闭,造成锁定导出的XLS文件,要关闭程序后该进程才退出,网上也有一种方法就是我方法ToExcel1()用到的那个方法(详见下载的代码),但我写ToExcel2()方法的时候发现这种关闭的方法突然不好使了,这我用到了另一种方法KillSpecialExcel,调用的时候这样调用this.KillSpecialExcel(appExcel)大家可以试下把ToExcel2()改为ToExcel1()的关闭EXCEL方法看是否有效,代码如下
#region 结束EXCEL.EXE进程的方法
///
/// 结束EXCEL.EXE进程的方法
///
/// EXCEL对象
[DllImport("user32.dll", SetLastError = true)]
static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
public void KillSpecialExcel(Microsoft.Office.Interop.Excel.Application m_objExcel)
{
try
{
if (m_objExcel != null)
{
int lpdwProcessId;
GetWindowThreadProcessId(new IntPtr(m_objExcel.Hwnd), out lpdwProcessId); System.Diagnostics.Process.GetProcessById(lpdwProcessId).Kill();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
2.代码里写了个加载EXCEL到DATAGRIDVIEW的方法,就是想本来就是在写EXCEL操作相关嘛,干脆数据源也用EXCEL,该方法就是把EXCEL作为OLEDB数据源这样来操作的,你是不是想到前面我所提到的四种导出EXCEL中的《方法2通过OLEDB把EXCEL做为数据源来写》呢,对了,有兴趣你可以尝试修改下通过OLEDB这样来导出EXCEL,当然,新手朋友可以看下如何加载EXCEL数据,你应该希望去尝试接着如何把这些数据储存到数据库之类的操作
3.System.Windows.Forms.Application.DoEvents(),当运行 Windows 窗体时,它将创建新窗体,然后该窗体等待处理事件。该窗体在每次处理事件时,均将处理与该事件关联的所有代码。所有其他事件在队列中等待。在代码处理事件时,应用程序并不响应。例如,当将另一窗口拖到该窗口前面时,该窗口不重新绘制。如果在代码中调用 DoEvents,则您的应用程序可以处理其他事件。例如,如果您有向 添加数据的窗体,并将 DoEvents 添加到代码中,那么当将另一窗口拖到您的窗体上时,该窗体将重新绘制。如果从代码中移除 DoEvents,那么在按钮的单击事件处理程序执行结束以前,您的窗体不会重新绘制。
这里说远一点,关于如何防止UI假死其实还有更科学的方法,就是使用委托加线程异步执行,把耗时的操作另起进程执行,从而减轻UI的压力,使得前台UI流畅运行而不假死,多用this.Invoke等,但在我程序用我也测试过反而直接DoEvents效果还好的多,而且线程和委托可能对新手朋友稍微复杂了些,推荐一般应用用DoEvents足够了哦。