C# Office COM 加载项

Office COM 加载项开发笔记

一、实现接口 IDTExtensibility2

这是实现 Office COM 加载项最基本的接口

添加 COM 引用 Microsoft Add-In Designer 即可

对应文件 Extensibility.dll 只包含 IDTExtensibility2 接口其中和用到的枚举 ext_ConnectMode、ext_DisconnectMode,
可以减少模块引用自行复制代码到自己项目中,注意 IDTExtensibility2 不可被混淆

⚠ 注意:开发 Office 或 WPS COM 加载项添加 COM 引用时,需要安装对应的套件才能找到相关的 COM 组件,添加 WPS COM 引用时会受到两者安装的先后顺序和管理员权限影响,导致无法添加引用,若 VS 报错无法添加,需要卸载 Office 才能顺利添加。但下文会提到仅需引用其中一套 COM 组件即可同时兼容 Office 和 WPS

#if BrandName1
namespace BrandName1 // 品牌1
#else
namespace BrandName2 // 品牌2
#endif
{[Obfuscation] // 不可被混淆[ComVisible(true)] // COM 组件类可见, 并且类型要设为公开 public[Guid("XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")] // CLSID// [ProgId("PDFelement.OfficeAddIn")] // Office COM 加载项的 ProgID 须与类全名一致public class OfficeAddIn : IDTExtensibility2{public void OnConnection(object application, ext_ConnectMode connectMode,object addInInst, ref Array custom){MessageBox.Show("OnConnection"); // 注册成功的加载项将会在对应应用启动时弹窗}// 其他 IDTExtensibility2 的接口方法...}
}

⚠ 注意:Office COM 加载项须保证类的 ProgID 与类全名完全匹配, ProgID 特性未设置时默认使用类的全名,故也无需设置;并且类不可被混淆,被其继承的接口也不可被混淆

ProgID 与产品和对应功能相关,文件关联也会用到,建议名称是 .,故示例中以 BrandName1 为名称空间,OfficeAddIn 为类名,那么 ProgID 就是 BrandName1.OfficeAddIn。为区分品牌,在品牌条件编译中使用不同的名称空间,而不是不同的类名,这样更符合规范,也更好编写注册的代码

二、注册 Office COM 加载项

COM CLSID 和 Office 产品的注册表都有 HKCU、HKLM 和 64、32位的项,为了提高兼容性,可在这些注册表项下都添加上注册信息

注册 COM 组件

C# 注册 COM 组件一般通过调用 RegAsm.exe 文件来注册,区分位数和运行时版本

%windir%\Microsoft.NET\Framework[64]_"ver"_\RegAsm.exe MyCOM.dll /codebase [/u]

RegAsm.exe 作用就是添加注册表项,避免系统缺失该文件,也为了添加日志输出,可自行写注册表实现

{HKCU|HKLM}\Software\ClassesProgID● "" = 'ProgID'CLSID● "" = 'CLSID'[Wow6432Node\]CLSID\'CLSID'● "" = 'ProgID'Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}InprocServer32● "" = "mscoree.dll"● "Assembly" = 'Assembly.FullName'● "Class" = 'ProgID'● "CodeBase" = 'Assembly.CodeBase'● "RuntimeVersion" = 'Environment.Version' ● "ThreadingModel" = "Both"ProgId● "" = 'ProgID'

⚠ 注意:RegAsm.exe 注册方式会用到反射,如果不将引用到的程序集文件放到同目录下,并且系统未注册 COM 类继承的接口时,会出错导致注册失败。如果注册方式和 RegAsm.exe 一样会用到类型本身,为避免用户未安装 COM 组件相关的应用,需要打包引用到的程序集

⚠ 注意:同一个 COM 组件项目创建不同品牌的程序集并注册时,如果两个程序集文件名相同、签名相同、版本相同,则会导致两者程序集全名相同,导致系统无法区分。区分方式是三者至少有一个不同,最简单的方式就是条件编译设置不同版本号

⚠ 注意:为提高兼容性,目标平台选择 AnyCPU 即可兼容 64/32 位系统和软件。作为 COM 组件运行时,是作为被 .NET 虚拟机进程引用的程序集来运行的,只需将目标框架设为 .NET Framework 3.5, 不需要 app.config 文件就可以兼容 3.5 和 4.0,无需编译多个框架版本。支持 .dll 和 .exe 文件,只要是 .NET 程序集就可以,如果是可将自身注册为 COM 组件 exe,那在注册时还是需要 app.config 的

添加到 Office 加载项列表

需要在加载项列表下新建加载项类 ProgID 同名子项,并添加三个必要的注册表键值

{HKCU|HKLM}\Software\[Wow6432Node\]Microsoft\Office<app>AddIns'ProgID'● FriendlyName = "加载项列表中显示的友好名称"● Description = "加载项列表中显示的描述"● LoadBehavior = 3 (启动时连接和加载)

另外还可以添加 CommandLineSafe = 1, 指示命令行操作安全,可能可以减少弹窗警告

经过这两步注册示例插件后,启动对应的 Office 应用时,就会弹出消息框,验证注册成功了

三、实现接口 IRibbonExtensibility

这个接口用于在 Office 应用的 Ribbon 中添加自定义 UI

添加 COM 引用 Microsoft Office  Object Library 即可, 是 Office 版本号

为提高兼容性,可以安装 Office 2007 获取到 12.0 版本的 COM,并将对应的文件 Office.dll 复制到项目目录中,并修改引用为相对文件,避免在其他未安装 Office 2007 的电脑上无法生成。注意此接口也要被加载项类继承,故不可被混淆

此接口只有一个 GetCutsomUI 的方法,需要返回 XML 格式的字符串

为了代码可读性,建议使用编写和加载 XML 资源文件的方式

并且在 VS 中编写 XML 添加名称空间后在编写元素属性时将会有候选词列表,十分方便

<?xml version="1.0" encoding="utf-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui"><ribbon><tabs><tab id="TestTab" getLabel="GetLabel"><group id="TestGroup" getLabel="GetLabel"><button id="TestButton" size="large"onAction="OnButtonPressed"getLabel="GetLabel"getImage="GetImage"/></group></tab></tabs></ribbon>
</customUI>
public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
{public string GetLabel(IRibbonControl control){switch(control.ID){case "TestTab": return "Test Tab";case "TestGroup": return "Test Group";case "TestButton": return "Test Button";}return null;}public {Bitmap|IPictureDisp} GetImage(IRibbonControl control){// 返回控件图像,支持 Bitmap 或 IPictureDisp 类型返回值}public void OnButtonPressed(IRibbonControl control){MessageBox.Show("Test Button Clicked!");}
}

CustomUI 注意事项

建议携带 XML 声明部分指定utf-8编码,否则如果有中文会乱码

customUI元素中的名称空间,年月可以用2009/072006/01,但 Office 2007 不支持解析前者

控件的文本、图像、悬浮提示语等,都可使用对应的属性labelimage在 XML 中直接设置。也可以使用对应的回调方法getLabelgetImage。使用回调方法的方式需要在加载项类中声明同名公开方法,比如 XML 中编写getLabel="GetLabel",C# 中就须编写对应的public string GetLabel (IRibbonControl control)方法,类似于 WPF XAML 的事件绑定,支持多个控件使用同一个方法并根据控件的 id 返回合适的值

第 3 条中属性和回调方法互斥,只允许使用其中一个。另外图像还可使用内置图像属性imageMso ,与imagegetImage也是互斥的,比如 imageMso="FileSaveAs"使用内置的另存为图像

如果使用了 dynamicMenu 控件,其 getContent 方法也需要返回 XML 格式字符串,但与第 1 条不同,不能有 XML 声明部分,否则解析失败

大小写敏感,大小写错误会导致解析失败

使用透明背景图像

CustomUI 中控件的getImage方法支持直接返回Bitmap类型,Office 支持透明背景的Bitmap,但 WPS 不支持,会用浅灰色的背景填充。这里可以转换并返回IPictureDisp类型

另外值得一提的是,Office 在切换深色主题后,黑灰单色图像还会自动转换为白色图像,WPS 没有这个机制

IPictureDisp 在 COM 组件 OLE Automation 中定义,一般在添加 Microsoft Office  Object Library 引用时会自动添加上,对应文件是 stdole.dll,我们只需要用到 IPictureDisp 接口,同样可以减少模块引用自行复制代码到自己项目中

[DllImport("oleaut32.dll", ExactSpelling = true, PreserveSig = false)]
static extern IPictureDisp OleCreatePictureIndirect(ref PictDesc pictdesc,[MarshalAs(UnmanagedType.LPStruct)] Guid iid,[MarshalAs(UnmanagedType.Bool)] bool fOwn);struct PictDesc
{public int cbSizeofstruct;public int picType;public IntPtr hbitmap;public IntPtr hpal;public int unused;
}public static IPictureDisp CreatePictureIndirect(Bitmap bitmap)
{var picture = new PictDesc{cbSizeofstruct = Marshal.SizeOf(typeof(PictDesc)),picType = 1,hbitmap = bitmap.GetHbitmap(Color.Black), // 创建纯透明底色位图hpal = IntPtr.Zero,unused = 0,};return OleCreatePictureIndirect(ref picture, typeof(IPictureDisp).GUID, true);
}

Bitmap.GetHbitmap有无参和传参Color两个重载,无参重载在内部传参Color.LightGray调用另一重载,这应该和直接返回Bitmap在 WPS 中会有浅灰色填充相关。
需要注意的是,GetHbitmap方法内部不会使用到颜色的 Alpha 值, 创建纯透明背景图像句柄,应该使用Color.Black 255,0,0,0而不是Color.Transparent``0,255,255,255

CustomUI 刷新控件

  1. 利用customUI元素的onload回调方法,在 C# 中记录IRibbonUI对象,可调用其Invalidate方法刷新整个 UI,或者调用InvalidateControl(string id)根据 id 刷新指定控件
public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
{IRibbonUI ribbon;public void OnCustomUILoad(IRibbonUI ribbon){this.ribbon = ribbon;}internal void Invalidate(){ribbon?.Invalidate();}internal void InvalidateControl(string id){ribbon?.InvalidateControl(id);}
}
  1. dynamicMenu控件invalidateContentOnDrop="true"可在每次展开时重新触发getContent刷新内容

四、Office 互操作能力

需要添加引用对应 Office 应用的互操作库,在 VS 中可以很方便的跳转 MSDN 文档

添加 COM 引用 Microsoft   Object Library 即可

下文演示 Office 导出 PDF 能力,仅作演示,另外需要释放 COM 对象

ExportAsFixedFormat 方法有很多可选参数,支持设置打印页数、包含文档信息、生成书签等

⚠ 注意:Office 2007(只有 32 位版本)导出 PDF/XPS 会提示未安装此功能时,需要用到 Office 2010 才有的 EXP_PDF.dll 和 EXP_XPS.dll 文件,复制到 Office 2007 的共享目录即可

%CommonProgramFiles[(x86)]%\Microsoft Shared\OFFICE12

Word 导出 PDF

using Microsoft.Office.Interop.Word;public class OfficeAddIn : IDTExtensibility2, IRibbonExtensibility
{Application app;public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom){if (application is Application)app = (Application)Application;}public void OnButtonPressed(IRibbonControl control){app?.ActiveDocument?.ExportAsFixedFormat(fileName, WdExportFormat.wdExportFormatPDF);}
}

Excel 导出 PDF

using Microsoft.Office.Interop.Excel;
// 工作簿
app?.ActiveWorkbook?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);// 工作表
(app?.ActiveSheet as Worksheet)?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);// 图表, WPS 不支持
app.ActiveChart?.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);// 框选区域
var range = app.Selection as Range;
var sheet = range.Worksheet;
var area = sheet.PageSetup.PrintArea;
sheet.PageSetup.PrintArea = range.Address; // 设置打印区域为选定区域
sheet.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, fileName);
sheet.PageSetup.PrintArea = area; // 还原打印区域

PowerPoint 导出 PDF

using Microsoft.Office.Interop.PowerPoint;app?.ActivePresentation?.ExportAsFixedFormat(fileName, PpFixedFormatType.ppFixedFormatTypePDF);

Publisher 导出 PDF

using Microsoft.Office.Interop.Publisher;app?.ActiveDocument?.ExportAsFixedFormat(PbFixedFormatType.pbFixedFormatTypePDF, fileName);

Outlook 导出邮件为 PDF

using Microsoft.Office.Interop.Outlook;
using Microsoft.Office.Interop.Word;var mailItem = outlook?.ActiveExplorer()?.Selection?.OfType<MailItem>()?.FirstOrDefault();
var inspector = mailItem?.GetInspector;
var document = inspector?.WordEditor as Document;
document?.ExportAsFixedFormat(fileName, WdExportFormat.wdExportFormatPDF);
// GetInspector 会打开一个隐藏窗口,比较吃内存,需要及时关闭
inspector?.Close(OlInspectorClose.olPromptForSave);

五、实现 WPS COM 加载项

须在注册 Office COM 加载项基础上(包括添加到 Office 加载项列表),另外添加到 WPS 加载项列表

添加到 WPS 加载项列表

Word 对应 WPS,Excel 对应 ET,PowerPoint 对应 WPP,不区分 64/32位

HKCU\Software\kingsoft\office{WPS|ET|WPP}AddinsWL'ProgID' = ""

Office 与 WPS COM 组件对应表

⚠ 注意:WPS 和 Office 官方为了互相兼容,Office、Word、Excel、PowerPoint 相关的 COM 接口使用相同的 CLSID。如果插件需要兼容两者,对应的互操作库文件只需要一组,因程序集内名称空间不同,且用户基本只会安装其中一套,须复制互操作库文件到运行目录,否则无法同时兼容

#### Office #### WPS
Microsoft Add-In Designer
Extensibility.dll
Kingsoft Add-In Designer
Interop.AddInDesignerObjects.dll
Microsoft Office  Object Library
Office.dll
Upgrage WPS Office  Object Library
Interop.Office.dll
Microsoft Word  Object Library
Microsoft.Office.Interop.Word.dll
Upgrade Kingsoft WPS  Object Library
Interop.Word.dll
Microsoft Excel  Object Library
Microsoft.Office.Interop.Excel.dll
Upgrage WPS Spreadsheets  Object Library
Interop.Excel.dll
Microsoft PowerPoint  Object Library
Microsoft.Office.Interop.PowerPoint.dll
Upgrage WPS Presentation  Object Library
Interop.PowerPoint.dll

六、卸载清理注册表

除了清理上文中添加的 COM 组件和加载项的注册表,还可以清理以下相关的注册表:

  1. HKCU\Software\Microsoft\Office\<app>\AddinsData插件数据

  2. HKCU\Software\Microsoft\Office\<ver>\Common\CustomUIValidationCacheCustomUI 校验缓存

  3. HKCU\Software\Microsoft\Office\<ver>\<app>\Addins版本插件列表

  4. HKCU\Software\Microsoft\Office\<ver>\<app>\AddInLoadTimes版本加载次数

  5. HKCU\Software\Microsoft\Office\<ver>\<app>\Resiliency\NotificationReminderAddinDataOffice 禁用通知

七、其他问题

未加载,加载 COM 加载项时出现运行错误

这是一个比较令人头疼的问题,可能原因有很多,但 Office 没有报错日志,导致很难排查问题

微软官方博客给出了一些解答,个人也复现了一些情况:

  • 部署问题:COM 组件注册表内容缺失项或键值,需要注意 COM 组件与 Office 加载项注册表的位数

  • 运行问题:在 Outlook 中比较明显,本身就启动缓慢卡顿,切忌在启动时调用 Sleep 函数,轻则 Office 直接提示建议禁用插件,重则直接出现未加载的问题

Outlook 退出前操作

Outlook 16.0(其他版本未测试)退出时不会触发 OnBeginShutdown``OnDisconnection,原因未知,应该是 Outlook 自己的 Bug,故 Outlook 插件不要在这两个方法中进行退出前操作

经过测试,程序退出时会触发System.Windows.Forms.Application.ThreadExit,但是不会触发(来不及?)AppDomain.CurrentDomain.ProcessExit,可以利用前者来进行退出前操作,比如保存配置和释放资源

Office 应用关闭后进程不结束

出现此问题一般是 COM 对象资源未释放干净,但是频繁使用 Office 互操作很难保证所有 COM 对象都及时正确释放。为了让进程正确退出,不可使用Process.Kill等强制方法手动结束进程,一是强制结束进程可能会导致下次打开文档时会提示文档保存异常,二是插件可在程序运行中被手动卸载,可以使用卸载当前应用程序域的方式友好解决问题

public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
{try{AppDomain.Unload(AppDomain.CurrentDomain);}catch (CannotUnloadAppDomainException){// ignored}
}

相关资料

如何使用 Visual C# .NET 生成 Office COM 加载项 - Office

[MS-CUSTOMUI]: CustomUI |Microsoft 学习

COM Add-In 加载失败疑难解答 |Microsoft 学习

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/908787.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20241309 实验二《Python程序设计》实验报告

20241309 2024-2025-2 《Python程序设计》实验二报告 课程:《Python程序设计》 班级: 2413 姓名: 梅良谦 学号:20241309 实验教师:王志强 实验日期:2025年3月26日 必修/选修: 公选课 一、实验内容 1.设计并完成一个完整的应用程序,完成加减乘除模等运算,功能多多益善。…

【操作系统】进程管理(二)

一、前言之前已经介绍了操作系统的各个模块,现在来具体深入学习操作系统中的进程管理。 二、进程的基本概念在未配置OS的系统中,程序的执行方式是顺序执行,即必须在一个程序执行完成后,才允许另外一个程序执行;在多道程序环境下,则允许多个程序并发执行。也正是程序的并发…

MybatisPlus--持久层接口

IService: Save()boolean save(T entity) //批量插入 boolean saveBatch(Collection<T> entityList) boolean saveBatch(Collection<T> entityList, int vatchSIze)SQL:INSERT INTO user (name, email) VALUES (John Doe, john.doe@example.com)批量SQL:INSERT…

CH58x/CH59x动态修改广播包

前言:在日常使用中我们可能有需要动态修改广播包的情况。从机设备不走连接将一些传感器数据通过广播包显示出来 程序中提供了接口函数进行动态修改不用再先关闭广播再重新开启。/******************************************************************************** @fn …

docker 容器部署nginx+keepalived实现高可用

准备两台机器分别部署nginx和keepalived,事先部署好docker容器环境。主机名 ip地址 部署容器park2-0007 10.9.102.62 nginx+keepalivedpark2-0008 10.9.102.63 nginx+keepalived分别启动两个nginx容器docker run -d \--name my-nginx -p 8005:8005 \-v ./nginxdir/default.c…

GraphRAG介绍

一、RAG原理 用户输入了一个指令Instruct,RAG将其与Document store(向量库)中的预存文本进行匹配,然后将符合条件的筛选文本(Retrieved Documents)与指令Instruct,共同合成为一个增强型的Prompt,并将该增强型Prompt喂给大模型,最终大模型根据此增强型Prompt,生成最终的Re…

清理Docker数据卷volumes

原文链接地址 清理Docker数据卷volumes1.查看磁盘使用 2.查看Docker数据卷及磁盘使用情况 3.Docker 删除无用数据卷今天突然发现跑服务器的磁盘满了。记录下进入服务查看到Docker-Overlay2磁盘空间的清理方法:清理Docker的数据卷volumes Docker在长时间使用的情况下,经常需…

Web开发SpringBoot流程性的学习----回顾补充1

HTML(HyperText Markup Language)**:超文本标记语言 超文本:超越了文本的限制,比普通文本更强大。除了文字信息,还可以定义图片、音频、视频等内容。 标记语言:由标签构成的语言HTML标签都是预定义好的。例如:使用展示超链接,展示图片,展示视频。 HTML代码直接在浏览器中运…

【我的青春coding物语果然有问题!】第四次上机卡题复盘

最近事多 今天才写出来 实际做的时候变量名犯了很多很唐的错误 这里就不一一说了 我们看一下今天要讲的题目 05:正整数的任意进制转换 将 p 进制 n 转换为 q 进制。p 和 q 的取值范围为[2,36],其中,用到的数码按从小到大依次为:0,1,2,3,4,5,6,7,8,9,A,B,...,Z…

【PCIE711-214】基于PCIe总线架构的4路HD-SDI/3G-SDI视频图像模拟源

产品概述 PCIE711-214是一款基于PCIE总线架构的4路SDI视频模拟源。该板卡为标准的PCIE插卡,全高尺寸,适合与PCIE总线的工控机或者服务器,板载协议处理器,可以通过PCIE总线将上位机的YUV 422格式视频数据下发通过SDI接口播放出去,从而模拟SDI协议标准的视频流。该板卡支持4…

3 Packet (Mbuf) Library

Packet(MBuf)库概述: 这个库提供了分配和释放缓冲区(mbuf)的能力,DPDK 应用可以使用这些缓冲区来存储各种类型的数据,比如:网络数据包(最常见) 控制信息(control data) 事件(events) 或其他需要临时存储的数据这些 mbuf 缓冲区的底层是通过 Mempool 管理的,即使…