1.4    添加属性和方法

 

C++ 程序员痛苦的原因之一就是类声明(通常是 .h 文件)和类定义(通常是 .cpp 文件)的分离。分离后就不得不同时维护这两个文件。任何时候,在一个文件添加一个成员函数,必须同时复制到另一个文件。手动完成这项工作是一个非常枯燥的过程。而对于用 C++ 编写 COM 的程序员来说,这项工作会更加枯燥,他们还必须维护 IDL 文件中的同样定义。在向接口添加属性和方法的时候,希望 C++ 开发环境能帮助把 IDL 定义的方法翻译为 C++ 语言(如果可以,也适当包括一些 ATL 属性),分别写入到 .H .CPP 文件中,并给实现代码留下适当的空间。现在, Visual Studio 已经完全实现了这些功能。

 

Class View 里的 COM 接口上点击右键,在弹出的上下文子菜单中选择添加新的属性或者方法。图 1-7 显示了给 COM 接口添加属性的对话框。添加属性参数时可以指定参数类型和参数的方法(比如 [in] [out] )。

 

r_1-7.JPG
1-7 添加属性对话框

 

1-8 展示了添加属性向导的 IDL Attributes 标签可以设置的选项。选择不同的属性会在工程的 IDL 文件中插入不同的定义代码。任何情况下他们对类型库的影响都是一样的。有部分的属性只应用于少数环境中,图 1-8 所示的默认选择值通常能满足大多数的需要。向导结束后,无论是添加、删除、修改,你都可以直接在 IDL 文件中改变这些属性。

 

r_1-8.JPG
1-8 接口属性的 IDL 特性

 

下面的阴影代码演示了向导生成的框架代码,我们仅仅只需要提供适当的实现代码(非阴影显示)。

 

STDMETHODIMP CCalcPi::get_Digits(LONG* pVal) {

  *pVal = m_nDigits;

  return S_OK;

}

 

STDMETHODIMP CCalcPi::put_Digits(LONG newVal) {

  if( newVal < 0 )

    return Error(L"Can't calculate negative digits of PI");

  m_nDigits = newVal;

    return S_OK;

}

 

同样的,在 Class View 里接口的右键菜单可以选择添加方法。图 1-9 演示了添加方法的向导对话框。通过参数类型组合框、参数名称文本框、添加 / 删除按钮,可以给方法添加不同的输入、输出参数。

 

r_1-9.JPG
1-9 添加方法向导对话框

 

添加后,向导会自动的更新 IDL 文件、 .H 头文件的接口定义,生成适当的 C++ 代码,提供我们框架以实现特殊的功能。阴影部分就是添加实现代码后留下的由向导生成的代码。

 

STDMETHODIMP CCalcPi::CalcPi(BSTR* pbstrPi) {

  _ASSERTE(m_nDigits >= 0);

 

  if( m_nDigits ) {

    *pbstrPi = SysAllocStringLen(L"3.", m_nDigits+2);

    if( *pbstrPi ) {

      for( int i = 0; i < m_nDigits; i += 9 ) {

        long nNineDigits = NineDigitsOfPiStartingAt(i+1);

        swprintf(*pbstrPi + i+2, 10, L"%09d", nNineDigits);

      }

      // Truncate to number of digits

      (*pbstrPi)[m_nDigits+2] = 0;

    }

  }

  else

    *pbstrPi = SysAllocString(L"3");

 

  return *pbstrPi ? S_OK : E_OUTOFMEMORY;

}

 

关于 COM 异常的说明,以及 ATL Error 函数( put_Digits 函数里),参考第四章“ ATL 对象”。

 

1.5    实现其他接口

 

       COM 的核心是接口,大多数 COM 对象都实现不止一个接口。即使是前面介绍的、由向导生成的 ATL 简单对象也实现了四个接口(一个自定义接口和三个标准接口)。如果希望你基于 ATL COM 类实现其他的接口,必须先定义接口。比如,你可以在工程的 IDL 文件中添加如下的接口定义:


[
    object,
    uuid("27ABEF5D-654F-4D85-81C7-CC3F06AC5693"),
    helpstring("IAdvertiseMyself Interface"),
    pointer_default(unique)
]
interface IAdvertiseMyself : IUnknown {
    [helpstring("method ShowAd")]
    HRESULT ShowAd(BSTR bstrClient);

};  


在工程中实现这个接口,只需要在
C++ 实现类的继承列表里添加继承项,然后把接口添加到 COM_MAP 中:

 

class ATL_NO_VTABLE CCalcPi :

    public ICalcPi,

    public IAdvertiseMyself {

 

BEGIN_COM_MAP(CCalcPi)

    COM_INTERFACE_ENTRY(ICalcPi)

    COM_INTERFACE_ENTRY(IAdvertiseMyself)

    ...

END_COM_MAP()

 

如果 IAdvertiseMyself 接口的方法中需要抛出 COM 异常,向导生成的 ISupportErrorInfo 实现也必须进行如下的修改。只需要简单的把 IID 添加到生成的数组中就可以了:


STDMETHODIMP CCalcPi::InterfaceSupportsErrorInfo(REFIID riid) {
   static const IID* arr[] = {
        &IID_ICalcPi,
        &IID_IAdvertiseMyself
    };
    for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++) {
         if (InlineIsEqualGUID(*arr[i],riid))
            return S_OK;
    }
    return S_FALSE;
}

以上修改完毕后,就需要实现这个新接口的 ShowAd 方法。

STDMETHODIMP CCalcPi::ShowAd(BSTR bstrClient) { 
        CComBSTR bstrCaption = OLESTR("CalcPi hosted by ");
   
        bstrCaption += (bstrClient && *bstrClient ?
bstrClient : OLESTR("no one"));    
        CComBSTR bstrText = OLESTR("These digits of pi brought to you by CalcPi!");
    
        MessageBox(0, COLE2CT(bstrText), COLE2CT(bstrCaption),
MB_SETFOREGROUND);
       
return S_OK;
}

  Visual Studio 提供了向导来简化上面的操作过程。在 Class 视图右键点击,从弹出菜单里选择 Add=>Implement Interface ,显示图 1-10 所示的实现接口向导对话框。通过向导可以很方便实现已经在类型库定义的接口。向导能够自动从当前工程的类型库提起接口信息。当然,你也可以选择另一种实现方法:在 IDL 文件定义接口,然后使用 MIDL 编译 IDL 文件,再参考编译输出的类型库实现这些接口。通过向导中的选择按钮可以选择三种不同的类型库:当前工程的类型库( Project );已注册类型库( Registry );未注册的类型库( File ),此时可以通过后面的浏览按钮选择文件路径。在 PiSvr 例子工程中,类型库是编译生成的 IDL 文件输出的,选择 Project 项就可以得到当前所有可用的接口。

 

r_1-10.JPG
1-10 实现接口向导

需要注意的是在向导的可实现接口列表里并没有已经实现的接口(例子中的 ICalcPi )。不幸的是,实现接口向导不支持类型库中没有的接口,它不能实现很多标准的 COM 接口,比如: IPersist IMarshal IOleItemContainer

 

更不幸的是,实现接口向导有 BUG 。在例子中,向导在接口基类列表添加如下的代码:


class ATL_NO_VTABLE CCalcPi :
    ... the usual stuff ...
    public IDispatchImpl<ICalcPi, &IID_ICalcPi, &LIBID_PiSvrLib,
        /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDispatchImpl<IAdvertiseMyself,
        &__uuidof(IAdvertiseMyself), &LIBID_PiSvrLib,
         /* wMajor = */ 1, /* wMinor = */ 0>
{
...

 

粗体部分的代码就是向导所加。向导把 IDispatchImpl 作为了基类模板,而 IDispatchImpl 是在实现双接口的时候才会使用。 IAdvertiseMyself 不是双接口,所以向导应该直接的从这个接口继承,要修改这个 BUG 很简单,只需要用下面的语句替换上面粗体部分即可:

 

public IAdvertiseMyself

 

即使有这个 BUG ,在实现一些庞大的接口时,向导的作用还是很明显。向导除了更新基类列表和 COM_MAP 外,也实现了接口所有方法的框架。在一些庞大的接口中,可以节省很多输入时间。不幸的是,框架只添加在 .H 头文件,而 .CPP 文件没有。

 

关于 ATL 允许 COM 类实现接口的其他方法,请参考第六章“接口映射”。关于 ShowAd 方法中使用的 CComBSTR 和字符串转换程序,请参考第二章“字符串和文本”。

 

 

1.6    支持脚本

 

任何时候,在 ATL 简单对象向导中如果选择双接口类型,定义的接口就是从 IDispatch 继承,并且在生成的 IDL 文件中以 dual 属性标识。因为是从 IDispatch 接口继承,我们所定义的接口就可以被脚本客户程序使用,如活动服务页( ASP )、网络浏览器( IE )和 Windows 脚本宿主( WSH )。当 COM 类支持 IDispatch 时,就可以在脚本环境中使用对象。下面就是在 HTML 中使用 CalcPi 对象实例的例子:

 

<object classid="clsid:859512CF-E4D8-450C-AF09-6578FE2F6DC2"

        id=objPiCalculator>

</object>

 

<script language=vbscript>

  ' Set the digits property

  objPiCalculator.digits = 5

 

  ' Calculate pi

  dim pi

  pi = objPiCalculator.CalcPi

 

  ' Tell the world!

  document.write "Pi to " & objPiCalculator.digits & _

    " digits is " & pi

</script>

 

关于如何处理脚本相关的数据类型: BSTR VARIANT ,请参考第二章“字符串和文本”、第三章“ ATL 智能类型”。

 

1.7    添加永久性

 

ATL 提供了基类以支持对象的永久性,即是把对象保存到永久性媒体(比如磁盘),然后从媒体中恢复。 COM 对象只要实现一些永久性接口就可以暴露这项功能: IPersistStreamInit IPersistStorage IPersistPropertyBag ATL 提供三个接口对应的实现: IPersistStreamInitImpl IPersistStorageImpl IPersistPropertyBagImpl COM 对象支持永久性只需要从这三个基类任意继承一个、并把接口添加到 COM_MAP ,在对应的实现基类里添加 m_bRequiresSave 数据成员。

 
class ATL_NO_VTABLE CCalcPi :
  public ICalcPi,
  public IAdvertiseMyself,
  public IPersistPropertyBagImpl<CCalcPi> {
public:
  ...
  // ICalcPi
public:
  STDMETHOD(CalcPi)(/*[out, retval]*/ BSTR* pbstrPi);
  STDMETHOD(get_Digits)(/*[out, retval]*/ long *pVal);
  STDMETHOD(put_Digits)(/*[in]*/ long newVal); 
public:
  BOOL m_bRequiresSave; //  支持永久性的基类使用
private:
   long m_nDigits;
};  

但是,现在工作还没有完成。 ATL 的永久性实现还需要知道你希望把对象的什么数据保存、恢复。 ATL 的永久性实现所依赖的这些信息存在于 PROP_MAP 对象属性表中,表中保存了我们希望在会话中保存的属性名称和派发标识符(在 IDL 文件中定义)的映射。因此,假设下面的接口:

 
[
object,
...
]
interface ICalcPi : IDispatch {
    [propget, id(1)] HRESULT Digits([out, retval] LONG* pVal);
    [propput, id(1)] HRESULT Digits([in] LONG newVal);
};

在我们实现
ICalcPi 时,应该如果包含 PROP_MAP

class ATL_NO_VTABLE CCalcPi : ...
{ 
...
public:
BEGIN_PROP_MAP(CCalcPi) 
      PROP_ENTRY("Digits", 1, CLSID_NULL)
END_PROP_MAP()
};

如果我们实现了 IPersistPropertyBag 接口,那么 IE 的例子代码可以使用 <param> 标签,使用永久性来扩展支持对象属性的初始化。

 

<object classid="clsid:E5F91723-E7AD-4596-AC90-17586D400BF7"

        id=objPiCalculator>

        <param name=digits value=5>

</object>

 

<script language=vbscript>

  ' Calculate pi

  dim pi

  pi = objPiCalculator.CalcPi

 

  ' Tell the world!

  document.write "Pi to " & objPiCalculator.digits &_

    " digits is " & pi

</script>

 

关于 ATL 永久性实现的更多信息,请参考第七章“ ATL 的永久性”。