ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VC COM ATL 开发 Excel 插件:从编译注册到事件驱动实战

VC COM ATL 开发 Excel 插件:从编译注册到事件驱动实战 简介这份资源面向具备一定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.cIID/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 静态链接冲突别选 /MTMIDL 输出目录工程根目录和.dsp里ExcelAddin_i.c的相对路径保持一致3.2 编译、注册与在 Excel 中验证加载编译通过后得到ExcelAddin.dll接下来注册。用管理员权限的 cmdregsvr32 /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 对象弹个框确认加载成功 CComPtrIDispatch 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\AddinsHKLM下同名键在部分版本被忽略。解决用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 CComObjectRootExCComSingleThreadModel, public CComCoClassCExcel2000Addin, CLSID_Excel2000Addin, public IDispatchImplIExcel2000Addin, IID_IExcel2000Addin, LIBID_EXCELADDINLib, public IDispEventImpl1, 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) { CComPtrExcel::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」这条链路再动业务代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表