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. excel表格粘贴到网页的功能

    背景 项目有表格功能,表格过大,一个一个填,过于麻烦. 需要从excel表复制的功能. 过程 监听paste事件,根据事件提供的clipboardData属性,获取数据. 根据换行符 \n 和tab符 ...

  2. Ubuntu开启root账户步骤

    在VMware中新建一个Ubuntu,经常使用sudo 太麻烦,还是开启root账户吧. 1.打开 终端: 输入下列命令sudo gedit /usr/share/lightdm/lightdm.co ...

  3. Python实战:为Prometheus开发自定义Exporter

    Python实战:为Prometheus开发自定义Exporter 在当今的微服务架构和容器化部署环境中,监控系统的重要性不言而喻.Prometheus作为一款开源的系统监控和警报工具,以其强大的功能 ...

  4. Nmap 脚本使用

    Nmap 脚本使用 使用 Nmap 脚本是扩展 Nmap 功能的一种高效方式,允许用户执行从简单的服务检测到复杂的漏洞利用的各种任务.通过指定 --script 选项,并结合相应的脚本名称或类型,用户 ...

  5. ulimit命令 控制服务器资源

    命   令:ulimit功   能:控制shell程序的资源语 法:ulimit [-aHS][-c <core文件上限>][-d <数据节区大小>][-f <文件大 小 ...

  6. 降阶公式/ARC173F

    ARC173F 题意 给定 \(n,A,B\),初始有一个集合 \(S=\{1,2,\dots,A,A +1,A+2,\dots,A+B\}\).进行如下操作 \(n-1\) 次使得剩下 \(n\) ...

  7. C++ 使用MIDI库演奏《晴天》

    那些在MIDI库里徘徊的十六分音符 终究没能拼成告白的主歌   我把周杰伦的<晴天>写成C++的类在每个midiEvent里埋藏故事的小黄花   调试器的断点比初恋更漫长而青春不过是一串未 ...

  8. SMMS图床Java接口上传

    前言 个人项目开发中,网站建设中需要用到大量的图片以及用户上传的图片,如果服务器带宽小,磁盘容量小将所有的图片信息全部存储在服务器上不太现实,这里建议将图片数据存储在对象存OSS上或者将图片保存在图床 ...

  9. FANUC发那科工业机器人减速器维修小细节

    在现代工业生产中,FANUC发那科机器人已成为不可或缺的一部分.然而,随着时间的推移,发那科机械手减速器可能会出现故障,影响机器人的正常工作. 一.了解减速器的结构与工作原理 在开始FANUC发那科机 ...

  10. 2024电子取证“獬豸杯”WP

    简介: 竞赛为个人赛,工具自备,只发证书(还没用,公告这么写的哈)竞赛选手们将对模拟的案件进行电子数据调查取证,全面检验参赛选手电子数据取证的综合素质和能力. 检材链接: https://pan.ba ...