1.4
添加属性和方法
C++
程序员痛苦的原因之一就是类声明(通常是
.h
文件)和类定义(通常是
.cpp
文件)的分离。分离后就不得不同时维护这两个文件。任何时候,在一个文件添加一个成员函数,必须同时复制到另一个文件。手动完成这项工作是一个非常枯燥的过程。而对于用
C++
编写
COM
的程序员来说,这项工作会更加枯燥,他们还必须维护
IDL
文件中的同样定义。在向接口添加属性和方法的时候,希望
C++
开发环境能帮助把
IDL
定义的方法翻译为
C++
语言(如果可以,也适当包括一些
ATL
属性),分别写入到
.H
和
.CPP
文件中,并给实现代码留下适当的空间。现在,
Visual Studio
已经完全实现了这些功能。
在
Class View
里的
COM
接口上点击右键,在弹出的上下文子菜单中选择添加新的属性或者方法。图
1-7
显示了给
COM
接口添加属性的对话框。添加属性参数时可以指定参数类型和参数的方法(比如
[in]
和
[out]
)。
图
1-7
添加属性对话框
图
1-8
展示了添加属性向导的
IDL Attributes
标签可以设置的选项。选择不同的属性会在工程的
IDL
文件中插入不同的定义代码。任何情况下他们对类型库的影响都是一样的。有部分的属性只应用于少数环境中,图
1-8
所示的默认选择值通常能满足大多数的需要。向导结束后,无论是添加、删除、修改,你都可以直接在
IDL
文件中改变这些属性。
图
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
演示了添加方法的向导对话框。通过参数类型组合框、参数名称文本框、添加
/
删除按钮,可以给方法添加不同的输入、输出参数。
图
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
项就可以得到当前所有可用的接口。
图
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
的永久性”。