一、用控件還是用OLEAutomation?
這個問題應該說很常見。我也在任何可能的情況下堅持我的主張:用BCB 6提供的Server控件組。如果你是用Delphi 6/7版本開發,那麼用Delphi提供的Server控件組。
這樣做有什麼好處?我個人認為至少有如下兩個:
第一,維護結構化+OO的程序設計風格。例如:
ExcelApplication1->set_DisplayAlerts(0,false);
ExcelApplication1->Quit();
又如:
int SheetCount=ExcelWorkbook1->Worksheets->Count;
這些代碼都還是比較直觀的。而且很具有OO的美感。
第二,強類型檢查勝於弱類型檢查。
如果使用OleGetProperty或者OleSetProperty函數,那麼對傳遞給這兩個函數的參數,是沒有辦法控制的。例如,我完全可以隨心設置一個單元格的屬性FOO為100。但是這樣的函數調用在運行期一定會出錯。而使用Server控件就不會有這個問題——編譯期就不能通過。可以在編譯期發現並糾正的錯誤,不要留到運行期去解決。這是我的主張。
二、必要的輔助手段
使用BCB編寫控制Excel的程序,是很繁瑣的。因為有時編譯通過後會出現難以琢磨的異常!又有一種很無助的感覺:BCB在這上面的幫助簡直是BS。CodeInsight的速度又是相當的慢,無法忍受!
不過,我們不要輕易放棄。根據我的經驗,在BCB下要想好好掌握Excel編程,必須掌握三個獲得幫助的途徑。這三個獲得幫助的途徑就是(以我個人喜好的程度降序排列):
Excel本身的宏命令。我現在的習慣是,如果我要在BCB中實現一個功能,但是卻不知道相應的方法和參數,我就會打開Excel,在隨便一個Sheet中用宏記錄下我要進行的操作,然後研究宏代碼。這樣做,肯定能獲得正確的方法名,但是對參數的數量和類型卻不敢保證:這是因為VBA可以選擇不PASS一些缺省參數。根據我的個人經驗,這樣做的有效性應該在80%左右,簡易性在100%。
Office自帶的VBA幫助。這個幫助在Office安裝過程中一般不是缺省安裝的。對於Office 2003中文版的用戶,應該檢查C:\Program Files\Microsoft Office\OFFICE11\2052\下是否有類似VBAxxnn.chm文件。其中xx應該是如下字符串之一:AC(Access),GR(Microsoft Graphics),OF(Office),OL(Outlook),OWS(Office Web Service),PB(Publisher),PP(PowerPoint),WD(Word),XL(Excel)。而nn是對應的Office應用的版本號。如果沒有這些文件,請運行Office安裝程序,在自定義安裝中將這些文件安裝即可。這些文件,對於我們理解Office應用中的DOM模型是非常有用的。其有效性為85%,簡易性約為80%。
BCB6中關於Server的頭文件。這些頭文件的位置應該是在:C:\Program Files\Borland\CBuilder6\Include\Vcl\下。我這裡只截一張圖。從這張圖中,我們也可以看到,BCB6對Office的支持只到Office 2000。Delphi 7可以支持到Office XP。不過它們都能很好的在Office 2003下工作。自Delphi 8之後,我們應該用Interop來操作Office了。這個方法的有效性是100%——因為你一定可以在裡面找到BCB支持的方法,但是簡易性只有10%——因為這些頭文件都相當的大:excel_2k.h的大小是8M+!
這三個手段應該互相幫襯,才能在更短的時間內找到正確的方法。而且通過這三個途徑找到的方法、參數應該是互相對應、互相一致的。例如,移動一個Sheet到另一個Sheet的Move方法,其在上述三個幫助途徑中的定義分別如下:
在Excel的VBA宏裡:Sheets("Sheet2").Move After:=Sheets(3)
Excel的VBA幫助:見下圖
BCB的Excel頭文件:
// *********************************************************************//
// Interface: IWorksheets
// Flags: (4112) Hidden Dispatchable
// GUID: {000208B1-0001-0000-C000-000000000046}
// *********************************************************************//
interface IWorksheets : public IDispatch
{
public:
... ...
HRESULT STDMETHODCALLTYPE Copy(VARIANT Before/*[in,opt]*/= TNoParam(),
VARIANT After/*[in,opt]*/= TNoParam(),
... ...
HRESULT STDMETHODCALLTYPE Move(VARIANT Before/*[in,opt]*/= TNoParam(),
VARIANT After/*[in,opt]*/= TNoParam(),
long lcid/*[in]*/= TDefLCID()); // [637]
... ...
三、編程指南
(一) ExcelApplication的啟動、退出
我們平時所說的啟動/退出Excel,在BCB6中應該被確切的說成是ExcelApplication的啟動/退出。對應的控件是。用來啟動、退出的代碼分別如下:
void __fastcall TMainForm::EABtnClick(TObject *Sender)
{
try
{
EA->Connect();
}
catch(...)
{
ShowMessage("Can not launch server");
}
EA->set_Caption((WideString)"Excel Server Invoked by BCB");
EA->set_Visible(0,true);
}
__fastcall TMainForm::~TMainForm()
{
EA->set_DisplayAlerts(0,false);
EA->Quit();
}
注意,Office控件多以接口形式互相關聯,所以連接的方法也被貫之以Connect的名稱。由於Excel Application一定是頂層對象,所以它的Connect方法不需要指定參數。
set_Caption是用來設置Excel Application的標題。請注意要用WideString,而不是String。
set_Visible(0, true)是使Excel Application可見。這裡參數0是Locale ID。
Excel控件的封裝是很奇怪的——也許是因為BCB 6只支持到Office 2000而目前我們基本都是要操作Office 2003而引起的版本兼容問題。一般來說,我們希望直接使用類似屬性的操作方法來修改。但是,有些屬性,如Visible,就必須用“set_屬性/get_屬性 ”的方式進行操作。這不是說Excel Application控件中沒有Visible這個屬性。這個屬性確實存在於Excel Application控件中,但是如果你想直接操作Visible屬性,那麼無論你寫成:“EA->Visible=true;”還是“EA->Visible[0]=true;”,編譯時都會出錯!提示“TExcelApplication::Visible is not accessible. ”。而更奇怪的是,如果我們直接操作屬性,那麼即使編譯能夠通過,也可能沒有實際效用!有興趣的讀者可以測試一下Caption屬性的操作。
一般而言,用set_屬性/get_屬性的方式,一定可以操作一個屬性。但是,有些Set方法的命名也不遵守這個規律,後文即將涉及。
退出時,可以根據個人的喜好,設置DisplayAlerts屬性。如果設置為true,那麼在退出時,如果對Sheet或Workbook進行了一些需要存盤的操作,會有一個確認框出現。
以前曾經有一種說法,說是這樣啟動並正常退出的Excel Application,退出後會在內存中留下一個Excel進程。在我的機器上,沒有這樣的情形。
需要補充的是ExcelApplication控件的ConnectKind屬性。它一共有五個可選值:ckAttachToInterface/ckNewInstance/ckRunningOrNew/ckRemote/ckRunning。一般我都會用ckRunningOrNew。其它的選項我倒沒有進行過測試。
(三) ExcelWorksheet的操作
在上文連接Workbook的代碼中,我也同時連接了TExcelWorksheet,其控件圖標是。所以完整的代碼段如下:
void __fastcall TMainForm::EWBBtnClick(TObject *Sender)
{
EWB->ConnectTo(EA->Workbooks->Add(TNP, 0));
// Connect to worksheet as well
EWS1->ConnectTo(EWB->Worksheets->get_Item(V("Sheet1")));
EWS2->ConnectTo(EWB->Worksheets->get_Item(V(2)));
EWS3P=EWB->Worksheets->get_Item(V("Sheet3"));
EWS3->ConnectTo(EWS3P);
EWS3->Activate();
}
我們知道,缺省情況下,一個空白的Excel Workbook有三個空白的Worksheet,所以上文中我用三個ExcelWorksheet控件來連接這三個Worksheet。
我們既可以用表的名字(如“Sheet1”),也可以用表的序號(如“2”)來作為一個表的索引號。請注意V方法,它也是我定義的一個宏:
#define V TVariant
所以,它只是一個用來構造TVariant參數的宏。它和上面的TNP宏都是蠻有用的定義。
下面是一些針對Excel Worksheet的操作,不再一一詳細說明。
void __fastcall TMainForm::MoveSheetBtnClick(TObject *Sender)
{
EWS1->Move(TNP, V(EWB->Worksheets->get_Item(V("Sheet3"))), 0);
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::RenameBtnClick(TObject *Sender)
{
String NewName;
InputQuery("Rename Excel Worksheet", "Input a new name", NewName);
EWS1->set_Name((WideString)NewName);
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::CreateBtnClick(TObject *Sender)
{
EWS4->ConnectTo(EWB->Sheets->
Add(TNP,V(EWB->Worksheets->get_Item(V("Sheet3"))),V(1),V(xlWorksheet)));
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::DelSheetBtnClick(TObject *Sender)
{
EWS2->Delete(0);
}
我們已經操作到了Worksheet級別,但是在我們日常操作中,接觸最多的是Range(范圍)和Cell(單元格),在後文我們將繼續深入討論,並討論如何連接數據庫、如何畫數據圖,以及如何用TExcelQueryTable加速數據導入的方法。
這樣生成的Excel Application只是一個空架子。我們要增加Workbook(工作簿)。
(二) ExcelWorkbook的創建和相關操作
如果我們把ExcelWorkbook簡單的理解為等價於一個xls文件,應該不會差別太大,而且應該對我們有幫助。它的控件圖標是。我們來看如何創建Workbook,代碼如下:
void __fastcall TMainForm::EWBBtnClick(TObject *Sender)
// 本文轉自 C++Builder研究 - http://www.ccrun.com/article.asp?i=1044&d=xr3888
{
EWB->ConnectTo(EA->Workbooks->Add(TNP, 0));
... ...
}
首先,我們要連接到一個Workbook的接口上去,這裡我們用的是新增一個Workbook的方式。注意TNP參數,我們會在很多場合使用它。它是我自己程序中定義的一個宏:
#define TNP TNoParam()
而第二個參數0,則又是Locale ID(簡稱LID)。
ExcelWorkbook還可以用來連接——或者說“打開”更恰當——一個現有的Workbook(一個xls文件),具體代碼如下:
EWB->ConnectTo(EA->Workbooks->Open((WideString)"c:\\temp\\test.xls",
TNP, TNP, TNP, TNP,
TNP, TNP, TNP, TNP,
TNP, TNP, TNP, TNP, 0));
上述代碼中打開了位於c:\temp下的test.xls文件。這個方法有很多參數,一般我都會傳遞TNP給它。具體參數的含義,可以參考相關文檔。
(三) ExcelWorksheet的操作
在上文連接Workbook的代碼中,我也同時連接了TExcelWorksheet,其控件圖標是。所以完整的代碼段如下:
void __fastcall TMainForm::EWBBtnClick(TObject *Sender)
{
EWB->ConnectTo(EA->Workbooks->Add(TNP, 0));
// Connect to worksheet as well
EWS1->ConnectTo(EWB->Worksheets->get_Item(V("Sheet1")));
EWS2->ConnectTo(EWB->Worksheets->get_Item(V(2)));
EWS3P=EWB->Worksheets->get_Item(V("Sheet3"));
EWS3->ConnectTo(EWS3P);
EWS3->Activate();
}
我們知道,缺省情況下,一個空白的Excel Workbook有三個空白的Worksheet,所以上文中我用三個ExcelWorksheet控件來連接這三個Worksheet。
我們既可以用表的名字(如“Sheet1”),也可以用表的序號(如“2”)來作為一個表的索引號。請注意V方法,它也是我定義的一個宏:
#define V TVariant
所以,它只是一個用來構造TVariant參數的宏。它和上面的TNP宏都是蠻有用的定義。
下面是一些針對Excel Worksheet的操作,不再一一詳細說明。
void __fastcall TMainForm::MoveSheetBtnClick(TObject *Sender)
{
EWS1->Move(TNP, V(EWB->Worksheets->get_Item(V("Sheet3"))), 0);
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::RenameBtnClick(TObject *Sender)
{
String NewName;
InputQuery("Rename Excel Worksheet", "Input a new name", NewName);
EWS1->set_Name((WideString)NewName);
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::CreateBtnClick(TObject *Sender)
{
EWS4->ConnectTo(EWB->Sheets->
Add(TNP,V(EWB->Worksheets->get_Item(V("Sheet3"))),V(1),V(xlWorksheet)));
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::DelSheetBtnClick(TObject *Sender)
{
EWS2->Delete(0);
}
我們已經操作到了Worksheet級別,但是在我們日常操作中,接觸最多的是Range(范圍)和Cell(單元格),在後文我們將繼續深入討論,並討論如何連接數據庫、如何畫數據圖,以及如何用TExcelQueryTable加速數據導入的方法。