ActiveX / OCX 现在该如何处理 - 保留、封装、替换的判断表

· 更新日期: · · COM, ActiveX, OCX, .NET, Windows 开发, 现代化改造

更新记录(3 条,最后更新 2026年09月03日)

本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。

本文此前是日文原文的节译,缺少大量章节、表格、Mermaid 图、图题、脚注与 FAQ。现已改写为日文原文的完整译文,技术主张与日文版一致,并补上了此前缺失的图与表,同时统一了全篇的术语译法。 查看更新前的版本 (DOI: 10.5281/zenodo.22276597)
补充了日文原文中已有的咨询引导(consultation_services)。正文内容没有改动。 查看更新前的版本 (DOI: 10.5281/zenodo.22170276)
在 5.2 中补充了 DllSurrogate,作为动手写自己的辅助 EXE 之前值得先考虑的一个选项。如果目标本身是 in-proc 的 COM 服务器,只要给它的 CLSID 加上 AppID,并在该 AppID 键下写入空字符串的 DllSurrogate,就能让它跑到进程外,自己一行代码都不用写。不过 surrogate 并不会消除位数差异:消失的只是必须装进同一个进程这条限制,调用会变成跨进程,封送和进程间通信的开销照旧。新增的表格划出了 surrogate 够用的情形与仍然需要自己写辅助 EXE 的情形之间的界线,并补充了两条参考资料。注册步骤本身放在另一篇文章里,这里只给出链接。 查看更新前的版本 (DOI: 10.5281/zenodo.21615443)
首次发布
引用本文(DOI: 10.5281/zenodo.21615442)

本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。

小村 豪(2026)。《ActiveX / OCX 现在该如何处理 - 保留、封装、替换的判断表》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615442 https://comcomponent.com/zh-CN/blog/2026/03/12/001-activex-ocx-keep-wrap-replace-decision-table/

DOI(最新版本)
10.5281/zenodo.21615442
DOI(此版本)
10.5281/zenodo.22281953

一提到 ActiveX / OCX,项目里的气氛通常都会稍微沉重一点。

  • VB6 或古老的 C++ / MFC 应用仍在现役
  • 工业设备或测量仪器的 SDK 只提供 OCX
  • 公司内部网站以 ActiveX 为前提,一直脱不开 IE 模式
  • 明明想从 32bit 迁到 64bit,却有一个 OCX 在那里摇头

不过,这里无论是“因为老旧就全部扔掉”,还是“因为还在运行就永久保留”,都太草率了。 真正重要的是分辨出:这个 ActiveX / OCX 究竟只是单纯的 UI 部件,还是承载着业务规格或设备规格的边界面。

本文会按照容易判断的顺序,整理发现 ActiveX / OCX 时, 应该选择 保留、封装、还是替换。

适用的场景,例如:

  • VB6 / MFC / WinForms 系列的现有桌面应用
  • 向 C# / .NET 的分阶段迁移
  • 包含 WebBrowser / IE 模式的旧版界面
  • 包含供应商提供的 ActiveX 控件的 Windows 应用

目录

  1. 先说结论(一句话)
  2. 本文所说的 ActiveX / OCX
  3. 先看判断表
    • 3.1. 整体图
    • 3.2. 保留的判断
    • 3.3. 封装的判断
    • 3.4. 替换的判断
    • 3.5. 浏览器依赖单独考虑
  4. 容易搞乱判断的论点
    • 4.1. 是 UI 部件,还是承载规格的部件
    • 4.2. 32bit / 64bit 与进程边界
    • 4.3. 注册、分发、权限、许可
    • 4.4. STA / 消息循环 / 回调
    • 4.5. 有没有测试、能否观测
  5. 各类典型场景的建议
    • 5.1. 目前仍稳定运行的内部桌面应用
    • 5.2. 想把 32bit OCX 搬到 64bit 一侧
    • 5.3. IE / WebBrowser 前提的界面
    • 5.4. 承载设备控制或专有规格的 ActiveX
  6. 常见的反模式
  7. 开始迁移时的检查清单
  8. 大致的使用区分
  9. 总结
  10. 这类咨询比较合适
  11. 参考资料

图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 24 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle

1. 先说结论(一句话)

  • 看到 ActiveX / OCX 时,首先要判断的不是“是不是老旧”,而是这个部件承担了什么职责
  • 如果只是单纯的 UI 部件,替换相对容易
  • 如果承载着设备控制、报表、专有文件格式,或者多年运维形成的习惯,与其立刻重新实现,先封装 会更安全
  • 如果在桌面端稳定运行、改动范围很小,保留 也完全是合理的选择
  • 浏览器上的 ActiveX 依赖,可以续命,但未来空间很窄,更适合以替换为优先
  • 32bit 的 OCX 不能直接加载到 64bit 进程中,这一点无法靠意志力跨越
  • 注册、依赖 DLL、管理员权限、许可证、STA / MTA 等实现之外的摩擦,往往才是真正的难点
  • “干脆全面重写”与“因为害怕就永久冻结”,这两种做法出问题的概率都很高

归纳一下,判断的顺序是这样的:

  1. 这个 OCX 承载着什么
  2. 是否必须在同一个进程中使用
  3. 32bit / 64bit、注册、浏览器依赖会不会卡住
  4. 要不要先建立可测试的边界,再进行替换

按这个顺序看,会明显更容易整理清楚。

判断的顺序按照这个 OCX 承载着什么、是否必须在同一个进程中使用、会不会卡在 32bit/64bit 或注册或浏览器依赖上、要不要先建立可测试的边界再替换的顺序来看。这个 OCX 承载着什么是否必须在同一进程bitness、注册、浏览器会不会卡住先建立边界再替换吗

图1:不看“是否老旧”,而按这个顺序看,保留、封装、替换就更容易理清。

2. 本文所说的 ActiveX / OCX

先把本文用词的范围定下来。

词语 本文中的含义
COM Windows 的二进制兼容组件模型。公开接口、注册、Apartment Model 等都是它的基础
ActiveX / OCX 在实务中,经常被用来泛指基于 COM 的控件及其周边资产。尤其常包含 .ocx 形式的 UI 控件,以及嵌入 IE / 容器中的部件
WebBrowser / IE 系依赖 即使不是 ActiveX 本身,也包括那些以“IE 的世界观”为前提的嵌入式浏览器或相关联动。在判断上属于相当接近的问题

严格来说,ActiveX 和 COM 并不是同一件事。 不过在实务中,遇到麻烦的点相当相似。

  • 32bit / 64bit 是否匹配
  • 注册和依赖 DLL 如何分发
  • 运行在哪个宿主 / 容器中
  • 是否会在 STA、消息循环、回调上卡住
  • 是否还残留着浏览器依赖

本文会把这些实务上的判断点汇总起来讨论。

实务中遇到麻烦的地方严格来说 ActiveX 与 COM 并不是同一件事,但 bitness 是否匹配、注册与依赖 DLL 的分发、运行在哪个宿主中、STA 与回调的前提、浏览器依赖这些实务中遇到麻烦的地方相当相似。ActiveX / OCX 项目bitness 是否匹配注册与依赖 DLL 的分发运行在哪个宿主中STA 与回调的前提是否还残留浏览器依赖

图2:即使 ActiveX 与 COM 严格来说是两回事,实务中卡住的地方也集中在这一带。

另外,先把后文会理所当然出现的缩写整理一下。因为第 7 章的检查清单里说“梳理出 ProgID 和 CLSID”,如果不知道这是什么就动不了手。

术语 全称 / 读法 含义
CLSID Class ID 唯一标识 COM 组件实现(类)的 GUID。注册表中的注册也以这个值为键
ProgID Programmatic Identifier 给 CLSID 起的、人能读懂的名字。是 Excel.Application 这样的字符串
IID Interface ID 唯一标识 COM 接口的 GUID。与 CLSID 是两回事
TLB Type Library 以二进制形式保存接口、方法、参数类型等类型信息的文件。VB6 或 .NET 能“带类型地”调用,靠的就是它
RegAsm Assembly Registration Tool .NET Framework 附带的工具。把 .NET 程序集注册到注册表,使其能从 COM 使用
AxHost — 用于在 Windows Forms 上宿主 ActiveX 控件的基类
AxImp ActiveX Control Importer 从 OCX 生成 Windows Forms 用包装程序集的工具
in-proc / out-of-proc 进程内 / 进程外 是与调用方在同一进程中运行(DLL 或 OCX),还是在独立进程中运行(EXE 形式的服务器)
LocalServer — 把 COM 服务器做成 EXE、在独立进程中运行的形态。可以跨越 bitness 的壁垒,也能隔离崩溃
Reg-Free COM / side-by-side 免注册 COM 不做注册表注册,而是用应用一侧清单里写的信息来解析 COM 的机制
design-time / runtime 许可证 开发时 / 运行时 供应商提供的控件中,在开发机上把控件贴到界面时和在分发目标上运行时,许可证的处理方式有时是分开的
adapter / facade — 把现有细碎 API 换成对自己更方便的粗粒度 API 的一种设计模式
STA / MTA Single / Multi Threaded Apartment COM 的线程模型。它决定了可以从哪个线程调用的约定

3. 先看判断表

3.1. 整体图

先看下面这张表,大致的方针就能定下来。

情况 首选做法 理由
依赖浏览器上的 ActiveX 偏向替换 因为 Edge 本体不支持 ActiveX,IE 模式的定位只是续命措施
桌面应用中 OCX 稳定运行、改动范围小 偏向保留 因为现在去动它的成本往往更大
只想让周边 .NET 化,但控件行为难以摸清 偏向封装 因为先整理边界更安全
想把 32bit OCX 直接放进 64bit 进程 封装 / 调整架构 因为这是 in-proc 无法跨越的边界
只作为 UI 部件使用,且有替代方案 偏向替换 因为表面替换往往就够了
因供应商停止维护、签名、注册、依赖 DLL 而屡屡出事 偏向替换 因为运维成本已经表现为技术负债
内含设备控制、报表、专有协议 偏向封装 因为不先固定行为,替换成本就估不清楚
是否是是否否是否是否存在 ActiveX / OCX依赖浏览器?优先替换IE 模式只是续命措施主要是UI部件?有同等替代方案?考虑替换先封装并整理边界承载设备控制/专有规格/报表逻辑?先封装补齐测试后再分阶段替换注册/bitness/分发很痛苦?重新审视架构考虑 out-of-proc / 跨进程连接 / Reg-Free COM保留也是现实的选择

图3:第一个分支是是否依赖浏览器,之后再由是不是 UI 部件、有没有替代方案、是否承载规格来决定方针。

下面按顺序逐一来看各种情况。

3.2. 保留的判断

不能因为是 ActiveX / OCX 就立刻列为替换对象。只要满足以下条件,保留往往就是成本最低的选择。

  • 使用范围是封闭的,公司内部分发或随设备出厂的运行环境是固定的
  • 该控件目前仍稳定运行,变更需求不大
  • 供应商仍在维护,或自己公司具备最低限度的维护能力
  • 不依赖浏览器,在桌面端现有宿主中就能完整闭环
  • 32bit / 64bit 的前提暂时不需要改变

这里重要的是,保留 不等于 放任不管。如果选择保留,至少应该做到以下几点。

  • 用文档记录支持的 OS、bitness、必需的依赖 DLL、注册步骤
  • 把安装、注册、卸载从人工记忆改成脚本或安装程序
  • 准备在干净环境下的冒烟测试
  • 尽量把对该控件的调用收敛到一处,而不是散落在整个应用中

最糟糕的情况是,“因为还能用就不去动它”持续了 10 年,最后没人能说清楚前提条件是什么。 越是选择保留,让前提可见化 就越重要。

与保留判断配套的可见化保留的判断不等于放任不管,而要配套做好前提的文档化、注册步骤的脚本化、干净环境下的冒烟测试、调用位置的收敛。保留的判断用文档记录前提把注册步骤脚本化准备冒烟测试把调用收敛到一处

图4:保留不等于放任不管,越是选择保留,越要把前提的可见化一起做好。

3.3. 封装的判断

在实务中,这个选择往往是工作量最大的部分。

这里所说的“封装”,是指把 ActiveX / OCX 关进一个狭窄的边界内部, 对外则以新的 API 或新的界面部件呈现。

这样做相当有效。 因为如果在旧部件的行为还没摸清楚的阶段就直接进入全面重写,很容易同时陷入发掘规格和重现故障的双重困境。 先把旧部件隔离起来,只整理边界 会更安全。

封装这个选择的结构把 ActiveX / OCX 关进狭窄的边界内部,对外以新的 API 或界面部件呈现,从而避免在行为尚未摸清的阶段全面重写所带来的发掘规格与重现故障的双重困境。ActiveX / OCX关进狭窄的边界内部以新的 API 呈现周边只看到新的入口避开发掘规格与重现故障的双重困境

图5:“封装”就是隔离旧部件并搭出新入口,它在全面重写的前一阶段最见效。

封装的方式有几种类型。

封装方式 适合场景 需要关注的点
WinForms 宿主 + AxHost / Aximp 想嵌入现有桌面界面、只想保留少数几个界面 STA、事件、设计时依赖、许可证
32bit helper EXE / COM LocalServer / 跨进程桥接 想迁向 64bit、想把崩溃隔离开 进程间通信、启动顺序、监控、部署
.NET 一侧的 COM 兼容入口 想保留现有 COM 调用方,同时更新内部实现 IID / CLSID / TLB / 注册方式 / bitness

方针定下来之后,第一步该从哪里开始,这里也写一下。

封装方式 首先要做的事 详细步骤
WinForms 宿主 + AxHost 在 Visual Studio 中右键点击工具箱 →“选择工具箱项”→ 从“COM 组件”选项卡中选择目标。命令行则用 aximp COM/OCX/ActiveX 开发中容易踩坑的注册与 bitness 陷阱
32bit helper EXE / LocalServer 把 32bit 的 EXE 一侧注册为 COM 服务器,从 64bit 一侧以 out-of-proc 方式调用 COM 有用的案例研究 - 从 32bit 应用调用 64bit DLL 时
Reg-Free COM 在应用一侧的清单中写上 file 和 comClass,让它不经注册表注册就能解析 什么是 Reg-Free COM - 免注册使用 COM 的机制
.NET 一侧的 COM 兼容入口 把 .NET 一侧公开为 COM,必要时用 dscom 生成 TLB 从 VBA 带类型地使用 .NET 8 DLL 的方法 - COM 公开与 dscom TLB

aximp 从 Visual Studio 的 Developer Command Prompt 中执行。

aximp C:\path\to\MyControl.ocx

这样会生成两样东西:COM 类型的 runtime callable wrapper,以及从 AxHost 派生的 Windows Forms 用包装。文件名不是取自原来的文件名,而是 由 ProgID 决定,这一点要注意。按 Microsoft 文档中的例子,msdxm.ocx 会产出 MediaPlayer.dll 和 AxMediaPlayer.dll。做法是把后者加入引用,再把 AxMediaPlayer 贴到窗体上。

aximp 生成的东西把 OCX 交给 aximp,会生成 COM 类型的 runtime callable wrapper 和 AxHost 派生的 Windows Forms 用包装两样东西,把后者加入引用并贴到窗体上。文件名不是取自原来的文件名,而是由 ProgID 决定。目标 OCX执行 aximpCOM 类型的包装 DLLAxHost 派生的包装 DLL加入引用并贴到窗体上文件名由 ProgID 决定

图6:aximp 的输出是两个 DLL,贴到窗体上的是 AxHost 派生的那个包装。

如果用 Reg-Free COM,应用一侧的清单最小形式是这样。

<?xml version="1.0" encoding="utf-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity type="win32" name="MyApp" version="1.0.0.0" />
  <file name="MyControl.ocx">
    <comClass
      clsid="{01234567-89AB-CDEF-0123-456789ABCDEF}"
      threadingModel="Apartment"
      progid="MyCompany.MyControl.1" />
  </file>
</assembly>

如果用 LocalServer 方式,注册表上是把 EXE 的路径写入 HKEY_CLASSES_ROOT\CLSID\{CLSID}\LocalServer32。用 ATL 或 MFC 做的 EXE 服务器,按惯例大多支持用 MyServer.exe /regserver 和 /unregserver 自行注册与注销。不过注册表的视图是按 bitness 分开的,所以 32bit 的 EXE 会注册到 32bit 一侧的注册表 这一点务必留意。

LocalServer 注册与注册表视图LocalServer 方式会把 EXE 的路径写进 CLSID 下的 LocalServer32,按 regserver 的惯例可以自行注册,但注册表的视图按 bitness 分开,32bit 的 EXE 会注册到 32bit 一侧的注册表。32bit64bitEXE 服务器把路径注册到 LocalServer32EXE 是哪种 bitness?注册到 32bit 一侧的视图注册到 64bit 一侧的视图

图7:LocalServer 注册到哪里,取决于按 bitness 分开的注册表视图。

特别重要的一点是,封装时不要把旧 API 原样复制 200 个。 如果那样做,只是把旧的束缚原样搬进新代码而已。

封装时,注意以下几点会明显改善质量。

  • 做成粗粒度的方法
  • 不让界面代码直接触碰 OCX
  • 在边界处记录失败时所需的日志
  • 在边界上明确超时、重试、异常转换的职责
  • 让未来的替代实现也能用同一个接口换入

有时也会希望在新的 .NET 一侧只保留 COM 入口。 这种情况下,“内部实现更新,但 COM 契约保持不变”是一种现实的架构。 不过,用 .NET Framework 时代的感觉认为“先 RegAsm 一下就行”未必成立。 现在 .NET 的 COM host、TLB、bitness、Registration-Free COM 的处理方式,最好提前设计清楚,后面会省不少事。 关于这一带,从 VBA 带类型地使用 .NET 8 DLL 的方法 - COM 公开与 dscom TLB 和 什么是 Reg-Free COM - 免注册使用 COM 的机制 分别写到了具体步骤。

在边界上确定的职责封装时要做成粗粒度的方法,不让界面代码直接触碰 OCX,在边界上确定日志、超时和异常转换的职责,并让未来的替代实现也能用同一个接口换入。封装的边界粗粒度的方法在边界处记录日志超时与异常转换用同一入口在未来换入不要复制 200 个旧 API

图8:封装的价值在于把职责集中到边界上,照抄旧 API 是发挥不出这个价值的。

3.4. 替换的判断

适合替换的,主要是 表面的老旧 成为问题的场景。

以下情况更适合优先替换。

  • 该 ActiveX 只是作为 UI 部件使用
  • 供应商已经推出面向 .NET / WPF / WebView2 的后继产品
  • 浏览器依赖或 IE 前提拖了后腿
  • 注册、签名、管理员权限、安全设置每次都会出问题
  • 已经有可以验证替代实现的测试或业务场景

反过来,仅仅因为看起来老旧,就把承载着设备控制或报表逻辑的部件一口气丢掉,通常会变成一场泥潭。

如果要替换,先从 UI 开始。

  • 表格
  • 日历
  • 树形控件
  • 浏览器显示部分
  • 简单的输入辅助

这些相对容易替换。另一方面,有些控件看起来像 UI,内部却相当复杂。

  • 供应商提供的设备控制 ActiveX
  • 与打印或报表生成一体化的控件
  • 内含专有文件格式读写的控件
  • 承载 COM 回调或线程前提的控件

如果看错这个差异,工作量估算会一下子失控。

替换判断的分辨方法只作为 UI 部件使用且有替代方案就容易替换,但看起来像 UI、实际却承载设备控制、报表、专有格式、线程前提的部件内部很重,贸然丢掉会变成泥潭。只是表面老旧承载着一整块规格外观背后是什么容易替换一口气丢掉会变成泥潭转向先封装的判断

图9:适不适合替换,取决于能否分辨“表面的老旧”和“内部的厚重”。

3.5. 浏览器依赖单独考虑

这一点相当特殊,需要单独考虑。

浏览器上的 ActiveX,与桌面端的 OCX 不同, 今后继续沿着现有方向发展下去的理由已经相当薄弱。

原因很简单:现代浏览器的技术基础已经不再以此为主战场。 Microsoft Edge 本体并不支持 ActiveX。 IE 模式则是针对已设置的站点使用 IE 系引擎,作为让 ActiveX 等部分 IE 功能继续运行的兼容层来使用。

也就是说,

  • 现在能做到 续命
  • 但作为长期设计而言,未来空间并不宽

就是这么回事。

浏览器上 ActiveX 的定位Microsoft Edge 本体不支持 ActiveX,IE 模式是针对已设置站点使用 IE 系引擎的兼容层,所以现在能做到续命,但作为长期设计未来空间并不宽。浏览器上的 ActiveX在 Edge 本体上跑不起来用 IE 模式可以续命长期的未来空间很窄以替换为优先来看待

图10:依赖浏览器的 ActiveX 要把续命和长期设计分开考虑,并以替换为优先。

嵌入 Windows 应用中的 WebBrowser 控件也会遇到同样的情况。 WebBrowser 依然带着 IE 系的世界观,如果只是想显示 HTML,从现在开始的新工作更自然的做法是把 WebView2 作为第一选择。

不过这里需要注意的是,WebView2 并不是 WebBrowser 的完全替代件。

  • 依赖 IE DOM 的脚本
  • ActiveX 依赖
  • window.external 相关的前提
  • 以安全区域或内网为前提的行为

这些都不会原样迁移过去。 如果要替换,不仅是渲染引擎,浏览器与原生代码的连接面 也需要重新设计。

不会原样迁到 WebView2 的东西WebView2 并不是 WebBrowser 控件的完全替代件,依赖 IE DOM 的脚本、ActiveX 依赖、window.external 相关的前提、以安全区域为前提的行为都不会原样迁移过去。从 WebBrowser 迁到 WebView2只显示 HTML 的话是第一选择不会原样迁过去的东西依赖 IE DOM 的脚本ActiveX 依赖以 window.external 为前提安全区域相关的行为

图11:WebView2 是渲染引擎的替换件,但接不下 IE 世界观的那层连接面。

4. 容易搞乱判断的论点

4.1. 是 UI 部件,还是承载规格的部件

这是最重要的一点。

老旧的表格或日历控件,只要看外观和事件是否兼容,事情大多就能推进。 而承载设备控制、报表或专有格式的 ActiveX,外观背后往往是一整块规格。

看起来同样是“界面上的一个控件”,实际差异可能有这么大。

  • 只是一个列表显示部件
  • 用专有协议向设备发送指令的部件
  • 内部已经处理了超时、重连、重发、异常吸收的部件
  • 背负着打印或导出格式兼容性的部件

对后者贸然重新实现,通常会变成一场规格发掘项目。 这类情况先封装会更安全。

是 UI 部件还是承载规格的部件看起来同样是界面上的控件,但只是列表显示部件和承载向设备发指令、重连、报表兼容的部件之间差异很大,对后者贸然重新实现会变成规格发掘项目。界面上的控件只是显示部件承载着一整块规格的部件看兼容性事情就能推进贸然重新实现会变成规格发掘先封装会更安全

图12:最重要的分辨。即使外观相同,背后有没有一整块规格会改变推进方式。

4.2. 32bit / 64bit 与进程边界

这一点经常被忽视,但相当本质。

in-proc 的 OCX 必须与加载它的进程 bitness 一致。 也就是说,不能把 32bit OCX 原样加载进 64bit 应用。

这时现实可行的选项,大致是以下三种。

  • 暂时让宿主应用也保持 32bit
  • 关进独立的 32bit 进程,与 64bit 一侧通过 IPC 或 out-of-proc COM 连接
  • 从能够去掉该 OCX 依赖的地方开始先行替换

“反正是 Any CPU,应该能解决吧”这种想法,通常不管用。 即使在新的 .NET 一侧构建 COM 兼容入口,managed 代码表面上的样子和实际 COM host 的 bitness 也是两个不同的问题。 如果在这里草率开始,就会出现能编译通过、但在部署环境里跑不起来这种令人头疼的情况。

32bit OCX 与 64bit 化的三个选项32bit OCX 无法以 in-proc 方式加载进 64bit 进程,因此只有让宿主保持 32bit、关进独立的 32bit 进程并用 IPC 或 out-of-proc COM 连接、从能去掉依赖的地方开始替换这三个选项。32bit OCX 无法 in-proc让宿主保持 32bit关进独立的 32bit 进程从能去掉的地方开始替换用 IPC 或 out-of-proc COM 连接

图13:bitness 的壁垒靠意志力跨不过去,现实可行的选项就落在这三个上。

4.3. 注册、分发、权限、许可

技术上能调用,却在分发环节死掉。 这在 ActiveX / OCX 上相当常见。

容易出问题的地方是这一带。

  • regsvr32 的执行方式依赖于个人经验
  • 依赖 DLL 的部署方式是隐性的
  • 需要管理员权限,却没有落实到运维流程中
  • 供应商控件的 design-time / runtime 许可证是分开的
  • 在开发机上能跑,但在干净环境中跑不起来

这些问题即使一行代码都不改,也足以让项目停下来。

有些情况下,免注册的架构或 side-by-side 部署方式能带来帮助,但它们不是万能的魔法。 仍然需要确认与容器一侧或分发方式的兼容性。

归根结底,ActiveX / OCX 的迁移不仅是实现层面的工作,也是分发设计 的工作。 如果把这件事往后拖,往往会在最后阶段摔得很惨。

卡在分发上的难点regsvr32 依赖个人经验、依赖 DLL 的部署是隐性的、管理员权限没有落实到运维流程、许可证按 design-time 与 runtime 分开,这些即使一行代码都不改也足以让项目停下来。分发设计regsvr32 依赖个人经验依赖 DLL 是隐性的管理员权限不在流程中许可证一分为二不动代码也会停下来

图14:技术上能调用却在分发上死掉,这类难点要与实现分开来单独设计。

4.4. STA / 消息循环 / 回调

ActiveX / OCX 不只是单纯的 DLL 调用。 它有时会带有 COM 线程模型或消息循环的前提。

尤其需要注意的是下面这些场景。

  • 只有在 UI 线程前提下才能稳定运行
  • 明明是 STA 前提,却被 MTA 一侧随意调用
  • 同步调用过程中回调却返回了
  • 接收事件所在的线程前提比较模糊

这类问题最初往往以“偶尔会卡住”“偶尔事件收不到”这种灵异故事的面貌出现。 但其本质大多是违反了前提条件。

因此,无论是封装还是替换, 在哪个线程创建、在哪个线程调用、在哪里接收事件 最好提前固定下来。

提前固定线程前提如果不提前固定在哪个线程创建、在哪个线程调用、在哪里接收事件,就会变成偶尔卡住、偶尔收不到事件这种违反前提条件的灵异故事。提前固定的三点在哪个线程创建在哪个线程调用在哪里接收事件含糊不清就会变成违反前提的灵异故事

图15:“偶尔会卡住”的本质大多是违反前提条件,靠提前固定线程约定来防范。

4.5. 有没有测试、能否观测

替换之所以困难,不仅是因为代码老旧。 更是因为没有一个标准,能用来说明“表现是否一致”。

哪怕只有以下这些东西,也会有很大帮助。

  • 按操作场景划分的冒烟测试
  • 输入输出样例
  • 界面截图或报表样例
  • 错误模式与预期行为
  • 超时或设备未连接时的日志

尤其涉及设备或报表时,往往会出现实物的行为比规格书更可信这种奇怪的现象。 如果这里没有观测手段,替换工作就会变成一场考古发掘。

观测手段支撑着替换如果没有冒烟测试、输入输出样例、报表样例、错误模式与预期行为这些观测手段,就没有标准来说明表现是否一致,替换工作会变成考古发掘。有没有有没有观测手段能说明表现一致替换变成考古发掘冒烟测试和各类样例

图16:替换的难度不取决于代码有多旧,而取决于有没有能说明“表现一致”的观测手段。

5. 各类典型场景的建议

5.1. 目前仍稳定运行的内部桌面应用

建议是 偏向保留。

满足以下条件的话,往往不需要勉强剥离。

  • 仅限公司内部使用
  • 目标终端或 OS 相对固定
  • 该 OCX 只在少数几个界面中使用
  • 改造需求较小,生命周期也能预估

不过,不应该原样裸放在那里, 而应该至少把调用位置收敛起来,这样后面会更省事。

也就是说,方针是这样。

  • 目前保留
  • 但先整理好边界
  • 等需要替换时,能从这里直接着手

这种三段式的方式是比较自然的选择。

5.2. 想把 32bit OCX 搬到 64bit 一侧

建议是 封装 / 调整架构。

这里如果正面硬闯,会走入死路。 因为无法把 32bit OCX 以 in-proc 方式放进 64bit 进程。

现实中,比较好处理的方式是把它关进 32bit 的辅助进程或 LocalServer 一侧, 再让 64bit 应用通过粗粒度的 API 与其通信。

32bit OCX32bit 辅助进程 / LocalServer64bit .NET 应用32bit OCX32bit 辅助进程 / LocalServer64bit .NET 应用用粗粒度 API 发起请求in-proc 调用返回结果 / 事件转换后的结果

图17:把 OCX 关进 32bit 辅助进程,再让 64bit 应用隔着粗粒度 API 使用它的架构。

这里的要点是,不要把细粒度的方法原样全部转发。 进程间边界一旦流转大量细碎调用,很快就会变得难受。

  • 把粒度收敛到 1 次操作 = 1 次请求 左右
  • 把返回值和错误整理成有意义的单位
  • 在边界处记录日志

保持这种形式,之后真正替换内部实现时也会更轻松。

上面是以自己写辅助 EXE 为前提写的,不过在这之前还有一段,有个选项值得先确认一下。

如果那个 OCX / DLL 是 in-proc 的 COM 服务器,也就是能用 InprocServer32 注册的东西,那就可以把它放进 Windows 自带的代理进程里,以 out-of-proc 的 Local Server 形式对外提供。 只要给 CLSID 加上 AppID,再往那个 AppID 键里写入空字符串的 DllSurrogate 就行,自己这边一行代码都不用写。 注册的步骤,整理在 COM/OCX/ActiveX 开发中容易踩坑的注册与 bitness 陷阱 的 3.5 中。

不过,代理进程并不会消除 bitness 的差异。 消失的只是 必须放在同一个进程里 这条约束,调用会变成 out-of-proc,封送和进程间通信的成本仍然照样存在。

选哪一个,大致可以用下面这张表来决定。

情况 选择 理由
靠方法调用和事件就能闭环的自动化对象 先试代理进程 因为只靠注册就能变成 out-of-proc,不需要写代码
交互的类型可以用 IDispatch 或已注册的 proxy / stub 封送 先试代理进程 因为跨越边界所需的处理已经现成
该 CLSID 上已经有 LocalServer32 之类的 EXE 注册 代理进程派不上用场 因为 EXE 服务器或服务的启动始终优先
想作为贴在窗体上负责绘制的可视化控件使用 考虑别的架构 因为窗口的前提是位于宿主进程内部
细粒度调用非常频繁,原样中继会很重 自己写辅助 EXE 因为需要自己持有一层,把调用收拢成粗粒度 API
自定义接口没有封送器 自己写辅助 EXE 因为准备 proxy / stub,或者自己决定边界上的类型会更快
想自己掌控初始化顺序、重连、超时和日志 自己写辅助 EXE 因为代理进程的进程生存期由 COM 掌管

也就是说,代理进程是 值得先试一次的最小手段,自己写 EXE 则是 想自己设计边界时的手段。 3.1 的表里“想把 32bit OCX 直接放进 64bit 进程 → 封装 / 调整架构”这一行并没有变。请把它读作:那个“封装”的内部还分成两段。

5.3. IE / WebBrowser 前提的界面

建议是 优先替换。

这是“现在能跑”与“今后也能轻松维护”不太一致的领域。 IE 模式在兼容性上确实帮了很大忙,但前提依然是 IE 系的那一套。

因此,比较清晰的思路是这样区分。

  • 为了不影响公司业务,先用 IE 模式续命
  • 但不要把续命与长期设计混为一谈
  • 替换方向可以从 WebView2、纯 Web、原生 UI + Web 混合方案中选择

续命一侧的具体做法也写一下。IE 模式并不是“装上 Edge 就自动生效”的东西,只有用策略指定了目标站点之后,才会用 IE 系引擎打开。配置的入口有以下三个。

做法 配置 补充
逐一列举站点 在 Microsoft Edge 78 及以后的组策略“Configure the Enterprise Mode Site List”中,指定企业模式站点列表 XML 的位置 这是最基本的形式
沿用旧 IE 一侧的列表 Internet Explorer 的策略“Use the Enterprise Mode IE website list” 如果 Edge 一侧也有策略,则以 Edge 的为准
把整个内网都转过去 启用 Microsoft Edge 77 及以后的组策略“Send all intranet sites to Internet Explorer” 覆盖范围会变大,所以它替代不了资产盘点

前提是 Windows 和 Edge 都装了最新更新、已引入 Microsoft Edge 的管理模板、并且在 Windows 功能中启用了 Internet Explorer 11。缺了这些,IE 模式就会失败。

另外,IE 模式下 ActiveX 控件和 Browser Helper Object 是可以运行的。也就是说 续命是真的能做到的。正因如此,如果不定好结束条件就一直用下去,就会再也脱不开身。关于怎么剥离,整理在 IE 模式依赖系统的脱离指南 中。

IE 模式续命的思路IE 模式只有用策略指定目标站点之后才会生效,ActiveX 也能运行,所以续命是真的能做到的,但不要把续命与长期设计混为一谈,不定好结束条件就会脱不开身。用策略指定目标站点以 IE 模式打开连 ActiveX 一起续命定好结束条件再使用不定的话就脱不开身

图18:IE 模式的续命正因为“真的有效”,才更要与结束条件配套使用。

尤其是如果 WebBrowser 控件只是被当作一个简单的 HTML 查看器使用, 那么替换的优先级就相当高。

而如果浏览器内的 ActiveX 还承担着本地文件、设备、签名、专有插件之类的角色, 那就不只是渲染引擎的更换,而是 原生联动的重新设计。 这部分的话题会明显更重。

5.4. 承载设备控制或专有规格的 ActiveX

建议是 先封装。

这类部件外表简单,内部却相当厚重。 即使 SDK 的资料很薄,也可能因为在现场长期运行, 暗中积累了这些行为。

  • 连接失败时的等待方式
  • 超时后的重试
  • 事件顺序
  • 用来吸收实际设备怪癖的规避处理
  • 异常或错误代码的解读方式

这类部件如果因为“反正是老旧的”就重新实现,现场测试很大概率会炸。

因此,从下面这个顺序开始会比较安全。

  1. 把现有部件关进边界内部
  2. 补充日志,让内部发生的事情变得可见
  3. 收集测试场景与实际设备的行为模式
  4. 之后再切出可以替换的范围

看起来不起眼,但在实务中,这往往是最有效的做法。

承载规格的 ActiveX 的推进方式按照把现有部件关进边界内部、补充日志让内部发生的事情可见、收集测试场景与实际设备的行为模式、之后再切出可替换范围的顺序来推进。关进边界内部补充日志实现可见化收集场景与实际设备的行为模式切出可替换的范围

图19:承载设备控制或专有规格的部件,按这个顺序推进,现场测试就不容易炸。

6. 常见的反模式

反模式 问题所在 首选修正方式
因为有 ActiveX 就全面重写 容易出现规格遗漏和工作量爆炸 先盘点资产,切出边界
试图把 32bit OCX 直接放进 64bit 应用 原理上不可行 隔离到 32bit 一侧,或调整架构
界面代码到处直接调用控件 API 容易变得无法替换 收敛到 adapter / facade
靠人工方式执行 regsvr32 步骤 环境差异会导致每次都出事 考虑安装程序、脚本、清单化
有 IE 模式就觉得安心 容易把续命和长期方案混为一谈 制定替换计划与结束条件
替换前没有记录行为 无法判定是否完成 准备冒烟测试、样例数据、日志

其中在实务中特别常见的有 3 个。

  1. 急于全面重写
  2. 轻视 bitness 的壁垒
  3. 把 API 散落到整个应用中

只要避开这 3 点,出问题的概率就会明显下降。

特别常见的三个反模式急于全面重写、轻视 bitness 的壁垒、把控件 API 散落到整个应用中,只要避开这三点,出问题的概率就会明显下降。急于全面重写避开这三点轻视 bitness 的壁垒把 API 散落到整个应用出问题的概率明显下降

图20:反模式中遇到率最高的就是这三个,光是避开效果就很大。

7. 开始迁移时的检查清单

ActiveX / OCX 的项目,与直接动手实现相比,先做资产盘点会更顺利。顺序大致如下。

  1. 梳理使用中的 OCX / DLL
    • 文件名、版本、ProgID、CLSID、供应商、是否有许可证
  2. 梳理在哪里使用
    • 界面、功能、报表、设备、批处理、Office 联动等
  3. 确认 bitness 与宿主条件
    • 32bit / 64bit、in-proc / out-of-proc、STA 前提、浏览器依赖
  4. 确认分发条件
    • 注册方式、依赖 DLL、管理员权限、静默安装、干净环境下能否复现
  5. 制作冒烟测试
    • 不只是正常路径,也包括失败时、未连接时、超时时
  6. 建立边界
    • adapter、service、facade、跨进程桥接等
  7. 以 1 个界面、1 个功能、1 台设备等小单位先行验证
  8. 从验证成功的边界开始,逐步扩大 保留 / 封装 / 替换 的范围

如果跳过这些步骤,之后甚至很难说清楚“当时到底难在哪里”。

8. 大致的使用区分

情况 首选方案
仅限公司内部使用、稳定运行、改动很小 保留
只想让周边 .NET 化 封装
32bit / 64bit 相冲突 封装 / 调整架构
IE / WebBrowser / 浏览器 ActiveX 依赖 替换
单纯的 UI 部件且有替代方案 替换
承载设备控制、报表、专有规格 封装
注册或分发每次都出问题 封装或替换

拿不准的时候,先分辨清楚 这是 UI 部件,还是承载规格的边界面,判断就不容易出错。

9. 总结

如何处理 ActiveX / OCX,不是靠因为是遗留资产就讨厌它来决定的事情。

首先要看的要点有 4 个。

  1. 这个部件只是单纯的 UI,还是承载规格的边界面
  2. 是否必须在同一个进程中使用
  3. 32bit / 64bit、注册、浏览器依赖、许可证会不会卡住
  4. 替换之前,能否观测到它的行为

只要看清这 4 点,大致就能这样整理。

  • 稳定运行且生命周期可预估,就保留
  • 只想让周边现代化,就封装
  • 是 UI 部件或依赖浏览器,就替换
  • 承载着一整块规格的部件,先封装再分阶段替换

遗留技术不是拿来嘲笑的对象,而是 承载着历史与契约的实物。 不过,与这些实物共处,仍然需要边界设计。

一旦能够把保留、封装、替换结合起来思考, ActiveX / OCX 的项目就会突然变成一个可以驾驭的问题。

总结的整理稳定运行且生命周期可预估就保留,只想让周边现代化就封装,是 UI 部件或依赖浏览器就替换,承载着一整块规格的部件先封装再分阶段替换。ActiveX / OCX 的处理方式稳定运行就保留周边现代化就封装UI 或浏览器依赖就替换一整块规格就封装后分阶段替换

图21:只要看清这 4 个要点,保留、封装、替换就能整理成这个形状。

10. 这类咨询比较合适

这个主题,哪怕只是在正式开发之前做 方针整理,也很容易产生价值。

例如,以下这些咨询就相当合适。

  • 想盘点一下到底该替换哪些 OCX
  • 只想先整理清楚 32bit / 64bit 的卡点
  • 想迁向 .NET,但希望保留 COM 入口
  • 想比较供应商已停止维护的 ActiveX 的续命方案与撤退方案
  • 想看看能从哪里开始剥离 IE / WebBrowser 依赖
  • 想先安全地分离出 1 个界面、1 个功能

ActiveX / OCX 的项目,胜负往往取决于 边界怎么划分,而不是实现本身。 在全面改造之前,先从现状梳理、架构比较、迁移顺序设计入手,就已经很有意义。

11. 参考资料

  • Microsoft Learn: AxHost Class (System.Windows.Forms)
    • https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.axhost
  • Microsoft Learn: Aximp.exe(Windows 窗体 ActiveX 控件导入器)
    • https://learn.microsoft.com/ja-jp/dotnet/framework/tools/aximp-exe-windows-forms-activex-control-importer
  • Microsoft Learn: How to: Add ActiveX Controls to Windows Forms
    • https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-add-activex-controls-to-windows-forms
  • Microsoft Learn: Expose .NET Core components to COM
    • https://learn.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com
  • Microsoft Learn: Registration-Free COM Interop
    • https://learn.microsoft.com/en-us/dotnet/framework/interop/registration-free-com-interop
  • Microsoft Learn: DllSurrogate
    • https://learn.microsoft.com/en-us/windows/win32/com/dllsurrogate
  • Microsoft Learn: Registering the DLL Server for Surrogate Activation
    • https://learn.microsoft.com/en-us/windows/win32/com/registering-the-dll-server-for-surrogate-activation
  • Microsoft Learn: 关于 Microsoft Edge 的常见问题
    • https://learn.microsoft.com/ja-jp/deployedge/microsoft-edge-frequently-asked-questions
  • Microsoft Learn: What is Internet Explorer (IE) mode?
    • https://learn.microsoft.com/en-us/deployedge/edge-ie-mode
  • Microsoft Learn: WebBrowser Class (System.Windows.Forms)
    • https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.webbrowser
  • Microsoft Learn: Introduction to Microsoft Edge WebView2
    • https://learn.microsoft.com/en-us/microsoft-edge/webview2/
  • KomuraSoft Blog: COM 的 STA/MTA 基础知识 - 避免挂起的思路
    • /zh-CN/blog/2026/01/31/000-sta-mta-com-relationship/
  • KomuraSoft Blog: 从 C# 使用 C++ 原生 DLL 时,为什么最好用 C++/CLI 做一层包装
    • /zh-CN/blog/2026/03/07/000-cpp-cli-wrapper-for-native-dlls/
  • KomuraSoft Blog: COM 有用的案例研究 - 从 32bit 应用调用 64bit DLL 时
    • /zh-CN/blog/2026/01/25/002-com-case-study-32bit-to-64bit/
  • KomuraSoft Blog: 什么是 COM / ActiveX / OCX - 区别与关系一并解说
    • /zh-CN/blog/2026/03/13/000-what-is-com-activex-ocx/
  • KomuraSoft Blog: 从 VBA 带类型地使用 .NET 8 DLL 的方法 - COM 公开与 dscom TLB
    • /zh-CN/blog/2026/03/16/007-dotnet8-dll-typed-vba-com-dscom-tlb/
  • KomuraSoft Blog: 什么是 Reg-Free COM - 免注册使用 COM 的机制
    • /zh-CN/blog/2026/03/16/011-what-is-reg-free-com/
  • KomuraSoft Blog: COM/OCX/ActiveX 开发中容易踩坑的注册与 bitness 陷阱
    • /zh-CN/blog/2026/04/15/001-com-ocx-activex-pitfalls-visual-studio-bitness-admin-rights/
  • KomuraSoft Blog: IE 模式依赖系统的脱离指南
    • /zh-CN/blog/2026/04/25/003-ie-mode-internal-web-system-life-extension-and-exit/
  • KomuraSoft Blog: IE 模式之后用 WebView2 就够了吗 —— ActiveX 跑不起来的制约与现实的迁移设计
    • /zh-CN/blog/webview2-embed-web-ui-in-windows-apps/

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

ActiveX / OCX 应该替换吗?
不应该单纯看“是否老旧”,而要看这个部件承担了什么职责来判断。如果只是有替代品的普通 UI 部件,就替换;如果承载着设备控制、报表或专有文件格式,就先封装,理清边界;如果一直稳定运行且改动范围很小,保留也是现实的选择。“干脆全面重写”和“因为害怕就永久冻结”这两种做法出问题的概率都很高。
32bit 的 OCX 能从 64bit 应用中使用吗?
以 in-proc 方式是不能的。OCX 必须与加载它的进程 bitness 一致,这是原理上的限制。现实可行的选项有三个:暂时让宿主应用也保持 32bit;把它关进一个独立的 32bit 进程(辅助 EXE 或 COM LocalServer),再通过 IPC 或 out-of-proc COM 与 64bit 一侧连接;或者从能够去掉该 OCX 依赖的地方开始先行替换。
浏览器中的 ActiveX 依赖该怎么处理?
更适合以替换为优先。Microsoft Edge 本体并不支持 ActiveX,IE 模式的定位只是一种续命措施。WebBrowser 控件同样带着 IE 系的世界观,如果只是想显示 HTML,WebView2 是第一选择。不过 WebView2 并不是完全对等的替换件,依赖 IE DOM 的脚本以及 window.external 相关的前提,都不会原样迁移过去。
“封装”ActiveX / OCX 具体是指做什么?
是指把 ActiveX / OCX 关进一个狭窄的边界内部,对外则以新的 API 或新的界面部件呈现。常见的形式有 WinForms 宿主 + AxHost、通过 32bit 辅助 EXE 或 LocalServer 做跨进程桥接,以及在 .NET 一侧提供 COM 兼容入口。封装时不要把旧 API 原样大量复制,而应该做成粗粒度的方法,在边界上明确日志、超时和异常转换的职责,并让未来的替代实现也能用同一个接口无缝换入。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表