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\Classes
ProgID
● "" = '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 学习

C# Office COM 加载项的更多相关文章

  1. VSTO学习笔记(三) 开发Office 2010 64位COM加载项

    原文:VSTO学习笔记(三) 开发Office 2010 64位COM加载项 一.加载项简介 Office提供了多种用于扩展Office应用程序功能的模式,常见的有: 1.Office 自动化程序(A ...

  2. Office加载项

    出自我的个人主页 Alvin Blog 前言 前一段时间公司做了有关Excel 加载项的开发,也遇到了很多坑,所以在此记录一下,有两个原因,1.留给以后在用到加载项的时候,复习所用,避免 跳进同一个坑 ...

  3. Office加载项安装

    出自我的个人主页 Alvin Blog 前言 Excel加载项离不开安装,Excel加载项本身安装及其简单,但这是在申请下来Office开发者账户之后,再次之前都得自行安装 线上安装 微软申请开发者账 ...

  4. Office启动加载vs。。。项

    PowerPoint: 选项->加载项->Chinese Translation Addin->管理[COM加载项]转到->取消Chinese Translation Addi ...

  5. word加载项打包发布注意事项总结

    最近在做一个word加载项,发布的时候还是有很多坑的现在总结一下:发布工具为Advanced Installer 11.0 网盘地址:http://pan.baidu.com/s/1i4GK3g5 1 ...

  6. VSTO - 使用Excel加载项生成表和图表

    此示例显示如何创建Excel的加载项,使用户可以在其工作表中选择库存符号,然后生成一个新工作表,显示库存的历史性能. 工作表包含数据表和图表. 介绍Excel加载项通常不知道工作表包含什么.典型的加载 ...

  7. IE加载项

    加载项   加载项也称为ActiveX控件.浏览器扩展.浏览器帮助应用程序对象或工具栏,可以通过提供多媒体或交互式内容(如动画)来增强对网站的体验. 但是,某些加载项可导致计算机停止响应或显示不需要的 ...

  8. 教您如何在Word的mathtype加载项中修改章节号

    在MathType数学公式编辑器中,公式编号共有五部分内容:分别是章编号(Chapter Number).节编号(Section Number).公式编号(Equation Number).括号(En ...

  9. 如何在word文档中添加mathtype加载项

    MathType是强大的数学公式编辑器,通常与office一起使用,mathtype安装完成后,正常情况下会在word文档中的菜单中自动添加mathtype加载项,但有时也会出现小意外,mathtyp ...

  10. word中手动添加endnote的加载项

    用Endnote管理文献,在写作的同时插入引文,这对于写文章的朋友们来说太重要了.我今天遇到这个问题,花时间钻研了,觉得应该记录下来,相信也会方便大家.查了网上许多帖子依然不得解,可能是Word版本变 ...

随机推荐

  1. R语言学习数据挖掘

    1.用R计算数据基本统计量(均值) 学习机器学习和数据挖掘中的各种算法和模型,需要掌握统计学的基本概念.统计学是通过搜索.整理.分析数据等手段,以达到推断所测对象的本质,并预测对象未来走势的一门综合性 ...

  2. SSL和HTTPS

    转载: 链接 随着互联网的发展,给我们的生活带来便利的同时,也伴随着很多网络钓鱼.信息泄露.网络诈骗等事件的频繁发生,企业网站被钓鱼网站仿冒,遭受经济损失,影响品牌形象. 如果网站不使用SSL证书,数 ...

  3. Spring5 框架基本概念

    课程内容介绍1.Spring 框架概述2.IOC 容器(1)IOC 底层原理(2)IOC 接口(BeanFactory)(3)IOC 操作 Bean 管理(基于 xml)(4)IOC 操作 Bean ...

  4. 补充:字符编码ASCII、 ISO8859-1、GB2312、GBK、Unicode、UTF-8

    补充:字符编码  编码表的由来计算机只能识别二进制数据,早期由来是电信号.为了方便应用计算机,让它可以识别各个国家的文字.就将各个国家的文字用数字来表示,并一一对应,形成一张表.这就是编码表. 常 ...

  5. 对rpc长连接与短连接的思考

    大家好,我是思无邪,某go中厂开发工程师,也是OSPP2024的学生参与者! 如果你觉得我的文章有帮助,记得三连支持一下哦! 目前正在深入研究源码,与你们一起进步,共同攻克编程难关! 欢迎关注我的公众 ...

  6. mysl 修改数据存储位置后服务启动后停止

    在 Windows 系统中安装完 mysql 后,如果是生产用的机器,通常会修改数据存储位置.基本步骤: 1. 停止 mysql 服务: 2. 修改 my.ini 文件中的 datadir=" ...

  7. protocol buffers(protobuf)安装教程

    本文按照mac讲解protobuf的安装,windows上比较好安装按照mac的基本流程就可以安装成功,mac上的安装有的时候比较容易出现问题 一.通过brew的方式安装(仅Mac) 需要mac中存在 ...

  8. 洛谷B4038 [GESP202409 三级] 平衡序列 题解

    原题传送门 前言 当我以一种十分激动的心情参加了GESP的2024-9的三级考试时. 打开了此题,然后--自以为是的拿着暴力一顿乱写!然后TLE. 直到结束我还是没有想出来! (太菜了!!!) 以一种 ...

  9. .net core 非阻塞的异步编程 及 线程调度过程

    本文主要分为三个部分: 1.语法格式 2.线程调度情况 3.编程注意事项 4.练一练 * 阅读提示 :鼠标悬停在 章节标题 上可见 文章目录 异步编程(Task Asynchronous Progra ...

  10. 【编程思维】临近实施 WPF 下拉框闪烁问题!!

    私以为架构是业务开发的发展历史,顺应大方向而生,再为贴切时刻的用户需求,持续微改动. 我本以为了解这个软件的架构没甚意思,加快的开发速度不能过渡到下一个别的软件去: 却不知以小窥大,关键还是计算机思维 ...