aliases:

  • |-
    深入解析
    ActiveX 工作原理与机制

ActiveX 工作原理与机制:UML 建模与实现方案

1. 项目概述

ActiveX 是基于 COM 的可重用组件技术,主要用于在 Web 浏览器(如 Internet Explorer)或 OLE 容器中嵌入交互式控件。一个典型的 ActiveX 控件:

  • 实现一组标准接口:IUnknown, IOleObject, IOleControl, IDispatch, IPersistStreamInit
  • 支持属性(Properties)、方法(Methods)、事件(Events)
  • 可通过 <object> 标签嵌入 HTML 页面
  • 需要自注册DllRegisterServer 写入注册表控件标记)
  • 支持持久化(保存/加载属性状态)
  • 可选可视化(在容器中绘制自身)

本项目实现一个 MathCtrl ActiveX 控件,提供加减运算,具有 Value 属性,支持 Click 事件,并在网页中演示。


2. 项目文件结构

activex_demo/
├── README.md
├── docs/
│   └── uml.md                       # UML 图
├── idl/
│   └── MathCtrl.idl                 # 控件接口定义
├── src/
│   ├── MathCtrl.h                   # 控件类声明
│   ├── MathCtrl.cpp                 # 控件实现
│   ├── MathCtrlFactory.h
│   ├── MathCtrlFactory.cpp
│   ├── MathCtrlPropPage.h           # 属性页(可选)
│   ├── MathCtrlPropPage.cpp
│   ├── dllmain.cpp
│   ├── MathCtrl.def
│   └── MathCtrl.rgs                 # 注册表脚本
├── web/
│   ├── test.html                    # 测试页面
│   └── script.js
├── bin/                             # 输出 DLL / OCX
└── tools/
    └── register.bat

3. UML 建模(Mermaid)

3.1 类图 – ActiveX 控件的接口实现

fires

«interface»

IUnknown

+QueryInterface()

+AddRef()

+Release()

«interface»

IOleObject

+...

+SetClientSite()

+GetClientSite()

+DoVerb()

«interface»

IOleControl

+...

+GetControlInfo()

+OnMnemonic()

«interface»

IDispatch

+GetTypeInfoCount()

+GetTypeInfo()

+GetIDsOfNames()

+Invoke()

«interface»

IPersistStreamInit

+GetClassID()

+IsDirty()

+Load()

+Save()

+InitNew()

«interface»

IViewObject

+...

+Draw()

«interface»

IProvideClassInfo

+GetClassInfo()

CMathCtrl

-LONG m_cRef

-LONG m_value

-IOleClientSite* m_pClientSite

+QueryInterface()

+AddRef()

+Release()

+GetTypeInfoCount() : ...

+Add(LONG a, LONG b, LONG* result)

+get_Value() / put_Value()

+Fire_Click()

+Draw()

+Load() / Save()

«dispinterface»

IMathCtrlEvents

+Click()

3.2 序列图 – 浏览器加载 ActiveX 控件

CMathCtrl MathCtrl.dll COM库 IE/Container CMathCtrl MathCtrl.dll COM库 IE/Container 用户交互... 解析 <object> 的 CLSID LoadLibrary DllGetClassObject 类工厂 CreateInstance IUnknown QueryInterface(IOleObject) IOleObject::SetClientSite IOleObject::DoVerb(OLEIVERB_INPLACEACTIVATE) 创建窗口 通知位置大小 IOleInPlaceObject::SetObjectRects 绘制自身 调用方法/设置属性 触发事件 通过 IDispatch 调用脚本

3.3 活动图 – ActiveX 控件持久化流程

容器创建控件

调用 IPersistStreamInit::InitNew

控件设置默认属性

用户修改属性值

容器保存文档?

调用 IPersistStreamInit::Save

控件将 m_value 等写入流

继续运行

容器加载文档?

调用 IPersistStreamInit::Load

控件从流恢复属性

重绘

3.4 部署图 – ActiveX 控件在客户端的分布

渲染错误: Mermaid 渲染失败: No diagram type detected matching given configuration for text: deployment node "Web Server" { artifact "test.html" artifact "MathCtrl.cab" # 压缩包 } node "Client PC" { artifact "IE Browser" artifact "MathCtrl.dll" [ installed in system32 ] artifact "Registry" [ HKCR\CLSID\{...} ] } test.html --> IE Browser : HTTP IE Browser --> MathCtrl.dll : 加载 COM 控件 MathCtrl.dll --> Registry : 查询 CLSID IE Browser --> MathCtrl.cab : 下载(若未安装)

4. IDL 定义(包括控件、事件)

文件idl/MathCtrl.idl

import "oaidl.idl";
import "ocidl.idl";

// 事件接口(源接口)
[
    uuid(0A1B2C3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D),
    hidden
]
dispinterface _IMathCtrlEvents
{
    properties:
    methods:
        [id(1)] void Click();
};

// 控件主接口
[
    object,
    uuid(1B2C3D4E-5F6A-7B8C-9D0E-1F2A3B4C5D6E),
    dual,
    oleautomation,
    pointer_default(unique)
]
interface IMathCtrl : IDispatch
{
    [propget, id(1)] HRESULT Value([out, retval] LONG* pVal);
    [propput, id(1)] HRESULT Value([in] LONG newVal);
    [id(2)] HRESULT Add([in] LONG a, [in] LONG b, [out, retval] LONG* result);
    [id(3)] HRESULT Sub([in] LONG a, [in] LONG b, [out, retval] LONG* result);
};

// 控件 coclass
[
    uuid(2C3D4E5F-6A7B-8C9D-0E1F-2A3B4C5D6E7F),
    control,                              // 标记为控件
    helpstring("MathCtrl Control")
]
coclass MathCtrl
{
    [default] interface IMathCtrl;
    [default, source] dispinterface _IMathCtrlEvents;
};

MIDL 编译:midl MathCtrl.idl 生成 .h, .tlb, _i.c, _p.c


5. 控件实现(C++)

5.1 头文件 MathCtrl.h

#include "MathCtrl.h"  // MIDL generated
#include <ocidl.h>     // IOleControl, IOleObject etc.

class CMathCtrl : public IMathCtrl,
                  public IOleObject,
                  public IOleControl,
                  public IPersistStreamInit,
                  public IViewObject,
                  public IProvideClassInfo,
                  public IConnectionPointContainer
{
private:
    LONG m_cRef;
    LONG m_value;                     // Value 属性
    DWORD m_dwCookie;                 // 连接点 cookie
    IConnectionPointContainer* m_pCPC; // 连接点容器
    IConnectionPoint* m_pConnPt;      // 事件连接点

public:
    CMathCtrl();
    ~CMathCtrl();

    // IUnknown
    STDMETHODIMP QueryInterface(REFIID riid, void** ppv);
    STDMETHODIMP_(ULONG) AddRef();
    STDMETHODIMP_(ULONG) Release();

    // IMathCtrl
    STDMETHODIMP get_Value(LONG* pVal);
    STDMETHODIMP put_Value(LONG newVal);
    STDMETHODIMP Add(LONG a, LONG b, LONG* result);
    STDMETHODIMP Sub(LONG a, LONG b, LONG* result);

    // IDispatch (from IMathCtrl) – use typeinfo
    STDMETHODIMP GetTypeInfoCount(UINT* pctinfo);
    STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo);
    STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId);
    STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams,
                        VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr);

    // IOleObject
    STDMETHODIMP SetClientSite(IOleClientSite* pClientSite);
    STDMETHODIMP GetClientSite(IOleClientSite** ppClientSite);
    STDMETHODIMP SetHostNames(LPCOLESTR szContainerApp, LPCOLESTR szContainerObj);
    STDMETHODIMP Close(DWORD dwSaveOption);
    STDMETHODIMP SetMoniker(DWORD dwWhichMoniker, IMoniker* pmk);
    STDMETHODIMP GetMoniker(DWORD dwAssign, DWORD dwWhichMoniker, IMoniker** ppmk);
    STDMETHODIMP InitFromData(IDataObject* pDataObject, BOOL fCreation, DWORD dwReserved);
    STDMETHODIMP GetClipboardData(DWORD dwReserved, IDataObject** ppDataObject);
    STDMETHODIMP DoVerb(LONG iVerb, LPMSG lpmsg, IOleClientSite* pActiveSite, LONG lindex, HWND hwndParent, LPCRECT lprcPosRect);
    STDMETHODIMP EnumVerbs(IEnumOLEVERB** ppEnumOleVerb);
    STDMETHODIMP Update();
    STDMETHODIMP IsUpToDate();
    STDMETHODIMP GetUserClassID(CLSID* pClsid);
    STDMETHODIMP GetUserType(DWORD dwFormOfType, LPOLESTR* pszUserType);
    STDMETHODIMP SetExtent(DWORD dwDrawAspect, SIZEL* psizel);
    STDMETHODIMP GetExtent(DWORD dwDrawAspect, SIZEL* psizel);
    STDMETHODIMP Advise(IAdviseSink* pAdvSink, DWORD* pdwConnection);
    STDMETHODIMP Unadvise(DWORD dwConnection);
    STDMETHODIMP EnumAdvise(IEnumSTATDATA** ppenumAdvise);
    STDMETHODIMP GetMiscStatus(DWORD dwAspect, DWORD* pdwStatus);
    STDMETHODIMP SetColorScheme(LOGPALETTE* pLogpal);

    // IOleControl
    STDMETHODIMP GetControlInfo(CONTROLINFO* pCI);
    STDMETHODIMP OnMnemonic(LPMSG pMsg);
    STDMETHODIMP OnAmbientPropertyChange(DISPID dispID);
    STDMETHODIMP FreezeEvents(BOOL bFreeze);

    // IPersistStreamInit
    STDMETHODIMP GetClassID(CLSID* pClassID);
    STDMETHODIMP IsDirty();
    STDMETHODIMP Load(IStream* pStm);
    STDMETHODIMP Save(IStream* pStm, BOOL fClearDirty);
    STDMETHODIMP GetSizeMax(ULARGE_INTEGER* pcbSize);
    STDMETHODIMP InitNew();

    // IViewObject
    STDMETHODIMP Draw(DWORD dwDrawAspect, LONG lindex, void* pvAspect, DVTARGETDEVICE* ptd, HDC hdcTargetDev, HDC hdcDraw, LPCRECTL lprcBounds, LPCRECTL lprcWBounds, BOOL (CALLBACK* pfnContinue)(ULONG_PTR), ULONG_PTR dwContinue);
    STDMETHODIMP GetColorSet(DWORD dwDrawAspect, LONG lindex, void* pvAspect, DVTARGETDEVICE* ptd, HDC hicTargetDev, LOGPALETTE** ppColorSet);
    STDMETHODIMP Freeze(DWORD dwDrawAspect, LONG lindex, void* pvAspect, DWORD* pdwFreeze);
    STDMETHODIMP Unfreeze(DWORD dwFreeze);
    STDMETHODIMP SetAdvise(DWORD dwAspects, DWORD dwAdvf, IAdviseSink* pAdvSink);
    STDMETHODIMP GetAdvise(DWORD* pAspects, DWORD* pAdvf, IAdviseSink** ppAdvSink);

    // IProvideClassInfo
    STDMETHODIMP GetClassInfo(ITypeInfo** ppTI);

    // IConnectionPointContainer
    STDMETHODIMP EnumConnectionPoints(IEnumConnectionPoints** ppEnum);
    STDMETHODIMP FindConnectionPoint(REFIID riid, IConnectionPoint** ppCP);

    // Helper: Fire event
    HRESULT Fire_Click();
};

5.2 实现文件 MathCtrl.cpp(关键部分)

#include "MathCtrl.h"
#include "MathCtrl_i.c"  // IID/CLSID

static ITypeLib* g_pTypeLib = NULL;

// 连接点容器实现(简化,使用标准 ATL 模式,这里手工实现)
class CProxy_IMathCtrlEvents : public IConnectionPoint
{
    // ... 标准连接点实现,用于管理事件 sink
};

CMathCtrl::CMathCtrl() : m_cRef(1), m_value(0), m_pConnPt(NULL)
{
    // 创建连接点对象
    // 实际可用 ATL 简化,这里略
}

CMathCtrl::~CMathCtrl() { }

STDMETHODIMP CMathCtrl::QueryInterface(REFIID riid, void** ppv)
{
    if (riid == IID_IUnknown) *ppv = static_cast<IMathCtrl*>(this);
    else if (riid == IID_IMathCtrl) *ppv = static_cast<IMathCtrl*>(this);
    else if (riid == IID_IDispatch) *ppv = static_cast<IMathCtrl*>(this);
    else if (riid == IID_IOleObject) *ppv = static_cast<IOleObject*>(this);
    else if (riid == IID_IOleControl) *ppv = static_cast<IOleControl*>(this);
    else if (riid == IID_IPersistStreamInit) *ppv = static_cast<IPersistStreamInit*>(this);
    else if (riid == IID_IViewObject) *ppv = static_cast<IViewObject*>(this);
    else if (riid == IID_IProvideClassInfo) *ppv = static_cast<IProvideClassInfo*>(this);
    else if (riid == IID_IConnectionPointContainer) *ppv = static_cast<IConnectionPointContainer*>(this);
    else return E_NOINTERFACE;
    AddRef();
    return S_OK;
}

STDMETHODIMP_(ULONG) CMathCtrl::AddRef() { return InterlockedIncrement(&m_cRef); }
STDMETHODIMP_(ULONG) CMathCtrl::Release()
{
    ULONG ref = InterlockedDecrement(&m_cRef);
    if (ref == 0) delete this;
    return ref;
}

// 属性 Value 的实现
STDMETHODIMP CMathCtrl::get_Value(LONG* pVal)
{
    *pVal = m_value;
    return S_OK;
}
STDMETHODIMP CMathCtrl::put_Value(LONG newVal)
{
    m_value = newVal;
    // 通知容器属性变化(可选)
    return S_OK;
}

STDMETHODIMP CMathCtrl::Add(LONG a, LONG b, LONG* result)
{
    *result = a + b;
    // 触发 Click 事件示例
    Fire_Click();
    return S_OK;
}
STDMETHODIMP CMathCtrl::Sub(LONG a, LONG b, LONG* result)
{
    *result = a - b;
    return S_OK;
}

// 绘制:在矩形内显示当前 Value
STDMETHODIMP CMathCtrl::Draw(DWORD dwDrawAspect, LONG lindex, void* pvAspect, DVTARGETDEVICE* ptd, HDC hdcTargetDev, HDC hdcDraw, LPCRECTL lprcBounds, LPCRECTL lprcWBounds, BOOL (CALLBACK* pfnContinue)(ULONG_PTR), ULONG_PTR dwContinue)
{
    if (hdcDraw == NULL) return E_INVALIDARG;
    // 简单绘制一个带边框的矩形,显示 "Value: xxx"
    RECT rc = { lprcBounds->left, lprcBounds->top, lprcBounds->right, lprcBounds->bottom };
    FillRect(hdcDraw, &rc, (HBRUSH)GetStockObject(WHITE_BRUSH));
    FrameRect(hdcDraw, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH));
    char text[32];
    sprintf_s(text, "Value: %ld", m_value);
    DrawTextA(hdcDraw, text, -1, &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    return S_OK;
}

// 持久化:保存 m_value
STDMETHODIMP CMathCtrl::Load(IStream* pStm)
{
    ULONG cbRead;
    return pStm->Read(&m_value, sizeof(m_value), &cbRead);
}
STDMETHODIMP CMathCtrl::Save(IStream* pStm, BOOL fClearDirty)
{
    ULONG cbWritten;
    return pStm->Write(&m_value, sizeof(m_value), &cbWritten);
}
STDMETHODIMP CMathCtrl::InitNew()
{
    m_value = 0;
    return S_OK;
}

// 事件触发
HRESULT CMathCtrl::Fire_Click()
{
    if (m_pConnPt == NULL) return S_OK;
    IUnknown** ppSinks;
    // 枚举连接点,调用每个 sink 的 Invoke (DISPID 1)
    // 简化:仅调用第一个 sink
    // 实际需使用 IEnumConnections
    return S_OK;
}

// 其他接口方法(如 DoVerb 创建窗口等)必须实现,此处省略细节

5.3 类工厂与 DLL 导出

// dllmain.cpp
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
    if (rclsid != CLSID_MathCtrl) return CLASS_E_CLASSNOTAVAILABLE;
    static CClassFactory factory; // 实现 IClassFactory
    return factory.QueryInterface(riid, ppv);
}

STDAPI DllCanUnloadNow() { /* 返回 S_OK 如果无活动对象 */ }

STDAPI DllRegisterServer()
{
    // 注册 COM 类,添加 Control 子键,注册类型库
    // 使用 .rgs 脚本或直接写注册表
    return S_OK;
}

6. 注册表脚本(.rgs)

文件src/MathCtrl.rgs

HKCR
{
    CLSID\{2C3D4E5F-6A7B-8C9D-0E1F-2A3B4C5D6E7F} = s 'MathCtrl Control'
    {
        InprocServer32 = s '%MODULE%'
        {
            val ThreadingModel = s 'Apartment'
        }
        Control = s ''                         // ActiveX 控件标记
        TypeLib = s '{...}'                    // 类型库 IID
        MiscStatus = s '131456'                // OLEMISC 标志
        ProgID = s 'MathCtrl.MathCtrl.1'
        VersionIndependentProgID = s 'MathCtrl.MathCtrl'
    }
    // 类型库注册(通常由 regsvr32 自动处理)
}

7. HTML 测试页面

文件web/test.html

<!DOCTYPE html>
<html>
<head>
    <title>ActiveX MathCtrl Demo</title>
    <script type="text/javascript">
        function doAdd() {
            var ctrl = document.getElementById("mathCtrl");
            var a = parseInt(document.getElementById("a").value);
            var b = parseInt(document.getElementById("b").value);
            var result = ctrl.Add(a, b);
            document.getElementById("result").innerHTML = result;
        }
        function doSub() {
            var ctrl = document.getElementById("mathCtrl");
            var a = parseInt(document.getElementById("a").value);
            var b = parseInt(document.getElementById("b").value);
            var result = ctrl.Sub(a, b);
            document.getElementById("result").innerHTML = result;
        }
        function getValue() {
            var ctrl = document.getElementById("mathCtrl");
            alert("Current Value = " + ctrl.Value);
        }
        function setValue() {
            var ctrl = document.getElementById("mathCtrl");
            var newVal = parseInt(document.getElementById("newVal").value);
            ctrl.Value = newVal;
        }
    </script>
</head>
<body>
    <h1>ActiveX MathCtrl 测试</h1>
    <object id="mathCtrl" classid="clsid:2C3D4E5F-6A7B-8C9D-0E1F-2A3B4C5D6E7F" width="200" height="80">
        <param name="Value" value="42">
    </object>
    <br/>
    <label>A: <input type="text" id="a" value="10"/></label>
    <label>B: <input type="text" id="b" value="5"/></label>
    <button onclick="doAdd()">Add</button>
    <button onclick="doSub()">Sub</button>
    <p>Result: <span id="result"></span></p>
    <hr/>
    <label>Set Value: <input type="text" id="newVal" value="100"/></label>
    <button onclick="setValue()">Set</button>
    <button onclick="getValue()">Get</button>
    <p>控件内部会显示当前 Value。</p>
</body>
</html>

8. 构建与部署

8.1 编译

使用 Visual Studio 或 MinGW。需要链接 ole32.lib, oleaut32.lib, uuid.lib

# 编译 IDL
midl MathCtrl.idl

# 编译 DLL
cl /LD /I. MathCtrl.cpp dllmain.cpp MathCtrl_i.c /FeMathCtrl.dll /link /DEF:MathCtrl.def

# 注册控件
regsvr32 MathCtrl.dll

8.2 部署到浏览器

由于 IE 对 ActiveX 有安全限制,需要:

  • 将站点加入受信任的站点
  • 设置 IE 安全级别允许 ActiveX 控件。
  • 签名控件(代码签名证书)以避免提示。

也可以将控件打包为 .cab 文件,并在 <object> 中指定 codebase

<object classid="..." codebase="MathCtrl.cab#version=1,0,0,0" ...>

9. 深入机制解析

9.1 ActiveX 与普通 COM 组件的区别

  • 必须实现特定接口IOleObject(与容器通信)、IOleControl(接收键盘消息等)、IPersistStreamInit(持久化)、IViewObject(绘制)。
  • 注册表标记Control 子键表示这是一个控件,容器可据此加载设计器。
  • 事件支持:通过连接点(IConnectionPointContainer)暴露事件接口。
  • 属性页(可选):通过 ISpecifyPropertyPages 允许用户在容器中编辑属性。

9.2 ActiveX 的安全机制

  • 初始化安全:实现 IObjectSafety 接口,声明控件是脚本安全的(INTERFACESAFE_FOR_UNTRUSTED_CALLER)和初始化安全的(INTERFACESAFE_FOR_UNTRUSTED_DATA)。
  • 签名与验证:控件应使用 Authenticode 签名,防止篡改。

9.3 与 MFC/ATL 的关系

开发 ActiveX 控件通常使用 ATL(Active Template Library),它提供了大量模板类和宏,简化了接口实现。上述手工实现是为了展示原理,实际项目应使用 ATL。

9.4 ActiveX 的衰落

由于安全问题和浏览器标准化(NPAPI → PPAPI → 无插件),现代浏览器(Chrome, Firefox, Edge 新版)不再支持 ActiveX。仅在 IE 和某些遗留系统中使用。


10. 总结

本项目通过一个完整的 ActiveX 控件示例,展示了:

  • IDL 定义控件接口和事件。
  • C++ 实现必须支持 IOleObject, IOleControl, IPersistStreamInit, IViewObject, IConnectionPointContainer 等接口。
  • 注册时添加 Control 子键标记为控件。
  • HTML 嵌入通过 <object> 标签,JavaScript 调用方法、读写属性、响应事件。
  • 安全需要实现 IObjectSafety 并签名。

所有 UML 图使用 Mermaid 绘制,代码结构清晰,可作为学习 ActiveX 原理的参考。虽然 ActiveX 已属历史,但其设计思想(组件化、容器交互、持久化、事件)深刻影响了后来的 Web 组件标准(如 Web Components)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐