首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

純 SDK 環境下調用 ActiveX 控制項:從建立到事件處理完整實戰

     編輯:關於VC++

前言:魚與熊掌能否兼得?

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

讀者預期

  • 熟悉 SDK 程式設計與 COM 組件調用方式 (相關知識不再贅述)。
  • 可不懂 ATL,本文僅用其 OLE 容器部分,適當處說明。
  • 可不懂 COM 連接點,文中將詳細介紹。

準備工作:建立專案與資源

  1. VC++ 新建 Win32 Application (名為 ActiveX)。
  2. 新增 Resource Script,加入對話框 IDD_MAIN_DLG。
  3. 對話框右鍵 Insert ActiveX Control...,選 Microsoft Agent Control 2.0。
  4. 完成對話框。

骨架代碼

#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 支援,_Module 保存模組資訊,Init/Term 包含 CoInitialize/OleInitialize 等初始化卸載。
  • #import 匯入 Agent 控制項型別庫,介面封裝在 AgentObjects 命名空間。
  • 專案設定加入 atl.lib。

編譯後產生 agentctl.tlh/tli,前者含介面 C++ 支援與智慧指標,後者更有趣。

使用 ActiveX 控制項

因對話框含 ActiveX,必須用 AtlAxCreateDialog 而非 CreateDialog:

g_hDlgMain = AtlAxCreateDialog( hInstance, MAKEINTRESOURCE(IDD_MAIN_DLG), NULL, (DLGPROC)MainDlgProc, 0 );
  • 含 ActiveX 的對話框需用 AtlAxCreateDialog。
  • g_hDlgMain 為全域變數,供其他類使用。
  • 用無模式對話框可建立不可見對話框,僅顯示 Agent 助手。

在對話框回呼 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;

兩點說明:

  1. 實際開發需嚴謹判斷每個 COM 方法回傳值,文中為篇幅省略,完整代碼見配套原始碼。
  2. 使用 Agent 前必須設 Connected = -1,否則後續方法全失敗 (MFC 封裝下可省略)。

連接點:處理 ActiveX 事件

助手說「右鍵選隱藏結束」,但右鍵隱藏後程式未退出——未處理 Hide 事件。

MFC 事件映射表

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

SDK 必須從 COM 底層機制——連接點入手

什麼是連接點?: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;
};

關鍵解析

  1. 繼承 IDispatch 而非 _AgentEvents:用 OLE/COM Object Viewer 可見 _AgentEvents : IDispatch。若繼承 _AgentEvents 必須全實作所有方法 (即使回 E_NOTIMPL) 工程浩大。只需處理 Hide 一事件,繼承 IDispatch 並在 Invoke 攔截即可。
  2. IUnknown 實作:接收器按 COM 規範實作 AddRef/Release/QueryInterface,但可簡化 QueryInterface,IDispatch 三方法直接回 E_NOTIMPL。
  3. Invoke 攔截事件:COM 語言無關特性意味 VB 等腳本也能處理事件,故所有事件經 IDispatch::Invoke 調度 (MFC 事件映射表可證)。檢查調度 ID、參數類型、個數確認無誤後呼叫自訂 OnHide。
  4. 簡化版可行但不建議:僅判斷 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 的完整流程與原理介紹完畢。因代碼按文章邏輯分段引用,可能造成實作困擾,敬請見諒。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved