調用 ActiveX 控制項向來棘手:MFC/VCL 等封裝框架雖易用但 EXE 體積龐大;純 SDK 雖精簡卻無從下手。本文介紹一種「玩轉」ActiveX 的方法,涵蓋從控制項調用到事件處理的所有細節。所謂「SDK」實為借助 ATL 的 OLE 容器支援,純 SDK 實現 OLE 容器過於繁瑣。
ActiveX)。IDD_MAIN_DLG。

#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;
}
_Module 保存模組資訊,Init/Term 包含 CoInitialize/OleInitialize 等初始化卸載。#import 匯入 Agent 控制項型別庫,介面封裝在 AgentObjects 命名空間。atl.lib。
編譯後產生 agentctl.tlh/tli,前者含介面 C++ 支援與智慧指標,後者更有趣。
因對話框含 ActiveX,必須用 AtlAxCreateDialog 而非 CreateDialog:
g_hDlgMain = AtlAxCreateDialog( hInstance, MAKEINTRESOURCE(IDD_MAIN_DLG), NULL, (DLGPROC)MainDlgProc, 0 );
AtlAxCreateDialog。g_hDlgMain 為全域變數,供其他類使用。在對話框回呼 WM_INITDIALOG 中操作控制項:
CAxWindow wndAgent = GetDlgItem( hDlg, IDC_AGENT );
IAgentCtlExPtr pAgent; // 來自 agentctl.tlh
HRESULT hr = wndAgent.QueryControl( __uuidof(IAgentCtlEx), (LPVOID*)&pAgent );
測試代碼 (置於 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") );
// 連接控制項 (關鍵!MFC 可省略)
hr = pAgent->put_Connected( (VARIANT_BOOL)-1 );
// 取得角色列表、載入 Merlin、顯示、移至螢幕中心、說話
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;

兩點說明:
Connected = -1,否則後續方法全失敗 (MFC 封裝下可省略)。助手說「右鍵選隱藏結束」,但右鍵隱藏後程式未退出——未處理 Hide 事件。
BEGIN_EVENTSINK_MAP(CActiveXDlg, CDialog)
ON_EVENT(CActiveXDlg, IDC_AGENT, 7 /* Hide */, OnHideAgent, VTS_BSTR VTS_I2)
END_EVENTSINK_MAP()
什麼是連接點?:COM 伺服器需將介面開放給客戶端,客戶端實作介面供伺服器回調,即連接點事件。用兩個 C++ 類模擬:
class CSink { public: void DoSomeOtherThings() { cout << "I still want to do some other things..." << 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 非嚴格介面,但模擬 COM 伺服器。CSink 為「接收器」,處理回調事件。SetSink 為「連接」過程,DoSomeOtherThings 為事件處理實作。COM 接收器可視為無 CLSID 的 COM 組件類,只需給出實現並提供指標給伺服器。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() { 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) { // Hide 事件 ID = 7
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;
}
// 處理 Hide 事件
STDMETHODIMP OnHide(_bstr_t CharacterID, short Cause) {
PostMessage(g_hDlgMain, WM_CLOSE, 0, 0); return S_OK;
}
private: ULONG m_uRef;
};
_AgentEvents : IDispatch。若繼承 _AgentEvents 必須全實作所有方法 (即使回 E_NOTIMPL) 工程浩大。只需處理 Hide 一事件,繼承 IDispatch 並在 Invoke 攔截即可。AddRef/Release/QueryInterface,但可簡化 QueryInterface,IDispatch 三方法直接回 E_NOTIMPL。IDispatch::Invoke 調度 (MFC 事件映射表可證)。檢查調度 ID、參數類型、個數確認無誤後呼叫自訂 OnHide。dispIdMember == 7 即呼叫 OnHide 也能動,但為嚴謹建議保留完整檢查。// 設置連接點開始
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 IConnectionPoint *pCP = NULL;
static IUnknown *pSinkUnk = NULL;
static DWORD dwCookie = 0;
// 程式結束時釋放
pCP->Unadvise(dwCookie);
pCP->Release();
pSinkUnk->Release(); // 必須 Release,因前面 QueryInterface 增加引用計數
至此,調用 ActiveX 的完整流程與原理介紹完畢。因代碼按文章邏輯分段引用,可能造成實作困擾,敬請見諒。