nop has asked for the wisdom of the Perl Monks concerning the following question:

I often use perl to write to tab-delimited files named with ".xls" extensions. Under Windows, you can click on these files and they open in Excel. This isn't a perfect solution: they're really just text files so Excel complains when you try to save or close them later, and you can't put data on multiple "tabs" on the worksheet.

I've tried to build a module using OLE to send Perl output to Excel in a cleaner fashion (code below). As with the text file approach, "\t" delimit fields (cols) and "\n" delimit records (rows). The module seems to work... more or less. One strange thing is that the resulting files can be opened OK from within Excel, but you can't click on them from the desktop. If you do, under NT they open but yield a mangled display.

Any ideas on why the excel files that result from this code don't click open properly from the desktop?

Thanks!

The module:
package Excel; use strict; use Win32::OLE; use File::Basename; use FindBin qw($Bin); my $ex; my $FIELD_SEP = "\t"; my $RECORD_SEP = "\n"; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; bless ($self, $class); $self->init(@_); return $self; } sub init { my ($self, $fname) = @_; unlink ($fname); my ($name,$dir,$type) = fileparse($fname, '\..*'); if ($type ne ".xls") {die "invalid name $fname: must have .xls typ +e";} if ($dir eq ".\\") {$dir=$Bin . "/";} $self->{fname} = $dir . $name . ".xls"; eval {$ex = Win32::OLE->GetActiveObject('Excel.Application')}; die "Excel not installed" if $@; unless (defined $ex) { $ex = Win32::OLE->new('Excel.Application', sub {$_[0]->Quit;}) or die "Oops, cannot start Excel"; $ex->LetProperty("SheetsInNewWorkbook",1); } $self->{book} = $ex->Workbooks->Add or die "bad add"; } sub print { my ($self, $sheet, $what) = @_; if (!$self->{$sheet}{sheet}) { $self->{$sheet}{sheet} = $self->{book}->Worksheets->Add; $self->{$sheet}{sheet}->{Name} = $sheet; $self->{$sheet}{row} = 1; $self->{$sheet}{col} = 1; } my $row = $self->{$sheet}{row}; my $col = $self->{$sheet}{col}; my $NEWLINE = '_NEWLINE_'; $what =~ s/$RECORD_SEP/$FIELD_SEP$NEWLINE$FIELD_SEP/g; for my $cell (split(/\t/,$what)) { if ($cell eq $NEWLINE) { $row++; $col=1; next; } $self->{$sheet}{sheet}->Cells($row,$col)->{Value} = $cell; $col++; } $self->{$sheet}{row} = $row; $self->{$sheet}{col} = $col; } sub DESTROY { my $self = shift; $self->{book}->SaveAs({Filename=>$self->{fname}}); $self->{book}->Close({SaveChanges=>1, FileName=>$self->{fname}}); } 1;
Some driver code:
use Excel; my $s = Excel->new("here.xls"); $s->print("one", "Hello World\nThis\tis\ta\ttest\n"); $s->print("one", join("\t", 1 ..10)); $s->print("one", "bye\n"); $s->print("one", join("\n",3 .. 5) . scalar(localtime)); $s->print("two", join("\n",13 .. 25) . scalar(localtime)); $s->print("eight", join("\n",13 .. 20) . scalar(localtime)); my $t = Excel->new("c:/temp/there.xls"); $t->print("two", join("\n",13 .. 25) . scalar(localtime));

Replies are listed 'Best First'.
(jcwren) RE: Win32::OLE and Excel
by jcwren (Prior) on Sep 18, 2000 at 20:25 UTC
    Are you familiar with the Spreadsheet::WriteExcel module? It's not very well fleshed out, but it will let you build a .XLS file with text and number fields (it doesn't do much else. No formulas, colors, multi-sheet workbooks, etc). You didn't mention what you're writing into the spreadsheet, so this may be insufficient for your needs.

    I'm wishing that someone (besides me) would write a full BIFF5 compliant Excel module. There seems to be a reasonable demand for it, but the whole BIFF5 spec is rather convoluted.

    --Chris

    e-mail jcwren
(Guildenstern) Re: Win32::OLE and Excel
by Guildenstern (Deacon) on Sep 18, 2000 at 20:06 UTC
    I ran your code, and I am able to open the file in NT with no problems. I tried:
    1. Open Excel, open file from menu (and tool bar). OK
    2. Double-click on file in folder. OK
    3. Move file to desktop, double-click. OK
    4. Drag file onto Excel icon to open. OK

    I'm running NT4 SP4 with Office 97, so I don't know how your specific setup differs. Since it seems to work for me, my best guess would be that somehow something's goofy with your Excel setup. I know that file associations and such get mangled at random. If you have more specific errors, maybe I can test some more.
    Update: Somehow I missed the last two lines in your test script. Adding those lines makes goofy stuff happen. I get these errors:
    retrying default method at D:/Perl/site/lib/Win32/OLE/Lite.pm line 156 +. Can't call method "Add" on an undefined value at D:/Perl/site/lib/Exce +l 36. retrying default method at D:/Perl/site/lib/Win32/OLE/Lite.pm line 156 +. retrying default method at D:/Perl/site/lib/Win32/OLE/Lite.pm line 156 +.

    Also, I get an Excel prompt asking if I would like to save "Book1". I've tried saying yes and no, but I get the same output - none. For some reason, neither .xls file is created.
    If I have Excel running then run the script, both files are created and they're loaded right into Excel. Strange.

    Guildenstern
    Negaterd character class uber alles!
      Yeah, the simple solution will be to write only one output file... My problems, too, arose when I tried to have two books or more books under one instance of Excel... Sigh.
        Maybe you need to add an explicit Close that does the same thing as DESTROY. That way you could have one at a time open, which might avoid problems. As an example, I changed your test code to this:
        use Excel; { my $s = Excel->new("here.xls"); $s->print("one", "Hello World\nThis\tis\ta\ttest\n"); $s->print("one", join("\t", 1 ..10)); $s->print("one", "bye\n"); $s->print("one", join("\n",3 .. 5) . scalar(localtime)); $s->print("two", join("\n",13 .. 25) . scalar(localtime)); $s->print("eight", join("\n",13 .. 20) . scalar(localtime)); } { my $t = Excel->new("c:/temp/there.xls"); $t->print("two", join("\n",13 .. 25) . scalar(localtime)); }
        and it worked perfectly. (Notice the braces around the two sections.) Putting the sections in blocks like that forces DESTROY to be called before the next new is called.

        Guildenstern
        Negaterd character class uber alles!
Re: Win32::OLE and Excel
by vladdrak (Monk) on Sep 19, 2000 at 03:20 UTC
    I do a fair amount of work with Win32::OLE and it's always a good idea to check errors after certain operations. Otherwise, debugging can be quite painful down the road, or if one OLE call dies and snowballs. I use an OLE error checking sub like so:
    $obj=Win32::OLE->GetObject("IIS://localhost/W3SVC/1"); &OLEErrChk("Binding to W3SVC1",1)
    That way you can also trap common errors and act on them, also.
    sub OLEErrChk { my $operation=shift; my $critical=shift; my $errnum=Win32::OLE->LastError; if(!$errnum) { if ($errnum=~/0x80070094/) { print "$operation Failed (in use) trying something..\n"; &Something; } else { print "$operation Failed: $errnum\n"; die if ($critical); } } else {print "$operation was Successful.\n"} }
    Hope that helps, Vlad
Re: Win32::OLE and Excel
by Anonymous Monk on Sep 20, 2000 at 02:18 UTC
    I don't develop on Win machines, but it occurred to me that that you're using a UNIX line separator:

    my $RECORD_SEP = "\n";

    Maybe when you open from excel it transaltes automatically into Win line separator (\r\n) Try:

    my $RECORD_SEP = "\r\n";

    Just a guess.
    -Matthew