简介:这份资源面向具备一定C++基础、希望深入理解COM组件机制并动手扩展Excel功能的开发者,核心是用Visual C++结合COM与ATL为Office Excel编写自定义插件。压缩包共23个文件,约20KB,以h头文件、c与cpp源文件为主,辅以def模块定义、idl接口描述、tlb与tlh类型库、rgs注册脚本及rc资源文件,完整覆盖从接口声明、对象实现到组件注册的工程骨架。资源围绕ATL COM项目模板展开,涉及接口声明、IUnknown实现、类型库生成、注册脚本编写以及与Excel对象模型交互等关键环节,并包含按钮位图等界面资源,便于读者对照工程结构理解插件从编译到注册的完整链路。目前已有531人学习下载,适合作为学习COM与ATL开发Excel插件的入门参考,帮助读者掌握组件注册、接口调用与Office互操作的基本思路。
1. 从一份 VC COM ATL 的 Excel 插件源码包说起
手里这份visual c++ VC COM ATL为Office Excel编写插件.zip,第一次解压看到ExcelAddin.dsw、ExcelAddin.dsp、ExcelAddin.idl、Excel2000Addin.rgs这一串文件名时,我下意识反应是:这是一份典型的 VC6 时代 ATL COM 加载项工程,不是那种套壳 VSTO 的现代方案。它解决的事情很具体——让 Excel 在启动时通过注册表里的 CLSID 找到你的 COM 组件,加载成 AddIn,然后你就能在IDTExtensibility2的OnConnection里拿到Application对象,去读写单元格、挂菜单、订阅事件。适合谁?适合还在维护老 Office 插件、需要理解 COM 加载项底层加载链路、或者被microsoft visual c++ 14.0 is required这类构建报错折腾过、想回头把 COM 注册机制搞明白的从业者。这份包里的.tlb、_p.c、_i.c是 MIDL 编译产物,说明它走的是完整 IDL 定义接口的正统路线,不是纯#import糊上去的。
2. 拆开工程看结构:IDL、RGS 与 MIDL 产物各自管什么
2.1 从 ExcelAddin.idl 到 _i.c/_p.c 的编译链路
COM 加载项能被 Excel 认出来,前提是接口和 coclass 有稳定的 IID/CLSID,并且这些标识被写进类型库和注册表。这份工程里ExcelAddin.idl是源头,ExcelAddin.tlb是 MIDL 编译出来的类型库,ExcelAddin_i.c和ExcelAddin_p.c分别是接口 IID 定义和代理/桩代码。很多人拿到这种包直接双击.dsw就编译,结果报一堆midl找不到,其实是因为.dsp里配置了自定义构建步骤去调 MIDL,环境变量没配好就断链。
先看 IDL 里最关键的两段,通常长这样:
import "oaidl.idl"; import "ocidl.idl"; import "MSADDNDR.idl"; // Office 加载项必须的 IDispatch 派生接口定义 [ uuid(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX), helpstring("IExcelAddin Interface"), dual, nonextensible, oleautomation ] interface IExcelAddin : IDispatch { [id(1), helpstring("初始化加载项")] HRESULT OnConnection( [in] IDispatch* Application, [in] ext_ConnectMode ConnectMode, [in] IDispatch* AddInInst, [in, out] SAFEARRAY(VARIANT)* custom); };dual表示双接口,既能早期绑定也能后期绑定;oleautomation让接口走自动化 marshal,省得自己写代理。MSADDNDR.idl是 Office 加载项的类型库,IDTExtensibility2就在里面,没有它ext_ConnectMode这些枚举根本编不过。
编译顺序上,MIDL 会先生成ExcelAddin.h(接口声明)、ExcelAddin_i.c(IID/CLSID 常量)、ExcelAddin_p.c(代理桩)。这三个文件不要手动改,改了就等着链接期符号对不上。常见做法是在.dsp的 Pre-link step 里写:
midl /nologo /mktyplib203 /win32 /Oicf ExcelAddin.idl/Oicf是优化选项,生成更紧凑的代理代码;/win32指定目标平台。如果换成 x64 构建,这里要改成/amd64,否则生成的桩代码位数不对,注册能过但调用直接崩。
2.2 RGS 脚本与注册表写入的对应关系
Excel2000Addin.rgs是 ATL 的注册脚本,编译后会嵌进资源,DllRegisterServer被调用时执行。它决定了 Excel 能不能在HKCU\Software\Microsoft\Office\Excel\Addins下找到你的加载项。典型内容:
HKCR { NoRemove CLSID { ForceRemove {你的CLSID} = s 'ExcelAddin Class' { ForceRemove Programmable InprocServer32 = s '%MODULE%' { val ThreadingModel = s 'Apartment' } TypeLib = s '{你的TypeLib GUID}' Version = s '1.0' } } NoRemove Software { NoRemove Microsoft { NoRemove Office { NoRemove Excel { NoRemove Addins { ForceRemove ExcelAddin.Connect = s 'ExcelAddin' { val Description = s 'Excel 加载项' val FriendlyName = s 'ExcelAddin' val LoadBehavior = d 3 val CommandLineSafe = d 0 } } } } } } }LoadBehavior = 3表示启动时加载并保持连接,这是加载项最常用的值;改成9是加载一次不常驻,16是按需加载。ThreadingModel = Apartment对 Office 加载项基本是硬要求,Excel 主线程是 STA,写成Both或Free会在跨线程调用时出玄学问题。%MODULE%是 ATL 的占位符,注册时自动替换成 DLL 全路径,别手写死路径。
提示:RGS 里
ForceRemove会在注册前先删旧键,调试阶段反复注册不会残留脏数据;但正式安装包建议去掉,避免误删用户配置。
3. 从零编译到 Excel 里跑起来:环境、构建与加载验证
3.1 环境配置与 .dsp 工程的关键选项
这份包是 VC6 工程格式(.dsw/.dsp),用 VS2019/2022 打开会触发工程升级向导。升级本身能过,但有两个坑:一是 VC6 的stdafx.h预编译头配置和现代 MSVC 不兼容,二是 ATL 版本差异导致CComPtr、IDispEventImpl的模板参数在某些版本下报错。我一般会先确认 ATL 组件装了没有——VS Installer 里「使用 C++ 的桌面开发」下勾选「适用于最新 v143 生成工具的 C++ ATL」。
如果不想升级工程,直接用 VC6 编译也行,但要装microsoft visual c++ 2005 sp1 atl security update redistributable那类 ATL 安全更新,否则老 ATL 头文件在部分系统上会触发编译告警甚至失败。目标平台选 x86,因为很多老 Office 是 32 位,x64 的 Excel 加载 32 位 DLL 会直接报「不是有效的 Win32 应用程序」。
工程属性里重点看三处:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 字符集 | 多字节 | VC6 工程默认 MBCS,改 Unicode 要同步改所有CString用法 |
| 运行时库 | 多线程 DLL (/MD) | 和 ATL 静态链接冲突,别选 /MT |
| MIDL 输出目录 | 工程根目录 | 和.dsp里ExcelAddin_i.c的相对路径保持一致 |
3.2 编译、注册与在 Excel 中验证加载
编译通过后得到ExcelAddin.dll,接下来注册。用管理员权限的 cmd:
regsvr32 /s ExcelAddin.dll/s静默注册,不弹框。注册成功后去注册表确认两处:HKCR\CLSID\{你的CLSID}\InprocServer32的默认值是不是 DLL 全路径,HKCU\Software\Microsoft\Office\Excel\Addins\ExcelAddin.Connect的LoadBehavior是不是 3。如果LoadBehavior被 Excel 自动改成了 2(禁用),说明加载时抛异常被 Office 拉黑了,这时候要去「文件 → 选项 → 加载项 → 禁用项目」里恢复,或者直接删掉HKCU\Software\Microsoft\Office\Excel\Resiliency下的对应键。
验证加载是否成功,最直接的办法是在OnConnection里写一行:
STDMETHODIMP CExcelAddin::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) { // 拿到 Excel Application 对象,弹个框确认加载成功 CComPtr<IDispatch> spApp = Application; if (spApp) { MessageBox(NULL, _T("ExcelAddin 已加载"), _T("提示"), MB_OK); } return S_OK; }Application是 Excel 传进来的Application对象指针,ConnectMode区分是启动加载还是手动加载。如果这个框没弹出来,先查LoadBehavior,再查 DLL 依赖的microsoft visual c++ redistributable版本对不对——缺运行库时regsvr32会报0x8007007e,但很多人只看「注册成功」四个字就以为没事。
4. 避坑与排查:加载项不显示、注册失败、事件不触发
4.1 现象:Excel 启动后加载项列表里没有你的项
原因通常是注册表写到了HKLM而不是HKCU,或者 RGS 里Addins路径拼错。Office 加载项优先读HKCU\Software\Microsoft\Office\Excel\Addins,HKLM下同名键在部分版本被忽略。解决:用regedit对比 RGS 脚本逐级核对,确认ExcelAddin.Connect这个键名和ProgID一致。
4.2 现象:regsvr32 报「模块已加载,但找不到入口点 DllRegisterServer」
原因一般是 DLL 导出表里没有DllRegisterServer,或者.def文件没把导出符号列全。这份包里有ExcelAddin.def,检查里面有没有:
EXPORTS DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE DllRegisterServer PRIVATE DllUnregisterServer PRIVATE少一个都会导致注册失败。PRIVATE表示不参与按名称导入,但导出表里必须有。
4.3 现象:编译时报从"ATL::CString"转换为"const std::string"失败
这是 ATL 的CString和标准库std::string混用导致的。VC6 的CString没有隐式转换到std::string的运算符,现代 ATL 也没有。解决:显式转换,std::string s = CT2A(csStr);,或者统一用CString别混。如果工程里同时#import了msado15.dll之类生成_com_ptr_t的库,还要注意_bstr_t和CString的转换方向。
4.4 现象:事件回调不触发,断点进不去
原因多半是连接点没建对。IDispEventImpl的模板参数里DIID_ExcelAppEvents的 GUID 必须和 Excel 实际的事件接口 IID 一致,抄错一位就静默失败。解决:用OleView打开 Excel 的类型库,找到AppEvents的 IID 复制过来,别手敲。另外AtlAdvise要在OnConnection里调用,OnDisconnection里AtlUnadvise,顺序反了会导致 Excel 退出时挂起。
4.5 现象:调试时 Excel 卡死,VS 附加进程后断在CoCreateInstance
这是 STA 线程重入问题。Excel 主线程在等你返回,你又在OnConnection里同步调用了 Excel 的某个方法,形成死锁。解决:把耗时或回调 Excel 的逻辑放到IDispatch::Invoke的异步路径,或者用PostMessage丢回消息队列,别在OnConnection里直接操作Range。
5. 进阶:用 IDispEventImpl 订阅 Excel 事件并做单元格级验证
加载项跑起来只是第一步,真正体现 COM 加载项价值的是事件驱动。这份工程里Excel2000Addin.h和Excel2000Addin.cpp是连接点实现的核心,我一般会这样挂SheetChange事件:
// Excel2000Addin.h class ATL_NO_VTABLE CExcel2000Addin : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CExcel2000Addin, &CLSID_Excel2000Addin>, public IDispatchImpl<IExcel2000Addin, &IID_IExcel2000Addin, &LIBID_EXCELADDINLib>, public IDispEventImpl<1, CExcel2000Addin, &DIID_AppEvents, &LIBID_ExcelLib, 1, 0> { public: BEGIN_SINK_MAP(CExcel2000Addin) SINK_ENTRY_EX(1, DIID_AppEvents, 0x0000061D, OnSheetChange) END_SINK_MAP() STDMETHOD(OnSheetChange)(IDispatch* Sh, IDispatch* Target); // ... };SINK_ENTRY_EX里的0x0000061D是SheetChange的 DISPID,不同 Excel 版本这个值可能不同,用OleView查AppEvents的DISPID确认。IDispEventImpl的第一个模板参数1是连接点 ID,要和SINK_ENTRY_EX的第一个参数对上。
实现里做单元格级验证:
STDMETHODIMP CExcel2000Addin::OnSheetChange(IDispatch* Sh, IDispatch* Target) { CComPtr<Excel::Range> spRange; Target->QueryInterface(__uuidof(Excel::Range), (void**)&spRange); if (spRange) { CComVariant vVal; spRange->get_Value(&vVal); // 取变更后的值 if (vVal.vt == VT_R8 && vVal.dblVal < 0) { spRange->put_Interior(/* 标红 */); } } return S_OK; }QueryInterface到Excel::Range需要#import生成的Excel.tlh,这份包里没有,得自己从 Office 安装目录的EXCEL.EXE导入。验证事件是否生效,最土但最可靠的办法是在OnSheetChange里写日志文件,别依赖弹框——弹框在事件回调里容易和 Excel 的消息循环打架。
从那以后我每次拿到这种老 COM 加载项包,都强制先跑一遍「注册 → 查注册表 → 启动 Excel → 看 LoadBehavior → 断点 OnConnection」这条链路,再动业务代码。希望帮到你。
本文还有配套的精品资源,点击获取