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

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 与类全名完全匹配, 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 复制代码
<?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>
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/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 接口,同样可以减少模块引用自行复制代码到自己项目中

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<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(其他版本未测试)退出时不会触发 OnBeginShutdownOnDisconnection,原因未知,应该是 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

[MS-CUSTOMUI]: CustomUI |Microsoft 学习

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

相关推荐
Dymc18 天前
【Visio使用教程】
office·visio
狂小虎24 天前
亲测解决笔记本触摸板使用不了Touchpad not working
电脑·office
lisw051 个月前
office或者word排版中,复制/黏贴进来文字不会自动换行,如何处理?
word·office
大画家DHJ1 个月前
Word 插入图片会到文字底下解决方案
word·office·插入图片会到文字底下·嵌入型·上下型环绕·手动拖动图片·难受
Ylsh37021 个月前
Excel文件合并、拆分工具 、 Excel数据批量转Word
windows·经验分享·github·excel·office
Ylsh37021 个月前
SheetDataMerge合并工作表(excel)内多行同类数据的小工具。
windows·excel·开源软件·office
YvesHe2 个月前
【WPS&Office】汇总
wps·office
麻雀小妖2 个月前
如何在WPS和Word/Excel中直接使用DeepSeek功能
ai·wps·office·deepseek
Z3r4y2 个月前
【钓鱼】基于office的一些钓鱼技法
网络安全·office·钓鱼·宏病毒