一、前言
同志們、朋友們、各位領導,大家好。
| |
VCKBASE 不得了, |
|
| |
網友眾多文章好。 |
|
| |
組件設計怎麼學? |
|
| |
知識庫裡悶頭找! |
|
| |
|
摘自---楊老師打油集錄 |
在 VCKBASE 的頂力支援下,在各位網友回帖的鼓勵下,我才能順利完成系列論文的前三回。書到本回,我們終於開始寫代碼啦。寫點啥那?恩,有了!咱們先從如何調用現成的簡單的組件開始吧,同時也順便介紹一些相關的知識。
二、組件的啟動和釋放
在第三回中,大家用“小本本”記錄了一個原則:COM 組件是運行在分布式環境中的 。於是,如何啟動組件立刻就遇到了嚴重的問題,大家看這段代碼:
p = new 對象;
p->對象函數();
delete p;
這樣的代碼再熟悉不過了,在本地進程中運行是不會有問題的。但是你想想,如果這個對象是在“地球另一邊”的電腦
上,結果會如何?嘿嘿,C++ 在設計 new
的時候,可沒有考慮遠端實現呀(電腦語言當然不會,也沒必要去設計)。因此啟動組件、調用介面的功能,當然就由 COM 系統來實現了。
圖一 組件調用機制
由可以看出,當調用組件的時候,其實是依靠代理(運行在本地)和存根(運行在遠端)之間的通訊完成的。具體來說,當客戶程式通過
CoCreateInstance() 函數啟動組件,則代理接管該調用,它和存根通訊,存根則它所在的本地(相對於客戶程式來說就是遠程了)執行
new 操作載入對象。對於初學者,你可以不用理它,代理和存根對我們來說是透明的。只要大約知道是怎麼一回事就一切OK了。
問題又來了,這個遠端對象什麼時候消滅呢?在第二回介
紹介面概念的時候,當時我們特意忽略了兩個函數,就是IUnknown::AddRef()和IUnknown::Release(),從函數名就能猜到
了,一個是對內部引用記數器(Ref)加1,一個是釋放(減1),當記數器減為0的時候,就是釋放的機會啦。看起來很複雜,沒辦法,因為這是在介紹原理。
其實在我們寫程式的時候到比較簡單,請大家遵守幾個原則:
1、啟動組件得到一個介面指標(Interface)後,不要調用AddRef()。因為系統知道你得到了一個指標,所以它已經幫你調用了AddRef()函數;
2、通過QueryInterface()得到另一個介面指標後,不要調用AddRef()。因為......和上面的道理一樣;
3、當你把介面指標賦值給(儲存到)另一個變數中的時候,請調用AddRef();
4、當不需要再使用介面指標的時候,務必執行Release()釋放;
5、當使用智能指標的時候,可以省略指標的維護工作;(注1)
三、記憶體配置和釋放
自從學習了C語言,老師就教導我們說:對於動態記憶體的申請和釋放,一定要遵守“誰申請,誰釋放”的原則。在此原則的指導下,不僅是我、不僅是你,就連特級大師都設計了這樣怪怪的函數:
| 函數 |
說明 |
評論 |
| GetWindowText(HWND,LPTSTR,int) |
取得視窗標題。需要在參數中給出儲存標題所使用的記憶體指標,和這塊記憶體的尺寸。 |
暈!我又不知道視窗標題的長度,居然還要我提供尺寸?!沒辦法,只能估摸著給一個大一些的尺寸吧。 |
| sprintf(char *,const char *,...) |
格式化一個字串。這個函數不用給出緩衝區的長度啦。 |
恩,雖然不用給出長度了,但你敢給個小尺寸嗎?哼! |
int CListBox::GetTextLen(int) CListBox::GetText(int,LPTSTR) |
取得列表窗中子項目的標題。需要調用兩個函數,先取得長度,然後分配記憶體,再實際取得標題內容。 |
真煩! |
說實在的,不但函數調用者感覺彆扭,就連函數設計者心情也不會爽的,而這一切都是為了滿足所謂“誰申請,誰釋放”的原則。
解決這個問題最好的方式就是:函數內部根據實際需要動態申請記憶體,而調用者負責釋放。這雖然違背了上述原則,但 COM
從方便性和效率出發,確實是這麼設計的。
| |
C語言 |
C++語言 |
Windows 平台 |
COM |
IMalloc 介面 |
BSTR |
| 申請 |
malloc() |
new |
GlobalAlloc() |
CoTaskMemAlloc() |
Alloc() |
SysAllocString() |
| 重新申請 |
realloc() |
|
GlobalReAlloc() |
CoTaskRealloc() |
Realloc() |
SysReAllocString() |
| 釋放 |
free() |
delete |
GlobalFree() |
CoTaskMemFree() |
Free() |
SysFreeString() |
以上這些函數必須要按類型配合使用(比如:new 申請的記憶體,則必須用 delete 釋放)。在 COM 內部,當然你可以隨便使用任何類型的記憶體配置釋放函數,但組件如果需要與客戶進行記憶體的互動,則必須使用上表中的後三類函數族。
1、BSTR 記憶體在上回書中,已經有比較豐富的介紹了,不再重複;
2、CoTaskXXX()函數族,其本質上就是調用C語言的函數(malloc...);
3、IMalloc 介面又是對 CoTaskXXX() 函數族的一個封裝。封裝後,同時增強了一些功能,比如:IMalloc::GetSize()可以取得尺寸,使用 IMallocSpy 可以監視記憶體的使用;
四、參數傳遞方向
在C語言的函式宣告中,尤其當參數為指標的時候,你是看不出它傳遞方向的。比如:
void fun(char * p1, int * p2); 請問,p1、p2 哪個是入參?哪個是出參?甚或都是入參或都是出參?由於牽扯到記憶體配置和釋放等問題,COM 需要明確標註參數方向。以後我們寫程式,就類似下面的樣子:
HRESULT Add([in] long n1, [in] long n2, [out] long *pnSum); // IDL檔案(注2)
STDMETHOD(Add)(/*[in]*/ long n1, /*[in]*/ long n2, /*[out]*/ long *pnSum); // .h檔案
如果參數是動態分配的記憶體指標,那麼遵守如下的規定:
| 方向 |
申請人 |
釋放人 |
提示 |
| [in] |
調用者 |
調用者 |
組件接收指標後,不能重新分配記憶體 |
| [out] |
組件 |
調用者 |
組件返回指標後,調用者“愛咋咋地”(注3) |
| [in,out] |
調用者 |
調用者 |
組件可以重新分配記憶體 |
五、樣本程式
樣本一、由 CLSID 得到 ProgID。(程式以 word 為例子。如果運行不正確,嘿嘿,你沒有安裝 word 吧?)
::CoInitialize( NULL );
HRESULT hr;
// {000209FF-0000-0000-C000-000000000046} = word.application.9
CLSID clsid = {0x209ff,0,0,{0xc0,0,0,0,0,0,0,0x46}};
LPOLESTR lpwProgID = NULL;
hr = ::ProgIDFromCLSID( clsid, &lpwProgID );
if ( SUCCEEDED(hr) )
{
::MessageBoxW( NULL, lpwProgID, L"ProgID", MB_OK );
IMalloc * pMalloc = NULL;
hr = ::CoGetMalloc( 1, &pMalloc ); // 取得 IMalloc
if ( SUCCEEDED(hr) )
{
pMalloc->Free( lpwProgID ); // 釋放ProgID記憶體
pMalloc->Release(); // 釋放IMalloc
}
}
::CoUninitialize();
樣本二、如何使用“瀏覽檔案夾”選擇對話窗。
CString BrowseFolder(HWND hWnd, LPCTSTR lpTitle)
{
// 調用 SHBrowseForFolder 取得目錄(檔案夾)名稱
// 參數 hWnd: 父視窗控制代碼
// 參數 lpTitle: 視窗標題
char szPath[MAX_PATH]={0};
BROWSEINFO m_bi;
m_bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT;
m_bi.hwndOwner = hWnd;
m_bi.pidlRoot = NULL;
m_bi.lpszTitle = lpTitle;
m_bi.lpfn = NULL;
m_bi.lParam = NULL;
m_bi.pszDisplayName = szPath;
LPITEMIDLIST pidl = ::SHBrowseForFolder( &m_bi );
if ( pidl )
{
if( !::SHGetPathFromIDList ( pidl, szPath ) ) szPath[0]=0;
IMalloc * pMalloc = NULL;
if ( SUCCEEDED ( ::SHGetMalloc( &pMalloc ) ) ) // 取得IMalloc分配器介面
{
pMalloc->Free( pidl ); // 釋放記憶體
pMalloc->Release(); // 釋放介面
}
}
return szPath;
}
樣本三、在視窗中顯示一幅 JPG 圖象。
void CxxxView::OnDraw(CDC* pDC)
{
::CoInitialize(NULL); // COM 初始化
HRESULT hr;
CFile file;
file.Open( "c://aa.jpg", CFile::modeRead | CFile::shareDenyNone ); // 讀入檔案內容
DWORD dwSize = file.GetLength();
HGLOBAL hMem = ::GlobalAlloc( GMEM_MOVEABLE, dwSize );
LPVOID lpBuf = ::GlobalLock( hMem );
file.ReadHuge( lpBuf, dwSize );
file.Close();
::GlobalUnlock( hMem );
IStream * pStream = NULL;
IPicture * pPicture = NULL;
// 由 HGLOBAL 得到 IStream,參數 TRUE 表示釋放 IStream 的同時,釋放記憶體
hr = ::CreateStreamOnHGlobal( hMem, TRUE, &pStream );
ASSERT ( SUCCEEDED(hr) );
hr = ::OleLoadPicture( pStream, dwSize, TRUE, IID_IPicture, ( LPVOID * )&pPicture );
ASSERT(hr==S_OK);
long nWidth,nHeight; // 寬高,MM_HIMETRIC 模式,單位是0.01毫米
pPicture->get_Width( &nWidth ); // 寬
pPicture->get_Height( &nHeight ); // 高
////////原大顯示//////
CSize sz( nWidth, nHeight );
pDC->HIMETRICtoDP( &sz ); // 轉換 MM_HIMETRIC 模式單位為 MM_TEXT 像素單位
pPicture->Render(pDC->m_hDC,0,0,sz.cx,sz.cy,
0,nHeight,nWidth,-nHeight,NULL);
////////按視窗尺寸顯示////////
//CRect rect;GetClientRect(&rect);
//pPicture->Render(pDC->m_hDC,0,0,rect.Width(),rect.Height(),
//0,nHeight,nWidth,-nHeight,NULL);
if ( pPicture ) pPicture->Release();// 釋放 IPicture 指標
if ( pStream ) pStream->Release(); // 釋放 IStream 指標,同時釋放了 hMem
::CoUninitialize();
}
樣本四、在案頭建立捷徑
在閱讀代碼之前,先看一下關於“捷徑”組件的結構。
圖二、捷徑群組件的介面結構
從結構圖中可以看出,“捷徑”組件(CLSID_ShellLink),有3個(其實不止)介面,每個介面完成一組相關功能的函數。
IShellLink 介面(IID_IShellLink)提供捷徑的參數讀寫功能(見圖三),IPersistFile
介面(IID_IPersistFile)提供捷徑持久性檔案的讀寫功能。對象的持久性(注5),是一個非常常用,並且功能強大的介面家族。但今天,
我們只要瞭解其中兩函數,就可以了:IPersistFile::Save()和IPersistFile:Load()。(注6)
圖三、捷徑中的各種屬性
#include < atlconv.h >
void CreateShortcut(LPCTSTR lpszExe, LPCTSTR lpszLnk)
{
// 建立塊捷方式
// 參數 lpszExe: EXE 檔案全路徑名
// 參數 lpszLnk: 捷徑檔案全路徑名
::CoInitialize( NULL );
IShellLink * psl = NULL;
IPersistFile * ppf = NULL;
HRESULT hr = ::CoCreateInstance( // 啟動組件
CLSID_ShellLink, // 捷徑 CLSID
NULL, // 彙總用(注4)
CLSCTX_INPROC_SERVER, // 進程內(Shell32.dll)服務
IID_IShellLink, // IShellLink 的 IID
(LPVOID *)&psl ); // 得到介面指標
if ( SUCCEEDED(hr) )
{
psl->SetPath( lpszExe ); // 全路徑程式名
//psl->SetArguments(); // 命令列參數
//psl->SetDescription(); // 備忘
//psl->SetHotkey(); // 快速鍵
//psl->SetIconLocation(); // 表徵圖
//psl->SetShowCmd(); // 視窗尺寸
// 根據 EXE 的檔案名稱,得到目錄名
TCHAR szWorkPath[ MAX_PATH ];
::lstrcpy( szWorkPath, lpszExe );
LPTSTR lp = szWorkPath;
while( *lp ) lp++;
while( ''//'' != *lp ) lp--;
*lp=0;
// 設定 EXE 程式的預設工作目錄
psl->SetWorkingDirectory( szWorkPath );
hr = psl->QueryInterface( // 尋找持久性檔案介面指標
IID_IPersistFile, // 持久性介面 IID
(LPVOID *)&ppf ); // 得到介面指標
if ( SUCCEEDED(hr) )
{
USES_CONVERSION; // 轉換為 UNICODE 字串
ppf->Save( T2COLE( lpszLnk ), TRUE ); // 儲存
}
}
if ( ppf )ppf->Release();
if ( psl )psl->Release();
::CoUninitialize();
}
void OnXXX()
{
CreateShortcut(
_T("c://winnt//notepad.exe"), // 記事本程式。注意,你的系統是否也是這個目錄?
_T("c://Documents and Settings//Administrator//案頭//我的記事本.lnk")
);
// 案頭上建立捷徑(lnk)檔案的全路徑名。注意,你的系統是否也是這個目錄?
// 如果用程式實現尋找案頭的路徑,則可以查註冊表
// HKEY_CURRENT_USER/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders
}
七、小結
本回介紹的內容比較實用。大家不要只抄襲代碼,而一定要理解它。結合 MSDN
的說明去思索代碼、理解其含義。好了,想方設法把代碼忘掉!三天后(如過你還沒有忘記,那就再過三天),你在不參考範例程式碼,但可以隨便翻閱 MSDN
的情況下,自己能獨立地再次完成這四個常式,那麼恭喜你,你已經入門了:0) 從下回開始,我們要用 ATL 做 COM
的開發工作啦,您老人家準備好了嗎?
作業,留作業啦......
1、你已經學會如何建立捷徑了,那麼你知道怎麼讀取它的屬性嗎?(如果寫不出這個程式,那麼你就不用繼續學習了。因為......動點腦筋呀!我還沒有見過象你這麼笨的學生呢!)
2、樣本程式三中使用了 IPicture 介面顯示一個 JPG 圖象。那麼你現在去完成一個功能,把 JPG 檔案轉換為 BMP 檔案。
注1:智能指標的概念和用法,後續介紹。
注2:IDL 檔案,下回就要介紹啦。
注3:東北話,想幹什麼都可以,反正我不管啦。
注4:彙總,也許在第30回中介紹吧:-)
注5:持久性,IPersistXXXXXX是一個非常強大的介面家族,後續介紹。
注6:想知道 IShellLink、IPersistFile介面的所有函數嗎?別愣著,快去看MSDN呀......