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
的永久性”。