Delphi与Excel的亲密接触

    技术2022-05-11  64

           Delphi作为一个出色的RAD,强大的数据库功能是其最重要的特色之一,但是操纵困难的QuickReport控件常常不能满足数据库报表的需要。如果你的报表非常复杂,或者要求灵活地改变格式,那么使用Excel作为报表服务器是一个不错的选择。Delphi从版本5开始提供的Excel组件极大地简化了OLE自动化技术的应用。不过缺漏多多的帮助文件一直是Delphi最令人诟病的地方,这些新组件也不例外,本文试图对此作一较详细地介绍。

           Excel的对象模型是一个树状的层次结构,根是应用程序本身,工作簿WorkBook是根对象的属性对象,本文主要讨论的用于数据交换的WorkSheet则是工作簿的属性对象,详情参阅MSOffice提供的Excel VBA帮助文件。在Delphi中控制Excel首先要与服务器程序建立连接,打开工作簿,然后与目标工作表交换数据,最后断开连接。

     

           打开Excel工作簿

     

           我们的例子从一个带有TStringGrid(当然要填上一些数据)和两个按钮的主窗体开始,从控制面板的Servers页签中拖一个TExcelApplication控件放到窗体上。首先把ConnectKind设为ckRunningOrNew,表示如果能够检测到运行的Excel实例则与其建立联系,否则启动Excel。另外,如果希望程序一运行即与服务器程序建立联系,可以把AutoConnect属性设为True。 与Excel建立联系只要一条语句就可以了: Excel . Connect; 也许你已经注意到Servers页签上还有其他几个Excel控件,这些控件通过ConnectTo方法可以与前面的Excel联系在一起:

           ExcelWorkbook1.ConnectTo(Excel . ActiveWorkbook);

           ExcelWorksheet1.ConnectTo(Excel . ActiveSheet as _Worksheet);

           ExcelWorksheet2.ConnectTo(Excel . Worksheets.Item['Sheet2'] as _Worksheet);

          要注意,使用ConnectTo方法前必须先打开相应的工作簿或工作表,另外这些控件在多数情况下并不会带来额外的便利,因此最好只使用一个TExcelApplication。 一旦与Excel服务器建立联系,就可以创建新的工作簿:

          var wkBook : _WorkBook;

          LCID : Integer;

          ... LCID := GetUserDefaultLCID();

          wkBook := Excel.Workbooks.Add(EmptyParam, LCID);

          Add函数的第一个参数用于定义新建工作簿所使用的模板,可以使用xlWBATChart、lWBATExcel4IntlMacroSheet、 xlWBATExcel4MacroSheet或者xlWBATWorksheet常量,也可以是已有的xls文件名。这里的EmptyParam是Variants单元与定义的变量,表示使用默认的通用模板创建新工作簿。 如果打开已有的xls文档,则应把要打开的文件名作为第一个参数传递给Open函数:

          wkBook:=Excel.WorkBooks.Open(edtDesFile.text,EmptyParam,EmptyParam,       

                                                               EmptyParam,EmptyParam,EmptyParam,EmptyParam,

                                                               EmptyParam,EmptyParam,EmptyParam,EmptyParam,

                                                               EmptyParam,EmptyParam,LCID);

          要知道,所有的数据操作主要是针对活动工作表而言的,下面的语句使用一个_WorkSheet变量代表当前的活动单元格。如果知道工作表的名称,其中的索引号可以用工作表名代替:

              wkSheet:=wkBook.Sheets[1] as _WorkSheet;

          完成数据交换后需要保存工作簿:

             Excel.ActiveWorkBook.SaveAs ('MyOutput', EmptyParam,EmptyParam, EmptyParam, EmptyParam,

                  EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, LCID);

          或者:

             Excel.ActiveWorkBook.Save(LCID);

          最后要关闭工作簿并断开与Excel的连接:

               wkBook.Close(True, SaveAsName, EmptyParam, LCID); //Excel.Quit; Excel.Disconnect;

         这里的Close方法包含有保存的功能,第一个参数说明在关闭工作簿之前是否保存所做的修改,第二个参数给出要保存的文件名,第三个参数用于多位作者处理文档的情况。第二行要求终止Excel的运行。

     

            与工作表交换数据

     

            输入数据是对活动工作表的某个单元格或区域进行的,Range与cells都是工作表的对象属性。Cells是单元格的集合,如果没有指定具体位置可以代表整个工作表的所有单元格,但一般使用它是为了引用某个具体的单元格,比如WS.Cells.Item[1,1]就表示最左上角的单元格A1,注意在VBA中Item是Cells的默认属性可以省略,但在Delphi中就没有这种便利了。为单元格赋值要引用其Value属性,不言而喻,该属性是一个Variant变量,例如:

                wkSheet.Cells.Item[1, 1].Value := '通讯录';

            当然你也可以为单元格指定公式:

                var AFormula:String; ……

                AFormula:='=Rand()'; wkSheet.Range['F3','G6'].Value:=AFormula;

           上面的方法非常直接简单,但是速度非常慢,不适合作大型报表。那么能不能把所有的数据依次传递给Excel呢?我们可以使用Range,这个对象代表工作表中的一个区域,象我们用鼠标拖出的那样,一般是一个矩形区域,只要给定其左上角和右下角单元格的位置就可以了,如Range[‘C3’,’J42’]。 这里还有一个小问题,因为如果数据超出26列(比如有100列)或者需要在运行中确定目标区域范围的话,使用字符名称标记单元格就比较麻烦。回想一下,既然“C3”是单元格的标记,那么我们当然也可以使用Cells,比如Range[Cells.Item[1,1], Cells.Item[100,100]]。 可以想象,Range的值应该是数组,但是绝对不能用Delphi中的Array给它赋值!要记住,在Delphi中,Excel对象的值总是Variant类型的。

                 var

                 Datas : Variant;

                 Ir, ic: Integer;

                 ……

                Datas:= varArrayCreate([1,ir,1,ic],varVariant); //这里创建100*100的动态数组

                …… //这里为数组元素赋值

                with wkSheet do Range[cells.Item[3,1],cells.Item[ir+2,ic]].Value:=Datas;

            要注意,工作表与Range都有Cells属性,为了明确起见,这里使用了with语句。此外,Range是有方向性的,用VarArrayCreate建立的一维数组只能赋给单行的Range,如果要为单列的Range定义值,必须使用二维数组,比如:

               Datas:=VarArrayCreate([1,100,1,1], varVariant);//创建100*1的动态数组。

            顺便提一下,Cells.Item[]实际上返回的也是Range对象。 从工作表中取回数据基本上是写数据的逆过程,稍微需要注意的是如何确定工作表的数据范围:

               var

               ir, ic : Integer;

               ……

              wkSheet.Cells.SpecialCells(xlCellTypeLastCell,EmptyParam).Activate;

              ir := Excel.ActiveCell.Row;

              ic := Excel.ActiveCell.Column;

         这里巧妙地利用特殊单元格函数SpecialCells取得包含数据的最后一个单元格。

     

         数据编辑

     

         下面是数据编辑的两个例子。

            var DestRange: OleVariant;

            begin

                DestRange := Excel.Range['C1', 'D4'];

                Excel.Range['A1', 'B4'].Copy(DestRange);

        上面的例子复制了8个单元格的内容。如果给Copy函数传递一个空参数,则该区域的数据被复制到剪贴板,以后可以用Paste方法粘贴到别的位置。

           var WS: _Worksheet;

           ……

          Excel.Range['A1', 'B4'].Copy(EmptyParam); //在一个工作表中复制数据到剪贴板

          WS := Excel.Activesheet as _Worksheet; //改变活动工作表

          WS.Range['C1', 'D4'].Select;

          WS.Paste(EmptyParam, EmptyParam, lcid); //把剪贴板中的内容粘贴到新的工作表中

     

           格式设置

     

          选择Excel作为报表服务器主要是因为它强大的格式化能力。

          我们首先把标题“通讯录”进行单元格合并,居中显示,然后修改字体为18磅的“隶书”,粗体:

             with wkSheet.Range['A1','D1'],Font do begin

                 Merge(True); //合并单元格

                 HorizontalAlignment:= xlCenter;

                 Size:=18;

                 Name:='隶书';

                 FontStyle:=Bold;

             end;

          如果单元格内容较长,将有部分内容无法显示,通常的做法是双击选定区域右侧的边线是各列的宽度自动适应内容的长度。在Delphi中通过AutoFit方法也可实现自适应的列宽行高,需要注意的是该方法仅能用于整行整列,否则会提示OLE方法拒绝执行的错误:

             wkSheet.Columns.EntireColumn.AutoFit;

           中式报表通常需要上下封顶的表格线,可以使用Borders集合属性。要注意,VBA中的集合对象通常都有一个缺省的Item属性,Delphi中是不能省略的。Weight属性用于定义表格线的粗细:

              with Aname.RefersToRange,Borders do begin

                   HorizontalAlignment:= xlRight;

                   Item[xlEdgeBottom].Weight:=xlMedium;

                   Item[xlEdgeTop].Weight:=xlMedium;

                   Item[xlInsideHorizontal].Weight:=xlThin;

                   item[xlInsideVertical].Weight:=xlThin;

               end;

     

          页面设置与打印

     

          页面设置是通过工作表的PageSetUp对象属性设置的。Excel VBA中预设了40余种纸张常量,需要注意的是某些打印机只支持其中的一部分纸张类型。属性Orientation用于控制打印的方向,常量landscape = 2表示横向打印。布尔属性CenterHorizontally和CenterVertically用于确定打印的内容是否在水平和垂直方向上居中。

            with wkSheet.PageSetUp do begin

                  PaperSize:=xlPaperA4; //Paper type A4

                  PrintTitleRows := 'A1:D1'; //Repeat this row/page

                  LeftMargin:=18; //0.25" Left Margin

                  RightMargin:=18; //0.25" will vary between printers

                  TopMargin:=36; //0.5"

                  BottomMargin:=36; //0.5"

                  CenterHorizontally:=True;

                  Orientation:=1; //横向打印(landscape)=2, portrait=1

              end;

         打印报表可以调用工作表的PrintOut方法,VBA定义的该方法共有8个可选参数,前两个用于规定起止页,第三格式打印的份数,不过在Delphi中为其在最后增加了一个LCID参数,而且该参数不能使用EmptyParam。类似地,打印预览方法PrintPreview在VBA中没有参数,而在Delphi中调用需要两个参数。

             // wkBook.PrintPreview(True,LCID);  //for previewing

             wkSheet.PrintOut(EmptyParam,EmptyParam,1, EmptyParam,EmptyParam,EmptyParam,

                         EmptyParam,EmptyParam,LCID);

     

           命名区域与宏

     

          如果报表的格式比较复杂,为特定的表格区域命名然后按名引用是一种比较好的方法。Names是WorkBook的一个集合对象属性,它有一个的Add方法可以完成这项工作。

              Var Aname : Excel2000.Name;

              ……

              Aname := wkBook.Names.Add('通讯录','=Sheet1!$A$3:$D$7', EmptyParam, EmptyParam,

                                  EmptyParam,EmptyParam,EmptyParam,EmptyParam,

                                  EmptyParam,EmptyParam,EmptyParam);

          其中Add函数的第一个参数是定义的名称,第二个参数是名称所表示的单元格区域。要注意区域名称的类型必须使用限定符,如果使用类型库(D4),则限定符为Excel_TLB。此外,命名的区域应使用绝对引用方式,即加上“$”符号。 一旦命名了一个区域,就可以使用这个名称来引用它,下面的一行代码使通讯录内容以粗体显示:

              AName.RefersToRange.Font.Bold:=True;

          不过最令人惊喜的也许是你能够在Delphi中动态地修改Excel宏程序!下面的代码为我们的工作簿创建了一个宏,在关闭工作簿时记录上一次访问的时间:

               var LineNo: integer;

                     CM: CodeModule;

                     sDate:String;

                begin

                   CM := WkBook.VBProject.VBComponents.Item('ThisWorkbook').Codemodule;

                   LineNo := CM.CreateEventProc('BeforeClose', 'Workbook');

                   SDate:='上次访问日期:'+DateToStr(Date());

                   CM.InsertLines(LineNo + 1, ' Range("B2").Value = "'+sDate+'"');

               End;

          修改宏需要在前面的uses一节加上一个单元:VBIDE2000,如果使用类型库则相应的单元为VBIDE_TLB。这段代码的关键是CodeModule对象,遗憾的是在Excel VBA help文中找不到该对象的踪迹,只能去检索MSDN了。 Delphi4及以前的版本 Delphi4没有提供TExcelApplication对象,需要引入类型库使用OLE自动化技术,Excel97的类型库是Excel8.olb。这两种方法的主要区别在于与服务器程序建立连接的方法,下面是通过类型库控制Excel的程序框架:

              uses Windows, ComObj, ActiveX, Excel_TLB;

              var

                  Excel: _Application;

                  LCID: integer;

                  Unknown:IUnknown;

                  Result: HResult;

               begin

                  LCID := LOCALE_USER_DEFAULT;

                  Result := GetActiveObject(CLASS_Application, nil, Unknown); //尝试捕获运行中的程序实例

                  if (Result = MK_E_UNAVAILABLE) then Excel := CoApplication.Create //启动新的程序实例

                  else begin {检查GetActiveObject方法调用过程中的错误}

                    OleCheck(Result);

                    OleCheck(Unknown.QueryInterface(_Application, Excel));

                  end;

                  …… //进行数据处理

                  Excel.Visible[LCID] := True;

                  // Excel.DisplayAlerts[LCID] := False; //显示提示对话框

                  Excel.Quit;

               End;

          这里没有采用通常的try…except结构,是因为例外处理机制要进行复杂的OLE检查,降低了except部分的执行速度。要注意,不同的Delphi版本生成的伴随函数CoApplication和一些常量名可能不同,应查看相应的类型库。在调用Quit方法之前,一定要释放程序中创建的所有工作簿和工作表变量,否则Excel可能驻留在内存中运行(可以按下Ctrl+Alt+Del查看)。调用GetActiveObject捕获程序实例还有一个小问题,如果Excel处于最小化运行状态,可能出现只显示程序主框架而用户区不可见的情况。 此外,如果不希望引入类型库,还可以采用滞后绑定的方法,不过速度要慢许多。下面的例子声明了一个Variant变量来代表Excel应用程序:

               var Excel: Variant;

               ……

               try

                 Excel := GetActiveOleObject('Excel.Application');

               except

                 Excel := CreateOleObject('Excel.Application');

               end;

               Excel.Visible := True;

          采用滞后绑定时,编译器不对调用的Excel对象方法进行检查,而把这些工作交给服务器程序在执行时完成,这样VBA所设置的大量默认参数(经常有十几个)就发挥了应有的作用,因此这种方法有一个意料不到的好处——代码简洁:

               var WBk, WS, SheetName: OleVariant;

               .…..

               WBk := Excel.WorkBooks.Open('C:/Test.xls');

               WS := WBk.Worksheets.Item['SheetName'];

               WS.Activate;

               ……

               WBk.Close(SaveChanges := True);

               Excel.Quit;

            除了运行速度慢以外,如果要使用类型库中定义的常量,就只能自己动手了:

               const xlWBATWorksheet = -4167;          

               ……

               XLApp.Workbooks.Add(xlWBatWorkSheet);

            最后不要忘记关闭Excel之后释放变量:

               Excel := Unassigned;

            以下是本文例子中所用的源代码,在Delphi6+MSOffice2000下通过。

               unit Unit1;

               interface

               uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs,

                        OleServer, Excel2000, Grids, StdCtrls;

               type

                   TForm1 = class(TForm)

                      Button1: TButton;

                      StringGrid1: TStringGrid;

                      Excel: TExcelApplication;

                      procedure FormActivate(Sender: TObject);

                      procedure Button1Click(Sender: TObject);

                  private { Private declarations }

                      procedure Write2Xls;

                      procedure OpenExl;

                      procedure CloseExl;

                      procedure AddFormula;

                      procedure NameSheet;

                      procedure Formats;

                      procedure AddMacro;

                      procedure Retrieve;

                      procedure Printit;

                  public { Public declarations }

                 end;

     

                 var Form1: TForm1;

     

                 implementation {$R *.dfm}

                 uses VBIDE2000;

                 var

                      ir,ic:Integer;

                      wkSheet:_WorkSheet;

                      LCID:Integer;

                      wkBook:_WorkBook;

                      AName:Excel2000.Name;

     

             procedure TForm1.FormActivate(Sender: TObject);

             begin

                 with StringGrid1 do begin

                     Rows[0].CommaText:='姓名,性别,年龄,电话';

                     Rows[1].CommaText:='张三,男,25,010-33775566';

                     Rows[2].CommaText:='李四,男,47,012-6574906';

                     Rows[3].CommaText:='周五,女,18,061-7557381';

                     Rows[4].CommaText:='孙涛,女,31,3324559';

                  end;

             end;

     

             procedure TForm1.OpenExl;

             begin

                 with Excel do begin

                     Connect; LCID:=GetUserDefaultLCID();

                     wkBook:=WorkBooks.Add(EmptyParam,LCID);

                     wkSheet:=wkBook.Sheets[1] as _WorkSheet;

                  end;

              end;

     

             procedure TForm1.Write2Xls;

              var

                     Datas:Variant;

                      i,j:Integer;

              begin

                     ir:=StringGrid1.RowCount;

                     ic:=StringGrid1.ColCount;

                     Datas:=varArrayCreate([1,ir,1,ic],varVariant);

                     for i:=1 to ir do

                         for j:=1 to ic do

                              Datas[i,j]:=StringGrid1.Cells[j-1,i-1];

                     with wkSheet do begin

                              Activate(LCID);

                         Cells.Item[1,1].Value:='通讯录';

                         Range[cells.Item[3,1],cells.Item[ir+2,ic]].Value:=Datas;

                    end;

                         // Excel.Visible[LCID]:=True;

                    Datas:=Unassigned;

               end;

     

             procedure TForm1.Retrieve;

             var

                     Datas:Variant;

                     i,j:Integer;

              begin

                     with wkSheet do begin

                        Cells.SpecialCells(xlCellTypeLastCell,EmptyParam).Activate;

                        ir:=Excel.ActiveCell.Row;

                        ic:=Excel.ActiveCell.Column;

                        Datas:=Range[Cells.Item[1,1],Cells.Item[ir,ic]].Value;

                        with StringGrid1 do begin

                              ColCount:=ic;

                              RowCount:=ir;

                              ScrollBars:=ssBoth;

                              for i:=0 to ir-1 do

                                  for j:=0 to ic-1 do

                                     Cells[j,i]:=Datas[i+1,j+1];

                         end;

                         Datas:=UnAssigned;

                     end;

               end;

     

             procedure TForm1.CloseExl;

             const

                 SaveAsName='test.xls';

             begin

                  wkBook.Close(True,SaveAsName,EmptyParam,LCID);

                  Excel.Quit;

                  Excel.Disconnect;

             end;

     

             procedure TForm1.NameSheet;

             begin

                 AName:=wkBook.Names.Add('通讯录','=Sheet1!$A$3:$D$7',EmptyParam,EmptyParam,

                         EmptyParam,EmptyParam,EmptyParam,EmptyParam,

                         EmptyParam,EmptyParam,EmptyParam);

             end;

     

             procedure TForm1.AddFormula;

             var

                 AFormula:String;

             begin

                 AFormula:='=Rand()';

                 wkSheet.Range['F3','G6'].Value:=AFormula;

             end;

     

             procedure TForm1.Formats;

             begin

                 with wkSheet.Range['A1','D1'],Font do

                 begin

                     Merge(True); //合并单元格

                     HorizontalAlignment:= xlCenter;

                     Size:=18; Name:='隶书';

                     FontStyle:=Bold;

                  end;

                 wkSheet.Columns.EntireColumn.AutoFit;

                 with Aname.RefersToRange,Borders do begin

                    HorizontalAlignment:= xlRight;

                    Item[xlEdgeBottom].Weight:=xlMedium;

                    Item[xlEdgeTop].Weight:=xlMedium;

                    Item[xlInsideHorizontal].Weight:=xlThin;

                    item[xlInsideVertical].Weight:=xlThin;

                end;

             end;

     

             procedure TFOrm1.AddMacro;

             var

                 LineNo: integer;

                CM: CodeModule;

                sDate:String;

              begin

                  CM := WkBook.VBProject.VBComponents.Item('ThisWorkbook').Codemodule;

                  LineNo := CM.CreateEventProc('BeforeClose', 'Workbook');

                  SDate:='上次访问日期:'+DateToStr(Date());

                  CM.InsertLines(LineNo + 1, ' Range("B2").Value = "'+sDate+'"');

              end;

     

             procedure TForm1.Printit;

             begin

                 with wkSheet.PageSetUp do begin

                     PaperSize:=xlPaperA4; //Paper type A4

                     PrintTitleRows := 'A1:D1'; //Repeat this row/page

                     LeftMargin:=18; //0.25" Left Margin

                     RightMargin:=18; //0.25" will vary between printers

                     TopMargin:=36; //0.5"

                     BottomMargin:=36; //0.5"

                     CenterHorizontally:=True;

                     Orientation:=1; //横向打印(landscape)=2, portrait=1

                  end;

                  wkSheet.PrintOut(EmptyParam,EmptyParam,1, EmptyParam,EmptyParam,EmptyParam,

                                  EmptyParam,EmptyParam,LCID);

             end;

     

             procedure TForm1.Button1Click(Sender: TObject);

             begin

                     try

                        OpenExl;

                        Write2xls;

                        AddFormula; NameSheet;

                        Formats; PrintIt;

                        AddMacro;

                        ReTrieve;

                     finally

                        CloseExl;

                     end;

               end;

     

               end.


    最新回复(0)