用 C++Builder 写 COM 版的 Hello world!

前几日用 OICQ 联系上一位快有两年不见的朋友,闲聊中得知他在用 BCB 做 COM , 碰上一些麻烦,我便自告奋勇要教他,刚讲了没几句便被同事拉去吃饭,便和朋友约好发 e-Mail 给他。第二天我便开始做 一个 COM 版的 Hello world! ,仗着用 Delphi 写过几个简单的 COM ,以为用 BCB 也差不多,给果碰到不少问题 (幸好没有当时教他,不然一定出丑)。弄了半天才成功,于是把那个发给他的 Mail 整理了一下贴到这里来。


开始本来想写个 EXE 的(即 Out of process 的) COM object ,但发现 很多 COM 做成 EXE 都有问题,而我又不知道我那朋友熟不熟 DLL (即 In process ),而且 DLL 又不好调试, 只好改为 EXE 型的 Automation ,这是最容易的 COM 类型了吧。

开始做 Automation :

1.File|New Application (做 Automation 要有 Form ,即使是个空的也不要拿掉);

2.File|New... 选 ActiveX 页上的 Automation Object;

在对话框中输入 COM 名: AutoTest ,其它就用默认值。

3.在打开的 Type Library Editor (就是那个标题为 Project1.tlb 东东)中,左边的树中的 IAutoTest 上点右键, New 一个 Property ,会产生两个同名的东东,分别用于 Get 和 Set ,取名为 Hello (两个会自动变为一样的), 也可以在右边的 Attributes 页中的 Name 一页中输入 Hello 。然后同样在 Attributes 页中输入参数类型: BSTR , 或者在 Parameters 页中将 Type 改为 BSTR(Set) 和 BSTR *(Get ,注意,列表中的 BSTR 没有 * ,要自已输入一个)。

4.然后点顶上那个 Refresh Implementation 的按钮(按钮上的小图片是一张纸,里面有两个绿色的箭头成环绕状), 这一步很重要,每次修改完 TLB 文件都要点这个按钮再存盘,它将产生相应的代码。

5.在 AutoTestImpl.h 文件中找到如下内容:

// IAutoTest
public:

STDMETHOD(get_Hello(BSTR* Value));
STDMETHOD(set_Hello(BSTR Value));

这是 Refresh Implementation 时自动产生的代码,在后面加上:

private :
WideString FData;

Type Library Editor 产生的 CPP/H 文件除了这个以 Impl (即 Implementation )结尾的以外,还有两个,分别 是以 _ATL 和 _TLB 结尾的: _ATL 是自动产生的,通常不用变它,当然如果你有特别的要求并且你对 ATL 又很熟, 也可以改改它; _TLB 是由 Type Library Editor 自动维护,无须修改,改了也没用,你的任何修改都会 在 Refresh Implementation 时被改回来的。

再看 AutoTestImpl.cpp 文件,找到如下内容:

STDMETHODIMP TAutoTestImpl::get_Hello(BSTR* Value)
{
try
{

}
catch(Exception &e)
{
return Error(e.Message.c_str(), IID_IAutoTest);
}
return S_OK;
};

这也是自动产生的东东,在这个 try 中输入:

    *Value = FData;

在 Set 的 Try 中输入:

    FData = Value;

注意一定要用 try..catch 括起来,否则..。 Delphi 就不用,这没办法,因为 BCB 是用 ATL 的嘛,而 Delphi 是 用 Borland 自已搞的 DAX(Delphi ActiveX Extension) , BCB 就是在这点上和 Delphi 有很大不同。

6.Save All ,编译。

7.在命令行下运行:

Project1 /regserver

注册这个 COM ,注销则用:

Project1 /unregserver

记得删除 COM 之前一定要先注销,不然你只好到注册表里慢慢找了。


使用 Automation 的前期联编或叫先联编(Early Binding)的用法:

1.File|New Application

2.在 Project2 中加入 Project1_TLB.CPP

3.在 Unit2.h 中加入:

#include "Project1_TLB.h"

和:

private:    // User declarations
TCOMIAutoTest MyTest; // 加入的

4.在 Form 上放一个 Label 和一个 Button 。双击BUTTON,在事件响应中输入:

    try {
if ( !MyTest )
{
MyTest = CoAutoTest::Create( );
MyTest.Hello = WideString( "Hello world!" );
}
Label1->Caption = MyTest.Hello;
}
catch ( EOleSysError &e )
{
ShowMessage( e.Message );
}

5.Save All ,编译。

6.运行后,点 Button ,你会看到 Project1 也运行起来,同时 Label1 显示 "Hello world!" 。

当 Project2 退出时, Project1 也自动退出。会了吧。

注意:采用先联编的方法调用 COM 时,要求在调用者的机器上注册类型库(TLB),当然如果不是用 DCOM 的话,调用者与被调用者是在一台机器上, 这一点是肯定的具备的,但如果用 DCOM 就要注意了。

为了更好地表现 Project2 对 Project1 的操纵,我们可以对 Project1 作一点修改:

先在 Unit1 的 Form1 上放一个 Label ,再选择 AutoTestImpl.cpp ,然后 Alt+F11 选择 Unit1 确定即可在 AutoTestImpl 单元中加入

#include "Unit1.h"

在 AutoTestImpl.cpp 中,我们刚才在上面加过代码的地方,在 Set 的 Try 中:

    FData = Value;

一句之后加入:

    Form1->Label1->Caption = Value;

然后重新编译一下 Project1 即可,不用再注册了。

再次运行 Project2 ,点 Button ,这时不但会出现前面的效果,连 Project1 的 Form 里那个 Label 也显示 "Hello world!" 了。


另一种使用 Automation 的方法,被称为后期联编或叫后联编(Late Binding)的用法:

1.File|New Application

2.在Unit3.cpp中加入:

#include <ComObj.hpp>

3.在 Form 上放一个 Label 和一个 Button 。双击BUTTON,在事件响应中输入:

    Variant v = CreateOleObject( "Project1.AutoTest" );
v.Exec( PropertySet( "Hello" ) << "Hello world!" );
Label1->Caption = v.Exec( PropertyGet( "Hello" ) );

4.Save All ,编译。

5.运行后,点 Button ,你会看到 Project1 也运行起来,同时 Label1 显示 "Hello world!" , 然后 Project1 就退出了。

可能你已经发现,这个例子跟上个例子有一个不同之处在于:在这个例子中 Project1 只是运行一下子就退出了,而在 Project2 的例子中, Project1 直到 Project2 退出时才退出。这是因为在两个例子中 COM 变量是不同的,在这个例子中, COM 变量是一个 Variant 型的局部变量, 当 Button 事件处理完成后, v 即被释放,相应的 Project1 也就退出了;而在 Project2 的例子中, COM 变量是 Form1 的一个成员, 它要到 Form1 被释放时,才会被释放,因为 Form1 是主窗体,只有退出时才会被释放,这也就是在那个例子中, Project1 会跟 Project2 一起退出的原因。

实际上, COM 是由一个引用计数管理的,当存在多个对此 COM 的引用时,只有当这些引用全都释放时,这个 COM 才会结束。


补充:还是那个朋友不久前发了一个 e-Mail 问我关于用 IDispatch 接口访问 COM 的方法, 我以为 BCB 可能跟 Delphi 用类似的方法实现吧,于是简单地把在 Delphi 里用的方法套到 BCB 程序里,结果出错,研究多日没有结果, 只好发了个 e-Mail 向 Comanche(太可怕) 求救,总算是弄明白了(后来才发现,其实 BCB 里其实是有相应的 Example 的):

使用 IDispatch 接口(本质上说,后期联编也是间接使用了 IDispatch 接口来实现的)的用法:

1.File|New Application

2.在 Project4 中加入 Project1_TLB.CPP

3.在 Unit4.h 中加入:

#include <ComObj.hpp>
#include "Project1_TLB.h"

4.在 Form 上放一个 Label 和一个 Button 。双击BUTTON,在事件响应中输入:

    IAutoTestDisp v;
v.Bind( CreateOleObject( "Project1.AutoTest" ) );
try
{
v->Hello = WideString( "Hello world!" );
Label1->Caption = v->Hello;
}
__finally
{
v.Unbind( );
}

5.Save All ,编译。

6.运行后,点 Button ,你会看到 Project1 也运行起来,同时 Label1 显示 "Hello world!" , 然后 Project1 就退出了。

这个结果与 Project3 是完全一样的,也是用了局部变量。而程序则跟前两个例子都有点像,如 Project2 那样要把 Project1_TLB 单元加进来, 如 Project3 那样要 #include <ComObj.hpp> ,其实后联编和用 IDispatch 两种方法从本质上没太大区别,这两种方法和先联编的最大区别是: 先联编一定要注册,而这两种方法都不需要。后联编实际上是通过 IDispatch 的 GetIDsOfNames 和 Invoke 两个成员方法来实现的(具体请参见相关参考书), 而 Project4 的例子也是,只是方法不同而已:后联编是由 BCB 实现的,而 Project4 是在 Project1_TLB 单元实现的 (所以 Project4 要比 Project3 多加入一个 Project1_TLB 单元),详情可见其中关于 IAutoTestDisp 接口的部分, 因为用的是 ATL ,所以它其实是从模版类 IAutoTestDispT 通过 typedef 而得到的。


严格说来,上面三个调用 Automation 的例子只用了两种方法: VTable(虚表)映射和 IDispatch 。 因为 BCB 写的 COM 是用双重接口的,所以同时支持这两种方法。 Project2 是 VTable 映射的例子,通过将 COM 的方法映射到 C++ 的虚表中, 使 C++ 可以像调用原生的 Class 一样调用 COM ,这是最有效率,并且最直观的方法,但是需要在客户端注册相应的类型库(TLB); Project3 和 Project4 是两种不同的调用 IDispatch 接口的实现(其实还有一种,就是自己调用 IDispatch 的 GetIDsOfNames/Invoke 来实现), IDispatch 接口通过查询来取得 COM 所支持的方法,并调用之,效率较低,但它不需要注册类型库,并且可以用于像 VB 或脚本语言之类不支持虚表的语言。

其实写 COM 还是 Delphi 的 DAX(Delphi ActiveX eXtension)最好,简单得如同用 VB 写 COM (特别是在用到 Variant 类型时),而又比 VB 强大得多,比如线程模式等,而且支持双重接口,可以有高的效率。 BCB 为了保持和 VC 的兼容, 是使用 ATL 的,但是 Borland 又想使 BCB 像 Delphi 那样好,就仿照 DAX 的方法在 ATL 中加了一层,因为种种原因,不可能做到像 DAX 那样, 显得有点不伦不类,并且会有一些问题,比如会自动产生很多用处不大的代码,甚至偶尔有一点小 Bug ,所以我觉得还不如完全用 ATL 好了。 当然,不可否认, Borland 这么一做的确要比完全用 ATL 简单得很多。

这就是 COM ,有意思吧。用 BCB 写 COM 就是这么简单。

猛禽 Oct.25-2k, Jun.27-01