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