有些时候我们希望将输出的数据存到EXCEL文件或一些符号分隔文本文件(像逗号分隔、Tab符分隔)。CSpreadSheet类封装了对这些文件的读写操作。CSpreadSheet类的目标是简单易用,其主要的特性如下:
1、构造一个新的EXCEL文件或符号分隔文本文件,并在其中写如行或单个的单元。
2、读取行、列或单个单元。
3、覆盖、插入或追加行。
4、将已存在或新的EXCEL文件转化成符号分隔文本文件;反之亦然。
在阐述本类的个类函数之前,我们先看看本类的一些限制条件:
1、需要MFC
2、可能或不能支持unicode。(没测试过)
3、使用ODBC读写EXCEL文件,因此存在ODBC驱动的限制(我不知道ODBC有哪些限制)。
4、必须在EXCEL文件中有头行,并且头行中的各列的内容是各不相同的(因为本类将EXCEL文件看作一个数据库)。
5、不能删除EXCEL文件的表(sheet),只能删除表中的内容。
6、各单元的内容被看作字符串,不管它在EXCEL文件中是什么类型。
我已经插入了一些查错代码,但是可能我漏掉了一些。因此使用本类时,用户应注意这些限制条件,以防止以外错误。
下面是一些类的详细的描述,分为两个部分:
第一个部分是为了那些只想知道怎么去使用这个类的人而写。它包含了一些有用的函数的简短描述和怎样去使用的信息。之后会有一个例子说明这个类的大部分的函数。
第二个部分是为了那些想了解这个类的所有函数和变量的细节的人而写。这个部分会阐述函数怎样工作,限制条件是什么,以及可能发生的错误;这些函数如何经过大量的修正而得到最后的代码。这会对那些希望修改这个类以适应他的需求的人有一些帮助。
好,现在准备好看长篇大论了吗?
Part 1. 如何使用CSpreadSheet
下面是CSpreadSheet类的一些有用的函数,根据是否用于EXCEL和符号分隔文本或者用于两者之一来组织。
公共函数:
CSpreadSheet(CString File, CString SheetOrSeparator, bool Backup = true)
bool AddHeaders(CStringArray &FieldNames, bool replace = false)
bool AddRow(CStringArray &RowValues, long row = 0, bool replace = false)
bool AddCell(CString CellValue, short column, long row = 0)
bool AddCell(CString CellValue, CString column, long row = 0, bool Auto = true)
bool ReadRow(CStringArray &RowValues, long row = 0)
bool ReadColumn(CStringArray &ColumnValues, short column)
bool ReadColumn(CStringArray &ColumnValues, CString column, bool Auto = true)
bool ReadCell (CString &CellValue, short column, long row = 0)
bool ReadCell (CString &CellValue, CString column, long row = 0, bool Auto = true)
bool DeleteSheet()
bool DeleteSheet(CString SheetName)
bool Convert(CString SheetOrSeparator)
void BeginTransaction()
bool Commit()
bool RollBack()
bool GetTransactionStatus()
void GetFieldNames (CStringArray &FieldNames)
long GetTotalRows()
short GetTotalColumns()
long GetCurrentRow()
bool GetBackupStatus()
CString GetLastError()
EXCEL专用函数
bool ReplaceRows(CStringArray &NewRowValues, CStringArray &OldRowValues)
符号分隔文本文件专用函数
无
公共函数:
CSpreadSheet(CString File, CString SheetOrSeparator, bool Backup = true)
本构造函数会为了读取或写入(新建或已经存在的文件)打开一个EXCELspreadsheet或一个符号分隔文本spreadsheet(任何符号分隔,例如逗号分隔文件csv、tab等)。当是EXCELspreadsheet时,变量SheetOrSeparator是文件名;而当是符号分隔文本spreadsheet时,SheetOrSeparator是分隔符的类型。缺省情况下,构造函数会建立一个备份文件(符号分隔文本文件是.bak;EXCEL文件是名为CSpreadSheetBackup的文件)。
bool AddHeaders(CStringArray &FieldNames, bool replace = false)
本函数会添加头行到一个已经打开的文件。对于EXCELspreadsheet,头行中每一列的内容必须是唯一的。对于符号分隔spreadsheet,没有这个限制。如果是一个已经打开的spreadsheet,缺省情况下是在头行中加入新的列,而不是覆盖原有的列,除非你将raplace置为true。返回的值表明是否成功。注意:对于一个新的EXCELspreadsheet,这个函数必须在加入任何新行之前调用。对于符号分隔spreadsheet,本函数是可选的。
bool AddRow(CStringArray &RowValues, long row = 0, bool replace = false)
此函数会在打开的spreadsheet中添加、插入或覆盖一行。缺省是添加一行到spreadsheet的末尾。取决于replace的值,新的一行将插入到或覆盖指定的行。返回的值表明是否执行成功。注意:row=1指的是第一行,即头行。
bool AddCell(CString CellValue, short column, long row = 0)
bool AddCell(CString CellValue, CString column, long row = 0, bool Auto = true)
第一个AddCell函数在打开的spreadsheet中添加或覆盖一个单元。缺省情况下是在指定列的末尾添加单元数据。
第二个AddCell函数类似,只不过column是EXCEL列名(例如A、B、AE、EF)或头行的域名。如果你不想函数自动检测column变量是列名或头行的域名,将Atuo置为false。
返回的值表明是否执行成功。注意:row=1指的是第一行,即头行。
bool ReadRow(CStringArray &RowValues, long row = 0)
本函数从打开的spreadsheet中读取一行。缺省是读取下一行。即如果你连续调用本函数或执行一些其它语句后在调用本函数,第一次会读取第一行,而第二次会读取第二行。
返回的值表明是否正确读取,还是没有行可读。注意:row=1指的是第一行,即头行。
bool ReadColumn(CStringArray &ColumnValues, short column)
bool ReadColumn(CStringArray &ColumnValues, CString column, bool Auto = true)
第一个函数会在打开的spreadsheet中读取一列。变量column是列号。
第二个函数类似,只不过变量column是一个Excel列名(例如A、B、AE、EF)或一个头行的域名。
返回的值表明是否正确读取
bool ReadCell (CString &CellValue, short column, long row = 0)
bool ReadCell (CString &CellValue, CString column, long row = 0, bool Auto = true)
第一个函数从打开的spreadsheet中读取单元数据。缺省情况下读取下一行的单元(类似于ReadRow)。
第二个函数类似,只不过变量column是一个Excel列名(例如A、B、AE、EF)或一个头行的域名。
返回的值表明是否正确读取。
bool DeleteSheet()
bool DeleteSheet(CString SheetName)
第一个函数会删除打开的spreadsheet中的所有内容。
第二个函数会删除一个打开的Excel spreadsheet中的特定的表。
返回值表明删除是否成功。注意:这两个函数没有RollBack支持。
bool Convert(CString SheetOrSeparator)
本函数会转换一个EXCEL表到一个符号分割文本,反之亦然。如果从文本文件转换到EXCEL文件,变量separator(?译者注:原文如此)没有使用。返回值表明转换是否成功。
void BeginTransaction()
bool Commit()
bool RollBack()
以上三个函数类似于SQL中相对应的函数。使用BeginTransaction表明一个事务的开始,使用Commit保存所做的修改,使用RollBack撤消修改(即返回到BeginTransaction之前的状态)。Commit和RollBack的返回值分别表明保存和撤消是否成功。
bool GetTransactionStatus()
本函数返回Transaction的状态。如果Transaction已经开始,返回true;如果Transaction还未开始或已经结束返回false。
void GetFieldNames (CStringArray &FieldNames)
本函数会读取spreadsheet的头行。
long GetTotalRows()
本函数返回spreadsheet的总行数。
short GetTotalColumns()
对于EXCEL spreadsheet,本函数返回列的总数;对于符号分割spreadsheet,返回最大的列数。
long GetCurrentRow()
本函数返回spreadsheet中当前选择的行。当前选择行即是被ReadRow或ReadCell函数缺省情况下读取的行。
bool GetBackupStatus()
本函数返回spreadsheet的备份状态。如果执行了备份,返回true;否则返回false(用户的选择或发生了错误)。
CString GetLastError()
本函数会返回最后的错误信息。像一些函数如AddHeaders, AddRow, AddCell 等等,可能会发生一些错误。本函数会返回描述发生的错误信息。
EXCEL专用函数
bool ReplaceRows(CStringArray &NewRowValues, CStringArray &OldRowValues)
本函数会查找和替换新行和旧行。本函数不支持RollBack,因此不能撤消操作。返回值表明操作是否成功。
示例:
查看源代码拷贝至剪贴板打印代码
// 建立一个Excel spreadsheet,文件名test.xls,
// 工作表名TestSheet
CSpreadSheet SS("Test.xls", "TestSheet");
// 加入5*5的表
CStringArray sampleArray, testRow, Rows, Column;
CString tempString;
char alphabet = 'A';
SS.BeginTransaction();
for (int i = 1; i <= 5; i++)
{
sampleArray.RemoveAll();
for (int j = 1; j <= 5; j++)
{
tempString.Format("%c%d", alphabet++, i);
sampleArray.Add(tempString);
}
alphabet = 'A';
if (i == 1) // 加入头行
{
SS.AddHeaders(sampleArray);
}
else // 加入数据行
{
SS.AddRow(sampleArray);
}
}
// 建立一个测试用的行,以供添加、插入和覆盖
for (int k = 1; k <= 5; k++)
{
testRow.Add("Test");
}
SS.AddRow(testRow); // 添加测试行
SS.AddRow(testRow, 2); // 插入测试行到第二行
SS.AddRow(testRow, 4, true); // 用测试行覆盖第四行
SS.Committ();
SS.Convert(";"); // 将EXCEL spreadsheet 转化成以';'作为分割符的文本文件
// 输出总行数
printf("Total number of rows = %d\n\n", SS.GetTotalRows());
// 输出spreadsheet的全部
for (i = 1; i <= SS.GetTotalRows(); i++)
{
// Read row
SS.ReadRow(Rows, i);
for (int j = 1; j <= Rows.GetSize(); j++)
{
if (j != Rows.GetSize())
{
printf("%s\t", Rows.GetAt(j-1));
}
else
{
printf("%s\n", Rows.GetAt(j-1));
}
}
}
// 输出总列数
printf("\nTotal number of columns = %d\n\n", SS.GetTotalColumns());
// 读取并输出第二列
SS.ReadColumn(Column, 2);
for (i = 0; i < Column.GetSize(); i++)
{
printf("Column 2 row %d: %s\n", i+1, Column.GetAt(i));
}
// 读取和输出第三行第三列的单元
if (SS.ReadCell(tempString, 3, 3))
{
printf("\nCell value at (3,3): %s\n", tempString);
}
else
{
// 如不能读取输出错误信息
printf("Error: %s\n", SS.GetLastError);
}
// 建立一个Excel spreadsheet,文件名test.xls,
// 工作表名TestSheet
CSpreadSheet SS("Test.xls", "TestSheet");
// 加入5*5的表
CStringArray sampleArray, testRow, Rows, Column;
CString tempString;
char alphabet = 'A';
SS.BeginTransaction();
for (int i = 1; i <= 5; i++)
{
sampleArray.RemoveAll();
for (int j = 1; j <= 5; j++)
{
tempString.Format("%c%d", alphabet++, i);
sampleArray.Add(tempString);
}
alphabet = 'A';
if (i == 1) // 加入头行
{
SS.AddHeaders(sampleArray);
}
else // 加入数据行
{
SS.AddRow(sampleArray);
}
}
// 建立一个测试用的行,以供添加、插入和覆盖
for (int k = 1; k <= 5; k++)
{
testRow.Add("Test");
}
SS.AddRow(testRow); // 添加测试行
SS.AddRow(testRow, 2); // 插入测试行到第二行
SS.AddRow(testRow, 4, true); // 用测试行覆盖第四行
SS.Committ();
SS.Convert(";"); // 将EXCEL spreadsheet 转化成以';'作为分割符的文本文件
// 输出总行数
printf("Total number of rows = %d\n\n", SS.GetTotalRows());
// 输出spreadsheet的全部
for (i = 1; i <= SS.GetTotalRows(); i++)
{
// Read row
SS.ReadRow(Rows, i);
for (int j = 1; j <= Rows.GetSize(); j++)
{
if (j != Rows.GetSize())
{
printf("%s\t", Rows.GetAt(j-1));
}
else
{
printf("%s\n", Rows.GetAt(j-1));
}
}
}
// 输出总列数
printf("\nTotal number of columns = %d\n\n", SS.GetTotalColumns());
// 读取并输出第二列
SS.ReadColumn(Column, 2);
for (i = 0; i < Column.GetSize(); i++)
{
printf("Column 2 row %d: %s\n", i+1, Column.GetAt(i));
}
// 读取和输出第三行第三列的单元
if (SS.ReadCell(tempString, 3, 3))
{
printf("\nCell value at (3,3): %s\n", tempString);
}
else
{
// 如不能读取输出错误信息
printf("Error: %s\n", SS.GetLastError);
}Part 2. 对函数和变量的详细描述
CSpreadSheet(CString File, CString SheetOrSeparator, bool Backup = true)
程序首先检测文件名的后缀以决定文件是EXCEL文件(.xls)还是符号分割文件(任何别的后缀)。如果是EXCEL文件,则将m_bExcel置位true;否则 m_bExcel为false。文件名保存在m_sFile中。接着构造函数根据情况将SheetOrSeparator赋予m_sSheetName或m_sSeparator。
然后用Open函数打开文件。用另外的函数打开文件的原因是该函数还会被RollBack函数使用。成功打开文件后,行和列的总数会被存在m_dTotalRows和m_dTotalColumns。头行也会被存在m_aFieldNames中。当前行(存在m_dCurrentRow)被置为1,即头行。所有的行被存在内存中(m_aRows)。将行存在内存中允许实现一个撤消操作。接着,如果需要执行备份操作,并保存备份状态(m_bBackup)。
因为使用ODBC驱动打开EXCEL文件,因此EXCEL文件被看作数据库。因为EXCEL ODBC驱动的限制,只能从文件中读取记录集,而不能将记录集写入文件中,因此使用SQL语句执行写操作。即使这样,也只有部分子集的SQL能够使用。因此写入EXCEL文件可能繁重而慢。
bool AddHeaders(CStringArray &FieldNames, bool replace = false)
加入、覆盖、添加到一个符号分割spreadsheet直接而简单。但是对于EXCEL spreadsheet来说,就不能这样说了。这是因为EXCEL spreadsheet使用ODBC打开,第一行被ODBC看作存贮了数据库的域名,因此头行中的每一列的内容必须是唯一的。因此,当增加一个头行到一个Excel spreadsheet 中,函数必须首先判断头行的各列的内容是否唯一。这各工作被转到AddRow中处理。
bool AddRow(CStringArray &RowValues, long row = 0, bool replace = false)
bool AddCell(CString CellValue, short column, long row = 0)
bool AddCell(CString CellValue, CString column, long row = 0, bool Auto = true)
以上三个函数的实现十分接近,除了AddCell不支持插入操作。对单元的插入操作(向下插入还是向右插入呢?)太繁重了,又很少使用(至少对我来说)。如果谁有兴趣,欢迎修改本函数。我不会赶这淌浑水。
函数了流程是这样的:首先,函数检查用户是否指定行号。如果没有指定行号,新的行或单元会添加到spreadsheet的末尾。如果指定行号,程序检查该行号是否大于总行数。如果大于,添加空行直到指定行号之前,然后添加新行到spreadsheet的末尾。如果小于等于,程序检查是插入或覆盖。对于AddCell新单元会覆盖原来的数据。
对于EXCEL spreadsheet,还要检查另外一些东西。首先,如果指定行是头行,必须检查头行各列的内容是否重复。其次,检查头行列数是否减少,因为如果头行列数减少的话,将影响到EXCEL spreadsheet的完整性(不要忘记了EXCEL文件已经被ODBC看成了数据库)。如果指定的行不是头行,程序必须检查是否已经有了一个头行。然后,程序还要检查以保证新行的列数不大于头行的列数。
AddCell函数有两个重载版本,以使用户可以使用不同的方法指定列。可以用域名、EXCEL工作表中的列字母编号或从1开始的列号来指定列。当变量column是一个字符串,程序会自动检测column是域名还是列字母编号,检测的方法是根据字符串的长度,如果字符串的长度为1或2,则认为是列字母编号,否则认为是域名。对于列字母编号,程序调用CalculateColumnNumber获得正确的列号。对于域名,程序用它去匹配头行中的每一个列去获取正确的列号。变量Auto是用来处理域名长度为1或2的情况的。当Auto为false时,程序把conlumn当成域名。
bool ReadRow(CStringArray &RowValues, long row = 0)
bool ReadColumn(CStringArray &ColumnValues, short column)
bool ReadColumn(CStringArray &ColumnValues, CString column, bool Auto = true)
bool ReadCell (CString &CellValue, short column, long row = 0)
bool ReadCell (CString &CellValue, CString column, long row = 0, bool Auto = true)
这5个函数的实现相对简单。ReadRow是5个函数中最重要的一个。它被AddRow和AddCell调用。当是符号分割文本文件时,它还被ReadColumn和ReadCell调用。
ReadRow首先必须将每行分成各个列。使用分割符(m_sSeparator)分割行。对于EXCEL spreadsheet,分割符是",;.?"。程序首先试图分割基于假设文件有这样的语法:"行1"分割符"行2"分割符"行3"。举例来说,对于CSV文件:"column1","column2","column3"。如果程序用这样的方法分割行失败的话,它会试图基于这样的语法:行1分割符行2分割符行3。例如,对于CSV文件:column1,column2,column3。以上两种语法之外的文件是不支持的。如果符号分割文本文件属于第一种语法,程序会删除第一列和最后一列的双引号,这一步是必须的,是因为实现分割的方法。对于EXCEL spreadsheet,使用第一种语法存储在内存中。
ReadColumn和ReadCell函数依赖于ReadRow。对于ReadCell,它调用ReadRow去获取所需的单元的行,然后得到单元的数据。对于ReadColumn,它多次调用ReadRow去获取所需的列。
ReadColumn及ReadCell和AddCell的重载函数一样,ReadColumn及ReadCell对于column的使用是一样的。
bool ReplaceRows(CStringArray &NewRowValues, CStringArray &OldRowValues)
此函数使用SQL语句:UPDATE...SET...WHERE,在EXCEL spreadsheet中查找和替换行。因为使用SQL,因此本函数不使用于符号分割文本文件;另外,本函数只工作在磁盘文件中,对内存中的拷贝是不产生作用的,因此不能撤消操作。
bool DeleteSheet()
bool DeleteSheet(CString SheetName)
这两个重载函数对于符号分割文本文件的操作是直接的。基本上,它删除内存拷贝的行和域名。它还重置行和列的总数为0。对于EXCEL spreadsheet来说,它使用SQL语句:DROP TABLE。因此,删除操作不能撤消,因为它对磁盘文件直接操作。另外,由于ODBC驱动的限制,表的本身是不会被删除的,只是里面的内容被删除了。
bool Convert(CString SheetOrSeparator)
从EXCEL spreadsheet转化到符号分割文本文件十分简单。本函数可以用做批处理转换EXCEL spreadsheet到符号分割文件,反之亦然。然而,当你将符号分割文件转化到EXCEL的时候,记得头行各列内容不能相同的限制。
void BeginTransaction()
bool Commit()
bool RollBack()
这3个函数很有用,因为它们可使修改撤消。如果你添加大量行或单元,使用BeginTransaction有个好处。不使用BeginTransaction的情况下,每个添加操作都存到磁盘;但如果使用了BeginTransaction,直到Commit被调用,才会保存修改到磁盘。因此在添加大量行或单元时,使用BeginTransaction会大大提高速度。
对于符号分割文件,Commit的实现简单直接。对于EXCEL spreadsheet,实现Commit需要一些技巧。正如前面提到的:写入EXCEL文件需要用到SQL语句。我们应该先搞清楚工作表是否已经存在还是一个新表。两种情况都需要通过不同的途径使用CREATE TABLE语句。对于一个新的表,我们使用CREATE TABLE 表名。对于已存在的表,我们必须首先删除表。然而正如前面提到,删除EXCEL spreadsheet只不过是删除表的内容,实际上表并没有被删除。这样的后果是使用CREATE TABLE 表名时,ODBC驱动将返回一个表已存在的异常。然而,当你使用INSERT INTO语句插入行到表种,它也返回错误。解决此问题的方法是使用INSERT INTO语句之前,使用CREATE TABLE [表名$A1:IV65535]。我发现的另外一件事是INSERT INTO语句也不是那么简单。为了插入一行到你的表中,你必须使用INSERT INTO [sheetname$A1:IVx],x是随着表的行数不断增长的数。
bool Open()
本函数被构造函数和RollBack调用。本函数基本上是打开工作表,读出数据到内存,然后关闭文件。本函数不能被用户直接调用。
void GetExcelDriver()
本函数获取EXCEL-ODBC驱动的名称。它被构造函数调用,不能被用户直接调用。
short CalculateColumnNumber(CString column, bool Auto)
本函数转换EXCEL列字母编号或域名到列号。被AddCell、ReadCell和ReadColumn调用,不能被用户直接调用。
变量:
bool m_bAppend; // 表明是新的表还是以前建的表的标志
bool m_bBackup; // 表明备份状态的标志
bool m_bExcel; // 表明是否EXCEL文件的标志
bool m_bTransaction; // 事务状态标志
long m_dCurrentRow; // 当前行,从1开始
long m_dTotalRows; // 表的总行数
short m_dTotalColumns; // EXCEL表中的总列数;符号分割文本文件的最大列数
CString m_sSql; // 读取(reading)EXCEL表的SQL语句
CString m_sDsn; // 为读写打开EXCEL表的DSN语句
CString m_stempSql; // 临时的SQL语句
CString m_stempString; // 临时的函数用语句
CString m_sSheetName; // EXCEL表名
CString m_sExcelDriver; // EXCEL-ODBC驱动名
CString m_sFile; // 文件名
CString m_sSeparator; // 分割符
CString m_sLastError; // 最后发生的错误信息
CStringArray m_atempArray; // 临时字符串数组
CStringArray m_aFieldNames; // 表的头行
CStringArray m_aRows; // 表的所有行的内容
CDatabase *m_Database; // EXCEL表的数据库变量
CRecordset *m_rSheet; // EXCEL表的记录集
历史
当我开始写这个类,我先实现了读取EXCEL表。然而,不久发现写入EXCEL文件是不是那么好实现。因此我修改了程序,使它能读写EXCEL表。在SQL语句方面有些障碍。我花了很多时间去调试,才找到正确的SQL语句。接着,我加入处理符号分割文本文件的代码。一开始好象一切都好,我已经准备发表我的类了。然而,我越仔细考虑它,越感觉到有东西是错误的。在那个版本中,BeginTransaction, Commit 和 Rollback 函数只能使用在符号分割文本文件中。因为在我实现EXCEL的方法中,任何的修改都直接写到磁盘中。这样使得不能实现撤消修改的操作,在大量读写操作的时候,性能也不好。因此我决定修补EXCEL部分,并最终实现了类似于符号分割文本文件的功能,使BeginTransaction, Commit 和 Rollback函数能够使用。然而,我必须承认到这时候我已经精疲力尽了。因此我没有像第一版那样测试所有的函数。因此可能会有Bug。我欢迎任何改进这个类的建议或者Bug报告。最后,我希望任何人,如果修改了这个类,发布出来,让所有的人都从中受益。