Click here to Skip to main content
15,887,337 members
Articles / Desktop Programming / MFC
Article

Saving Excel 2.1 Workbook

Rate me:
Please Sign up or sign in to vote.
4.93/5 (56 votes)
12 Sep 20023 min read 411.4K   7.7K   112   129
Classes used to save data in Excel 2.1 Workbook format

Sample Image

Introduction

Recently I worked on a project that required exporting some reports containing various file statistics. The goal was having one format that would look good immediately and one easy accessible format for the user to import and build custom reports. For the first export type, plain HTML was used. But for importing, HTML is not really easy to use, so Excel file creation seemed a good choice.

Requirements

Since the Excel exported file is just for the user to have easy access to data, only a small part of the Excel file format features were needed. Also, there was no guarantee that Excel would even be installed on the computer where the reports are generated and since a port to Linux was imminent, it would be nice for the class to be platform independent. The goal was to be able to write something as this:

CMiniExcel miniexcel;

miniexcel(0,0) = "FileName";
miniexcel(0,1) = "Size (MB)";

miniexcel(1,0) = "c:\\bigfile.txt";
miniexcel(1,1) = 123.45;

miniexcel.SaveAs("Report.xls")

There are no real memory constraints, since Excel cannot handle over ~65K rows anyway. (Actually we also needed a single big report with more that 65K rows, and we had to save it as a "Comma separated values" file). So the whole excel worksheet could be kept in memory.

Excel file format

After a little bit of searching on the net, I decided on the Excel 2.1 Workbook format (description of the format found here). Even though this format is old, it can be imported by Excel or by other programs and it seemed suitable for our simple needs.

From the documentation of the Excel 2.1 format it turns out that an excel document is stored using a format called Binary File Format (BIFF). This format is a sequence of BIFF records, each record containing a certain property/value of the workgroup. Each BIFF record starts with a 2 byte number containing it's type and another 2 bytes for the size of the record. After this header, record-specific data will follow:

XX XX

 XX XX

 ....

Type  Length  Specific Data

From the several record types needed, only a few were really important for simple excel exporting. BOF and EOF records were unavoidable and also text strings (LABEL record) and numbers (NUMBER record) needed to be saved.

Implementation

First of all, to take care of the platform independence and also to make life easier when saving the BIFF records, I implemented a writer class (CLittleEndianWriter) that should be able to write 1 byte, 2 byte and 4 byte values in little endian byte order. Also, it needed to be able to save double values in IEEE format, since this is the format used to store real numbers (excel has a BIFF record fr storing a 16 bit unsigned integer, but it is not enough):

class LittleEndianWriter{
  ...
public:
  ...
  void Write1 (char v);
  void Write2 (int v);
  void Write4 (long v);
  void WriteFloatIEEE (float v);
  void WriteDoubleIEEE (double v); 
};

After this, it also seems pretty clear that since every single record is saved in a biff file format, it would be nice have a abstract BIFFRecord class and that every single record should be derived from it.

class BIFFRecord{
  ...

  /* Write a BIFF header for the opcode nRecno of length nRecLen */
  void Write (LittleEndianWriter *pWriter, int nRecNo, int nRecLen);

public:

  /* We should be able to write every type of BIFF records */
  virtual void Write (LittleEndianWriter *pWriter) = 0; 
}; 

This being done, implementing excel file saving is only adding the required BIFF records. I added in this demo project BOF, EOF, NUMBER and LABEL (others should be easy to add). We also need a generic container class, so that Excel file creation is easy. The container class is needed because BIFF records have to be in a specific order in the XLS file and it's nice if the container class does this for us:

class CMiniExcel{
  ...
public:
  ... 
  /* Access the columns in the excel document */
  ExcelCell &operator() (unsigned row, unsigned column);

  /* Write into a file. */
  void Write (FILE *dest);
};

Note that the operator () returns an ExcelCell. This is just a simple container for either numbers or strings and it makes like a lot easier when coding by not having to worry about cell types in the Excel Workbook.

Final notes

This is by no means a complete project. The saving is basic and error checking is almost none existing. Adding other BIFF records to the generic format should be pretty easy and if somebody finds it useful, I might add a few more into this demo source code.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRe: error C1010 Pin
tobberik6-Apr-04 2:27
tobberik6-Apr-04 2:27 
GeneralRe: error C1010 Pin
mirr200111-May-04 9:06
mirr200111-May-04 9:06 
GeneralRe: error C1010 Pin
Doug Joseph20-May-05 5:13
Doug Joseph20-May-05 5:13 
Question.net version? Pin
Huisheng Chen1-Apr-04 1:26
Huisheng Chen1-Apr-04 1:26 
AnswerRe: .net version? Pin
Colin Angus Mackay1-Apr-04 1:38
Colin Angus Mackay1-Apr-04 1:38 
GeneralSome errors and info Pin
knn4-Mar-04 4:47
knn4-Mar-04 4:47 
Questionchange font style and colors in spreadsheet? Pin
xxhimanshu23-Feb-04 19:14
xxhimanshu23-Feb-04 19:14 
Generalreading excel records-> problem Pin
ksnehal20-Feb-04 9:39
ksnehal20-Feb-04 9:39 
Dear all, (i am using C program to read the excel file)

I open a xls file in read binary "rb" mode. then I exclude first 512 bytes adn then reach to the BOF record. Now after I find out the file version and type I read the next record " OPCODE (2bytes) LENGTH (2 bytes) DATA (as specified in length). and I keep checking every opcode. I want to be able to print the page Header "this header appears when you print the excel pages"
now the OPCODE for HEADER record is 14h and FOOTER is 15h

So i check that !! But unfortunately the correct record does not get pulled.
Can anyone tell why?

following is my code
int get1byte(FILE * fp)
{
int i1 = 0;
if (!feof(fp))
i1 = fgetc(fp);
return i1;
}
int get2bytes (FILE *fp)
{
int i1,i2,finalval;
i1 = get1byte(fp);
i2 = get1byte(fp);
finalval = combint2byte(i1,i2);
return finalval;
}

getrecheader(int *opcode,int *length, FILE *fp)
{
*opcode = get2bytes(fp);
if (*opcode == 0x14)
printf("here");
*length = get2bytes(fp);
}
skiprecord(FILE *fp,int length)
{
fseek(fp,length,SEEK_CUR);
}

int
combint2byte(int int1, int int2) {
int b1 = int1 & 0xff;
int b2 = int2 & 0xff;
int val = b2 << 8 | b1;
return val;
}

char *
readdata(FILE *fp, int length) {
int i;
char *data;
data = malloc(length);
// printf("position: %d going to: %d\n",book->position, pos);
for (i = 0; i < length; i++) {
data[i] = fgetc(fp);
}

return data;
}
main()
{
char FileName[100];

int opcode,ver;
FILE *fp;
char *str;
//unsigned short s;
int length,type,unused,buildyear,buildid;

strcpy(FileName,"c:\\my_code\\exceltest\\test1.xls");
printf("%s\n",FileName);
//get header information
fp =fopen(FileName,"rb");
fseek(fp,512,SEEK_SET); //skip the first 512 bytes of a file.
//BOF
//fread(&s,2,2,fp);
opcode = get2bytes(fp);

switch(opcode)
{
case 0x09: printf( "\nBIFF2\n");
length = get2bytes(fp);
ver = get2bytes(fp);
printf("\nversion = %d",ver);
type = get2bytes(fp);
switch(type)
{
case 0x10 : printf("\nworksheet");
break;
case 0x20 : printf("\nchart");
break;
case 0x40 : printf("\nmacro sheet");
break;
}
break;
case 0x209: printf("\nbiff3");
length = get2bytes(fp);
ver = get2bytes(fp);
printf("\nversion = %d",ver);
type = get2bytes(fp);
switch(type)
{
case 0x10 : printf("\nworksheet");
break;
case 0x20 : printf("\nchart");
break;
case 0x40 : printf("\nmacro sheet");
break;
}
unused = get2bytes(fp);
break;
case 0x409: printf("\nbiff4");
length = get2bytes(fp);
ver = get2bytes(fp);
printf("\nversion = %d",ver);
type = get2bytes(fp);
switch(type)
{
case 0x10 : printf("\nworksheet");
break;
case 0x20 : printf("\nchart");
break;
case 0x40 : printf("\nmacro sheet");
break;
case 0x100: printf("\nWorkbook globals");
break;
}
unused = get2bytes(fp);
break;
case 0x809 : printf (" \nbiff5 - biff8");
length = get2bytes(fp);
printf("\n length = %d",length);
ver = get2bytes(fp);
printf("\nversion = %d",ver);
if(ver == 0x0500)
{
//BIFF5-BIFF7
printf("\nbiff5 - biff7");
type = get2bytes(fp);
switch(type)
{
case 0x05 : printf("\nworkbook global");
break;
case 0x06 : printf("\nVisual basic module");
break;
case 0x10 : printf("\nworksheet");
break;
case 0x20 : printf("\nchart");
break;
case 0x40 : printf("\nmacro sheet");
break;
case 0x100: printf("\nWorkbook globals");
break;
}
buildid = get2bytes(fp);
printf("\nbuild identifier %d",buildid);
buildyear = get2bytes(fp);
printf("\nbuild year %d",buildyear);
}//end of if
if(ver == 0x0600)
{
//BIFF5-BIFF7
printf("\nbiff8");
type = get2bytes(fp);
switch(type)
{
case 0x05 : printf("\nworkbook global");
break;
case 0x06 : printf("\nVisual basic module");
break;
case 0x10 : printf("\nworksheet");
break;
case 0x20 : printf("\nchart");
break;
case 0x40 : printf("\nmacro sheet");
break;
case 0x100: printf("\nWorkbook globals");
break;
}
buildid = get2bytes(fp);
printf("\nbuild identifier %d",buildid);
buildyear = get2bytes(fp);
printf("\nbuild year %d",buildyear);
fseek(fp,8,SEEK_CUR); //filehistory flag(4bytes) lowest excel
//version that can read all records
//in this file (4bytes)


}//end of if

break;
default: printf("\ncannot recognize file ");
break;
}
//try fetching records until you find header record.
while(!feof(fp))
{
getrecheader(&opcode,&length,fp);
if(opcode == 0x14)
{
printf("\nFound header record");
unused = get1byte(fp);
unused = get1byte(fp);
unused = get1byte(fp);
unused = get1byte(fp);
str = readdata(fp,length);
printf("This is the header = %s",str);
break;
}
else
{
skiprecord(fp,length);
}
}

fclose(fp);
}

thanks a lot
snehal
snehal@asu.edu
GeneralExample on adding a formular Pin
cpede22-Sep-03 4:51
cpede22-Sep-03 4:51 
GeneralRe: Example on adding a formular Pin
Pilda26-Feb-05 6:16
Pilda26-Feb-05 6:16 
GeneralRussian language Pin
Wax2-Sep-03 6:38
Wax2-Sep-03 6:38 
GeneralRe: Russian language Pin
Fad B17-Dec-03 8:09
Fad B17-Dec-03 8:09 
GeneralSetting Column Width Pin
ks127-Aug-03 8:27
ks127-Aug-03 8:27 
QuestionHow do I get Textbox from Shapes Pin
Jason Truong7-Aug-03 4:05
Jason Truong7-Aug-03 4:05 
QuestionHow to Insert image ?? Pin
bluesora6-Aug-03 16:21
bluesora6-Aug-03 16:21 
Generalmore info on the BIFF file format Pin
Anonymous28-Jul-03 12:18
Anonymous28-Jul-03 12:18 
GeneralPocket Excel using eVC++ Pin
Mahesh Varma10-Jul-03 2:00
Mahesh Varma10-Jul-03 2:00 
GeneralComparison of Cells Pin
.gandalf9-Jul-03 4:05
.gandalf9-Jul-03 4:05 
GeneralConflict with ofstream Pin
jjtop29-Jun-03 23:26
jjtop29-Jun-03 23:26 
GeneralRe: Conflict with ofstream Pin
Andrei Litvin3-Jul-03 1:29
Andrei Litvin3-Jul-03 1:29 
GeneralRe: Conflict with ofstream Pin
jjtop3-Jul-03 3:13
jjtop3-Jul-03 3:13 
Generalprinting Pin
Anonymous19-Jun-03 10:18
Anonymous19-Jun-03 10:18 
GeneralRe: printing Pin
Andrei Litvin22-Jun-03 15:47
Andrei Litvin22-Jun-03 15:47 
Generalwidth font Pin
Member 1270530-May-03 5:00
Member 1270530-May-03 5:00 
GeneralAdding a second work sheet Pin
Sri Banda13-May-03 7:52
Sri Banda13-May-03 7:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.