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\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/07
或2006/01
,但 Office 2007 不支持解析前者
控件的文本、图像、悬浮提示语等,都可使用对应的属性label
、image
在 XML 中直接设置。也可以使用对应的回调方法getLabel
、getImage
。使用回调方法的方式需要在加载项类中声明同名公开方法,比如 XML 中编写getLabel="GetLabel"
,C# 中就须编写对应的public string GetLabel (IRibbonControl control)
方法,类似于 WPF XAML 的事件绑定,支持多个控件使用同一个方法并根据控件的 id 返回合适的值
第 3 条中属性和回调方法互斥,只允许使用其中一个。另外图像还可使用内置图像属性imageMso
,与image
和getImage
也是互斥的,比如 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 刷新控件
- 利用
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);
}
}
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 组件和加载项的注册表,还可以清理以下相关的注册表:
HKCU\Software\Microsoft\Office\<app>\AddinsData
插件数据HKCU\Software\Microsoft\Office\<ver>\Common\CustomUIValidationCache
CustomUI 校验缓存HKCU\Software\Microsoft\Office\<ver>\<app>\Addins
版本插件列表HKCU\Software\Microsoft\Office\<ver>\<app>\AddInLoadTimes
版本加载次数HKCU\Software\Microsoft\Office\<ver>\<app>\Resiliency\NotificationReminderAddinData
Office 禁用通知
七、其他问题
未加载,加载 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 加载项的更多相关文章
- VSTO学习笔记(三) 开发Office 2010 64位COM加载项
原文:VSTO学习笔记(三) 开发Office 2010 64位COM加载项 一.加载项简介 Office提供了多种用于扩展Office应用程序功能的模式,常见的有: 1.Office 自动化程序(A ...
- Office加载项
出自我的个人主页 Alvin Blog 前言 前一段时间公司做了有关Excel 加载项的开发,也遇到了很多坑,所以在此记录一下,有两个原因,1.留给以后在用到加载项的时候,复习所用,避免 跳进同一个坑 ...
- Office加载项安装
出自我的个人主页 Alvin Blog 前言 Excel加载项离不开安装,Excel加载项本身安装及其简单,但这是在申请下来Office开发者账户之后,再次之前都得自行安装 线上安装 微软申请开发者账 ...
- Office启动加载vs。。。项
PowerPoint: 选项->加载项->Chinese Translation Addin->管理[COM加载项]转到->取消Chinese Translation Addi ...
- word加载项打包发布注意事项总结
最近在做一个word加载项,发布的时候还是有很多坑的现在总结一下:发布工具为Advanced Installer 11.0 网盘地址:http://pan.baidu.com/s/1i4GK3g5 1 ...
- VSTO - 使用Excel加载项生成表和图表
此示例显示如何创建Excel的加载项,使用户可以在其工作表中选择库存符号,然后生成一个新工作表,显示库存的历史性能. 工作表包含数据表和图表. 介绍Excel加载项通常不知道工作表包含什么.典型的加载 ...
- IE加载项
加载项 加载项也称为ActiveX控件.浏览器扩展.浏览器帮助应用程序对象或工具栏,可以通过提供多媒体或交互式内容(如动画)来增强对网站的体验. 但是,某些加载项可导致计算机停止响应或显示不需要的 ...
- 教您如何在Word的mathtype加载项中修改章节号
在MathType数学公式编辑器中,公式编号共有五部分内容:分别是章编号(Chapter Number).节编号(Section Number).公式编号(Equation Number).括号(En ...
- 如何在word文档中添加mathtype加载项
MathType是强大的数学公式编辑器,通常与office一起使用,mathtype安装完成后,正常情况下会在word文档中的菜单中自动添加mathtype加载项,但有时也会出现小意外,mathtyp ...
- word中手动添加endnote的加载项
用Endnote管理文献,在写作的同时插入引文,这对于写文章的朋友们来说太重要了.我今天遇到这个问题,花时间钻研了,觉得应该记录下来,相信也会方便大家.查了网上许多帖子依然不得解,可能是Word版本变 ...
随机推荐
- excel表格粘贴到网页的功能
背景 项目有表格功能,表格过大,一个一个填,过于麻烦. 需要从excel表复制的功能. 过程 监听paste事件,根据事件提供的clipboardData属性,获取数据. 根据换行符 \n 和tab符 ...
- Ubuntu开启root账户步骤
在VMware中新建一个Ubuntu,经常使用sudo 太麻烦,还是开启root账户吧. 1.打开 终端: 输入下列命令sudo gedit /usr/share/lightdm/lightdm.co ...
- Python实战:为Prometheus开发自定义Exporter
Python实战:为Prometheus开发自定义Exporter 在当今的微服务架构和容器化部署环境中,监控系统的重要性不言而喻.Prometheus作为一款开源的系统监控和警报工具,以其强大的功能 ...
- Nmap 脚本使用
Nmap 脚本使用 使用 Nmap 脚本是扩展 Nmap 功能的一种高效方式,允许用户执行从简单的服务检测到复杂的漏洞利用的各种任务.通过指定 --script 选项,并结合相应的脚本名称或类型,用户 ...
- ulimit命令 控制服务器资源
命 令:ulimit功 能:控制shell程序的资源语 法:ulimit [-aHS][-c <core文件上限>][-d <数据节区大小>][-f <文件大 小 ...
- 降阶公式/ARC173F
ARC173F 题意 给定 \(n,A,B\),初始有一个集合 \(S=\{1,2,\dots,A,A +1,A+2,\dots,A+B\}\).进行如下操作 \(n-1\) 次使得剩下 \(n\) ...
- C++ 使用MIDI库演奏《晴天》
那些在MIDI库里徘徊的十六分音符 终究没能拼成告白的主歌 我把周杰伦的<晴天>写成C++的类在每个midiEvent里埋藏故事的小黄花 调试器的断点比初恋更漫长而青春不过是一串未 ...
- SMMS图床Java接口上传
前言 个人项目开发中,网站建设中需要用到大量的图片以及用户上传的图片,如果服务器带宽小,磁盘容量小将所有的图片信息全部存储在服务器上不太现实,这里建议将图片数据存储在对象存OSS上或者将图片保存在图床 ...
- FANUC发那科工业机器人减速器维修小细节
在现代工业生产中,FANUC发那科机器人已成为不可或缺的一部分.然而,随着时间的推移,发那科机械手减速器可能会出现故障,影响机器人的正常工作. 一.了解减速器的结构与工作原理 在开始FANUC发那科机 ...
- 2024电子取证“獬豸杯”WP
简介: 竞赛为个人赛,工具自备,只发证书(还没用,公告这么写的哈)竞赛选手们将对模拟的案件进行电子数据调查取证,全面检验参赛选手电子数据取证的综合素质和能力. 检材链接: https://pan.ba ...