
前阵子接了个老项目的活甲方那边的上位机是十几年前用VB6写的界面土得掉渣但功能跑得好好的谁也不敢动。现在要在里面塞一个新做的数据可视化模块重写整个上位机不太现实源码都找不全了。最后定下来的方案就俩字塞控件。把新模块做成一个能拖到老界面上的ActiveX控件老程序里拖一个框新的界面就在里面跑起来。问题来了ActiveX这东西虽然老但界面部分真用MFC或者ATL从头写光画那几个动态图就够我喝一壶的。于是我就把主意打到了Qt身上——用Qt做界面爽再用ActiveQt把它包装成OCX这条路走通之后是真的省事。这篇就聊聊怎么在VS里配合Qt把一个看着还算现代的控件做成能被老宿主吃下去的ActiveX OCX包含环境、工程结构、核心实现和一堆我踩过的坑。1. 先把方向掰扯清楚为什么是Qt配ActiveX1.1 ActiveX控件这个老物件到底还活在哪些地方很多人一听ActiveX就觉得这是上个时代的遗物浏览器插件都被淘汰多少年了还折腾它干嘛。这话只对了一半浏览器里确实不让跑了但在桌面宿主程序里ActiveX活得比你想的滋润。典型场景有这么几类工控组态软件里嵌入的第三方功能面板、医疗仪器上位机里的波形展示控件、政企内部OA和报表系统里嵌的打印或者图表模块还有各种基于VB6、Delphi、早期MFC写的行业软件。这些系统有个共同点源码基本不可考改不动但业务还得继续往上加功能。这时候ActiveX控件就是一个几乎唯一的选择。宿主程序只要支持OLE容器就能把一个OCX当成普通控件拖进窗体然后通过属性、方法、事件跟它通信。对宿主来说它就是个黑盒对我们来说只要把OCX的接口约定好里面用啥技术实现随便。这就给了我们一个非常大的发挥空间——里面我用Qt怎么画都行外面宿主看到的只是一个规矩的COM组件。热词里那个在此页上的activex控件和本页上的其他部分的交互可能不安全的提示其实就是当年IE加载ActiveX时的安全警告现在桌面宿主里基本不会再看到这种弹窗但控件自身的注册和权限问题依然存在后面会细说。1.2 Qt做控件界面的甜头以及那个绕不开的MSVC为什么非得用Qt说白了就是用惯了就回不去了。用原生C加MFC画一个带圆角进度条、带动态阴影、还带平滑动画的面板你可能得手写几百行GDI或者Direct2D代码还得自己处理重绘、双缓冲、DPI缩放。Qt这边QSS几行样式就把panel控件圆角搞定了QPainter画自定义图形、QVariantAnimation做过渡动画、QChart直接出图表开发效率完全不是一个量级。热词里有人搜qt 自定义进度条、qt绘图、panel控件圆角这些都是Qt的常规操作做出来的控件外观确实能比MFC那套好看不止一个档次。但有个硬性前提你必须接受用ActiveQt做ActiveX服务器只能用MSVC编译器MinGW彻底没戏。原因在于ActiveQt的qaxserver模块底层依赖的是Windows的COM和OLE接口还要生成类型库Type Library这些东西是MSVC工具链和微软的MIDL工具才能配合出来的。MinGW没有对应的COM基础设施编译到链接阶段必挂。所以你在下载Qt的时候必须选MSVC版本比如qt-opensource-windows-x86-5.14.x里面的msvc2017或者msvc2019套件MinGW套件装了也白装做ActiveX是用不上的。1.3 两条路线对比选错了要多走半天弯路真要做ActiveX摆在你面前的无非两条路一条是老老实实MFC/ATL一条是Qt的ActiveQt。我把这俩拉个表你一看就明白差在哪。对比维度MFC/ATL 原生开发Qt ActiveQt界面开发效率低手写绘图代码多高QSSQPainter现成控件学习成本高COM/ATL模板要熟中会Qt基本能上手生成类型库需MIDL手动配置ActiveQt自动生成调试难度较高注册后调试麻烦中可写Qt容器调试依赖体积小只依赖MFC/CRT大需带一堆Qt DLL适合人群熟COM的老手熟Qt的开发者我自己是做Qt出身的选后者没有悬念。唯一的代价就是最终产出的控件会依赖Qt的运行时DLL部署时要多打包几个文件体积大点但换来的是开发速度翻倍这笔账怎么算都划算。如果甲方对体积极为敏感那才需要重新掂量。提示如果你的上位机宿主要求控件能在非常旧的系统上跑比如某些工控机还停在特定旧版本系统Qt版本要跟着往下降Qt 5.9甚至更早的版本可能才合适别一上来就装最新的。2. 环境准备版本对齐比什么都重要2.1 VS、Qt、Qt VS Tools三者版本必须咬合环境这一关我见过太多人栽在版本不匹配上。三个东西Visual Studio、Qt库、Qt VS Tools插件这三者的版本关系是个链条。经验是这样Qt 5.14/5.15推荐配VS2017或VS2019的MSVC工具集如果你用VS2022那就选Qt官方的msvc2019套件通常也能兼容ABI基本一致但偶尔会有运行时库的小问题。Qt VS Tools这个插件是从VS里管理Qt版本、创建Qt工程用的。装完之后在VS菜单里会多出一个Qt VS Tools进去配置Qt Versions把Qt安装目录下的msvc2019\bin\qmake.exe路径填进去。这一步如果填错后面新建工程会找不到Qt库编译直接报一堆找不到头文件的错。有个细节容易被忽略Qt VS Tools的版本要和你VS的版本对应。VS2019配Qt VS Tools 3.xVS2022配更晚的版本。装错了插件菜单可能不显示或者点了没反应。我建议直接用VS的扩展管理器在线装让它按你的VS版本自动挑合适的那一版。2.2 安装Qt时那几个必须勾上的组件用官方在线安装器或者离线包装Qt的时候很多人图省事全默认结果ActiveQt模块没装上后面工程里#include QAxFactory直接找不到文件。关键勾选项我给你列清楚MSVC套件本体比如MSVC 2019 64-bit / 32-bit这是做ActiveX的必需项。Qt ActiveQt模块这一项有时候藏得比较深在部分版本里它是默认包含的但某些精简安装里会被单列成可选组件一定要勾上。Qt Core/Gui/Widgets基础三件套做控件肯定要用Widgets。装完之后去Qt的安装目录看一眼路径类似于Qt\5.15.2\msvc2019\plugins\如果能找到axserver相关的文件夹说明ActiveQt装上了。qaxserver.prl、qaxcontainer.lib这些文件也要存在于lib目录里不然链接会报undefined reference。2.3 32位还是64位动手前就定死这是重灾区。ActiveX控件能不能被宿主加载位数必须和宿主完全一致。宿主是32位程序你做了一个64位的OCXregsvr32注册会说找不到模块或者直接静默失败。32位的宿主只能加32位的控件没有例外没有兼容模式。判断方法很简单打开宿主程序的进程任务管理器里看有没有带「(32位)」后缀。或者更直接一点看宿主的exe文件头用dumpbin /headers查machine字段是x86就是32位x64就是64位。确定之后Qt套件也要对应选x86还是x64那一版。我吃过一次亏甲方说我们的程序就在64位系统上跑的我就默认为64位结果做出来注册不上查了半天才发现他们的主程序其实是32位编译的只是跑在64位系统上而已。系统位数和程序位数是两码事别混。注意做ActiveX控件时建议直接选定一个位数从头做到尾。中途从32位切到64位工程文件、依赖库、注册命令全要重来一遍非常折磨。3. 从零搭一个能注册的ActiveX控件工程3.1 工程结构长什么样先看鸟瞰图Qt Creator里其实自带一个ActiveQt Server的工程模板新建工程时选它会给你生成一套骨架包含.pro文件、控件类、QAXFACTORY注册宏。但我更建议你用VS配合Qt VS Tools来做因为最终要调试、要生成ocxVS那边工具链更顺手。用VS的话你可以先拿Qt Creator生成模板再用VS打开或者直接手写工程文件。一个典型的ActiveX控件工程结构大概是这样的MyActiveXControl/ ├── MyActiveXControl.pro # 工程配置 ├── MyActiveXControl.h # 控件类声明 ├── MyActiveXControl.cpp # 控件类实现 ├── main.cpp # 工厂注册入口 ├── MyActiveXControl.rc # 资源文件含类型库、图标 ├── mycontrol.ico # 控件图标 └── MyActiveXControl.def # 导出符号定义其中main.cpp里放的是QAXFACTORY宏这是整条链路的注册中枢没有它编译出来的DLL就是普通DLLregsvr32会告诉你不是有效的OLE控件。3.2 .pro文件逐行拆解每一行都有讲究.pro文件是qmake的配置文件ActiveX这块的配置和普通Qt工程差别很大我逐行说。QT core gui widgets axserver TEMPLATE lib CONFIG qaxserver dll TARGET MyActiveXControl DEFINES QAXSERVER HEADERS MyActiveXControl.h SOURCES MyActiveXControl.cpp main.cpp RC_FILE MyActiveXControl.rc逐行解释QT axserver这一行是灵魂它告诉qmake引入ActiveQt的服务器模块链接时会带上qaxserver的库。漏了这一行QAXFACTORY的宏会找不到定义。TEMPLATE lib我们的产物是DLL所以是lib模板不是app。CONFIG qaxserver dllqaxserver让qmake走ActiveX生成流程会自动调用工具生成类型库dll表示输出动态库。TARGET MyActiveXControl最终生成的文件名Windows下会自动加.dll后缀虽然文件是DLL但ActiveX控件的惯例扩展名是.ocx这个后面可以改。RC_FILE资源文件里面注册了控件的类型库和图标这个文件通常是Qt Creator模板自动生成的不要轻易手改里面的GUID。如果你用MSVC在VS里打开这个.proQt VS Tools会把它转成一个vcxproj编译时调用的是nmake和qmake组合底层逻辑和Qt Creator一致只是IDE换了个壳。3.3 GUID从哪来千万别手抄别人的ActiveX控件的身份是靠GUID来区分的每个控件至少要有三个GUIDClassID控件类ID、InterfaceID接口ID、EventsID事件ID另外还有工厂和类型库的GUID。这些GUID必须是全球唯一的你如果偷懒复制了别人的GUID会导致注册表里控件身份冲突轻则加载不了重则把别的软件搞崩。生成GUID的工具很多最省事的就是VS自带的工具VS菜单「工具」→「创建GUID」选好格式复制出来。或者在PowerShell里跑[guid]::NewGuid()也能生成。格式都是那种{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}的带花括号字符串。Qt Creator的ActiveQt模板有一手很贴心的操作它在生成工程的时候会自动帮你生成一套随机GUID而且每次新建工程都不一样。所以最省心的做法还是用模板起手别自己从零手写。注意GUID一旦生成并注册进系统后续如果再改老系统里注册的旧记录会残留导致行为诡异。所以GUID定下来之后就不要再动工程版本升级时保持GUID不变只改Version信息。3.4 控件类骨架与QAXFACTORY宏详解控件的核心就是一个普通的QWidget派生类加上几个Q_CLASSINFO声明。我写一个能跑的最小骨架。头文件#ifndef MYACTIVEXCONTROL_H #define MYACTIVEXCONTROL_H #include QWidget #include QAxBindable class MyActiveXControl : public QWidget, public QAxBindable { Q_OBJECT Q_CLASSINFO(ClassID, {5E9B0F01-8A3C-4B7D-9E12-1A2B3C4D5E6F}) Q_CLASSINFO(InterfaceID, {5E9B0F02-8A3C-4B7D-9E12-1A2B3C4D5E6F}) Q_CLASSINFO(EventsID, {5E9B0F03-8A3C-4B7D-9E12-1A2B3C4D5E6F}) Q_CLASSINFO(Version, 1.0) Q_CLASSINFO(Description, My Qt based ActiveX control) Q_PROPERTY(int value READ value WRITE setValue) Q_PROPERTY(QString caption READ caption WRITE setCaption) public: explicit MyActiveXControl(QWidget *parent nullptr); int value() const; QString caption() const; public slots: void setValue(int v); void setCaption(const QString c); void reset(); signals: void valueChanged(int newValue); private: int m_value; QString m_caption; }; #endif这里的关键点Q_CLASSINFO(ClassID, ...)这四条是控件注册的身份证缺一不可值填前面生成的GUID。继承QAxBindable不是强制的但如果你希望控件支持宿主的数据绑定比如把控件某个属性绑到数据库字段就要继承它并实现对应的虚函数。public slots里的函数会被ActiveQt自动暴露成控件的方法宿主可以直接调用。signals里的信号会被自动暴露成控件的事件宿主可以订阅。再看主入口main.cpp这里放工厂注册#include QAxFactory #include MyActiveXControl.h QAXFACTORY_BEGIN( {1A2B3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D}, // 类型库GUID {1A2B3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D}) // 应用程序GUID QAXCLASS(MyActiveXControl) QAXFACTORY_END()QAXFACTORY_BEGIN的两个参数分别是类型库和应用的GUID这两个也是唯一的别和控件的三个GUID重复。QAXCLASS(MyActiveXControl)这一行声明了要把哪个类注册成ActiveX控件。如果你有多个控件类就在这里多写几行QAXCLASS。4. 让控件真能用的核心实现4.1 属性、方法、事件是怎么暴露给宿主的ActiveQt最舒服的地方在于它把Qt的元对象系统Meta-Object直接映射成了COM的接口。你不用手写IDL不用管vtable布局只要用Qt的语法声明编译时ActiveQt就能把类型库生成出来。映射关系一张表说清楚Qt侧声明映射到COM宿主能做什么Q_PROPERTY属性Property读取/设置属性值public slots方法Method调用方法signals事件Event订阅并响应Q_ENUMS枚举属性/方法的参数类型属性那条尤其值得说。宿主通过COM访问控件属性本质上是调用控件暴露的getter/setter函数。Qt这边的Q_PROPERTY必须同时给READ和WRITE才会变成可读写属性只给READ就是只读。宿主端VB或者C#里可以直接obj.Value 5那样写背后就调到了你的setValue。方法就更容易理解了。public slots里声明的函数宿主可以像调用普通函数一样调参数和返回值会被ActiveQt做类型转换。常用的int、double、QString、bool这些基础类型都支持直接映射。4.2 信号与事件宿主怎么收到控件的通知控件内部发生了什么事怎么通知宿主靠信号。比如控件里有个数据更新了你emit valueChanged(newValue)宿主那边如果订阅了valueChanged事件就会收到回调。但要注意一点宿主端订阅事件的方式和语言有关。VB6里用Private Sub Control_ValueChanged(ByVal newValue As Long)这样的形式C#里用事件委托MFC里则是通过连接点Connection Point机制。ActiveQt会为每个信号生成一个对应的连接点接口宿主端按自己的语言惯例去接就行。如果宿主不订阅信号发出去就丢了这是COM事件的正常行为不用管。控件端不用操心谁在听只管emit。提示信号参数类型尽量用简单类型QString会被转成BSTRint转成LONG。尽量避免用自定义的复杂结构体做信号参数宿主端不一定认识。4.3 界面绘制Qt的Widget就是控件的脸控件最终显示出来的样子就是你QWidget派生类里paintEvent画出来的或者你往里塞的子控件布局出来的。这里有个关键理解ActiveX控件的可视区域由COM容器分配的矩形决定控件要自己响应尺寸变化。用Qt的话你正常写QWidget的resizeEvent和paintEvent就行Qt会处理后面的消息循环。控件在宿主里显示的时候宿主会给控件分配一个窗口区域Qt的子窗口系统挂上去剩下的和普通QWidget程序没区别。这意味着你在Qt Creator里开发调试界面时看到的效果和最终在宿主里显示的效果基本一致不会出现本地好看一进宿主就变形的情况。这里可以放心用QSS做样式圆角、渐变、阴影都能正常渲染唯一要注意的是别依赖半透明跨窗口特效因为宿主窗口的分层渲染不一定支持容易出黑边或者闪烁。普通的控件内部半透明是没问题的。5. 编译、注册与部署的完整流程5.1 编译产物怎么变成可注册的OCX编译成功后你会得到一个MyActiveXControl.dll。ActiveX控件的惯例扩展名是.ocx为了让它看起来专业点可以在.pro里加一行QMAKE_EXTENSION_SHLIB ocx这样生成的直接就是.ocx文件。如果懒得改把dll改名为ocx也不影响功能因为Windows加载时看的是内容不是扩展名。注册的命令是regsvr32。但这里有两个坑必须说第一32位控件必须用32位的regsvr32。64位系统里有两个regsvr32C:\Windows\System32\regsvr32.exe是64位的C:\Windows\SysWOW64\regsvr32.exe是32位的。注意名字容易让人记混——System32里装的是64位程序SysWOW64里装的是32位程序。注册32位控件要用SysWOW64里那个。第二注册需要管理员权限因为要写注册表。普通权限跑regsvr32会提示失败。注册命令# 32位控件用管理员权限打开cmd C:\Windows\SysWOW64\regsvr32.exe /s D:\build\MyActiveXControl.ocx # 64位控件 C:\Windows\System32\regsvr32.exe /s D:\build64\MyActiveXControl.ocx反注册就把regsvr32换成/u参数。/s是静默模式不加会弹个成功提示框。5.2 依赖DLL为什么注册成功后还是加载不了注册成功只是第一步控件能不能被加载还取决于它运行时依赖的DLL能不能找到。Qt编译出来的控件依赖一堆Qt运行时至少要带这些Qt5Core.dllQt5Gui.dllQt5Widgets.dllQt5AxServer.dll或Qt6对应的名字平台插件目录 platforms\qwindows.dll这些DLL要么放到控件的同目录要么放到系统PATH能找到的地方。我一般建议全部放在控件同目录下这样最省事也方便打包分发。有个测试依赖是否齐全的土办法写一个最简单的Qt程序用QAxWidget去加载这个控件看能不能加载成功。加载不了十有八九是缺DLL。这个容器程序下一节细说。5.3 写个Qt容器程序专治加载疑难杂症调试ActiveX控件最麻烦的地方在于宿主程序往往是个黑盒加载失败就给你一个模糊的错误你根本不知道哪一步出的问题。这时候一个自己写的容器程序能救命。用QAxContainer模块写个极简的容器#include QApplication #include QAxWidget #include QDebug int main(int argc, char *argv[]) { QApplication app(argc, argv); QAxWidget *widget new QAxWidget; widget-setControl({5E9B0F01-8A3C-4B7D-9E12-1A2B3C4D5E6F}); // 用ClassID加载 if (widget-isNull()) { qDebug() 控件加载失败检查注册和依赖DLL; return -1; } widget-resize(800, 600); widget-show(); return app.exec(); }用setControl时可以直接传ClassID也可以传控件的ProgID比如MyActiveXControl.MyActiveXControl。加载成功与否用isNull()判断。这招我用得最多能迅速把宿主环境问题和控件本身问题分开。你的容器程序位数也必须是32位或64位的对应版本不然同样加载不了。这就是为什么我开头反复强调位数要先定死。6. 踩坑实录那些年我掉进去的坑6.1 那个经典报错80040200到底怎么回事热词里有一条call to dliregisterserver for spr32x60.0cx returns 80040200 activex cont这个80040200错误码在ActiveX注册里非常经典。它是CO_E_ERRORINDLL或者说是控件内部DllRegisterServer返回失败。常见原因有几个第一类依赖DLL缺失。DllRegisterServer执行时会去加载控件依赖的库如果Qt的运行时DLL找不到尤其是AxServer那一个注册函数会在内部失败返回这个码。解决就是把所有依赖DLL放到同目录。第二类注册了错误位数的控件。用64位regsvr32去注册32位控件也会报类似的错。第三类控件内部初始化出错。比如你的控件类构造函数里访问了不该访问的资源注册时DllRegisterServer会尝试创建实例做自检这时候抛异常就注册失败。排查顺序我一般是先确认位数对不对再确认依赖DLL齐不齐最后再怀疑代码。前两个排除了第三个才需要上调试器。6.2 注册成功但宿主加载不出来注册提示成功但宿主里加载控件显示空白或者报错这种情况其实挺常见。可能的原因宿主程序是32位控件是64位或者反过来注册表里虽然记录了两边的信息但宿主只能找到自己位数的那个找不到就失败。控件的ProgID和宿主里写的对不上。有些宿主需要你手动填ProgID填错了就找不到。宿主用了IE内核触发了安全策略。某些老宿主基于IE的WebBrowser控件会有安全限制需要调整宿主的安全区域设置。线程模型不匹配。ActiveX控件的线程模型在注册表里是记录的默认是Apartment单线程套间。如果宿主是多线程环境加载时机不对会失败。排查这类问题的利器还是那个容器程序容器能加载但宿主不能基本可以锁定是宿主环境问题不是控件本身。6.3 常见问题速查表现象最可能原因处理办法regsvr32报80040200依赖DLL缺失/位数错/初始化失败补DLL、核对位数、调试构造regsvr32报不是有效OLE控件没生成类型库/QAXFACTORY缺失检查.pro的axserver配置注册成功宿主空白位数不匹配/ProgID写错对齐位数、核对ProgID控件显示但没响应事件没连接/方法签名不匹配检查连接点、核对类型编译报undefined reference没链接qaxserver库检查QT axserver控件界面变形DPI/尺寸处理没做实现resizeEvent、处理缩放类型库生成失败GUID格式错误/重复重新生成GUID、检查格式6.4 几个我自己的实操心得第一别在控件构造函数里做重活。注册阶段和加载阶段都会创建控件实例构造函数里如果做了耗时的初始化会导致注册超时或宿主卡死。重活放到showEvent或者一个专门的init方法里。第二QSS样式尽量写死在资源文件里。控件的运行环境不受你控制路径可能千奇百怪把样式表用qrc打包进二进制最稳妥。我试过把样式表放外部文件结果部署到甲方机器上路径找不到界面全变了样。第三给控件加上版本号和描述。Q_CLASSINFO里把Version和Description填好日后在系统里排查哪个版本的控件注册着一眼就能看清省得一堆同名的控件混在一起分不清。第四保留一套32位和64位的构建配置。虽然一次只做一个位数但工程里把两套配置都留好日后切位数时改个参数就行不用从头配置。我个人反复折腾下来最深的体会是ActiveX这层壳其实很薄真正决定成败的都是环境细节——位数、依赖、注册权限、GUID管理。界面和逻辑用Qt写反而最轻松。所以第一篇文章先把这些地基讲透等你把工程跑起来、控件能在系统里注册成功后面再往里面堆功能就是一个纯Qt开发的舒适过程了。下一篇我会接着聊控件和宿主之间更复杂的交互比如怎么在控件里回调宿主的函数、怎么处理多实例场景下的资源隔离以及类型库那一堆参数的细节。