ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MFC ActiveX控件开发实战:网页摄像头云台控制

MFC ActiveX控件开发实战:网页摄像头云台控制

简介:基于MFC的OCX控件开发教程与示例源代码,面向需要在Windows桌面应用中集成ActiveX控件,或通过Web页面调用OCX能力的开发者。整套内容以海康摄像头控制为实际案例,系统讲解从MFC ActiveX工程创建、CWnd控件类继承、DISPID接口设计、属性与事件实现,到控件注册及与硬件SDK交互的完整流程,并包含Web端嵌入与调用OCX的思路,适合有一定C++基础、正在做设备控制或浏览器混合方案的工程人员。压缩包共88个文件,约68.7MB,其中主要类型包括21个dll运行库、8个h头文件、5个cpp源码、4个lib库文件、2个ocx控件文件,另有PDF教程、HTML说明、工程配置文件及编译过程生成文件,目录层级清晰,便于按模块阅读和二次编译。目前已有487人学习下载,资料既提供文档讲解,也给出可运行的MFC工程与海康SDK接入示例,对掌握OCX开发要点、解决云台控制和Web调用问题有实际帮助。

1. 为什么还要写 MFC 的 OCX 控件:网页调用云台控制绕不过这条老路

基于 MFC 的 OCX 控件开发教程及示例源代码,这标题放到今天多少有点“老古董”味道。可我在做模拟项目 X 的时候,偏偏就是它救了我:一台内网里的网络摄像机,网页要实时预览、要按着按钮转云台,浏览器却只认 ActiveX。翻遍现代 Web 技术,没有比 OCX 更直接的通道。这套教程配合源码,适合正在对接老系统、需要把某个厂商 SDK 封装成控件再丢给网页调用的工程师。如果你还没被 ActiveX 的注册和安全设置虐过,正好拿它试试水。

2. MFC ActiveX 控件开发前夜:OCX 这个黑匣子怎么和网页对话

2.1 OCX 的实质:一个跑在客户端的 COM 组件

从浏览器角度想这个问题,摄像头 SDK 是本地 DLL,网页 JavaScript 没有权限直接加载它。想做到网页点按钮云台就转,必须有一个能被浏览器识别并加载的本地模块,这个模块就是 ActiveX 控件,后缀常见为 .ocx。ActiveX 是建立在 COM 之上的一套接口规范,只要实现了 IUnknown、IDispatch、IOleObject 等关键接口,浏览器就能把它当作一个“插件”实例化。

MFC 的厉害之处在于 COleControl 已经把这些接口全部包好了。开发者只需要在派生类里写自己的业务方法,不用自己手写 COM 接口表,也不用管引用计数怎么维护。网页调用 ocx1.Open() 时,COM 运行库先通过注册表里的 CLSID 找到控件 DLL,然后定位到 IDispatch::Invoke,最终进入 MFC 的分发映射,由映射找到 Open 对应的 C++ 成员函数。整个过程对网页是透明的,你只要在 classid 里写对一串 GUID。

理解这个链路后,你会发现 OCX 的所有麻烦都集中在客户端环境:杀毒软件可能拦截、浏览器安全级别可能禁掉、32 位与 64 位可能不匹配。后面讲避坑时会挨个说。

2.2 为什么是 MFC 而不是 ATL:向导生成的差距

很多人在选择开发 ActiveX 方案时会在 MFC 和 ATL 之间纠结。我直接说结论:封装厂商摄像头 SDK,选 MFC 能省一半时间。

表格整理一下两者差异:

对比项MFC ActiveXATL ActiveX
框架代码量向导生成几十个文件,包含窗口、属性页、类型库生成精简 COM 代码,但事件源、连接点要手动补
Web 脚本调用分发映射和事件映射开箱即用需要手写 IDL 和连接点,接口改动容易遗漏
部署依赖可能依赖 MFC 运行时,建议静态链接依赖少,但功能层要自己搭
适用场景快速封装 SDK、需要属性页、需要事件回调追求体积小、系统级 COM 组件

我在另一个项目里用 ATL 写过独立 COM 组件,确实体积小。但每当要加一个方法,都要检查接口声明、实现类、IDL 三处是否一致,漏一处编译就过不去。MFC 类向导添加方法时会同步更新分发映射、ODL 和头文件,几乎不会出现“方法没暴露给脚本”的情况。

代价是 MFC 控件默认依赖 MFC 运行库,发布机器如果没装 VC 运行库,控件注册后一加载就崩。解决方案是用静态链接 MFC,后面部署章节会具体说。

2.3 把接口先定明白:方法要返回值,事件要提前设计

在写代码前,先把控件对外接口定下来。以这个 OCX 控件为例,最小接口集合足够覆盖常见场景:

  • 属性:DeviceIP、Port、UserName、Password、Channel
  • 方法:Open()、Close()、StartPreview()、StopPreview()、PTZControl(cmd, stop)
  • 事件:OnLogined(BSTR ip)、OnDisconnected(BSTR reason)

先说方法返回值。厂商 SDK 的登录接口返回的是登录句柄,错误码也是 long,所以 Open() 返回 long 比返回 BOOL 实用得多。网页 JS 能拿到错误码,就能自己判断是登录失败还是网络超时。再看 IDL 片段,理解 id 怎么对应 JS:

[ uuid(1A2B3C4D-....-....-....-............), helpstring("CameraOCX Control"), control ] coclass CameraOCX { [default] interface _DCameraOCX; [default, source] dispinterface _DCameraOCXEvents; }; library CameraOCXLib { dispinterface _DCameraOCX { properties: [id(1)] BSTR DeviceIP; [id(2)] short Port; methods: [id(3)] long Open(); [id(4)] void PTZControl(short nCmd, short nStop); }; }

这里 id(1) 对应网页 JS 里的 ocx1.DeviceIP,id(3) 对应 ocx1.Open()。重要的是参数类型只能使用自动化兼容类型:BSTR、short、long、VARIANT_BOOL。如果你自定义一个结构体作为方法参数,网页脚本根本没法传值,这是新手最常见的“为什么 JS 调不了”的原因。

事件这块容易被忽略。网页想等设备断线通知,不能靠轮询,最好由 OCX 主动触发事件。在 IDL 里用 source dispinterface 声明事件,MFC 类向导会生成 FireEvent 调用。接口设计阶段就把事件想好,后面就不用返工。

2.4 安全标记:为什么控件在别的机器上经常被拦

ActiveX 控件默认会被浏览器当成不安全的插件。用户在受信任站点之外打开页面,会看到“此 ActiveX 控件可能不安全”之类的提示,甚至直接显示灰色 X。要减少这种干扰,有两个方向。

第一个方向是让控件实现 IObjectSafety 接口,声明自己“可安全执行脚本”和“可安全初始化”。MFC 向导不会自动生成这层代码,常见做法是写一个辅助类实现 IObjectSafety,再重写控件的 QueryInterface 暴露它。这个方案适合你要把控件放到公网或大量非受信任站点时使用。

第二个方向更粗暴,也更常见:在内网系统的部署中,直接把页面站点加入受信任区域,在组策略里允许运行未签名 ActiveX。这个做法省代码,但安全边界会变小。我一般建议开发阶段用第二种,发布前再补 IObjectSafety 标记。否则你会在不同客户机器上反复被安全拦截折磨,而且问题时有时无,非常玄学。

注意:即使浏览器弹出“是否允许运行 ActiveX 控件”,点击允许后 JS 访问控件方法仍可能失败。这类现象多半不是安全策略问题,而是控件本身加载失败或 32/64 位不匹配。

3. 完整开发步骤:从建工程到云台能转起来

3.1 建工程:VS 向导生成的东西已经能注册

用 Visual Studio 新建项目时,选择 MFC ActiveX Control。给项目命名,比如 CameraOCX。向导会生成 CCameraOCXCtrl、CCameraOCXPropPage 等类。编译一次,项目属性里默认会执行注册,你甚至不需要手动 regsvr32 就能在开发机用。

生成的控件类里有一段分发映射,这是 JS 调用入口的关键:

BEGIN_DISPATCH_MAP(CCameraOCXCtrl, COleControl) DISP_PROPERTY_NOTIFY_ID(CCameraOCXCtrl, "DeviceIP", dispidDeviceIP, m_strIP, OnIPChanged, VT_BSTR) DISP_FUNCTION_ID(CCameraOCXCtrl, "Open", dispidOpen, Open, VT_I4, VTS_NONE) DISP_FUNCTION_ID(CCameraOCXCtrl, "PTZControl", dispidPTZ, PTZControl, VT_EMPTY, VTS_I2 VTS_I2) END_DISPATCH_MAP()

DISP_FUNCTION_ID 最后两个参数分别是返回值类型和参数类型。VT_I4 是 long,VTS_I2 是 short。用类向导添加方法时默认可能生成 short,如果 SDK 接口需要 int,记得手动调整成 VTS_I4,否则参数范围不够。DISP_PROPERTY_NOTIFY_ID 里的 OnIPChanged 是属性变化通知,适合在网页修改 IP 后重新断开设备。

生成控件后,classid 从工程的 .rgs 文件或 .tlb 里找。复制那一长串 GUID,后面写 HTML 的

3.2 接入 SDK:Open 方法不要只写“返回 0”

网络摄像机厂商提供的大多是 C++ 接口的 SDK DLL。OCX 里的 Open 方法要完成网络初始化、设备登录、句柄保存这几件事。如果只是随便 return 0,后面云台控制绝对会失败,因为登录句柄根本没拿到。

下面是一个简化后的实现,保留关键流程:

long CCameraOCXCtrl::Open() { if (m_lLoginHandle > 0) return m_lLoginHandle; NET_DVR_Init(); // 某些厂商SDK需要先初始化 NET_DVR_USER_LOGIN_INFO loginInfo = {0}; // m_strIP 来自 OCX 属性,网页赋值后可直接使用 sprintf_s(loginInfo.sDeviceAddress, "%s", (LPCSTR)m_strIP); loginInfo.wPort = (WORD)m_nPort; sprintf_s(loginInfo.sUserName, "%s", (LPCSTR)m_strUser); sprintf_s(loginInfo.sPassword, "%s", (LPCSTR)m_strPassword); NET_DVR_DEVICEINFO_V40 devInfo = {0}; m_lLoginHandle = NET_DVR_Login_V40(&loginInfo, &devInfo); if (m_lLoginHandle < 0) { m_lErrorCode = NET_DVR_GetLastError(); return m_lErrorCode; } return m_lLoginHandle; }

这个方法里最容易被忽略的是重复登录。如果网页不小心调两次 Open,登录句柄会被覆盖但旧句柄没退出,最终 SDK 内部资源耗尽。所以第一行做了判断,已有句柄直接返回。m_nChannel 也建议作为属性暴露出来,因为 SDK 的云台控制接口通常需要通道号。如果你的设备是单通道,可以写死 1,但多通道设备不传对就不会控制。

登录失败时,把 NET_DVR_GetLastError 的返回值存到控件成员变量里,再在 JS 中通过另一个属性读取,这样调试不用看 VC 输出窗口。我当时在模拟项目里就靠这个办法快速定位了几个客户现场的密码错误问题。

3.3 云台控制方法:停止命令是纪律

云台控制是网页交互的重点。厂商 SDK 一般会提供类似 NET_DVR_PTZControl 的函数,参数包含登录句柄、通道号、云台动作、是否停止。MFC 控件方法封装如下:

void CCameraOCXCtrl::PTZControl(short nCommand, short nStop) { if (m_lLoginHandle <= 0) return; // nCommand: 云台动作编码,来自厂商SDK头文件 // nStop: 0 表示开始动作,1 表示停止动作 NET_DVR_PTZControl(m_lLoginHandle, m_nChannel, nCommand, nStop); }

这个方法本身不复杂,复杂的是网页端的交互纪律。云台控制按钮要求“按下持续转动,松开停止”,所以 JS 必须在 onmousedown 发送 nStop=0,在 onmouseup 发送 nStop=1。如果只发开始命令忘了停止,云台会一直转到机械限位,轻则电机堵转,重则损坏设备。

我在之前一个项目里吃过这个亏:当时只处理了 onmouseup,没处理 onmouseout。用户按住按钮后鼠标滑出按钮,onmouseup 就不会触发,云台就停不下来。从那以后,我要求云台相关按钮必须同时绑定 onmouseout 兜底。更保险的做法是在 OCX 内部加一个定时器,比如每 5 秒如果没有收到新的开始命令,就自动发一次停止命令。这个兜底策略虽然粗暴,但能在网页崩溃时避免设备一直转。

3.4 注册与部署:不要只带一个 ocx 文件

发布时,开发机器上注册没问题不代表目标机器能用。最简单的注册命令是:

regsvr32 /s CameraOCX.ocx

/s 表示静默执行。卸载用 /u。如果你在 64 位系统上注册 32 位 OCX,系统默认会调用 64 位 regsvr32,导致报错“模块已加载,但没有找到 DllRegisterServer 入口点”。这时需要手动运行 32 位版本的 regsvr32,路径在 C:\Windows\SysWOW64\regsvr32.exe。

只拷贝一个 ocx 文件是不够的。厂商 SDK 往往自带几个 DLL 和依赖运行库,比如 hcNetSDK.dll 之类的组件。我建议工程属性里把 MFC 使用方式改成“在静态库中使用 MFC”,这样目标机器不用额外装 VC 运行库。SDK 的 DLL 和安全模块还是得放在 ocx 同目录,并在注册命令前先放到 C:\Windows\System32 或控件目录。

给网页自动安装时,可以用 CAB 打包。首先建一个 .inf 文件:

[version] signature="$CHICAGO$" AdvancedINF=2.0 [Add.Code] CameraOCX.ocx=CameraOCX.ocx [CameraOCX.ocx] file-win32-x86=thiscab clsid={1A2B3C4D-....-....-....-............} RegisterServer=yes

再用 makecab 命令或者 INF 工具打包。网页 object 标签的 codebase 指向 cab 文件,浏览器下载后会自动解压注册。这里最大的坑是:没有签名的 CAB 在 IE 增强安全配置下基本都会被拦。企业内网环境一般可以通过组策略放行,但外网客户机就只能去签代码签名证书。如果只是为了内网自用,直接做一个批量安装脚本,比研究 CAB 签名快得多。

3.5 网页端调用:按钮按住,云台动起来

HTML 页面里的 object 标签,classid 必须是控制件真实的 CLSID。示例:

<object id="ocx1" classid="clsid:1A2B3C4D-....-....-....-............" width="640" height="480"> </object>

如果控件带窗口并预览画面,object 的宽高决定预览区域大小。网页加载后,先用 JS 设置属性再调用 Open:

ocx1.DeviceIP = "192.168.1.64"; ocx1.Port = 8000; ocx1.UserName = "admin"; ocx1.Password = "123456"; var ret = ocx1.Open(); if (ret < 0) { alert("登录失败,错误码:" + ret); }

云台按钮和鼠标事件:

function startPTZ(cmd) { if (ocx1.Open() > 0) ocx1.PTZControl(cmd, 0); } function stopPTZ(cmd) { ocx1.PTZControl(cmd, 1); } document.getElementById("btnUp").onmousedown = function() { startPTZ(38); }; document.getElementById("btnUp").onmouseup = function() { stopPTZ(38); }; document.getElementById("btnUp").onmouseout = function() { stopPTZ(38); };

这里 38 只是示例动作编码,不同 SDK 定义不同,要打开头文件确认。onmouseout 里的 stop 非常重要,因为鼠标可能从按下状态直接移出按钮,此时 onmouseup 不触发,云台就会一直转。调用前判断 Open 返回值,避免还没登录就发云台命令。

4. 避坑清单:OCX 从开发到部署这五个坑我全替你蹚过

4.1 注册报错“没有找到 DllRegisterServer 入口点”

  • 现象:regsvr32 执行后提示模块已加载,但找不到入口点,控件注册失败。
  • 原因:系统用 64 位 regsvr32 注册了 32 位 OCX,或者 OCX 依赖的 DLL 缺失,导致导出函数无法访问。
  • 解决:先确认控件是 32 位还是 64 位,再选择对应位数的 regsvr32。64 位系统注册 32 位组件用 C:\Windows\SysWOW64\regsvr32.exe。再把厂商 SDK 的 DLL 都放到 ocx 同目录或 System32 下。如果还报错,用依赖检查工具看缺失文件。

4.2 页面加载控件后白屏,JS 报“Automation 服务器不能创建对象”

  • 现象:网页上控件区域空白或红色叉,JScript 调用 ocx1.Open() 会弹脚本错误。
  • 原因:classid 写错、控件没注册成功、浏览器安全级别禁用 ActiveX、页面脚本在控件完全加载前执行。
  • 解决:用 regsvr32 重新注册,确认注册表里 classid 有一致的 InprocServer32 键值。将页面站点加入受信任区域,并启用“对未标记为可安全执行脚本的 ActiveX 控件初始化并执行脚本”。在 JS 里把操作放到 window.onload 或 setTimeout 延迟执行,给控件加载留时间。

4.3 云台一直转,松开鼠标还不停

  • 现象:按住“上”键云台向上,松开后仍继续转,只有再按一次其他方向才停。
  • 原因:只发了开始命令,没有发停止命令;onmouseup 没有触发;或 OCX 内部没把 stop 参数透传到 SDK。
  • 解决:所有云台按钮统一绑定 onmouseup 和 onmouseout 都发 stop。OCX 方法里检查 nStop 是否为 1,确保 SDK 的停止接口被调用。我在代码里增加了定时器兜底:超过 5 秒没有新命令就自动发一次停止,防止网页崩溃或用户切窗口导致鼠标事件丢失。

4.4 页面刷新后 IE 进程崩溃或内存暴涨

  • 现象:开发调试时刷新页面,第一次正常,第二次之后 IE 卡死,任务管理器里内存持续增长。
  • 原因:OCX 的 OnDestroy 没有关闭设备登录句柄,SDK 内部线程没有退出;控件销毁时设备回调还在触发,访问了已释放的内部成员。
  • 解决:在控件的 OnDestroy 里调用 Close() 和 SDK 清理接口,比如 NET_DVR_Cleanup()。网页端在 onbeforeunload 里也主动调一次 ocx1.Close(),双保险。如果用了静态回调函数,回调里不能直接调用非静态成员,必须通过 map 找到控件实例后再调 FireEvent,否则也会崩溃。

4.5 预览画面只显示一半,或者对象被页面遮挡

  • 现象:OCX 预览画面被网站的 div 遮挡,点击按钮没反应,或画面只渲染局部区域。
  • 原因:ActiveX 控件在浏览器里是以窗口方式嵌入的,默认 z-index 低于普通 HTML 元素;如果父容器用了 overflow 滚动,控件可能无法正确响应鼠标消息。
  • 解决:将 OCX 放在独立 div 中,不要设置 float 或苛刻的 overflow。iframe 包裹控件也可以,但注意 JS 跨域问题。云台按钮不要覆盖在 OCX 上方,避免页面元素抢占鼠标事件。如果必须覆盖,建议把按钮放到 OCX 旁边而不是用绝对定位压上去。

5. 进阶:用事件把设备断开推给网页,并搭一套固定验证流程

5.1 事件映射怎么发到网页

MFC ActiveX 控件自带事件机制,添加事件后类向导会生成类似这样的映射:

BEGIN_EVENT_MAP(CCameraOCXCtrl, COleControl) EVENT_CUSTOM_ID("OnDisconnected", dispidDisconnected, OnDisconnected, VTS_BSTR) END_EVENT_MAP()

在 SDK 的断线回调或登录失败分支里调用 FireEvent:

void CCameraOCXCtrl::NotifyDisconnected(CString strReason) { FireOnDisconnected(strReason); }

网页端用 FOR 属性绑定事件,注意这是 IE 特有写法:

<script for="ocx1" event="OnDisconnected(reason)" language="JScript"> alert("设备断开:" + reason); </script>

这一步能让网页在设备掉线时主动弹出提示,而不是用户盯着画面卡死了还不知道。事件回调参数类型也必须和 IDL 里一致,都声明成 BSTR,JS 拿到的 reason 才是字符串。如果回调里传 char*,网页端会出现乱码,这也是新手常踩的坑。

5.2 我的固定验证流程

每次改完代码,我都会强制走一遍完整流程:编译 Release 版 → 手动 regsvr32 注册 → 打开 OLE/COM Object Viewer 或注册表确认 classid → 用 IE 内核浏览器打开测试页面 → 在网页里设置属性 → 调 Open 看错误码 → 分别测试登录失败、正常登录、云台开始、云台停止 → 连续刷新页面五次看进程是否稳定 → 最后拔掉网线测一次断线回调。

这套流程看起来啰嗦,但能拦住九成部署事故。尤其“连续刷新五次”这一步,很多 demo 第一次加载没问题,刷新两次就崩溃,原因就是资源释放没做好。从那以后我每次发布 OCX 都强制走一遍这套验证,客户现场翻车率明显降下来了。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进