C++編碼轉換

每一種語言都有其自己處理對象的方式。例如,C++是在棧中創建對象,或者用new動態分配。因爲COM必須獨立於語言,所以COM庫爲自己提供對象管理例程。下面是對COM對象管理和C++對象管理所做的一個比較:

創建一個新對象
C++中,用new操作符,或者在棧中創建對象。
COM中,調用COM庫中的API。

刪除對象
C++中,用delete操作符,或將棧對象踢出。
COM中,所有的對象保持它們自己的引用計數。調用者必須通知對象什麼時候用完這個對象。當引用計數爲零時,COM對象將自己從內存中釋放。
由此可見,對象處理的兩個階段:創建和銷燬,缺一不可。當創建COM對象時要通知COM庫使用哪一個接口。如果這個對象創建成功,COM庫返回所請求接口的指針。然後通過這個指針調用方法,就像使用常規C++對象指針一樣。

創建COM對象
爲了創建COM對象並從這個對象獲得接口,必須調用COM庫的API函數,CoCreateInstance()。其原型如下:
HRESULT CoCreateInstance (
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID* ppv
);


以下是參數解釋:

rclsid
coclass的CLSID,例如,可以傳遞CLSID_ShellLink創建一個COM對象來建立快捷方式。

pUnkOuter
這個參數只用於COM對象的聚合,利用它向現有的coclass添加新方法。參數值爲null表示不使用聚合。

dwClsContext
表示所使用COM服務器的種類。本文使用的是最簡單的COM服務器,一個進程內(in-process)DLL,所以傳遞的參數值爲CLSCTX_INPROC_SERVER。注意這裏不要隨意使用CLSCTX_ALL(在ATL中,它是個缺省值),因爲在沒有安裝DCOM的Windows95系統上會導致失敗。

riid
請求接口的IID。例如,可以傳遞IID_IShellLink獲得IShellLink接口指針。

ppv
接口指針的地址。COM庫通過這個參數返回請求的接口。

當你調用CoCreateInstance()時,它負責在註冊表中查找COM服務器的位置,將服務器加載到內存,並創建你所請求的coclass實例。

以下是一個調用的例子,創建一個CLSID_ShellLink對象的實例並請求指向這個對象IShellLink接口指針。

HRESULT hr;
IShellLink* pISL;

hr = CoCreateInstance ( CLSID_ShellLink, // coclass 的CLSID
NULL, // 不是用聚合
CLSCTX_INPROC_SERVER, // 服務器類型
IID_IShellLink, // 接口的IID
(void**) &pISL ); // 指向接口的指針

if ( SUCCEEDED ( hr ) )
{
// 用pISL調用方法
}
else
{
// 不能創建COM對象,hr 爲出錯代碼
}

首先聲明一個接受CoCreateInstance()返回值的HRESULT和IShellLink指針。調用CoCreateInstance()來創建新的COM對象。如果hr接受到一個表示成功的代碼,則SUCCEEDED宏返回TRUE,否則返回FALSE。FAILED是一個與SUCCEEDED對應的宏用來檢查失敗代碼。

刪除COM對象

前面說過,你不用釋放COM對象,只要告訴它們你已經用完對象。IUnknown是每一個COM對象必須實現的接口,它有一個方法,Release()。調用這個方法通知COM對象你不再需要對象。一旦調用了這個方法之後,就不能再次使用這個接口,因爲這個COM對象可能從此就從內存中消失了。
如果你的應用程序使用許多不同的COM對象,因此在用完某個接口後調用Release()就顯得非常重要。如果你不釋放接口,這個COM對象(包含代碼的DLLs)將保留在內存中,這會增加不必要的開銷。如果你的應用程序要長時間運行,就應該在應用程序處於空閒期間調用CoFreeUnusedLibraries() API。這個API將卸載任何沒有明顯引用的COM服務器,因此這也降低了應用程序使用的內存開銷。
繼續用上面的例子來說明如何使用Release():

// 像上面一樣創建COM 對象, 然後,

if ( SUCCEEDED ( hr ) )
{
// 用pISL調用方法
// 通知COM 對象不再使用它
pISL->Release();
}


接下來將詳細討論IUnknown接口。


基本接口——IUnknown

每一個COM接口都派生於IUnknown。這個名字有點誤導人,其中沒有未知(Unknown)接口的意思。它的原意是如果有一個指向某COM對象的IUnknown指針,就不用知道潛在的對象是什麼,因爲每個COM對象都實現IUnknown。

IUnknown 有三個方法:

AddRef() – 通知COM對象增加它的引用計數。如果你進行了一次接口指針的拷貝,就必須調用一次這個方法,並且原始的值和拷貝的值兩者都要用到。在本文的例子中沒有用到AddRef()方法。
Release() – 通知COM對象減少它的引用計數。參見前面的Release()示例代碼段。
QueryInterface() – 從COM對象請求一個接口指針。當coclass實現一個以上的接口時,就要用到這個方法。

前面已經看到了Release()的使用,但如何使用QueryInterface()呢?當你用CoCreateInstance()創建對象的時候,你得到一個返回的接口指針。如果這個COM對象實現一個以上的接口(不包括IUnknown),你就必須用QueryInterface()方法來獲得任何你需要的附加的接口指針。QueryInterface()的原型如下:

HRESULT IUnknown::QueryInterface (
REFIID iid,
void** ppv );

以下是參數解釋:

iid
所請求的接口的IID。
ppv
接口指針的地址,QueryInterface()通過這個參數在成功時返回這個接口。

讓我們繼續外殼鏈接的例子。它實現了IShellLink 和IPersistFile接口。如果你已經有一個IShellLink指針,pISL,可以從COM對象請求IPersistFile接口:

HRESULT hr;
IPersistFile* pIPF;
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );

然後使用SUCCEEDED宏檢查hr的值以確定QueryInterface()的調用情況,如果成功的話你就可以象使用其它接口指針那樣使用新的接口指針,pIPF。但必須記住調用pIPF->Release()通知COM對象已經用完這個接口。

仔細做好串處理
這一部分將花點時間來討論如何在COM代碼中處理串。如果你熟悉Unicode 和ANSI,並知道如何對它們進行轉換的話,你就可以跳過這一部分,否則還是讀一下這一部分的內容。

不管什麼時候,只要COM方法返回一個串,這個串都是Unicode串(這裏指的是寫入COM規範的所有方法)。Unicode是一種字符編碼集,類似ASCII,但用兩個字節表示一個字符。如果你想更好地控制或操作串的話,應該將它轉換成TCHAR類型串。

TCHAR和以_t開頭的函數(如_tcscpy())被設計用來讓你用相同的源代碼處理Unicode和ANSI串。在大多數情況下編寫的代碼都是用來處理ANSI串和ANSI WindowsAPIs,所以在下文中,除非另外說明,我所說的字符/串都是指TCHAR類型。你應該熟練掌握TCHAR類型,尤其是當你閱讀其他人寫的有關代碼時,要特別注意TCHAR類型。

當你從某個COM方法返回得到一個Unicode串時,可以用下列幾種方法之一將它轉換成char類型串:


1、調用 WideCharToMultiByte() API。

2、調用CRT 函數wcstombs()。

3、使用CString 構造器或賦值操作(僅用於MFC )。

4、使用ATL 串轉換宏。

WideCharToMultiByte()

你可以用WideCharToMultiByte()將一個Unicode串轉換成一個ANSI串。此函數的原型如下:

int WideCharToMultiByte (
UINT CodePage,
DWORD dwFlags,
LPCWSTR lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCSTR lpDefaultChar,
LPBOOL lpUsedDefaultChar );

以下是參數解釋:

CodePage
Unicode字符轉換成的代碼頁。你可以傳遞CP_ACP來使用當前的ANSI代碼頁。代碼頁是256個字符集。字符0——127與ANSI編碼一樣。字符128——255與ANSI字符不同,它可以包含圖形字符或者讀音符號。每一種語言或地區都有其自己的代碼頁,所以使用正確的代碼頁對於正確地顯示重音字符很重要。

dwFlags
dwFlags 確定Windows如何處理“複合” Unicode字符,它是一種後面帶讀音符號的字符。如è就是一個複合字符。如果這些字符在CodePage參數指定的代碼頁中,不會出什麼事。否則,Windows必須對之進行轉換。

傳遞WC_COMPOSITECHECK使得這個API檢查非映射複合字符。
傳遞WC_SEPCHARS使得Windows將字符分爲兩段,即字符加讀音,如e`。
傳遞WC_DISCARDNS使得Windows丟棄讀音符號。
傳遞WC_DEFAULTCHAR使得Windows用lpDefaultChar參數中說明的缺省字符替代複合字符。
缺省行爲是WC_SEPCHARS。

lpWideCharStr
要轉換的Unicode串。

cchWideChar
lpWideCharStr在Unicode 字符中的長度。通常傳遞-1,表示這個串是以0x00結尾。

lpMultiByteStr
接受轉換的串的字符緩衝

cbMultiByte
lpMultiByteStr的字節大小。

lpDefaultChar
可選——當dwFlags包含WC_COMPOSITECHECK | WC_DEFAULTCHAR並且某個Unicode字符不能被映射到同等的ANSI串時所傳遞的一個單字符ANSI串,包含被插入的“缺省”字符。可以傳遞NULL,讓API使用系統缺省字符(一種寫法是一個問號)。

lpUsedDefaultChar
可選——指向BOOL類型的一個指針,設置它來表示是否缺省字符曾被插入ANSI串。可以傳遞NULL來忽略這個參數。

我自己都有點暈菜了……!,萬事開頭難啊……,不搞清楚這些東西就很難搞清楚COM的串處理。何況文檔中列出的比實際應用的要複雜得多。下面就給出瞭如何使用這個API的例子:

// 假設已經有了一個Unicode 串 wszSomeString...

char szANSIString [MAX_PATH];

WideCharToMultiByte ( CP_ACP, // ANSI 代碼頁
WC_COMPOSITECHECK, // 檢查重音字符
wszSomeString, // 原Unicode 串
-1, // -1 意思是串以0x00結尾
szANSIString, // 目的char字符串
sizeof(szANSIString), // 緩衝大小
NULL, // 肥缺省字符串
NULL ); // 忽略這個參數

調用這個函數後,szANSIString將包含Unicode串的ANSI版本。
wcstombs()
這個CRT函數wcstombs()是個簡化版,但它終結了WideCharToMultiByte()的調用,所以最終結果是一樣的。其原型如下:
size_t wcstombs (
char* mbstr,
const wchar_t* wcstr,
size_t count );


以下是參數解釋:

mbstr
接受結果ANSI串的字符(char)緩衝。

wcstr
要轉換的Unicode串。

count
mbstr參數所指的緩衝大小。

wcstombs()在它對WideCharToMultiByte()的調用中使用WC_COMPOSITECHECK | WC_SEPCHARS標誌。用wcstombs()轉換前面例子中的Unicode串,結果一樣:

wcstombs ( szANSIString, wszSomeString, sizeof(szANSIString) );

CString
MFC中的CString包含有構造函數和接受Unicode串的賦值操作,所以你可以用CString來實現轉換。例如:

// 假設有一個Unicode串wszSomeString...
CString str1 ( wszSomeString ); // 用構造器轉換

CString str2;
str2 = wszSomeString; // 用賦值操作轉換

ATL宏
ATL有一組很方便的宏用於串的轉換。W2A()用於將Unicode串轉換爲ANSI串(記憶方法是“wide to ANSI”——寬字符到ANSI)。實際上使用OLE2A()更精確,“OLE”表示的意思是COM串或者OLE串。下面是使用這些宏的例子:

#include
// 還是假設有一個Unicode串wszSomeString...
{
char szANSIString [MAX_PATH];
USES_CONVERSION; // 聲明這個宏要使用的局部變量
lstrcpy ( szANSIString, OLE2A(wszSomeString) );
}

OLE2A()宏“返回”轉換的串的指針,但轉換的串被存儲在某個臨時棧變量中,所以要用lstrcpy()來獲得自己的拷貝。其它的幾個宏是W2T()(Unicode 到 TCHAR)以及W2CT()(Unicode到常量TCHAR串)。
有個宏是OLE2CA()(Unicode到常量char串),可以被用到上面的例子中,OLE2CA()實際上是個更正宏,因爲lstrcpy()的第二個參數是一個常量char*,關於這個問題本文將在以後作詳細討論。
另一方面,如果你不想做以上覆雜的串處理,儘管讓它還保持爲Unicode串,如果編寫的是控制檯應用程序,輸出/顯示Unicode串時應該用全程變量std::wcout,如:

wcout << wszSomeString;
但是要記住,std::wcout只認Unicode,所以你要是“正常”串的話,還得用std::cout輸出/顯示。對於Unicode串文字量,要使用前綴L標示,如:

wcout << L"The Oracle says..." << endl << wszOracleResponse;

如果保持串爲Unicode,編程時有兩個限制:

—— 必須使用wcsXXX() Unicode串處理函數,如wcslen()。
—— 在Windows 9x環境中不能在Windows API中傳遞Unicode串。要想編寫能在9x和NT上都能運行的應用,必須使用TCHAR類型,詳情請參考MSDN。

用例子代碼總結上述內容
下面用兩個例子演示本文所講的COM概念。代碼中還包含了本文的例子工程。

使用單接口COM對象
第一個例子展示的是單接口COM對象。這可能是你碰到得最簡單的例子。它使用外殼中的活動桌面組件對象類(CLSID_ActiveDesktop)來獲得當前桌面牆紙的文件名。請確認系統中安裝了活動桌面(Active Desktop)。

以下是編程步驟:

初始化COM庫。 (Initialize)
創建一個與活動桌面交互的COM對象,並取得IActiveDesktop接口。

調用COM對象的GetWallpaper()方法。

如果GetWallpaper()成功,則輸出/顯示牆紙文件名。

釋放接口(Release())。

收回COM庫(Uninitialize)。

WCHAR wszWallpaper [MAX_PATH];
CString strPath;
HRESULT hr;
IActiveDesktop* pIAD;

// 1. 初始化COM庫(讓Windows加載DLLs)。通常是在程序的InitInstance()中調用

// CoInitialize ( NULL )或其它啓動代碼。MFC程序使用AfxOleInit()。

CoInitialize ( NULL );

// 2. 使用外殼提供的活動桌面組件對象類創建COM對象。

// 第四個參數通知COM需要什麼接口(這裏是IActiveDesktop).


hr = CoCreateInstance ( CLSID_ActiveDesktop,
NULL,
CLSCTX_INPROC_SERVER,
IID_IActiveDesktop,
(void**) &pIAD );

if ( SUCCEEDED(hr) )
{
// 3. 如果COM對象被創建成功,則調用這個對象的GetWallpaper() 方法。
hr = pIAD->GetWallpaper ( wszWallpaper, MAX_PATH, 0 );

if ( SUCCEEDED(hr) )
{
// 4. 如果 GetWallpaper() 成功,則輸出它返回的文件名字。
// 注意這裏使用wcout 來顯示Unicode 串wszWallpaper. wcout 是
// Unicode 專用,功能與cout.相同。
wcout << L"Wallpaper path is:/n " << wszWallpaper << endl << endl;
}
else
{
cout << _T("GetWallpaper() failed.") << endl << endl;
}


// 5. 釋放接口。

pIAD->Release();
}
else
{
cout << _T("CoCreateInstance() failed.") << endl << endl;
}


// 6. 收回COM庫。MFC 程序不用這一步,它自動完成。

CoUninitialize();

在這個例子中,輸出/顯示Unicode 串 wszWallpaper用的是std::wcout。

使用多接口的COM對象

第二個例子展示瞭如何使用一個提供單接口的COM對象QueryInterface()函數。其中的代碼用外殼的Shell Link組件對象類創建我們在第一個例子中獲得的牆紙文件的快捷方式
以下是編程步驟:

初始化COM 庫。
創建一個用於建立快捷方式的COM 對象並取得IShellLink 接口。
調用IShellLink 接口的SetPath()方法
調用對象的QueryInterface()函數並取得IPersistFile接口。
調用IPersistFile 接口的Save()方法。
釋放接口

收回COM庫


CString sWallpaper = wszWallpaper; // 將牆紙路徑轉換爲ANSI

IShellLink* pISL;
IPersistFile* pIPF;

// 1. 初始化COM庫(讓Windows 加載DLLs). 通常在InitInstance()中調用
// CoInitialize ( NULL )或其它啓動代碼。MFC 程序使用AfxOleInit() 。

CoInitialize ( NULL );

// 2. 使用外殼提供的Shell Link組件對象類創建COM對象。.
// 第四個參數通知COM 需要什麼接口(這裏是IShellLink)。

hr = CoCreateInstance ( CLSID_ShellLink,
NULL,
CLSCTX_INPROC_SERVER,
IID_IShellLink,
(void**) &pISL );

if ( SUCCEEDED(hr) )
{
// 3. 設置快捷方式目標(牆紙文件)的路徑。
hr = pISL->SetPath ( sWallpaper );

if ( SUCCEEDED(hr) )
{
// 4. 獲取這個對象的第二個接口(IPersistFile)。
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
if ( SUCCEEDED(hr) )
{
// 5. 調用Save() 方法保存某個文件得快捷方式。第一個參數是

// Unicode 串。

hr = pIPF->Save ( L"C://wallpaper.lnk", FALSE );

// 6a. 釋放IPersistFile 接口。
pIPF->Release();
}
}


// 6. 釋放IShellLink 接口。
pISL->Release();
}


// 輸出錯誤信息部分這裏省略。

// 7. 收回COM 庫。MFC 程序不用這一步,它自動完成。
CoUninitialize();

處理HRESULT

這一部分準備用SUCCEEDED 和 FAILED宏進行一些簡單的出錯處理。主要是深入研究從COM方法返回的HRESULT,以便達到完全理解和熟練應用。
HRESULT是個32位符號整數,其非負值表示成功,負值表示失敗。HRESULT有三個域:程度位(表示成功或失敗),功能碼和狀態碼。功能碼錶示HRESULT來自什麼組件或程序。微軟給不同的組件多賦予功能碼,如:COM、任務調度程序等都有功能碼。功能碼是個16位的值,僅此而已,沒有其它內在含義;它在數字和意義之間是隨意關聯的;類似GetLastError()返回的值。
如果你在winerror.h頭文件中查找錯誤代碼,會看到許多按照[功能]_[程度]_[描述]命名規範列出的HRESULT值,由組件返回的通用的HRESULT(類似E_OUTOFMEMORY)在名字中沒有功能碼。如,
REGDB_E_READREGDB: 功能碼 = REGDB, 指“註冊表數據庫(registry database)”;程度 = E 意思是錯誤(error);描述 = READREGDB 是對錯誤的描述(意思是不能讀註冊表數據庫)。
S_OK: 沒有功能碼——通用(generic)HRESULT;程度=S;表示成功(success);OK 是狀態描述表示一切都好(everything’s OK)。

好在有一種比察看winerror.h文件更容易的方法來確定HRESULT的意思。使用VC提供的錯誤查找工具(Error Lookup)可以輕鬆查到爲HRESULT內建功能碼。例如,假設你在CoCreateInstance()之前忘了調用CoInitialize()。CoCreateInstance()返回的值是0x800401F0。你只要將這個值輸入到錯誤查找工具按“Look Up”按鈕,便可以看到錯誤信息描述“尚未調用CoInitialize”

另外一種查找HRESULT描述的方法是在調試器中。假設有一個HRESULT變量是hres。在Watch窗口的左邊框中輸入“hres,hr”,表示想要看的值,“hr”便會通知VC顯示HRESULT所描述的值。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章