COM組件設計與應用(四)

簡單調用組件

作者:楊老師

一、前言

  同志們、朋友們、各位領導,大家好。
 

  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呀......

 

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