VC++ ATL 编写 Excel COM 加载项:从编译注册到事件回调实战

发布时间:2026/10/12 13:40:37
VC++ ATL 编写 Excel COM 加载项:从编译注册到事件回调实战 简介这份资源面向具备一定C基础、希望深入COM组件开发的Windows程序员聚焦于用Visual C结合COM与ATL为Microsoft Office Excel编写自定义插件这一典型场景。压缩包共23个文件约20KB以h头文件、c与cpp源文件为主辅以def模块定义、idl接口描述、tlb类型库、rgs注册脚本及rc资源脚本等完整呈现了一个ATL COM插件工程从接口声明、对象实现到注册部署所需的各类骨架文件目录结构清晰便于对照理解工程组织方式。资源围绕Excel插件开发的关键环节展开涵盖COM接口声明、IUnknown实现、组件注册与Excel对象模型交互等核心知识点读者可借此掌握ATL宏的用法、类型库生成机制以及插件与Excel通信的基本思路。目前已有532人学习下载适合作为COM与ATL入门的实践参考也可作为扩展Excel功能的开发起点。1. 从一份 2000 年代的 VC 工程包说起它到底能跑出什么翻到一份名为VC COM ATL为Office Excel编写插件.zip的工程包第一反应不是“老古董”而是“这东西现在还能不能编译、能不能注册、能不能在 Excel 里点出来”。包里没有可执行文件全是源码和工程文件ExcelAddin.dsw、ExcelAddin.dsp、ExcelAddin.idl、ExcelAddin.cpp、Excel2000Addin.rgs、MSADDNDR.tlh还有dlldata.c、ExcelAddin_p.c这类 MIDL 代理/桩文件。它对应的是 VC 6.0 时代用 ATL 写 Excel COM 加载项的典型骨架目标是把一个进程内 COM 组件注册成 Excel 的 AddIn让 Excel 启动时加载它从而在菜单或工具栏上多出一个按钮背后调用你写的 C 逻辑。这份资源适合两类人一是需要维护老 Office 插件、接手历史代码的工程师二是想搞明白 COM/ATL 到底怎么跟 Excel 的 IDispatch 打交道、不想只看理论的人。它不解决“用 C# 写 VSTO”那种现代需求但它把 COM 注册、IDispatch 派发、连接点事件、类型库导入这些底层环节全摊在源码里对理解 Office 插件机制很有价值。下面按“先看清结构、再动手编译注册、最后排坑和进阶”的顺序拆。2. 拆开工程看骨架ATL 怎么把 COM 对象塞进 Excel2.1 工程文件与类型库的对应关系拿到一个老 VC 工程先别急着双击.dsw。VC 6.0 的.dsw是工作区文件.dsp才是项目文件里面记录了源文件列表、编译选项、MIDL 设置。这个包里ExcelAddin.dsp和ExcelAddin.dsw成对出现说明它是标准的 VC6 工作区结构。真正决定 COM 接口长什么样的是ExcelAddin.idlMIDL 编译器读它生成ExcelAddin.h、ExcelAddin_i.c、ExcelAddin_p.c、dlldata.c。其中ExcelAddin_i.c里是 IID、LIBID、CLSID 这些 GUID 的定义ExcelAddin_p.c是代理/桩代码dlldata.c是跨进程封送支持。MSADDNDR.tlh这个文件值得单独看一眼。它不是工程源码而是用#import导入 Office 类型库后自动生成的 C 头里面是IDispatch、_Application、Workbook、Worksheet等接口的智能指针封装。也就是说这个工程通过#import方式拿到 Excel 的 COM 接口而不是手写IDispatch::Invoke。常见做法是在StdAfx.h里加一行#import libid:00020813-0000-0000-C000-000000000046 version(1.3) raw_interfaces_only编译时自动生成.tlh和.tli。包里已经带了MSADDNDR.tlh说明作者当时已经跑通过一次导入。Excel2000Addin.rgs是注册脚本ATL 用它把 CLSID、ProgID、InprocServer32 这些键写进注册表。ExcelAddin.def导出DllGetClassObject、DllCanUnloadNow、DllRegisterServer、DllUnregisterServer四个标准函数缺一个 Excel 都找不到组件。Button1.bmp是工具栏按钮位图ExcelAddin.rc和Resource.h管资源。把这些文件串起来整个加载项的加载链路就清楚了Excel 读注册表 → 找到InprocServer32指向的 DLL → 调DllGetClassObject→ 拿到类厂 → 创建 AddIn 对象 → 通过IDispatch调OnConnection等方法。2.2 IDispatch 与连接点Excel 怎么回调你的代码Excel 加载项不是普通 COM 组件它必须实现IDispatch并且暴露一组特定名字的方法Excel 通过IDispatch::Invoke按名字调用。最核心的是OnConnection、OnDisconnection、OnAddInsUpdate、OnStartupComplete、OnBeginShutdown。这些名字来自 Office 的IDTExtensibility2接口。包里Excel2000Addin.h和Excel2000Addin.cpp就是实现这个接口的地方ExcelAddin.idl里应该能看到dispinterface或者interface的声明。连接点用于反向通知。如果插件要响应 Excel 的“新建工作簿”“保存”等事件需要实现IDispEventImpl并在OnConnection里通过IConnectionPointContainer找到 Excel 的事件源调用Advise建立订阅。包里没有单独的事件实现文件但Excel2000Addin.cpp里通常会有SINK_ENTRY宏和DispEventAdvise调用。这部分是很多新手翻车的地方忘了Advise就收不到事件忘了Unadvise退出时可能崩。下面这段是 ATL 加载项类的典型声明结构我按这个包的命名习惯还原出来方便对照// Excel2000Addin.h 里加载项类的大致结构 class ATL_NO_VTABLE CExcel2000Addin : public CComObjectRootExCComSingleThreadModel, public CComCoClassCExcel2000Addin, CLSID_Excel2000Addin, public IDispatchImplIExcel2000Addin, IID_IExcel2000Addin, LIBID_EXCELADDINLib, public IDispEventImpl1, CExcel2000Addin, DIID_AppEvents, LIBID_Office, 1, 0 { public: CExcel2000Addin() {} DECLARE_REGISTRY_RESOURCEID(IDR_EXCEL2000ADDIN) BEGIN_COM_MAP(CExcel2000Addin) COM_INTERFACE_ENTRY(IExcel2000Addin) COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP() // 连接点映射把 Excel 事件映射到本地处理函数 BEGIN_SINK_MAP(CExcel2000Addin) SINK_ENTRY_EX(1, DIID_AppEvents, 0x0000061C, OnNewWorkbook) END_SINK_MAP() STDMETHOD(OnConnection)(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom); STDMETHOD(OnDisconnection)(ext_DisconnectMode RemoveMode, SAFEARRAY** custom); void __stdcall OnNewWorkbook(IDispatch* Wb); };DECLARE_REGISTRY_RESOURCEID把注册信息指向资源里的.rgs脚本BEGIN_COM_MAP决定QueryInterface能返回哪些接口BEGIN_SINK_MAP把事件 DISPID 映射到成员函数。IDispEventImpl的第一个模板参数是连接点编号必须和SINK_ENTRY_EX里的编号一致否则事件收不到。OnConnection里拿到Application指针后通常要QueryInterface成Excel::_Application再DispEventAdvise建立事件订阅。这些步骤在包里都有对应代码只是 VC6 的语法和现在略有差异。3. 动手编译与注册从 .dsw 到 Excel 菜单按钮3.1 用现代 Visual Studio 打开老工程VC 6.0 的.dsw在 VS2019/2022 里不能直接打开会提示升级。常见做法是新建一个 ATL 项目把源文件逐个加进去或者用 VS 的“打开旧项目”向导让它自动转换。转换后重点检查三处字符集、目标平台、MIDL 设置。老工程默认多字节字符集现代 VS 默认 Unicode#import生成的.tlh在两种字符集下接口签名不同混用会报IDispatch参数类型不匹配。目标平台建议选 x86因为很多老 Office 是 32 位64 位 Excel 加载 32 位 DLL 会直接失败。转换后先别编译打开项目属性确认“MIDL → 输出”里生成的_i.c、_p.c、.h路径和工程里已有文件不冲突。如果原工程已经带了ExcelAddin_i.c而 MIDL 又生成一份链接时会报重复定义。解决办法是把已有文件从编译列表移除让 MIDL 重新生成或者关掉 MIDL 的自动生成、只用手工文件。这一步是血泪经验老工程里手工生成和自动生成混在一起最容易出 LNK2005。3.2 注册 DLL 与在 Excel 中启用加载项编译出ExcelAddin.dll后注册有两种方式。一是用regsvr32二是让安装程序调DllRegisterServer。开发阶段直接:: 以管理员身份运行注册 32 位组件 regsvr32 D:\build\ExcelAddin.dll :: 如果要在 64 位系统上注册 32 位组件用 SysWOW64 下的 regsvr32 C:\Windows\SysWOW64\regsvr32.exe D:\build\ExcelAddin.dll :: 卸载时加 /u regsvr32 /u D:\build\ExcelAddin.dllregsvr32会调用 DLL 的DllRegisterServer后者读.rgs脚本写注册表。注册成功后在 Excel 里点“文件 → 选项 → 加载项 → 管理COM 加载项 → 转到”应该能看到ExcelAddin或Excel2000Addin的条目。勾选后 Excel 会创建对象并调OnConnection。如果列表里没有说明注册表里HKEY_CLASSES_ROOT\CLSID\{你的CLSID}\InprocServer32没写对或者ProgID没注册。提示注册前先确认 DLL 的位数和 Excel 位数一致。32 位 Excel 只能加载 32 位 DLL64 位 Excel 只能加载 64 位 DLL。用任务管理器看 Excel 进程有没有(32 位)后缀。OnConnection里一般会加菜单或工具栏按钮。老代码常用CommandBar对象通过Application-CommandBars拿到然后Controls-Add加按钮再put_OnAction绑定一个宏名或直接挂事件。包里Button1.bmp就是给按钮用的。现代 Office 里 CommandBar 仍然可用但微软推荐 Ribbon。如果只是验证加载项是否跑起来先不加 UI在OnConnection里弹个MessageBox最直接。3.3 用 IDispatch 读写单元格的最小验证加载项跑起来后最直接的验证是读写当前活动单元格。下面这段代码放在OnConnection里能确认 COM 调用链通了// 在 OnConnection 中拿到 Application 后读写活动单元格 Excel::_ApplicationPtr spApp; spApp Application; // Application 是 OnConnection 传入的 IDispatch* if (spApp ! nullptr) { Excel::_WorkbookPtr spWb spApp-ActiveWorkbook; if (spWb ! nullptr) { Excel::_WorksheetPtr spSheet spWb-ActiveSheet; Excel::RangePtr spRange spSheet-Cells-Item[1][1]; spRange-Value2 _variant_t(LHello from ATL Addin); // 读回来验证 _variant_t v spRange-Value2; MessageBoxW(NULL, (LPCWSTR)_bstr_t(v), LAddin, MB_OK); } }Excel::_ApplicationPtr是#import生成的智能指针ActiveWorkbook、ActiveSheet、Cells、Item都是 Excel 对象模型里的标准成员。Value2比Value快因为它不处理日期和货币的 Variant 转换。_variant_t和_bstr_t是comdef.h里的封装负责 Variant 和 BSTR 的生命周期。这段代码能跑通说明类型库导入、IDispatch 调用、Excel 对象模型访问都正常。如果编译报Cells不是成员检查#import的版本号Office 2000 和 Office 2016 的类型库版本不同生成的接口名可能有差异。4. 避坑与排查老 ATL 加载项最容易翻车的五处4.1 注册成功但 Excel 加载项列表里没有现象regsvr32提示成功但 Excel 的 COM 加载项对话框里找不到条目。原因通常是.rgs脚本里只注册了InprocServer32没注册ProgID或AddIn相关键。Excel 识别加载项依赖HKEY_CLASSES_ROOT\CLSID\{...}\InprocServer32和HKEY_CLASSES_ROOT\YourProgID有些还需要在HKEY_CURRENT_USER\Software\Microsoft\Office\Excel\Addins下写LoadBehavior。解决检查.rgs内容确认ProgID和CLSID都写了必要时手工在注册表里补LoadBehavior3。4.2 编译报 MIDL 找不到 Office 类型库现象#import那行报cannot open type library。原因是指定的 LIBID 或版本号在当前机器上不存在或者 Office 没装完整。解决用OleView查看本机 Office 类型库的实际 LIBID 和版本把#import里的version(1.3)改成实际版本。常见做法是直接#import C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE让它从可执行文件导入但这样依赖路径换机器要改。4.3 事件收不到或退出时崩溃现象订阅了NewWorkbook事件但从不触发或者关闭 Excel 时崩溃。原因是DispEventAdvise没调用或者Unadvise在OnDisconnection里漏了。解决在OnConnection里拿到Application后立刻DispEventAdvise在OnDisconnection里先DispEventUnadvise再释放指针。顺序反了会导致 Excel 还在回调时对象已析构直接崩。4.4 32 位/64 位不匹配导致加载失败现象Excel 启动时提示“无法加载 COM 加载项”。原因是 DLL 位数和 Excel 位数不一致。解决用dumpbin /headers ExcelAddin.dll | findstr machine看是 x86 还是 x64再确认 Excel 位数。32 位 Excel 必须配 32 位 DLL注册也要用SysWOW64\regsvr32。4.5 多线程模型选错导致死锁现象加载项里做耗时操作时 Excel 卡死。原因是 ATL 默认CComSingleThreadModel而 Excel 是 STA apartment跨线程调用没封送。解决如果要在后台线程访问 Excel 对象必须把接口指针封送到目标线程或者改用CComMultiThreadModel并自己处理封送。简单做法是别在后台线程碰 Excel 对象所有 UI 操作放主线程。5. 进阶把加载项做成可部署的安装包与调试技巧5.1 用 .rgs 和安装程序完成注册与卸载开发阶段用regsvr32够了交付时得靠安装程序。.rgs脚本是 ATL 注册的文本描述安装程序可以调DllRegisterServer也可以直接按.rgs写注册表。关键键值包括HKCR\CLSID\{CLSID}默认值设为 ProgIDHKCR\CLSID\{CLSID}\InprocServer32默认值设为 DLL 全路径、ThreadingModel设为ApartmentHKCR\ProgID指向 CLSID。卸载时反向删除。如果安装程序用 InstallShield 或 WiX可以直接把.rgs转成注册表项避免依赖regsvr32。注意注册表里 DLL 路径变了要重新注册。如果安装到Program Files下路径含空格.rgs里写路径时注意转义。5.2 调试加载项的两种实用方式第一种是附加到 Excel 进程。在 VS 里“调试 → 附加到进程”选EXCEL.EXE然后在OnConnection或按钮处理函数里下断点。前提是加载项已经注册并在 Excel 里启用。第二种是改注册表让 Excel 启动时自动加载LoadBehavior设为 3加载时启动或 16按需加载。调试时建议设为 3省得每次手动勾选。如果断点不命中检查 DLL 是否被 Excel 真正加载用 Process Explorer 看EXCEL.EXE的模块列表里有没有你的 DLL。5.3 从 CommandBar 迁移到 Ribbon 的最小改动老代码用 CommandBar 加按钮现代 Office 里 Ribbon 是主流。迁移不必重写整个加载项可以在OnConnection里创建IRibbonExtensibility实现通过GetCustomUI返回 Ribbon XML。ATL 里实现IRibbonExtensibility需要加IDispatch和回调方法Ribbon XML 里用onAction指向回调。这样加载项在 Excel 2010 及以上显示为 Ribbon 按钮在更老版本里仍可回退到 CommandBar。包里没有 Ribbon 相关文件但这是这类加载项继续可用的常见做法。5.4 验证加载项是否真正释放的检查清单退出 Excel 后用任务管理器确认EXCEL.EXE进程消失。如果残留说明加载项里有对象没释放常见的是IDispatch指针忘了Release或者连接点没Unadvise。在OnDisconnection里把所有CComPtr置空确保引用计数归零。还可以在DllCanUnloadNow里打日志看 Excel 退出时有没有调它。如果没调说明还有外部引用。从那以后我每次拿到这种老 ATL 工程都先跑一遍“注册 → 加载 → 读写单元格 → 退出看进程”这条最小闭环再动业务代码。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询