C# Office COM 加载项
笔记哥 /
03-31 /
5点赞 /
0评论 /
396阅读
# 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
```csharp
#if BrandName1
namespace BrandName1 // 品牌1
#else
namespace BrandName2 // 品牌2
#endif
{
[Obfuscation] // 不可被混淆
[ComVisible(true)] // COM 组件类可见, 并且类型要设为公开 public
[Guid("XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")] // CLSID
// [ProgId("BrandName1.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](https://learn.microsoft.com/en-us/windows/win32/com/-progid--key) 与类全名完全匹配, 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 作用就是添加注册表项,避免系统缺失该文件,也为了添加日志输出,可自行写注册表实现
```csharp
{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 同名子项,并添加三个必要的注册表键值
```csharp
{HKCU|HKLM}\Software\[Wow6432Node\]Microsoft\Office
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
```
```c
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 条中属性和回调方法互斥,只允许使用其中一个。另外图像还可使用[内置图像属性](https://codekabinett.com/download/Microsoft-Office-2016_365-imageMso-Gallery.pdf)`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 接口,同样可以减少模块引用自行复制代码到自己项目中
>
```csharp
[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 刷新指定控件
```csharp
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
```csharp
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
```csharp
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
```csharp
using Microsoft.Office.Interop.PowerPoint;
app?.ActivePresentation?.ExportAsFixedFormat(fileName, PpFixedFormatType.ppFixedFormatTypePDF);
```
### Publisher 导出 PDF
```csharp
using Microsoft.Office.Interop.Publisher;
app?.ActiveDocument?.ExportAsFixedFormat(PbFixedFormatType.pbFixedFormatTypePDF, fileName);
```
### Outlook 导出邮件为 PDF
```csharp
using Microsoft.Office.Interop.Outlook;
using Microsoft.Office.Interop.Word;
var mailItem = outlook?.ActiveExplorer()?.Selection?.OfType()?.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位
```csharp
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\\AddinsData`插件数据
2. `HKCU\Software\Microsoft\Office\\Common\CustomUIValidationCache`CustomUI 校验缓存
3. `HKCU\Software\Microsoft\Office\\\Addins`版本插件列表
4. `HKCU\Software\Microsoft\Office\\\AddInLoadTimes`版本加载次数
5. `HKCU\Software\Microsoft\Office\\\Resiliency\NotificationReminderAddinData`Office 禁用通知
## 七、其他问题
### 未加载,加载 COM 加载项时出现运行错误
这是一个比较令人头疼的问题,可能原因有很多,但 Office 没有报错日志,导致很难排查问题
[微软官方博客](https://codekabinett.com/download/Microsoft-Office-2016_365-imageMso-Gallery.pdf)给出了一些解答,个人也复现了一些情况:
- 部署问题: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`等强制方法手动结束进程,一是强制结束进程可能会导致下次打开文档时会提示文档保存异常,二是插件可在程序运行中被手动卸载,可以使用卸载当前应用程序域的方式友好解决问题
```csharp
public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
{
try
{
AppDomain.Unload(AppDomain.CurrentDomain);
}
catch (CannotUnloadAppDomainException)
{
// ignored
}
}
```
## 相关资料
[如何使用 Visual C# .NET 生成 Office COM 加载项 - Office](https://learn.microsoft.com/en-us/previous-versions/office/troubleshoot/office-developer/office-com-add-in-using-visual-c "如何使用 Visual C# .NET 生成 Office COM 加载项 - Office")
[\[MS-CUSTOMUI\]: CustomUI |Microsoft 学习](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-customui/edc80b05-9169-4ff7-95ee-03af067f35b1 "[MS-CUSTOMUI]: CustomUI |Microsoft 学习")
[COM Add-In 加载失败疑难解答 |Microsoft 学习](https://learn.microsoft.com/en-us/archive/blogs/vsod/troubleshooting-com-add-in-load-failures "COM Add-In 加载失败疑难解答 |Microsoft 学习")
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\
本文来自投稿,不代表本站立场,如若转载,请注明出处:http//www.knowhub.vip/share/2/1881
- 热门的技术博文分享
- 1 . ESP实现Web服务器
- 2 . 从零到一:打造高效的金仓社区 API 集成到 MCP 服务方案
- 3 . 使用C#构建一个同时问多个LLM并总结的小工具
- 4 . .NET 原生驾驭 AI 新基建实战系列Milvus ── 大规模 AI 应用的向量数据库首选
- 5 . 在Avalonia/C#中使用依赖注入过程记录
- 6 . [设计模式/Java] 设计模式之工厂方法模式
- 7 . 5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
- 8 . SQL 中的各种连接 JOIN 的区别总结!
- 9 . JavaScript 中防抖和节流的多种实现方式及应用场景
- 10 . SaltStack 远程命令执行中文乱码问题
- 11 . 推荐10个 DeepSeek 神级提示词,建议搜藏起来使用
- 12 . C#基础:枚举、数组、类型、函数等解析
- 13 . VMware平台的Ubuntu部署完全分布式Hadoop环境
- 14 . C# 多项目打包时如何将项目引用转为包依赖
- 15 . Chrome 135 版本开发者工具(DevTools)更新内容
- 16 . 从零创建npm依赖,只需执行一条命令
- 17 . 关于 Newtonsoft.Json 和 System.Text.Json 混用导致的的序列化不识别的问题
- 18 . 大模型微调实战之训练数据集准备的艺术与科学
- 19 . Windows快速安装MongoDB之Mongo实战
- 20 . 探索 C# 14 新功能:实用特性为编程带来便利
- 相关联分享
- C# Office COM 加载项