程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 用SDK玩轉ActiveX

用SDK玩轉ActiveX

編輯:關於VC++

調用ActiveX控件?呃,這實在不是一件容易的事情:用各種封裝精良的Framework(MFC 、VCL等等)的話,最後成品EXE的體積難免偏大;用SDK雖然可以有效地減小這個體積,但是 往往又無從下手——總之,這似乎是一件魚與熊掌不能兼得的憾事。還好, “不容易”並不代表“不可能”,李馬在本文中要介紹給諸位的,就 是“玩轉”ActiveX的一種方法,這種方法包括了從ActiveX控件調用到ActiveX控 件事件處理的一切必要細節。當然,題目所說的“SDK”也並不是純粹的SDK,而 是借助了ATL的OLE支持,畢竟用SDK實現OLE容器太繁瑣了。

在開始正文之前,我還想 說明一下本文所面向的讀者群。首先,你必須對SDK的編程方式和COM組件的調用方式有所了 解,因為本文中的絕大部分示例代碼都與之相關,涉及到這方面的知識我也不會再加以解釋 ;其次,你可以不了解ATL,因為本文中對ATL的使用僅限於ActiveX的OLE容器,我也只是在 適當的地方給予簡要的說明;再次,你可以不了解COM連接點的知識,我在文中會給予詳細的 介紹。

那麼閒話毋庸贅敘,讓我們開始吧。

准備工作

現在讓我們來完 成代碼之外的事情,請按照以下步驟建立我們的工程:

1. 打開Visual C++,新建一 個Win32 Application(我名之為ActiveX)。

2. 新建一個Resource Script(資源腳 本),在其中添加一個對話框(我名之為IDD_MAIN_DLG)。

3. 在對話框上單擊右鍵 ,選擇“Insert ActiveX Control...”(如下圖)。在本文中,我以Microsoft Agent Control為例,所以在之後的列表之中選擇“Microsoft Agent Control 2.0”。

4. 完成後的對話框如下圖。

骨架代碼

現在就可以編寫代碼了。建立一個C++ Source File(C++源文件),在其中輸入下面的程序 骨架:

#include <atlbase.h>
CComModule _Module;
#include <atlwin.h>
#import "C:\WINNT\msagent\agentctl.dll"
using namespace AgentObjects;
#include "resource.h"
int WINAPI _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nShowCmd )
{
   _Module.Init( NULL, hInstance );
   _Module.Term();
   return 0;
}

然後,在工程設置中加入atl.lib,如下圖:

讓我們 再回過頭來看看上面的代碼。程序的頭三行就是我在本文開頭時所說到的“ATL的支持 ”,其中預處理的部分你大可以略去不管,你只需要了解的就是_Module這個全局變量 ,它保存了程序模塊的一些相關信息。並且,在WinMain之中的Init和Term已經包括了 CoInitialize、OleInitialize、CoUninitialize、OleUninitialize的初始化和卸載工作。

#import的一行表示導入Agent控件的類型庫,並且由於Agent控件的各個接口被封裝 在了library AgentObjects之中(這些東西可以使用Visual Studio自帶的工具 “OLE/COM Object Viewer”從agentctl.dll的類型庫接口定義之中看到),所以 要使用AgentObjects的命名空間——當然不用也無所謂,只不過是以後的使用會 稍稍麻煩一些。

現在你可以編譯鏈接這段代碼了。在編譯鏈接完成之後,你就可以在 工程目錄下的Debug或Release目錄下(取決於你的工程設置)發現名為agentctl.tlh和 agentctl.tli的兩個文件。你可以用文本方式打開它們看看,你會發現agentctl.tlh中是 agentctl.dll類型庫中各接口的C/C++支持以及各接口的智能指針定義;至於agentctl.tli之 中,則是一些更有趣的東西,在這裡我就不多介紹了。

使用ActiveX

骨架完成 後,就可以使用Agent這個ActiveX控件了。不過在使用之前,你需要把你曾經用來顯示對話 框的代碼寫成類似下面這個樣子:

g_hDlgMain = AtlAxCreateDialog( hInstance, MAKEINTRESOURCE( IDD_MAIN_DLG ), NULL, (DLGPROC)MainDlgProc, 0 );

對於這行 代碼我需要解釋三點。第一,由於我們的對話框中含有ActiveX控件,所以不能使用普通的 CreateDialog;第二,g_hDlgMain是一個全局變量,我需要在另一個類中使用它;第三,由 於我們需要顯示Agent助手而不顯示對話框,所以在此使用了無模式對話框——這 樣就可以創建一個不可見的對話框了。

現在可以在對話框的回調函數中使用ActiveX 控件了。以Agent控件為例,通常使用ActiveX是類似這個樣子:

CAxWindow wndAgent = GetDlgItem( hDlg, IDC_AGENT );

IAgentCtlExPtr pAgent; // IAgentCtlExPtr的 定義來自於agentctl.tlh

HRESULT hr = wndAgent.QueryControl( __uuidof( IAgentCtlEx ), ( LPVOID * )&pAgent );

然後,就可以利用pAgent指針對Agent 控件進行操作了。你可以在對話框回調函數中的WM_INITDIALOG中加入下面的代碼來測試效果 :

case WM_INITDIALOG:
     {
       CAxWindow wndAgent;
       IAgentCtlExPtr pAgent;
       IAgentCtlCharactersPtr pChars;
       IAgentCtlCharacterExPtr pMerlin;
       IAgentCtlRequestPtr pRequest;
       HRESULT hr;
       wndAgent = GetDlgItem( hDlg, IDC_AGENT );
       hr = wndAgent.QueryControl( __uuidof( IAgentCtlEx ), ( LPVOID * ) &pAgent );
       // 獲取角色文件路徑
       TCHAR szPath[MAX_PATH];
       GetWindowsDirectory( szPath, MAX_PATH );
       lstrcat( szPath, _T("\\msagent\\chars\\merlin.acs") );
        // 進行連接
       hr = pAgent->put_Connected( (VARIANT_BOOL)-1 );
       // 獲得角色列表
       hr = pAgent->get_Characters( &pChars );
       // 裝載角色
       pRequest = pChars->Load( _bstr_t("merlin"), CComVariant (szPath) );
       pMerlin = pChars->Character( _bstr_t("merlin") );
        // 顯示角色
       pMerlin->Show();
       // 計算屏幕中央坐標,並移動
       short x = ( GetSystemMetrics( SM_CXFULLSCREEN ) - pMerlin->GetWidth() ) / 2;
       short y = ( GetSystemMetrics( SM_CYFULLSCREEN ) - pMerlin->GetHeight() ) / 2;
       pRequest = pMerlin->MoveTo( x, y );
       pRequest = pMerlin->Speak( CComVariant("右鍵單擊我,選擇“隱 藏”以結束程序。") );
     }
     break;

這裡我有兩點需要說明。第一,事實上對COM接口的調用需要 非常嚴謹地判斷每個方法返回值的成功與否,而在這裡出於篇幅考慮我便將其一概略去,你 可以在配套源代碼中看到這些容錯處理;第二,在使用Agent控件之前必須將它的Connected 狀態置為真(-1)——也就是pAgent->put_Connected的一句,否則以下的方 法都會失敗,而在MFC的封裝下倒可以略去這一句,可能MFC有個自動連接的過程。

到 現在為止,你應該已經可以把這個助手顯示在屏幕上了,運行看看效果吧。

連接點

可 能你注意到了,在WM_INITDIALOG的最後一句,我讓Agent助手說了一句話:“右鍵單擊 我,選擇‘隱藏’以結束程序。”但事實上如果你運行這段代碼的話,你在 助手身上右擊鼠標並選擇“隱藏”,助手雖然隱藏了起來,程序卻並沒有退出, 這是怎麼回事呢?答案是顯而易見的——我並沒有處理Agent控件的Hide(隱藏) 事件。

那麼,又如何處理這個事件呢?——在MFC中,ActiveX控件的事件 是通過一張宏映射表來實現的,類似下面這個樣子:

BEGIN_EVENTSINK_MAP (CActiveXDlg, CDialog)
   //{{AFX_EVENTSINK_MAP(CActiveX)
   ON_EVENT(CActiveXDlg, IDC_AGENT, 7 /* Hide */, OnHideAgent, VTS_BSTR VTS_I2)
   //}}AFX_EVENTSINK_MAP
END_EVENTSINK_MAP()

對於SDK來說, 就沒有那麼簡單了。我們必須從COM最底層的機制入手,也就是連接點。

那麼,什麼 又是連接點呢?

在有的時候——比如我們這裡的ActiveX的事件處理,COM 服務器需要將一個接口開放給客戶端,然後由客戶端實現這個接口供服務器進行回調,這就 是COM的連接點事件。下面,我用兩個C++類來模擬連接點和COM服務器之間的關系。

class CSink
{
public:
   void DoSomeOtherThings()
   {
     cout << "I still want to do some other things, so this sentence is from sink." << endl;
   }
};
class ISomeInterface
{
   CSink *m_pSink;
public:
   ISomeInterface() : m_pSink( NULL ) {}
   void SetSink( CSink *pSink )
   {
     m_pSink = pSink;
   }
   void DoSomething()
   {
     cout << "Do something...." << endl;
     if ( NULL != m_pSink )
     {
       m_pSink->DoSomeOtherThings();
     }
   }
};

然後,在程序中這樣調用:

CSink sink;

ISomeInterface *p = new ISomeInterface;

p->SetSink( &sink );

p->DoSomething();

delete p;

如你所見, ISomeInterface並不是一個嚴格意義上的“接口”,而是一個實實在在的類,不 過既然這是段模擬代碼,所以我也沒有必要做得惟妙惟肖了——同樣,你也可以 把new和delete的過程看作一個接口CoCreateInstance和Release的過程。CSink類則是用來處 理ISomeInterface類回調事件的,在COM中我們稱之為“接收器”。那麼, ISomeInterface::SetSink就是設置連接點的“連接”過程, CSink::DoSomeOtherThings()則是接收器的事件處理實現。

接收器的實現

現 在,我們就要開始本文最核心的部分了。通常對於COM接收器來講,我們可以將它理解為一個 沒有CLSID的COM組件類,也就是說,我們只需要給出它的實現,並提供它的指針給服務器就 足夠了。而且對於我們的實現語言——C++來說,這個實現過程其實和編寫一個 C++類沒有什麼兩樣。那麼,首先讓我把這個實現完整地呈現給你,然後再逐一解釋吧。

// 處理連接點事件的接收器實現

class CSink : public IDispatch
{
public:
   // 構造/析構函數
   CSink() : m_uRef( 0 ) {}
   virtual ~CSink() {}
   // IUnknown接口實現
   STDMETHODIMP QueryInterface( REFIID iid, void **ppvObject )
   {
     if ( iid == __uuidof( _AgentEvents ) )
     {
       *ppvObject = (_AgentEvents *)this;
       AddRef();
       return S_OK;
     }
     if ( iid == IID_IUnknown )
     {
       *ppvObject = (IUnknown *)this;
       AddRef();
       return S_OK;
     }
     return E_NOINTERFACE;
   }
   ULONG STDMETHODCALLTYPE AddRef()
   {
     m_uRef++;
     return m_uRef;
   }
   ULONG STDMETHODCALLTYPE Release()
   {
     ULONG u = m_uRef--;
     if ( 0 == m_uRef )
     {
       delete this;
     }
     return u;
   }
   // IDispatch接口實現
   STDMETHODIMP GetTypeInfoCount( UINT *pctinfo )
   {
     return E_NOTIMPL;
   }
   STDMETHODIMP GetTypeInfo( UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
   {
     return E_NOTIMPL;
   }
   STDMETHODIMP GetIDsOfNames( REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
   {
     return E_NOTIMPL;
   }
   STDMETHODIMP Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams,
             VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr )
   {
     HRESULT hr = S_OK;
     if ( NULL != pDispParams && 7 == dispIdMember )
     {
       if ( 2 == pDispParams->cArgs )
       {
         if ( VT_I2 == pDispParams->rgvarg[0].vt && VT_BSTR == pDispParams->rgvarg[1].vt )
         {
           OnHide( pDispParams->rgvarg[1].bstrVal, pDispParams->rgvarg[0].iVal );
         }
         else // 類型錯誤
         {
           hr = DISP_E_TYPEMISMATCH;
         }
       }
       else // 參數個數錯誤
       {
         hr = DISP_E_BADPARAMCOUNT;
       }
     }
     return hr;
   }
   // 要處理的_AgentEvents事件
   STDMETHODIMP OnHide( _bstr_t CharacterID, short Cause )
   {
     PostMessage( g_hDlgMain, WM_CLOSE, 0, 0 );
     return S_OK;
   }
private:
   ULONG m_uRef;
};

1. 我們可以用OLE/COM Object Viewer從 agentctl.dll的類型庫接口定義之中看到以下的內容:

dispinterface _AgentEvents {

...

這樣我們可以很容易猜到,這個接口就是Agent控件開放給我們處理連接 點事件用的。這一句用C++的語法來表示,就是:

// 摘自 agentctl.tlh

  struct __declspec(uuid("f5be8bd4-7de6-11d0- 91fe-00c04fd701a5"))
   _AgentEvents : IDispatch
   {
     // ...

也就是說,這個接口繼承自IDispatch。IDispatch接口稱作 “調度”接口,通常用來實現對一些符號解釋型語言(如Visual Basic)調用COM 接口的支持。關於這個接口的詳細情況你可以參考MSDN,裡面有非常詳盡的介紹。在這裡我 使接收器亦繼承自IDispatch,是因為我只需要處理Hide一個事件,而若將接收器繼承自 _AgentEvent,那麼我必須完全實現_AgentEvent接口的全部方法,這將會是一個非常浩大的 工程——即使是將除Hide之外的所有方法都返回E_NOTIMPL。

2. 我是前說 過,可以將接收器理解為一個沒有CLSID的COM組件類。因此,接收器必須完全按照COM組件的 規格來實現,也就是你所看到的AddRef、Release和QueryInterface的部分。不過,接收器終 究有著它自己的特定性,所以我們可以簡化QueryInterface,並且可以將IDispatch的 GetTypeInfoCount、GetTypeInfo和GetIDsOfNames直接返回E_NOTIMPL。

3. 現在來到 CSink::Invoke的部分。既然COM擁有語言無關的特性,那麼就意味著像Visual Basic這樣的 符號解釋型語言也可以處理ActiveX控件的事件。從這一點我們可以猜想到,所有事件都是經 由IDispatch::Invoke調度的——事實上從MFC的事件映射表就可以看出來。所以 ,我們也通過Invoke來捕獲ID為7的Hide事件,在檢驗一切條件都符合後便調用我們自己的處 理函數OnHide。

4. 在CSink::Invoke中有著大量的條件判斷,這是為了代碼的嚴謹, 從調度ID、參數類型、參數個數等幾個方面來確定本次調用無誤之後才調用OnHide。也就是 說,CSink::Invoke和CSink::OnHide完全可以寫成這個樣子:

   STDMETHODIMP CSink::Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams,
                 VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr )
   {
     if ( 7 == dispIdMember )
     {
       OnHide();
     }
     return S_OK;
   }
   void CSink::OnHide()
   {
     PostMessage( g_hDlgMain, WM_CLOSE, 0, 0 );
   }

看起來的確是簡單多了,不過我還是建議你使用前面的方法。

連接點的設置

連接點的使用非常簡單,很模式化的代碼:

// 設置連 接點的過程開始

IConnectionPointContainer *pCPC = NULL;

// 查詢連接點 容器

hr = pAgent->QueryInterface( IID_IConnectionPointContainer, (void **)&pCPC );

// 查找連接點

hr = pCPC->FindConnectionPoint( __uuidof( _AgentEvents ), &pCP );

// 這時連接點容器已經沒用了,釋放之

pCPC->Release();

pCPC = NULL;

// 創建通知對象

CSink *pSink = new CSink;

hr = pSink->QueryInterface( IID_IUnknown, (void **) &pSinkUnk );

// 對連接點進行設置

hr = pCP->Advise( pSinkUnk, &dwCookie );

需要注意的是,這段代碼必須放在pAgent->put_Connected之後 ,否則連接點的設置就會失敗。另外,這段代碼中有幾個變量是定義在回調函數頭部的 static變量,如下:

static IConnectionPoint *pCP = NULL;

static IUnknown *pSinkUnk = NULL;

static DWORD dwCookie = 0;

在程序結束的時 候,這樣釋放連接點:

pCP->Unadvise( dwCookie );

pCP->Release ();

pSinkUnk->Release();

需要注意的是pSinkUnk->Release()一句。 由於先前進行了接口查詢QueryInterface使得引用計數增加,所以必須在結束使用的時候調 用Release。

到現在為止,調用ActiveX的大致過程和原理我已經介紹完了。由於示例 工程的代碼是一段一段地根據文章邏輯而無序引用的,所以這可能會給諸位帶來實現上的麻 煩,在此李馬給大家賠個不是。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved