Delphi與Excel的親密接觸[王安鵬([email protected]) 2002/4/14] 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、xlWBATExcel4IntlMacroSheet、 xlWBATExcel4MacroSheet或xlWBATWorksheet常數,也可以是現有的xls檔名。這裡的EmptyParam是Variants單元與定義的變量,表示使用預設的通用模板建立新工作簿。如果開啟已有的xls文檔,則應把要開啟的文件名稱作為第一個參數傳遞給Open函數: wkBook:=Excel.WorkBooks.Open(edtDesFile.text,EmptyParam,EmptyParam, EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam, EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam, EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam , 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:=Mediumium;Item[ =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" Leftgin 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,Param,EmptyParam,EmptyParam,EmptyParam, EmptyParam,Param,EmptyParam,EmptyParam,EmptyParam, EmptyParam,Param,EmptyParam,EmptyParam,EmptyParam, EmptyParam,Param,EmptyParam,EmptyParam,EmptyParam, EmptyParam,Param,Empty 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; procedure FormActivate(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private declarations } procedure Write2Xls; procedure OpenExl; procedure CloseExl; procedure AddFormula; procedure NameMac; Printit; public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} uses VBIDE2000; var ir,ic:Integer; wkSheet:_WorkSheet; LCID:Integer; wkBook_WorkBooks; 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:=Grid1. 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,icic ]].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['F33' .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[ xlThin; end; end; procedure TFOrm1.AddMacro; var LineNo: integer; CM: CodeModule; sDate:String; begin CM := WkBook.VBProject.VBComponents.Item('ThisWorkbook').Codemodule; LineNoEvent:= CM.CreateProc('BreateClose' ); 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 will between printers TopMargin:=36; //0.5" BottomMargin vary between printers TopMargin:=36; //0.5" BottomMargin v: =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.Button1C(SmptyParam,LCID); end; procedure TForm1.Button1C); begin try OpenExl; Write2xls; AddFormula; NameSheet; Formats; PrintIt; AddMacro; ReTrieve; finally CloseExl; end; end; end. Delphi與Excel的親密接觸[王安鵬([email protected]) 2002/4/14] 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、xlWBATExcel4IntlMacroSheet、 xlWBATExcel4MacroSheet或xlWBATWorksheet常數,也可以是現有的xls檔名。這裡的EmptyParam是Variants單元與定義的變量,表示使用預設的通用模板建立新工作簿。如果開啟已有的xls文檔,則應把要開啟的文件名稱作為第一個參數傳遞給Open函數: wkBook:=Excel.WorkBooks.Open(edtDesFile.text,EmptyParam,EmptyParam, EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam, EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam, EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParamtyParam , 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:=Mediumium;Item[ =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" Leftgin 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,Param,EmptyParam,EmptyParam,EmptyParam, EmptyParam,Param,EmptyParam,EmptyParam,EmptyParam, EmptyParam,Param,EmptyParam,EmptyParam,EmptyParam, EmptyParam,Param,EmptyParam,EmptyParam,EmptyParam, EmptyParam,Param,Empty 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; procedure FormActivate(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private declarations } procedure Write2Xls; procedure OpenExl; procedure CloseExl; procedure AddFormula; procedure Namey Name; Printit; public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} uses VBIDE2000; var ir,ic:Integer; wkSheet:_WorkSheet; LCID:Integer; wkBook_WorkBooks; 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:=Grid1. 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,icic ]].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['F33' .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[ xlThin; end; end; procedure TFOrm1.AddMacro; var LineNo: integer; CM: CodeModule; sDate:String; begin CM := WkBook.VBProject.VBComponents.Item('ThisWorkbook').Codemodule; LineNoEvent:= CM.CreateProc('BreateClose' ); 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 will between printers TopMargin:=36; //0.5" BottomMargin vary between printers TopMargin:=36; //0.5" BottomMargin v: =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.Button1C(SmptyParam,LCID); end; procedure TForm1.Button1C); begin try OpenExl; Write2xls; AddFormula; NameSheet; Formats; PrintIt; AddMacro; ReTrieve; finally CloseExl; end; end; end.