BCB中Excel的操作(一)
最近看到很多关于在BCB中操作Excel的帖子,不仅感慨万千:世界的轮回真是太精确了……
在此,我将我以前的一些关于BCB/Delphi中操作Excel的文章贴出来供大家参考。有些图片因为连接的问题无法显示,见谅。
============================
在本文中,我将结合我的个人经验,和大家讨论一下在BCB 6中开发Office(主要是Excel)程序的心得。
一、用控件还是用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编程,必须掌握三个获得帮助的途径。这三个获得帮助的途径就是(以我个人喜好的程度降序排列):
1. Excel本身的宏命令。我现在的习惯是,如果我要在BCB中实现一个功能,但是却不知道相应的方法和参数,我就会打开 Excel,在随便一个Sheet中用宏记录下我要进行的操作,然后研究宏代码。这样做,肯定能获得正确的方法名,但是对参数的数量和类型却不敢保证:这是因为VBA可以选择不PASS一些缺省参数。根据我的个人经验,这样做的有效性应该在80%左右,简易性在100%。
2. 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%。
3. 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+!
BCB6关于Office的帮助文件
这三个手段应该互相帮衬,才能在更短的时间内找到正确的方法。而且通过这三个途径找到的方法、参数应该是互相对应、互相一致的。例如,移动一个Sheet到另一个Sheet的Move方法,其在上述三个帮助途径中的定义分别如下:
1. 在Excel的VBA宏里:Sheets("Sheet2").Move After:=Sheets(3)
2. Excel的VBA帮助:见下图VBA帮助
3. 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的启动/退出。对应的控件是TExcelApplication控件。用来启动、退出的代码分别如下:
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-& gt;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。其它的选项我倒没有进行过测试。
这样生成的Excel Application只是一个空架子。我们要增加Workbook(工作簿)。
(二) ExcelWorkbook的创建和相关操作
如果我们把ExcelWorkbook简单的理解为等价于一个xls文件,应该不会差别太大,而且应该对我们有帮助。它的控件图标是ExcelWorkbook控件。我们来看如何创建Workbook,代码如下:
void __fastcall TMainForm::EWBBtnClick(TObject *Sender)
{
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,其控件图标是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加速数据导入的方法。